@oneie/claude 0.7.0 → 0.9.0

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.
Files changed (263) hide show
  1. package/agents/abm-strategist.md +89 -0
  2. package/agents/ads-meta.md +97 -0
  3. package/agents/analyst.md +173 -0
  4. package/agents/architect.md +280 -0
  5. package/agents/brand-guardian.md +88 -0
  6. package/agents/brand-strategist.md +92 -0
  7. package/agents/campaign-content.md +90 -0
  8. package/agents/campaign-email.md +88 -0
  9. package/agents/campaign-sms.md +87 -0
  10. package/agents/campaign-social.md +87 -0
  11. package/agents/cco.md +99 -0
  12. package/agents/ceo.md +106 -0
  13. package/agents/chairman.md +120 -0
  14. package/agents/cmo.md +101 -0
  15. package/agents/community-greeter.md +89 -0
  16. package/agents/community-moderator.md +92 -0
  17. package/agents/compliance.md +92 -0
  18. package/agents/copywriter.md +100 -0
  19. package/agents/creative-strategist.md +95 -0
  20. package/agents/cro.md +98 -0
  21. package/agents/cto.md +267 -0
  22. package/agents/customer-interviewer.md +93 -0
  23. package/agents/customer-researcher.md +89 -0
  24. package/agents/customer-success-manager.md +90 -0
  25. package/agents/customer-trainer.md +90 -0
  26. package/agents/cxo.md +100 -0
  27. package/agents/demand-creator.md +89 -0
  28. package/agents/demo-mover.md +83 -0
  29. package/agents/demo-specialist.md +89 -0
  30. package/agents/demo-thai-family-law.md +87 -0
  31. package/agents/designer.md +112 -0
  32. package/agents/discovery-caller.md +98 -0
  33. package/agents/doctor.md +139 -0
  34. package/agents/educate-coach.md +100 -0
  35. package/agents/elevate-tutor.md +83 -0
  36. package/agents/email-lifecycle-marketer.md +91 -0
  37. package/agents/engage-specialist.md +99 -0
  38. package/agents/events-coordinator.md +90 -0
  39. package/agents/foundation-builder.md +94 -0
  40. package/agents/funnel-architect.md +89 -0
  41. package/agents/gift-creator.md +100 -0
  42. package/agents/google-ads.md +103 -0
  43. package/agents/guide.md +292 -0
  44. package/agents/helpdesk-dispatcher.md +88 -0
  45. package/agents/hook-specialist.md +91 -0
  46. package/agents/identify-optimizer.md +101 -0
  47. package/agents/implementer.md +305 -0
  48. package/agents/incident-commander.md +120 -0
  49. package/agents/insights-lead.md +93 -0
  50. package/agents/journey-runner.md +113 -0
  51. package/agents/linkedin-ads.md +123 -0
  52. package/agents/live-sales-chat.md +90 -0
  53. package/agents/market-researcher.md +88 -0
  54. package/agents/media-buyer.md +96 -0
  55. package/agents/movers-customer-researcher.md +95 -0
  56. package/agents/movers-foundation-builder.md +96 -0
  57. package/agents/movers-market-researcher.md +97 -0
  58. package/agents/movers-pricing-strategist.md +171 -0
  59. package/agents/nurture-architect.md +99 -0
  60. package/agents/offer-architect.md +92 -0
  61. package/agents/onboarder.md +222 -0
  62. package/agents/onboarding-specialist.md +92 -0
  63. package/agents/operations-dashboard.md +98 -0
  64. package/agents/perf-engineer.md +348 -0
  65. package/agents/playbook-writer.md +71 -0
  66. package/agents/plg-strategist.md +91 -0
  67. package/agents/positioning-architect.md +88 -0
  68. package/agents/press-officer.md +89 -0
  69. package/agents/pricing-strategist.md +94 -0
  70. package/agents/privacy-officer.md +92 -0
  71. package/agents/referral-manager.md +91 -0
  72. package/agents/refine-analyst.md +102 -0
  73. package/agents/release-manager.md +261 -0
  74. package/agents/renewals-upsell-rep.md +90 -0
  75. package/agents/review-engineer.md +327 -0
  76. package/agents/rewards-steward.md +82 -0
  77. package/agents/sales-call-coach.md +94 -0
  78. package/agents/sales-closer.md +98 -0
  79. package/agents/security-auditor.md +313 -0
  80. package/agents/sell-closer.md +101 -0
  81. package/agents/share-amplifier.md +99 -0
  82. package/agents/social-media-manager.md +92 -0
  83. package/agents/storyteller.md +250 -0
  84. package/agents/strategist.md +83 -0
  85. package/agents/strategy-aligner.md +94 -0
  86. package/agents/support-agent.md +90 -0
  87. package/agents/tagger.md +245 -0
  88. package/agents/tech-writer.md +216 -0
  89. package/agents/test-engineer.md +243 -0
  90. package/agents/tiktok-ads.md +105 -0
  91. package/agents/tracking-engineer.md +92 -0
  92. package/agents/upsell-strategist.md +101 -0
  93. package/agents/voice.md +85 -0
  94. package/agents/w4-tools.md +153 -0
  95. package/agents/w4-verify.md +20 -138
  96. package/agents/workflow-optimiser.md +84 -0
  97. package/commands/close.md +814 -160
  98. package/commands/create.md +2 -2
  99. package/commands/deploy.md +554 -175
  100. package/commands/do.md +119 -109
  101. package/commands/fast.md +67 -0
  102. package/commands/improve.md +2 -2
  103. package/commands/one.md +418 -0
  104. package/commands/see.md +1 -1
  105. package/commands/sync.md +1 -1
  106. package/commands/tasks.md +222 -0
  107. package/commands/vespio.md +110 -0
  108. package/commands/vespio.remote.md +227 -0
  109. package/hooks/hooks.json +25 -79
  110. package/hooks/lib/governor-escape-match.sh +88 -0
  111. package/hooks/lib/hook.sh +4 -13
  112. package/hooks/lib/signal.sh +9 -2
  113. package/hooks/scripts/branch-pin.sh +6 -0
  114. package/hooks/scripts/config-protect.sh +6 -0
  115. package/hooks/scripts/do-outcome-gate.sh +19 -2
  116. package/hooks/scripts/git-add-guard.sh +43 -2
  117. package/hooks/scripts/governor-escape.sh +96 -0
  118. package/hooks/scripts/load-guard.sh +127 -0
  119. package/hooks/scripts/post-edit-check.sh +36 -29
  120. package/hooks/scripts/session-start.sh +34 -80
  121. package/hooks/scripts/task-complete-verify.sh +46 -40
  122. package/package.json +1 -1
  123. package/rules/documentation.md +9 -0
  124. package/scripts/ad-copy-lint.sh +656 -0
  125. package/scripts/agentverse-audit.sh +256 -0
  126. package/scripts/asi-walk.sh +435 -0
  127. package/scripts/astro-build-cached.sh +143 -0
  128. package/scripts/blocks-manifest-cached.sh +100 -0
  129. package/scripts/blocks-manifest.mjs +497 -0
  130. package/scripts/blocks-ratchet.sh +116 -0
  131. package/scripts/blocks-render-probe.mjs +529 -0
  132. package/scripts/blocks-usage.mjs +334 -0
  133. package/scripts/chat-context-check.sh +89 -0
  134. package/scripts/close-metrics.sh +558 -0
  135. package/scripts/close-owner.sh +268 -0
  136. package/scripts/db-sync-lock-check.sh +116 -0
  137. package/scripts/deploy-dev.sh +35 -0
  138. package/scripts/deploy-emit.sh +298 -0
  139. package/scripts/deploy-gate-check.sh +128 -0
  140. package/scripts/deploy-ready.sh +78 -0
  141. package/scripts/deploy-record.sh +605 -0
  142. package/scripts/deploy.sh +1273 -0
  143. package/scripts/do-auto.sh +598 -48
  144. package/scripts/do-brief.sh +113 -0
  145. package/scripts/do-close.sh +1137 -0
  146. package/scripts/do-commit.sh +75 -0
  147. package/scripts/do-consumer-sweep.sh +18 -1
  148. package/scripts/do-cycle-shape-check.sh +160 -0
  149. package/scripts/do-decide.sh +476 -0
  150. package/scripts/do-derive-check.sh +436 -0
  151. package/scripts/do-fleet.sh +106 -28
  152. package/scripts/do-folder.sh +10 -1
  153. package/scripts/do-next.sh +106 -0
  154. package/scripts/do-orchestrate.sh +17 -5
  155. package/scripts/do-plan-json.mjs +201 -0
  156. package/scripts/do-plan-json.sh +8 -0
  157. package/scripts/do-preflight.sh +117 -0
  158. package/scripts/do-project.sh +157 -0
  159. package/scripts/do-prove-selftest.sh +108 -0
  160. package/scripts/do-prove.sh +295 -23
  161. package/scripts/do-rank.py +31 -1
  162. package/scripts/do-recon-cache.sh +7 -1
  163. package/scripts/do-recon-pack.sh +196 -0
  164. package/scripts/do-reconcile.sh +121 -4
  165. package/scripts/do-signal.sh +280 -23
  166. package/scripts/do-smoke.sh +18 -1
  167. package/scripts/do-test-gate.sh +80 -0
  168. package/scripts/do-tick.sh +102 -0
  169. package/scripts/do-tier.sh +6 -0
  170. package/scripts/do-triage.sh +182 -0
  171. package/scripts/do-ui-gate.sh +1 -1
  172. package/scripts/do-w4-gates.sh +451 -0
  173. package/scripts/do-walk.sh +12 -1
  174. package/scripts/env-sync.sh +173 -0
  175. package/scripts/factory-brief-check.sh +330 -0
  176. package/scripts/factory-check.sh +68 -14
  177. package/scripts/factory-close-check.sh +257 -0
  178. package/scripts/factory-emit.sh +211 -0
  179. package/scripts/factory-executor-check.mjs +353 -0
  180. package/scripts/factory-peak.sh +301 -0
  181. package/scripts/factory-repo.sh +118 -3
  182. package/scripts/factory-review-check.mjs +61 -0
  183. package/scripts/factory-ship.sh +61 -0
  184. package/scripts/factory-tasks-check.sh +18 -1
  185. package/scripts/factory-turn.sh +326 -0
  186. package/scripts/factory-walk.sh +396 -0
  187. package/scripts/factory-width.sh +57 -0
  188. package/scripts/fade-toxic.sh +4 -3
  189. package/scripts/fixtures/factory-brief-real.md +44 -0
  190. package/scripts/fixtures/triage-dupe.md +5 -0
  191. package/scripts/fleet-manifest.mjs +108 -0
  192. package/scripts/fleet-status.sh +110 -0
  193. package/scripts/full-suite-paths-check.sh +144 -0
  194. package/scripts/gate-reaper-check.sh +98 -0
  195. package/scripts/gate-reaper.sh +125 -0
  196. package/scripts/gate-run.sh +73 -0
  197. package/scripts/gc-content-check.sh +140 -0
  198. package/scripts/gen-dev-config.py +20 -0
  199. package/scripts/govern-bound-check.sh +60 -0
  200. package/scripts/govern-claims-check.sh +233 -0
  201. package/scripts/govern-mem-check.sh +290 -0
  202. package/scripts/governor-doors-check.sh +362 -0
  203. package/scripts/governor-escape-check.sh +171 -0
  204. package/scripts/health.sh +413 -0
  205. package/scripts/id-inventory.mjs +418 -0
  206. package/scripts/land.sh +551 -0
  207. package/scripts/lib/gc-finished.sh +77 -0
  208. package/scripts/lib/govern.sh +361 -0
  209. package/scripts/lib/govern.ts +756 -0
  210. package/scripts/lighthouse-run.sh +187 -0
  211. package/scripts/livekit-live-check.sh +61 -0
  212. package/scripts/livekit-ratchet.sh +46 -0
  213. package/scripts/load-guard-check.sh +49 -0
  214. package/scripts/machine-check.sh +102 -0
  215. package/scripts/machine-watch.sh +177 -0
  216. package/scripts/one-agents.mjs +415 -0
  217. package/scripts/one-resume.sh +78 -0
  218. package/scripts/orphan-baseline.json +182 -0
  219. package/scripts/orphan-modules.mjs +179 -0
  220. package/scripts/pr-body.sh +335 -0
  221. package/scripts/preview-fd-check.sh +289 -0
  222. package/scripts/promise-manifest.mjs +24 -1
  223. package/scripts/release.sh +322 -0
  224. package/scripts/roles-check.sh +946 -0
  225. package/scripts/sdk-build-cached.sh +64 -0
  226. package/scripts/signal-watch.sh +241 -0
  227. package/scripts/skills-publish.sh +94 -0
  228. package/scripts/speed-cache-check.sh +173 -0
  229. package/scripts/speed-check.mjs +907 -0
  230. package/scripts/speed-parity-check.sh +648 -0
  231. package/scripts/speed-waterfall-check.sh +355 -0
  232. package/scripts/substrate-env-parity.mjs +156 -0
  233. package/scripts/tasks-claim-race.mjs +108 -0
  234. package/scripts/tasks-loop.sh +185 -0
  235. package/scripts/test-cached.sh +255 -0
  236. package/scripts/test-full.sh +87 -0
  237. package/scripts/test-honesty.mjs +137 -0
  238. package/scripts/test-lanes.sh +166 -0
  239. package/scripts/test-speed.sh +94 -0
  240. package/scripts/triage-shape-check.sh +149 -0
  241. package/scripts/tsc-cached.sh +179 -0
  242. package/scripts/typedb-flake-check.sh +197 -0
  243. package/scripts/urls-lint.sh +15 -0
  244. package/scripts/verify-fast.sh +445 -0
  245. package/scripts/vespio-sync.sh +149 -0
  246. package/scripts/wf-check.mjs +104 -1
  247. package/scripts/worktree-preview.sh +879 -0
  248. package/scripts/worktree-up.sh +208 -0
  249. package/skills/livekit-agents/SKILL.md +285 -0
  250. package/skills/livekit-agents/references/freshness-rules.md +168 -0
  251. package/skills/shadcn/SKILL.md +1 -1
  252. package/skills/signal/SKILL.md +0 -1
  253. package/skills/voice/SKILL.md +94 -6
  254. package/skills/voice/corpus-check.sh +87 -0
  255. package/hooks/scripts/compact-hint.sh +0 -35
  256. package/hooks/scripts/gate-guard.sh +0 -83
  257. package/hooks/scripts/read-tracker.sh +0 -26
  258. package/hooks/scripts/session-end-verify.sh +0 -51
  259. package/hooks/scripts/stop-reflect.sh +0 -140
  260. package/hooks/scripts/sync-priority-todo.sh +0 -57
  261. package/hooks/scripts/sync-todo-docs.sh +0 -46
  262. package/hooks/scripts/tool-signal.sh +0 -48
  263. package/scripts/do-tasks-bridge.py +0 -366
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env bash
2
+ # blocks-manifest-cached.sh — run `blocks-manifest.mjs --check`, or skip it when
3
+ # nothing that feeds the manifest has moved.
4
+ #
5
+ # manifest: needs-env
6
+ #
7
+ # WHY. C2 wired `--check` into verify-fast.sh as Step 0 on 2026-09-09, which was
8
+ # right -- three committed artifacts were generated from one registry and gated by
9
+ # nothing. But it runs on EVERY fast-lane pass, and measured on this box, warm,
10
+ # twice in a row: 8.19s and 6.98s. The fast lane exists to be run on every edit;
11
+ # a fixed ~7s toll on a doc edit, a script edit or a test-only cycle is most of
12
+ # what the lane was built to avoid. Only ~1.0-1.3s of that is the `bunx tsx`
13
+ # resolution leg (measured: `bunx tsx --version` 1.33s then 1.03s) -- the rest is
14
+ # the real work, importing a 7,800-line config through tsx and walking 243 blocks.
15
+ # So vendoring tsx does not fix this. A memo does.
16
+ #
17
+ # WHY NARROWING IS SOUND HERE, AND IS NOT THE test-cached.sh MISTAKE. CLAUDE.md
18
+ # warns: do not narrow test-cached.sh's key, because those tests readFileSync
19
+ # across pay/, channels/, schema/, .claude/ and text/ at runtime, so an import
20
+ # graph is NOT a superset of what a test reads. blocks-manifest.mjs is the
21
+ # opposite shape. Its inputs are DECLARED at :33-38 -- config.tsx, families.ts,
22
+ # block-usage.json -- and the runner it generates does nothing but import
23
+ # config.tsx and walk puckConfig.components. Its inputs ARE its import graph, by
24
+ # construction. Read that precedent before reaching for it here; it is about a
25
+ # different failure.
26
+ #
27
+ # THE KEY IS A SUPERSET ON PURPOSE. Everything the check reads AND all three
28
+ # artifacts it compares against live under one.ie/web/src, so the fingerprint
29
+ # covers that whole tree rather than a hand-listed set of paths. A block
30
+ # component living somewhere the list did not anticipate is exactly how a memo
31
+ # goes unsound, and a drifted manifest reading as cached is far worse than 7s.
32
+ # The cost of the superset is that any src edit misses -- which is correct, and
33
+ # still skips every doc-only, script-only and test-only cycle.
34
+ #
35
+ # SAFETY, the direction that matters: a LOST stamp costs a run, never a skip.
36
+ # The stamp lives in GOVERN_DIR (machine-wide tmp, evaporates), so a fresh box, a
37
+ # cleared tmp or a new worktree all MISS. There is no path where a missing stamp
38
+ # reads as cached. Staged-but-uncommitted edits move the key too, because
39
+ # `ls-files -s` shows the INDEX while `diff` shows the working tree -- one leg
40
+ # alone would miss half of them (the scar sdk-build-cached.sh records).
41
+ #
42
+ # BLOCKS_MANIFEST_FORCE=1 always runs. --self-test drives the red half.
43
+ set -euo pipefail
44
+ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
45
+ GOVERN_DIR="${GOVERN_DIR:-${TMPDIR:-/tmp}/one-govern}"
46
+ mkdir -p "$GOVERN_DIR"
47
+
48
+ _fp() {
49
+ { git -C "$ROOT" ls-files -s one.ie/web/src 2>/dev/null
50
+ git -C "$ROOT" diff -- one.ie/web/src 2>/dev/null
51
+ git -C "$ROOT" ls-files --others --exclude-standard one.ie/web/src 2>/dev/null \
52
+ | while IFS= read -r u; do shasum -a 256 "$ROOT/$u" 2>/dev/null; done
53
+ } | shasum -a 256 | cut -d' ' -f1
54
+ }
55
+
56
+ _run_check() {
57
+ local out rc
58
+ out="$(mktemp)"
59
+ set +e
60
+ node "$ROOT/.claude/scripts/blocks-manifest.mjs" --check > "$out" 2>&1
61
+ rc=$?
62
+ set -e
63
+ cat "$out"; rm -f "$out"
64
+ return $rc
65
+ }
66
+
67
+ if [ "${1:-}" = "--self-test" ]; then
68
+ fails=0
69
+ probe="$ROOT/one.ie/web/src/lib/puck/.memo-probe.ts"
70
+ BLOCKS_MANIFEST_FORCE=1 bash "$0" >/dev/null 2>&1 || true
71
+ a="$(bash "$0" 2>&1)"
72
+ case "$a" in *cached*) echo "ok: second run on an unchanged tree is CACHED";;
73
+ *) echo "FAIL: unchanged tree re-ran: $a"; fails=$((fails+1));; esac
74
+ # RED HALF: a memo that cannot be shown to MISS is an unrun gate in a
75
+ # performance costume -- the exact class Step 0 was added to close.
76
+ printf 'export const __memoProbe = 1\n' > "$probe"
77
+ b="$(bash "$0" 2>&1)"
78
+ rm -f "$probe"
79
+ case "$b" in *cached*) echo "FAIL: a new file under src did NOT invalidate the memo"; fails=$((fails+1));;
80
+ *) echo "ok: a new file under src MISSES (red half proven)";; esac
81
+ c="$(bash "$0" 2>&1)"
82
+ case "$c" in *cached*) echo "ok: removing the probe returns to the earlier key";;
83
+ *) echo "note: post-probe run re-ran (fine — a miss is never wrong)";; esac
84
+ [ "$fails" -eq 0 ] && echo "blocks-manifest-cached --self-test: PASS" || { echo "blocks-manifest-cached --self-test: $fails FAIL"; exit 1; }
85
+ exit 0
86
+ fi
87
+
88
+ want="$(_fp)"
89
+ STAMP="$GOVERN_DIR/blocks-manifest.$want"
90
+
91
+ if [ "${BLOCKS_MANIFEST_FORCE:-0}" != "1" ] && [ -f "$STAMP" ]; then
92
+ echo "blocks-manifest --check: cached (one.ie/web/src unchanged since the last pass)"
93
+ exit 0
94
+ fi
95
+
96
+ _run_check || exit $?
97
+ # Only a PASS is memoised. A drift result is never stamped: the next run must
98
+ # see it again, and `test-cached.sh` records the same rule for the same reason.
99
+ rm -f "$GOVERN_DIR"/blocks-manifest.* 2>/dev/null || true
100
+ printf '%s' "$want" > "$STAMP"
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ // The React-free block vocabulary — text/blocks-todo.md C5 deliverable.
3
+ //
4
+ // Reads the (post-cut, C4) registry via the same tsx-import technique
5
+ // blocks-usage.mjs uses (bunx tsx against the absolute config.tsx path, so
6
+ // the file's own `@/` imports resolve exactly as they do in the app), pulls
7
+ // `pagesUsing` from the committed block-usage.json so the manifest carries a
8
+ // usage-ranked list, and writes one.ie/web/src/lib/puck/block-manifest.json —
9
+ // a plain-data file `channels/` can import with zero React/Puck runtime deps.
10
+ //
11
+ // Three artifacts, one build, all three checked: the manifest (the whole
12
+ // vocabulary, read by tooling), channels/src/generated/block-enum.ts (what the
13
+ // model is told), and one.ie/web/src/lib/puck/block-schema.generated.ts (what
14
+ // the browser normalises the streaming preview against).
15
+ //
16
+ // --check: regenerate in memory and diff against the committed file. No
17
+ // network access — everything it reads is local (config.tsx, block-usage.json).
18
+ //
19
+ // Usage:
20
+ // node .claude/scripts/blocks-manifest.mjs # write the file
21
+ // node .claude/scripts/blocks-manifest.mjs --check # diff, no write
22
+
23
+ import { execFileSync } from 'node:child_process'
24
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, mkdtempSync, rmSync } from 'node:fs'
25
+ import { tmpdir } from 'node:os'
26
+ import path from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
28
+
29
+ const __filename = fileURLToPath(import.meta.url)
30
+ const __dirname = path.dirname(__filename)
31
+ const REPO_ROOT = path.resolve(__dirname, '../..')
32
+ const WEB_DIR = path.join(REPO_ROOT, 'one.ie/web')
33
+ const CONFIG_PATH = path.join(WEB_DIR, 'src/lib/puck/config.tsx')
34
+ const FAMILIES_PATH = path.join(WEB_DIR, 'src/lib/puck/families.ts')
35
+ const USAGE_PATH = path.join(WEB_DIR, 'src/lib/puck/block-usage.json')
36
+ const OUT_PATH = path.join(WEB_DIR, 'src/lib/puck/block-manifest.json')
37
+ const ENUM_PATH = path.join(REPO_ROOT, 'channels/src/generated/block-enum.ts')
38
+ const SCHEMA_PATH = path.join(WEB_DIR, 'src/lib/puck/block-schema.generated.ts')
39
+
40
+ const args = process.argv.slice(2)
41
+ const CHECK = args.includes('--check')
42
+
43
+ // Chat-fit mapping mirrors src/lib/puck/chat-fit.ts's CHAT_FITS exactly — this
44
+ // script has no `@/` alias of its own to import that module from, and the
45
+ // fallback here matches chatFitFor's fallback ('compact' for anything unknown).
46
+ const CHAT_FITS = {
47
+ atomic: 'inline',
48
+ container: 'compact',
49
+ 'social-preview': 'compact',
50
+ app: 'compact',
51
+ section: 'full',
52
+ }
53
+
54
+ function readRegistry() {
55
+ const tmp = mkdtempSync(path.join(tmpdir(), 'blocks-manifest-'))
56
+ const runner = path.join(tmp, 'entries.mjs')
57
+ writeFileSync(
58
+ runner,
59
+ [
60
+ `import { puckConfig, PRE_COLLAPSE_COMPONENTS } from ${JSON.stringify(CONFIG_PATH)}`,
61
+ `import { BLOCK_FAMILIES } from ${JSON.stringify(FAMILIES_PATH)}`,
62
+ // A prop whose legal values are a closed set is only usable by an agent
63
+ // if the set travels with the name. `type: select | radio` is exactly
64
+ // that shape in Puck, so every one of them publishes its values — a
65
+ // rule, not a special case for `variant`.
66
+ 'const optionValuesOf = (fields) => {',
67
+ ' const out = {}',
68
+ ' for (const [key, f] of Object.entries(fields || {})) {',
69
+ ' if (!f || (f.type !== "select" && f.type !== "radio")) continue',
70
+ ' if (!Array.isArray(f.options) || f.options.length === 0) continue',
71
+ // Values are published RAW, not stringified: a select over booleans
72
+ // (`featured`) or numbers is legal in Puck, and "false" is not false.
73
+ ' out[key] = f.options.map((o) => {',
74
+ ' const v = o && typeof o === "object" ? o.value : o',
75
+ ' return typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? v : String(v)',
76
+ ' })',
77
+ ' }',
78
+ ' return out',
79
+ '}',
80
+ // The reachable variant vocabulary of a COLLAPSED family is its
81
+ // families.ts member list, NOT the collapsed block's own
82
+ // `fields.variant.options`. collapseFamilies spreads `canonicalDef.fields`
83
+ // LAST, so on any family whose canonical member already declared a
84
+ // `variant` select of its own, the canonical's values overwrite the
85
+ // family's — measured on Hero, Features, Stats and Cta (2026-08-18).
86
+ // Those overwritten values are NOT reachable: collapse's render is
87
+ // `byVariant[variant] ?? canonicalDef` with `variant` stripped, so
88
+ // Hero/"agency" renders the default Hero and the picked value is lost.
89
+ // BLOCK_FAMILIES is the declaration of record; the manifest publishes it.
90
+ // A member whose OWN fields carry a `type: "slot"` is unreachable from a
91
+ // chat turn (see buildManifest's note) — publishing its variant would
92
+ // advertise a value the model can pick and nothing can render. Only a
93
+ // REGISTERED member is tested: an absent one has no fields to read, and
94
+ // dropping it here would be a second, unrelated cut.
95
+ 'const hasSlotDef = (def) => Object.values(def?.fields || {}).some((f) => f && f.type === "slot")',
96
+ 'const familyVariants = {}',
97
+ 'for (const family of Object.values(BLOCK_FAMILIES)) {',
98
+ ' const usable = Object.entries(family.members)',
99
+ ' .filter(([member]) => !hasSlotDef(PRE_COLLAPSE_COMPONENTS[member]))',
100
+ ' .map(([, variant]) => variant)',
101
+ ' familyVariants[family.canonical] = [...new Set(usable)]',
102
+ '}',
103
+ // The per-VARIANT field sets — the half of the vocabulary the collapsed
104
+ // registration destroys. `collapseFamilies` unions every member's fields
105
+ // into one registration, so `declaredProps` on a family is a SUPERSET of
106
+ // what any single member's render reads: Hero declares both `headline`
107
+ // (HeroVideoDialog) and `heading` (every other member), and a model that
108
+ // fills `headline` on variant 'centered' gets silent default copy —
109
+ // measured 2026-08-19 on a real prod turn, where every one of six
110
+ // generated fields was invisible. What renders is `byVariant[variant]`,
111
+ // so what an agent must be told is that member's OWN fields.
112
+ 'const CHROME = new Set(["bgEffect", "bgPattern", "bgToken", "variant"])',
113
+ 'const fieldSpecs = (def) => {',
114
+ ' const out = {}',
115
+ ' for (const [key, f] of Object.entries(def?.fields || {})) {',
116
+ ' if (CHROME.has(key)) continue',
117
+ // A slot field needs Puck's DropZone pipeline — unfillable from a chat
118
+ // turn, same rule buildManifest applies to a whole slot-carrying block.
119
+ ' if (!f || f.type === "slot") continue',
120
+ ' const spec = { type: typeof f.type === "string" ? f.type : "text" }',
121
+ ' if (f.type === "array") spec.of = Object.keys(f.arrayFields || {})',
122
+ ' if (f.type === "select" || f.type === "radio") {',
123
+ ' const values = optionValuesOf({ [key]: f })[key]',
124
+ ' if (values) spec.options = values',
125
+ ' }',
126
+ ' out[key] = spec',
127
+ ' }',
128
+ ' return out',
129
+ '}',
130
+ // Mirrors collapse.ts canonicalVariantOf exactly: a member literally
131
+ // carrying 'default' wins, else the first member declared.
132
+ 'const canonicalVariantOf = (members) => {',
133
+ ' const values = Object.values(members)',
134
+ ' return values.includes("default") ? "default" : values[0]',
135
+ '}',
136
+ 'const familyOf = {}',
137
+ 'for (const family of Object.values(BLOCK_FAMILIES)) familyOf[family.canonical] = family',
138
+ 'const variantSchema = (name) => {',
139
+ ' const family = familyOf[name]',
140
+ ' if (!family) return { canonicalVariant: null, variants: { "": { fields: fieldSpecs(PRE_COLLAPSE_COMPONENTS[name]), semantics: PRE_COLLAPSE_COMPONENTS[name]?.metadata?.semantics || {}, renders: PRE_COLLAPSE_COMPONENTS[name]?.metadata?.renders || {} } } }',
141
+ ' const variants = {}',
142
+ ' for (const [member, variant] of Object.entries(family.members)) {',
143
+ ' const def = PRE_COLLAPSE_COMPONENTS[member]',
144
+ ' if (!def) continue',
145
+ // Same filter as familyVariants above, and it has to be the same: BLOCK_SCHEMA
146
+ // (built from `variants`) is what normalizeBlockProps maps a payload against,
147
+ // and it falls back to canonicalVariant for a variant it does not know. Leaving
148
+ // a slot member in `variants` while dropping it from `options.variant` would
149
+ // publish it in BLOCK_FIELD_DOC and normalise props onto it while the enum
150
+ // rejected the value — the two halves of one generated file disagreeing.
151
+ ' if (hasSlotDef(def)) continue',
152
+ ' variants[variant] = { fields: fieldSpecs(def), semantics: def.metadata?.semantics || {}, renders: def.metadata?.renders || {} }',
153
+ ' }',
154
+ ' return { canonicalVariant: canonicalVariantOf(family.members), variants }',
155
+ '}',
156
+ 'const out = {}',
157
+ // Whether a block can enter the chat vocabulary at all. For a non-family
158
+ // block this is exactly the old rule (its own fields carry no slot). For a
159
+ // COLLAPSED family the old rule was wrong: the union inherits a slot from
160
+ // any one member, so ProductDetail's `children` hid all nine Product
161
+ // renders. What renders is byVariant[variant], so the question is asked of
162
+ // the members — the family enters as long as it has a slot-free member to
163
+ // publish AND its canonical member is one of them (a slot-carrying
164
+ // canonical would make a no-variant call unrenderable, and it is the value
165
+ // normalizeBlockProps falls back to; no family is in that shape today).
166
+ 'const memberForVariant = (family, variant) => (Object.entries(family.members).find(([, v]) => v === variant) || [])[0]',
167
+ 'const chatableOf = (name, block, vs) => {',
168
+ ' const family = familyOf[name]',
169
+ ' if (!family) return !Object.values(block.fields || {}).some((f) => f && f.type === "slot")',
170
+ ' if (hasSlotDef(PRE_COLLAPSE_COMPONENTS[memberForVariant(family, vs.canonicalVariant)])) return false',
171
+ ' return Object.keys(vs.variants).length > 0',
172
+ '}',
173
+ 'for (const [name, block] of Object.entries(puckConfig.components)) {',
174
+ ' const m = block.metadata || {}',
175
+ ' const vs = variantSchema(name)',
176
+ ' const options = optionValuesOf(block.fields)',
177
+ ' if (familyVariants[name]) options.variant = familyVariants[name]',
178
+ ' out[name] = {',
179
+ ' category: typeof m.category === "string" ? m.category : "content",',
180
+ ' icon: typeof m.icon === "string" ? m.icon : "Square",',
181
+ ' description: typeof m.description === "string" ? m.description : "",',
182
+ ' surface: typeof m.surface === "string" ? m.surface : null,',
183
+ ' declaredProps: Object.keys(block.fields || {}).sort(),',
184
+ ' options,',
185
+ ' ...vs,',
186
+ ' chatable: chatableOf(name, block, vs),',
187
+ ' }',
188
+ '}',
189
+ 'console.log(JSON.stringify(out))',
190
+ ].join('\n'),
191
+ )
192
+ try {
193
+ const out = execFileSync('bunx', ['tsx', runner], { cwd: WEB_DIR, encoding: 'utf8' })
194
+ return JSON.parse(out.trim())
195
+ } finally {
196
+ rmSync(tmp, { recursive: true, force: true })
197
+ }
198
+ }
199
+
200
+ function readUsage() {
201
+ if (!existsSync(USAGE_PATH)) return {}
202
+ return JSON.parse(readFileSync(USAGE_PATH, 'utf8')).blocks ?? {}
203
+ }
204
+
205
+ function buildManifest() {
206
+ const registry = readRegistry()
207
+ const usage = readUsage()
208
+ const blocks = {}
209
+ for (const [name, entry] of Object.entries(registry)) {
210
+ // A `type: 'slot'` field (Columns, Grid) needs Puck's own <Render> DropZone
211
+ // pipeline to resolve — outside a real page it stays a raw array and
212
+ // BlockFrame throws. Nothing to stream into an empty layout container in
213
+ // one chat turn anyway, so it never enters the chat vocabulary. Decided
214
+ // per MEMBER for a collapsed family (`chatableOf` in the runner above),
215
+ // because the collapsed field union inherits a slot from any one of them.
216
+ if (!entry.chatable) continue
217
+ const { chatable: _drop, ...rest } = entry
218
+ blocks[name] = {
219
+ ...rest,
220
+ chatFit: entry.surface ? (CHAT_FITS[entry.surface] ?? 'compact') : 'compact',
221
+ pagesUsing: usage[name]?.pagesUsing ?? 0,
222
+ }
223
+ }
224
+ return {
225
+ measuredAt: new Date().toISOString().slice(0, 10),
226
+ count: Object.keys(blocks).length,
227
+ blocks,
228
+ }
229
+ }
230
+
231
+ function diffManifests(committed, fresh) {
232
+ const lines = []
233
+ const names = new Set([...Object.keys(committed.blocks ?? {}), ...Object.keys(fresh.blocks)])
234
+ for (const name of [...names].sort()) {
235
+ const a = committed.blocks?.[name]
236
+ const b = fresh.blocks[name]
237
+ if (!a) {
238
+ lines.push(`+ ${name} ${JSON.stringify(b)}`)
239
+ continue
240
+ }
241
+ if (!b) {
242
+ lines.push(`- ${name} ${JSON.stringify(a)}`)
243
+ continue
244
+ }
245
+ const sameProps = JSON.stringify(a.declaredProps) === JSON.stringify(b.declaredProps)
246
+ // `options` carries the legal values — the half of the vocabulary an agent
247
+ // cannot guess. Leaving it out of the diff would let it rot silently,
248
+ // which is the exact defect publishing it exists to close.
249
+ const sameOptions = JSON.stringify(a.options ?? {}) === JSON.stringify(b.options ?? {})
250
+ // `variants` carries the per-member field sets — the vocabulary an agent
251
+ // fills a block WITH. Same rule as `options`: out of the diff means it
252
+ // rots silently, and a rotted field name renders default copy in chat with
253
+ // nothing red anywhere.
254
+ const sameVariants = JSON.stringify(a.variants ?? {}) === JSON.stringify(b.variants ?? {})
255
+ if (
256
+ !sameOptions ||
257
+ !sameVariants ||
258
+ a.canonicalVariant !== b.canonicalVariant ||
259
+ a.category !== b.category ||
260
+ a.icon !== b.icon ||
261
+ a.description !== b.description ||
262
+ a.surface !== b.surface ||
263
+ a.chatFit !== b.chatFit ||
264
+ a.pagesUsing !== b.pagesUsing ||
265
+ !sameProps
266
+ ) {
267
+ lines.push(`~ ${name} committed=${JSON.stringify(a)} fresh=${JSON.stringify(b)}`)
268
+ }
269
+ }
270
+ if (committed.count !== fresh.count) lines.push(`~ count committed=${committed.count} fresh=${fresh.count}`)
271
+ return lines
272
+ }
273
+
274
+ // The channels-side enum. `z.enum()` needs a LITERAL TUPLE, so this is a .ts
275
+ // source rather than a second JSON copy — a JSON import types as string[] and
276
+ // would need a cast at the tool boundary. Emitting it here also means channels
277
+ // never imports across the web bundle root, which is what keeps `wrangler
278
+ // deploy --dry-run` green by construction rather than by luck.
279
+ //
280
+ // Ranked pagesUsing DESC then name ASC: the model reads the blocks people
281
+ // actually choose first, which is the whole point of ranking rather than
282
+ // truncating (a rare block stays reachable, it is just further down).
283
+ function buildEnumSource(manifest) {
284
+ const names = Object.entries(manifest.blocks)
285
+ .sort((a, b) => b[1].pagesUsing - a[1].pagesUsing || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
286
+ .map(([name]) => name)
287
+ // The variant vocabulary rides along in the SAME generated file. A block's
288
+ // `variant` names WHICH of a collapsed family's members renders, and an
289
+ // undeclared value is not an error anywhere downstream — collapseFamilies
290
+ // falls back to the canonical member, so a wrong guess renders a
291
+ // plausible-looking wrong block in silence. Publishing the legal values here
292
+ // is what lets emit_block reject one loudly instead.
293
+ const variants = Object.entries(manifest.blocks)
294
+ .map(([name, entry]) => [name, entry.options?.variant])
295
+ .filter(([, values]) => Array.isArray(values) && values.length > 0 && values.every((v) => typeof v === 'string'))
296
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
297
+ // The per-variant field schema. `variant` picks WHICH member renders, and
298
+ // that member reads its OWN fields — the collapsed registration's union is a
299
+ // superset, so a prop that survives the union filter can still be invisible.
300
+ // Measured 2026-08-19 against a real prod turn: the model wrote six fields of
301
+ // hero copy (`headline`, `subheadline`, `primaryCta`, `badge`, …) and the
302
+ // browser rendered HeroCentered's defaults — 0 of 6 visible. This table is
303
+ // what closes that: it is the truth `emit_block` normalises and rejects
304
+ // against, and the truth the tool description publishes.
305
+ const schemaEntries = Object.entries(manifest.blocks)
306
+ .filter(([, e]) => e.variants && Object.keys(e.variants).length > 0)
307
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
308
+
309
+ // The vocabulary the MODEL reads, one line per renderable member:
310
+ // Hero/centered: badgeLabel heading subheading primaryCtaLabel …
311
+ // Faq/default: title faqs[q|a]
312
+ // Array fields publish their item keys inline, because those are the half no
313
+ // agent can guess (`Faq/default` takes `faqs[q|a]`, `Faq/simple` takes
314
+ // `items[question|answer]` — same family, incompatible payloads).
315
+ const docLines = []
316
+ for (const [name, entry] of schemaEntries) {
317
+ for (const [variant, spec] of Object.entries(entry.variants)) {
318
+ const fields = Object.entries(spec.fields ?? {})
319
+ if (fields.length === 0) continue
320
+ // A `fields` entry is the EDITOR's form; `metadata.renders` names what the
321
+ // RENDER reads and the form cannot express (Puck has no nested string-array,
322
+ // so `PricingSection` renders `tiers[].features` and can never declare it).
323
+ // The model has no such constraint, so the published item list is the union
324
+ // — measured by .claude/scripts/blocks-render-probe.mjs, 2026-08-20.
325
+ const renders = spec.renders ?? {}
326
+ const body = fields
327
+ .map(([key, f]) => {
328
+ if (f.type !== 'array') return key
329
+ const items = [...(f.of ?? []), ...(renders[key] ?? [])]
330
+ return `${key}[${items.join('|')}]`
331
+ })
332
+ .join(' ')
333
+ docLines.push(`${variant ? `${name}/${variant}` : name}: ${body}`)
334
+ }
335
+ }
336
+
337
+ return [
338
+ '// GENERATED by .claude/scripts/blocks-manifest.mjs — do not edit by hand.',
339
+ '// Every block in the Puck registry, ranked by pagesUsing (desc), then name.',
340
+ `// count=${names.length}`,
341
+ '',
342
+ 'export const BLOCK_NAMES = [',
343
+ ...names.map((name) => ` '${name}',`),
344
+ '] as const',
345
+ '',
346
+ `// The legal \`variant\` values per block — ${variants.length} of the ${names.length} take one.`,
347
+ '// Source: BLOCK_FAMILIES (one.ie/web/src/lib/puck/families.ts) for a collapsed',
348
+ "// family, the field's own options for a block that just has a variant select.",
349
+ 'export const BLOCK_VARIANTS: Record<string, readonly string[]> = {',
350
+ ...variants.map(([name, values]) => ` ${name}: [${values.map((v) => `'${v}'`).join(', ')}],`),
351
+ '}',
352
+ '',
353
+ 'export interface BlockFieldSpec {',
354
+ " /** Puck field type — 'text' | 'textarea' | 'array' | 'select' | 'radio' | 'number' | … */",
355
+ ' type: string',
356
+ ' /** For `type: "array"` — the keys each ITEM object takes. */',
357
+ ' of?: readonly string[]',
358
+ ' /** For `type: "select" | "radio"` — the legal values. */',
359
+ ' options?: readonly (string | number | boolean)[]',
360
+ '}',
361
+ '',
362
+ 'export interface BlockVariantSpec {',
363
+ ' /** The fields the member that renders this variant actually reads. */',
364
+ ' fields: Record<string, BlockFieldSpec>',
365
+ " /** The block's own role -> prop declaration (heading/body/cta/image). */",
366
+ ' semantics: Record<string, string>',
367
+ ' /** Props the RENDER reads that the editor\'s fields cannot express, keyed',
368
+ ' * by the array field they belong to (Puck has no nested string-array, so',
369
+ ' * PricingSection renders tiers[].features and can never declare it). */',
370
+ ' renders?: Record<string, readonly string[]>',
371
+ '}',
372
+ '',
373
+ 'export interface BlockSchemaEntry {',
374
+ ' /** The variant that renders when none is sent. null for a non-family block. */',
375
+ ' canonicalVariant: string | null',
376
+ " /** Keyed by variant value; the single key '' for a non-family block. */",
377
+ ' variants: Record<string, BlockVariantSpec>',
378
+ '}',
379
+ '',
380
+ '// What each variant\'s RENDERING MEMBER reads. The collapsed registration',
381
+ '// unions every member\'s fields, so a prop can be declared on the family and',
382
+ '// still invisible on the member that renders — this is the per-member truth.',
383
+ 'export const BLOCK_SCHEMA: Record<string, BlockSchemaEntry> = {',
384
+ ...schemaEntries.map(([name, entry]) => ` ${JSON.stringify(name)}: ${JSON.stringify({ canonicalVariant: entry.canonicalVariant ?? null, variants: entry.variants })},`),
385
+ '}',
386
+ '',
387
+ `// The same table as prose, for the tool description the model reads — ${docLines.length} members.`,
388
+ 'export const BLOCK_FIELD_DOC = ' + JSON.stringify(docLines.join('\n')),
389
+ '',
390
+ ].join('\n')
391
+ }
392
+
393
+ // The BROWSER half of the same schema. `MessageList` draws the model's PARTIAL
394
+ // tool input through `BlockFrame` while it streams — before `emit_block.execute`
395
+ // and its normalisation have run — so a mis-named prop painted placeholder copy
396
+ // for the length of the stream and snapped to the real text when the output
397
+ // frame landed. `BlockFrame` now runs the SAME `normalizeBlockProps` against
398
+ // this table, so both frames show the same copy.
399
+ //
400
+ // Slim on purpose: only the four things normalizeBlockProps reads —
401
+ // `canonicalVariant`, `variants[v].fields[name].type`, `.of`, and
402
+ // `variants[v].semantics`. `options`, descriptions, categories and icons stay
403
+ // in block-manifest.json (202,196 bytes, measured 2026-08-19), which no `src/`
404
+ // file imports; this module ships in the chat island on every page.
405
+ //
406
+ // Emitted as a .ts source, not JSON, so it carries the SDK's `BlockSchema`
407
+ // type and a shape drift fails tsc instead of failing silently at render.
408
+ function buildSchemaSource(manifest) {
409
+ const entries = Object.entries(manifest.blocks)
410
+ .filter(([, e]) => e.variants && Object.keys(e.variants).length > 0)
411
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
412
+
413
+ let members = 0
414
+ const rows = entries.map(([name, entry]) => {
415
+ const variants = {}
416
+ for (const [variant, spec] of Object.entries(entry.variants)) {
417
+ members++
418
+ const fields = {}
419
+ for (const [key, f] of Object.entries(spec.fields ?? {})) {
420
+ // `of` is the only per-field detail the normaliser uses beyond the
421
+ // name — it remaps ARRAY ITEM keys (`faqs[q|a]` vs `items[question|
422
+ // answer]`). `options` is a tool-description concern, not a render one.
423
+ fields[key] = f.type === 'array' ? { type: f.type, of: f.of ?? [] } : { type: f.type }
424
+ }
425
+ // Always emitted, empty or not: `BlockVariantSpec.semantics` is required,
426
+ // so omitting it would need a cast and lose the type check this file is
427
+ // emitted as .ts to get.
428
+ variants[variant] = { fields, semantics: spec.semantics ?? {} }
429
+ }
430
+ return ` ${JSON.stringify(name)}: ${JSON.stringify({
431
+ canonicalVariant: entry.canonicalVariant ?? null,
432
+ variants,
433
+ })},`
434
+ })
435
+
436
+ return [
437
+ '// GENERATED by .claude/scripts/blocks-manifest.mjs — do not edit by hand.',
438
+ '// The fields each variant\'s RENDERING MEMBER reads, for the streaming',
439
+ '// preview. Puck\'s family collapse UNIONS every member\'s fields into one',
440
+ '// registration, so a prop can be declared on the family and still be',
441
+ '// invisible on the member that renders — this is the per-member truth.',
442
+ `// blocks=${entries.length} members=${members}`,
443
+ '',
444
+ "import type { BlockSchema } from '@oneie/sdk/blocks'",
445
+ '',
446
+ 'export const BLOCK_SCHEMA: BlockSchema = {',
447
+ ...rows,
448
+ '}',
449
+ '',
450
+ ].join('\n')
451
+ }
452
+
453
+ function main() {
454
+ if (CHECK) {
455
+ if (!existsSync(OUT_PATH)) {
456
+ console.error(`blocks-manifest --check: ${OUT_PATH} does not exist — run without --check first to seed it.`)
457
+ process.exit(1)
458
+ }
459
+ const committed = JSON.parse(readFileSync(OUT_PATH, 'utf8'))
460
+ const fresh = buildManifest()
461
+ const diff = diffManifests(committed, fresh)
462
+ // The enum is part of the same artifact: emitting it without checking it
463
+ // builds a file that silently rots, which is exactly what the manifest
464
+ // exists to prevent.
465
+ const enumFresh = buildEnumSource(fresh)
466
+ const enumCommitted = existsSync(ENUM_PATH) ? readFileSync(ENUM_PATH, 'utf8') : ''
467
+ if (enumCommitted !== enumFresh) {
468
+ diff.push(`~ ${ENUM_PATH} out of date — run: node .claude/scripts/blocks-manifest.mjs`)
469
+ }
470
+ // Same rule for the web-side schema: an emitted file that nothing compares
471
+ // is a file that rots, and a rotted field name here renders default copy in
472
+ // the streaming preview with nothing red anywhere.
473
+ const schemaFresh = buildSchemaSource(fresh)
474
+ const schemaCommitted = existsSync(SCHEMA_PATH) ? readFileSync(SCHEMA_PATH, 'utf8') : ''
475
+ if (schemaCommitted !== schemaFresh) {
476
+ diff.push(`~ ${SCHEMA_PATH} out of date — run: node .claude/scripts/blocks-manifest.mjs`)
477
+ }
478
+ if (diff.length > 0) {
479
+ console.error('blocks-manifest --check: drift detected')
480
+ for (const line of diff) console.error(line)
481
+ process.exit(1)
482
+ }
483
+ console.log('blocks-manifest --check: ok (no drift)')
484
+ process.exit(0)
485
+ }
486
+
487
+ const manifest = buildManifest()
488
+ writeFileSync(OUT_PATH, JSON.stringify(manifest, null, 2) + '\n')
489
+ mkdirSync(path.dirname(ENUM_PATH), { recursive: true })
490
+ writeFileSync(ENUM_PATH, buildEnumSource(manifest))
491
+ writeFileSync(SCHEMA_PATH, buildSchemaSource(manifest))
492
+ console.log(`wrote ${OUT_PATH} — count=${manifest.count}`)
493
+ console.log(`wrote ${ENUM_PATH}`)
494
+ console.log(`wrote ${SCHEMA_PATH}`)
495
+ }
496
+
497
+ main()