@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,116 @@
1
+ #!/usr/bin/env bash
2
+ # blocks-ratchet — the block-registry CENSUS REPORTER.
3
+ #
4
+ # NO CEILING IS ENFORCED. Owner directive, 2026-08-20: "you can add as many
5
+ # blocks as necessary. remove the 130 cap. remove any ceiling." Both gates this
6
+ # script used to hold — (1) count <= --max, (2) count <= the integer in
7
+ # one.ie/web/src/lib/puck/.blocks-baseline — are GONE. It counts, it prints,
8
+ # and it ALWAYS EXITS 0, including when it cannot count at all.
9
+ #
10
+ # The name is kept because callers grep for it; it is no longer a ratchet.
11
+ # `--max` and `--update` are still accepted so no caller breaks:
12
+ # --max N reported for context, NEVER enforced
13
+ # --update writes the current count to .blocks-baseline as a plain census
14
+ # record. It moves in EITHER direction now — the old "only ever
15
+ # lower" rule is part of the removed ratchet.
16
+ # The superseding contract for growing the registry is text/blocks-restore.md.
17
+ # text/blocks.md's registry-ceiling deliverable is superseded (see its dated
18
+ # 2026-08-20 note); nothing else in that promise changed.
19
+ #
20
+ # Count source: the same tsx-import trick as blocks-usage.mjs's
21
+ # readRegisteredNames() (.claude/scripts/blocks-usage.mjs:40-53) — a temp
22
+ # .mjs that imports puckConfig from config.tsx by absolute path and prints
23
+ # Object.keys(puckConfig.components), run with `bunx tsx` from one.ie/web.
24
+ # NEVER a grep over config.tsx — a grep counts source lines, not what Puck
25
+ # actually registers after collapseFamilies.
26
+ #
27
+ # TRAP (.claude/CLAUDE.md): under `set -o pipefail`, `<producer> | grep -q`
28
+ # can SIGPIPE the producer (exit 141) when grep matches early. Never pipe
29
+ # the count command into grep — capture its stdout to a variable first,
30
+ # then match with a here-string.
31
+ set -euo pipefail
32
+
33
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
34
+ WEB_DIR="$REPO_ROOT/one.ie/web"
35
+ CONFIG_PATH="$WEB_DIR/src/lib/puck/config.tsx"
36
+ BASELINE_PATH="$WEB_DIR/src/lib/puck/.blocks-baseline"
37
+
38
+ MAX=160
39
+ UPDATE=0
40
+
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --max)
44
+ MAX="$2"
45
+ shift 2
46
+ ;;
47
+ --max=*)
48
+ MAX="${1#--max=}"
49
+ shift
50
+ ;;
51
+ --update)
52
+ UPDATE=1
53
+ shift
54
+ ;;
55
+ *)
56
+ # A census reporter never fails a caller. Say it loudly, carry on.
57
+ echo "blocks-ratchet: ignoring unknown argument: $1" >&2
58
+ shift
59
+ ;;
60
+ esac
61
+ done
62
+
63
+ if [ -f "$BASELINE_PATH" ]; then
64
+ BASELINE="$(tr -d '[:space:]' < "$BASELINE_PATH")"
65
+ else
66
+ BASELINE=""
67
+ fi
68
+
69
+ # A census that cannot count says so — loudly, on stderr, and still exits 0.
70
+ # The likely cause is a config.tsx mid-edit in another window, which is not a
71
+ # verdict about the registry.
72
+ if [ ! -f "$CONFIG_PATH" ]; then
73
+ echo "blocks-ratchet census: UNAVAILABLE — config.tsx not found at $CONFIG_PATH"
74
+ echo "blocks-ratchet ok: no ceiling — census only (owner directive 2026-08-20)"
75
+ exit 0
76
+ fi
77
+
78
+ TMPDIR_RUN="$(mktemp -d)"
79
+ trap 'rm -rf "$TMPDIR_RUN"' EXIT
80
+
81
+ RUNNER="$TMPDIR_RUN/names.mjs"
82
+ cat > "$RUNNER" <<EOF
83
+ import { puckConfig } from $(node -e "console.log(JSON.stringify(process.argv[1]))" "$CONFIG_PATH")
84
+ console.log(JSON.stringify(Object.keys(puckConfig.components)))
85
+ EOF
86
+
87
+ # Capture producer output to a variable BEFORE any matching — never pipe
88
+ # this command directly into grep under set -o pipefail (SIGPIPE trap).
89
+ # stderr goes to a file, never merged into stdout: bun writes "Resolving
90
+ # dependencies" there, and merging it corrupts the JSON this parses.
91
+ ERRLOG="$TMPDIR_RUN/err.log"
92
+ if ! NAMES_JSON="$(cd "$WEB_DIR" && bunx tsx "$RUNNER" 2>"$ERRLOG")"; then
93
+ echo "blocks-ratchet census: UNAVAILABLE — config.tsx did not import"
94
+ cat "$ERRLOG" >&2
95
+ echo "blocks-ratchet ok: no ceiling — census only (owner directive 2026-08-20)"
96
+ exit 0
97
+ fi
98
+
99
+ # The name list is the LAST line — a cold bun can print install chatter first.
100
+ NAMES_LINE="$(printf '%s\n' "$NAMES_JSON" | tail -n 1)"
101
+
102
+ if ! COUNT="$(node -e "console.log(JSON.parse(process.argv[1]).length)" "$NAMES_LINE" 2>"$ERRLOG")"; then
103
+ echo "blocks-ratchet census: UNAVAILABLE — the registry did not print a name list"
104
+ cat "$ERRLOG" >&2
105
+ echo "blocks-ratchet ok: no ceiling — census only (owner directive 2026-08-20)"
106
+ exit 0
107
+ fi
108
+
109
+ # The census record. Moves in either direction; it gates nothing.
110
+ if [ "$UPDATE" -eq 1 ]; then
111
+ printf '%s' "$COUNT" > "$BASELINE_PATH"
112
+ BASELINE="$COUNT"
113
+ fi
114
+
115
+ echo "blocks-ratchet census: count=$COUNT max=$MAX (not enforced) baseline=${BASELINE:-none} (record, not a gate)"
116
+ echo "blocks-ratchet ok: no ceiling — census only (owner directive 2026-08-20)"
@@ -0,0 +1,529 @@
1
+ #!/usr/bin/env node
2
+ // Measures the gap between what a Puck block DECLARES and what it READS.
3
+ //
4
+ // A `fields` declaration is the EDITOR's form, not the render's prop list, so a
5
+ // component can read a prop it never declared — and that prop is then invisible
6
+ // to every static reader of the registry (block-manifest.json, BLOCK_SCHEMA,
7
+ // the emit_block tool description). No analysis of `fields` can find it; the
8
+ // only oracle is the rendered output. Known instance: PricingSection renders
9
+ // `tiers[].features` and declares no such item field.
10
+ //
11
+ // The technique is blocks-manifest.mjs's: `bunx tsx` against the absolute
12
+ // config.tsx path from a temp runner, so the file's own `@/` imports resolve
13
+ // exactly as they do in the app. Nothing is written to the repo — this script
14
+ // only measures, and prints.
15
+ //
16
+ // Usage:
17
+ // node .claude/scripts/blocks-render-probe.mjs # human summary
18
+ // node .claude/scripts/blocks-render-probe.mjs --json # machine form
19
+ // node .claude/scripts/blocks-render-probe.mjs --only Hero # one family/block
20
+ // node .claude/scripts/blocks-render-probe.mjs --limit 20 # first N members
21
+ // node .claude/scripts/blocks-render-probe.mjs --control # positive control
22
+ // node .claude/scripts/blocks-render-probe.mjs --extra tone,badge # widen the set
23
+ //
24
+ // Redirect stdout to a file rather than piping it — a full run's JSON is large
25
+ // enough to hit the SIGPIPE-141 trap documented in .claude/CLAUDE.md.
26
+
27
+ import { execFileSync } from 'node:child_process'
28
+ import { writeFileSync, mkdtempSync, rmSync } from 'node:fs'
29
+ import { tmpdir } from 'node:os'
30
+ import path from 'node:path'
31
+ import { fileURLToPath } from 'node:url'
32
+
33
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
34
+ const REPO_ROOT = path.resolve(__dirname, '../..')
35
+ const WEB_DIR = path.join(REPO_ROOT, 'one.ie/web')
36
+ const CONFIG_PATH = path.join(WEB_DIR, 'src/lib/puck/config.tsx')
37
+ const FAMILIES_PATH = path.join(WEB_DIR, 'src/lib/puck/families.ts')
38
+
39
+ const args = process.argv.slice(2)
40
+ const JSON_OUT = args.includes('--json')
41
+ const onlyAt = args.indexOf('--only')
42
+ const ONLY = onlyAt >= 0 ? (args[onlyAt + 1] ?? '') : ''
43
+ const limitAt = args.indexOf('--limit')
44
+ const LIMIT = limitAt >= 0 ? Number(args[limitAt + 1] ?? 0) : 0
45
+ // The positive control. A probe that finds nothing proves nothing until it is
46
+ // shown able to find something: --control re-runs the identical machinery
47
+ // against each member's OWN DECLARED text fields, which the render is supposed
48
+ // to read. Its detection rate is the ceiling on what the real run could have
49
+ // seen — every point below 100% is a member the probe is partly blind to.
50
+ const CONTROL = args.includes('--control')
51
+ // The candidate set is the ceiling on what this can find, so it has to be
52
+ // extendable without editing the snapshot below.
53
+ const extraAt = args.indexOf('--extra')
54
+ const EXTRA = extraAt >= 0 ? (args[extraAt + 1] ?? '').split(',').map((s) => s.trim()).filter(Boolean) : []
55
+
56
+ // Snapshot of the two candidate tables in packages/sdk/src/blocks.ts — the
57
+ // KEY_ROLE keys (every name a model has been OBSERVED to reach for) plus the
58
+ // ROLE_FIELDS values (the real repo field names those roles resolve to).
59
+ // Copied rather than imported because neither is exported from that module, so
60
+ // this is a snapshot: a name added to blocks.ts does not appear here by itself.
61
+ const COMMON_CANDIDATES = [
62
+ 'headline', 'heading', 'title', 'header', 'h1', 'name',
63
+ 'subheadline', 'subheading', 'subhead', 'subtitle', 'subline',
64
+ 'description', 'body', 'text', 'copy', 'blurb', 'summary', 'tagline', 'content',
65
+ 'eyebrow', 'kicker', 'overline',
66
+ 'badge', 'badgetext', 'badgelabel', 'pill', 'tag',
67
+ 'cta', 'ctatext', 'ctalabel', 'buttontext', 'buttonlabel',
68
+ 'primarycta', 'primaryctatext', 'primaryctalabel', 'action',
69
+ 'secondarycta', 'secondaryctatext', 'secondaryctalabel',
70
+ 'href', 'url', 'link', 'ctahref', 'primaryctahref', 'secondaryctahref',
71
+ 'image', 'imageurl', 'imagesrc', 'img', 'photo', 'src', 'picture', 'avatar', 'avatarurl',
72
+ 'icon', 'price', 'amount', 'cost', 'value', 'role', 'company',
73
+ 'question', 'q', 'answer', 'a',
74
+ 'label', 'detail', 'quote', 'trustLabel', 'badgeLabel',
75
+ 'primaryCtaLabel', 'ctaLabel', 'submitLabel', 'playLabel', 'primaryCta', 'ctaText',
76
+ 'secondaryCtaLabel', 'secondaryCta', 'ctaHref', 'primaryCtaHref', 'secondaryCtaHref', 'href2',
77
+ 'imageUrl', 'imageSrc', 'mediaImage', 'mockupImage', 'thumbnailImage', 'visual',
78
+ 'avatarUrl', 'coverImage', 'monthlyPrice', 'position',
79
+ ]
80
+
81
+ // No template literals and no `${` inside this body — it is itself interpolated
82
+ // into the runner file, and only the two import paths may cross that boundary.
83
+ const RUNNER_BODY = String.raw`
84
+ const CHROME = new Set(['bgToken', 'bgPattern', 'bgEffect', 'variant', 'children', 'puck', 'id', 'editMode'])
85
+ const ONLY = process.env.PROBE_ONLY || ''
86
+ const LIMIT = Number(process.env.PROBE_LIMIT || 0)
87
+ const COMMON = JSON.parse(process.env.PROBE_CANDIDATES || '[]')
88
+ const CONTROL = process.env.PROBE_MODE === 'control'
89
+ const isTextField = (spec) => spec && (spec.type === 'text' || spec.type === 'textarea')
90
+
91
+ // React logs an unknown-prop warning per probe render; at ~10k renders that is
92
+ // the slowest thing in the run and none of it is a result.
93
+ console.error = () => {}
94
+ console.warn = () => {}
95
+
96
+ let seq = 0
97
+ const nextSentinel = () => 'ZQP' + (++seq).toString(36) + 'QZ'
98
+ const stripTags = (html) => html.replace(/<[^>]*>/g, ' ')
99
+
100
+ // A canary is a name no component could plausibly read. If its value reaches
101
+ // the markup, the member spreads unknown props onto a DOM element and every
102
+ // candidate would "land" — the member is unprobeable, not clean. Only a STRING
103
+ // canary is sensitive: React drops non-string values from a DOM spread, so an
104
+ // array-shaped canary can go quiet on a member that genuinely spreads.
105
+ const CANARY_KEYS = ['zqCanaryAlphaProp', 'zqCanaryBetaProp']
106
+
107
+ const familyOf = {}
108
+ for (const family of Object.values(BLOCK_FAMILIES)) {
109
+ for (const [member, variant] of Object.entries(family.members)) {
110
+ familyOf[member] = { canonical: family.canonical, variant, members: Object.keys(family.members) }
111
+ }
112
+ }
113
+
114
+ const defOf = (name) => PRE_COLLAPSE_COMPONENTS[name]
115
+ const fieldsOf = (name) => (defOf(name) && defOf(name).fields) || {}
116
+
117
+ function renderOf(def, props) {
118
+ return renderToStaticMarkup(createElement(def.render, props))
119
+ }
120
+
121
+ function tryRender(def, props) {
122
+ try {
123
+ return { html: renderOf(def, props), threw: null }
124
+ } catch (err) {
125
+ return { html: null, threw: String((err && err.message) || err).slice(0, 200) }
126
+ }
127
+ }
128
+
129
+ // Shape ladder. A candidate whose value the component never coerces to a child
130
+ // (an array field handed a bare string) renders nothing and would read as
131
+ // "absent"; trying the plausible shapes in turn is what separates the two.
132
+ const OBJ = (s) => ({ label: s, text: s, title: s, name: s, value: s, href: '/' + s })
133
+ const TOP_SHAPES = [
134
+ ['string', (s) => s],
135
+ ['array-of-string', (s) => [s]],
136
+ ['array-of-object', (s) => [OBJ(s)]],
137
+ ['object', (s) => OBJ(s)],
138
+ ]
139
+ const ITEM_SHAPES = [
140
+ ['string', (s) => s],
141
+ ['array-of-string', (s) => [s]],
142
+ ['object', (s) => OBJ(s)],
143
+ ]
144
+
145
+ /** One candidate, walked down the shape ladder against each base. Stops at the
146
+ * first combination whose sentinel reaches the markup.
147
+ *
148
+ * Two bases, because a component that reads 'props.title ?? props.heading' is
149
+ * invisible to a probe run against defaultProps — the declared 'heading' is
150
+ * already set, so the fallback never fires and the undeclared 'title' reads as
151
+ * absent. The STRIPPED base removes every declared text field, which is what
152
+ * lets that read site answer. */
153
+ function probeOne(def, buildProps, candidate, shapes, bases) {
154
+ for (const [baseName, baseProps] of bases) {
155
+ for (const [shapeName, make] of shapes) {
156
+ const sentinel = nextSentinel()
157
+ const { html, threw } = tryRender(def, buildProps(baseProps, candidate, make(sentinel)))
158
+ if (threw || !html) continue
159
+ if (html.includes(sentinel)) {
160
+ return { shape: shapeName, base: baseName, visible: stripTags(html).includes(sentinel) }
161
+ }
162
+ }
163
+ }
164
+ return null
165
+ }
166
+
167
+ /** A copy of props with every declared text/textarea key removed. */
168
+ function stripText(props, specs) {
169
+ const out = { ...props }
170
+ for (const [k, spec] of Object.entries(specs || {})) if (isTextField(spec)) delete out[k]
171
+ return out
172
+ }
173
+
174
+ const results = []
175
+ let names = Object.keys(PRE_COLLAPSE_COMPONENTS)
176
+ if (ONLY) {
177
+ names = names.filter((n) => n === ONLY || (familyOf[n] && familyOf[n].canonical === ONLY))
178
+ }
179
+ if (LIMIT > 0) names = names.slice(0, LIMIT)
180
+
181
+ let done = 0
182
+ for (const name of names) {
183
+ done++
184
+ if (done % 20 === 0) process.stderr.write('[probe] ' + done + '/' + names.length + '\n')
185
+ const def = defOf(name)
186
+ const fam = familyOf[name]
187
+ const block = fam ? fam.canonical : name
188
+ const variant = fam ? fam.variant : ''
189
+ const key = fam ? block + '/' + variant : block
190
+ const fields = fieldsOf(name)
191
+ const declared = Object.keys(fields)
192
+ const row = { key, block, variant, member: name, findings: [], skipped: null, arrayFieldsProbed: 0, arrayFieldsSkipped: {} }
193
+
194
+ if (typeof def.render !== 'function') {
195
+ row.skipped = 'no-render'
196
+ results.push(row)
197
+ continue
198
+ }
199
+ if (Object.values(fields).some((f) => f && f.type === 'slot')) {
200
+ // A slot needs Puck's own DropZone pipeline; outside a page BlockFrame throws.
201
+ row.skipped = 'slot-field'
202
+ results.push(row)
203
+ continue
204
+ }
205
+
206
+ const base = def.defaultProps || {}
207
+ const baseline = tryRender(def, base)
208
+ if (baseline.threw) {
209
+ row.skipped = 'baseline-threw'
210
+ row.detail = baseline.threw
211
+ results.push(row)
212
+ continue
213
+ }
214
+ // A lazy() block unwraps to <Suspense fallback={null}> and renders '' without
215
+ // throwing (_block-oracle.ts). It answered nothing; it is not clean.
216
+ if (!baseline.html || baseline.html.length < 20) {
217
+ row.skipped = 'empty-baseline'
218
+ results.push(row)
219
+ continue
220
+ }
221
+
222
+ const canaryProps = { ...base }
223
+ const canarySentinels = []
224
+ for (const k of CANARY_KEYS) {
225
+ const s = nextSentinel()
226
+ canarySentinels.push(s)
227
+ canaryProps[k] = s
228
+ }
229
+ const canary = tryRender(def, canaryProps)
230
+ if (canary.html && canarySentinels.some((s) => canary.html.includes(s))) {
231
+ row.skipped = 'spreads-unknown-props'
232
+ results.push(row)
233
+ continue
234
+ }
235
+
236
+ // Candidate source (a): every OTHER member's declared field names in the
237
+ // same family. (c): the common-name snapshot from packages/sdk/src/blocks.ts.
238
+ const siblingFields = new Set()
239
+ if (fam) {
240
+ for (const sib of fam.members) {
241
+ if (sib === name) continue
242
+ for (const f of Object.keys(fieldsOf(sib))) siblingFields.add(f)
243
+ }
244
+ }
245
+ const declaredSet = new Set(declared)
246
+ const topCandidates = []
247
+ if (CONTROL) {
248
+ for (const [c, spec] of Object.entries(fields)) {
249
+ if (CHROME.has(c) || !isTextField(spec)) continue
250
+ topCandidates.push(c)
251
+ }
252
+ } else {
253
+ for (const c of [...siblingFields, ...COMMON]) {
254
+ if (declaredSet.has(c) || CHROME.has(c)) continue
255
+ if (topCandidates.includes(c)) continue
256
+ topCandidates.push(c)
257
+ }
258
+ }
259
+ row.candidates = topCandidates.length
260
+
261
+ const topBases = [['default', base]]
262
+ const stripped = stripText(base, fields)
263
+ if (Object.keys(stripped).length !== Object.keys(base).length) {
264
+ const sr = tryRender(def, stripped)
265
+ if (!sr.threw && sr.html && sr.html.length >= 20) topBases.push(['stripped', stripped])
266
+ }
267
+
268
+ for (const c of topCandidates) {
269
+ const hit = probeOne(def, (b, k, v) => ({ ...b, [k]: v }), c, TOP_SHAPES, topBases)
270
+ if (hit) {
271
+ row.findings.push({
272
+ prop: c,
273
+ where: 'top',
274
+ shape: hit.shape,
275
+ base: hit.base,
276
+ visible: hit.visible,
277
+ declaredElsewhereInFamily: siblingFields.has(c),
278
+ })
279
+ }
280
+ }
281
+
282
+ // (b) — array ITEM keys, where the known case lives. The item vocabulary is
283
+ // 'arrayFields', and that is the editor's item form, not the item's prop list.
284
+ for (const [fieldName, spec] of Object.entries(fields)) {
285
+ if (!spec || spec.type !== 'array') continue
286
+ const itemKeys = Object.keys(spec.arrayFields || {})
287
+ const seedItem = Array.isArray(base[fieldName]) && base[fieldName][0] && typeof base[fieldName][0] === 'object'
288
+ ? base[fieldName][0]
289
+ : {}
290
+ const baseItem = { ...seedItem }
291
+ for (const k of itemKeys) if (!(k in baseItem)) baseItem[k] = 'ZQFILL'
292
+
293
+ const siblingItemKeys = new Set()
294
+ if (fam) {
295
+ for (const sib of fam.members) {
296
+ if (sib === name) continue
297
+ const sf = fieldsOf(sib)[fieldName]
298
+ if (sf && sf.type === 'array') for (const k of Object.keys(sf.arrayFields || {})) siblingItemKeys.add(k)
299
+ }
300
+ }
301
+ const itemDeclared = new Set(itemKeys)
302
+ const itemCandidates = []
303
+ if (CONTROL) {
304
+ for (const [c, ispec] of Object.entries(spec.arrayFields || {})) {
305
+ if (CHROME.has(c) || !isTextField(ispec)) continue
306
+ itemCandidates.push(c)
307
+ }
308
+ } else {
309
+ for (const c of [...siblingItemKeys, ...COMMON]) {
310
+ if (itemDeclared.has(c) || CHROME.has(c)) continue
311
+ if (itemCandidates.includes(c)) continue
312
+ itemCandidates.push(c)
313
+ }
314
+ }
315
+ // Accounting happens BELOW the three skips, not here: a candidate on a
316
+ // field that was never probed is not a candidate probed, and a member with
317
+ // an unprobeable array field is not a member that answered clean.
318
+ const skipField = (why) => {
319
+ row.arrayFieldsSkipped[why] = (row.arrayFieldsSkipped[why] ?? 0) + 1
320
+ }
321
+ if (itemCandidates.length === 0) {
322
+ skipField('no-item-candidates')
323
+ continue
324
+ }
325
+
326
+ const itemCanaryItem = { ...baseItem }
327
+ const itemCanarySentinels = []
328
+ for (const k of CANARY_KEYS) {
329
+ const s = nextSentinel()
330
+ itemCanarySentinels.push(s)
331
+ itemCanaryItem[k] = s
332
+ }
333
+ const itemCanary = tryRender(def, { ...base, [fieldName]: [itemCanaryItem] })
334
+ if (itemCanary.html && itemCanarySentinels.some((s) => itemCanary.html.includes(s))) {
335
+ skipField('item-spreads-unknown-keys')
336
+ continue
337
+ }
338
+
339
+ const itemBaseline = tryRender(def, { ...base, [fieldName]: [baseItem] })
340
+ if (itemBaseline.threw || !itemBaseline.html) {
341
+ skipField('item-baseline-empty-or-threw')
342
+ continue
343
+ }
344
+
345
+ row.arrayFieldsProbed++
346
+ row.candidates = (row.candidates ?? 0) + itemCandidates.length
347
+
348
+ const itemBases = [['default', baseItem]]
349
+ const strippedItem = stripText(baseItem, spec.arrayFields)
350
+ if (Object.keys(strippedItem).length !== Object.keys(baseItem).length) {
351
+ const sr = tryRender(def, { ...base, [fieldName]: [strippedItem] })
352
+ if (!sr.threw && sr.html) itemBases.push(['stripped', strippedItem])
353
+ }
354
+
355
+ for (const c of itemCandidates) {
356
+ const hit = probeOne(
357
+ def,
358
+ (b, k, v) => ({ ...base, [fieldName]: [{ ...b, [k]: v }] }),
359
+ c,
360
+ ITEM_SHAPES,
361
+ itemBases,
362
+ )
363
+ if (hit) {
364
+ row.findings.push({
365
+ prop: fieldName + '[].' + c,
366
+ where: 'item',
367
+ field: fieldName,
368
+ itemKey: c,
369
+ shape: hit.shape,
370
+ base: hit.base,
371
+ visible: hit.visible,
372
+ declaredElsewhereInFamily: siblingItemKeys.has(c),
373
+ })
374
+ }
375
+ }
376
+ }
377
+
378
+ results.push(row)
379
+ }
380
+
381
+ process.stdout.write(JSON.stringify({ members: names.length, results }))
382
+ `
383
+
384
+ function buildRunner() {
385
+ // The runner lives in a temp dir, so a bare `react` specifier has no
386
+ // node_modules to walk up to. createRequire rooted at one.ie/web resolves the
387
+ // SAME files config.tsx's own ESM imports resolve to (node caches a CJS module
388
+ // by path, so there is one React instance and hooks still work).
389
+ return [
390
+ "import { createRequire } from 'node:module'",
391
+ `const require = createRequire(${JSON.stringify(path.join(WEB_DIR, 'package.json'))})`,
392
+ "const { renderToStaticMarkup } = require('react-dom/server')",
393
+ "const { createElement } = require('react')",
394
+ `import { PRE_COLLAPSE_COMPONENTS } from ${JSON.stringify(CONFIG_PATH)}`,
395
+ `import { BLOCK_FAMILIES } from ${JSON.stringify(FAMILIES_PATH)}`,
396
+ RUNNER_BODY,
397
+ ].join('\n')
398
+ }
399
+
400
+ function runProbe() {
401
+ const tmp = mkdtempSync(path.join(tmpdir(), 'blocks-render-probe-'))
402
+ const runner = path.join(tmp, 'probe.mjs')
403
+ writeFileSync(runner, buildRunner())
404
+ try {
405
+ const out = execFileSync('bunx', ['tsx', runner], {
406
+ cwd: WEB_DIR,
407
+ encoding: 'utf8',
408
+ maxBuffer: 512 * 1024 * 1024,
409
+ stdio: ['ignore', 'pipe', 'inherit'],
410
+ env: {
411
+ ...process.env,
412
+ PROBE_ONLY: ONLY,
413
+ PROBE_LIMIT: String(LIMIT || 0),
414
+ PROBE_MODE: CONTROL ? 'control' : 'gap',
415
+ PROBE_CANDIDATES: JSON.stringify([...new Set([...COMMON_CANDIDATES, ...EXTRA])]),
416
+ },
417
+ })
418
+ return JSON.parse(out.trim())
419
+ } finally {
420
+ rmSync(tmp, { recursive: true, force: true })
421
+ }
422
+ }
423
+
424
+ const HIGH_TRAFFIC = new Set(['Hero', 'Pricing', 'Faq', 'Features', 'Testimonials', 'Stats', 'Cta'])
425
+
426
+ function report(raw) {
427
+ const rows = raw.results
428
+ const probed = rows.filter((r) => !r.skipped)
429
+ const withFindings = probed.filter((r) => r.findings.length > 0)
430
+ const skippedBy = {}
431
+ for (const r of rows) if (r.skipped) skippedBy[r.skipped] = (skippedBy[r.skipped] ?? 0) + 1
432
+
433
+ const findings = []
434
+ for (const r of withFindings) for (const f of r.findings) findings.push({ ...f, key: r.key, member: r.member })
435
+ const visible = findings.filter((f) => f.visible)
436
+
437
+ const out = {
438
+ measuredAt: new Date().toISOString().slice(0, 10),
439
+ candidateSet: {
440
+ source: [
441
+ '(a) every OTHER member declared field name in the same BLOCK_FAMILIES family',
442
+ '(b) other members same-named array field arrayFields item keys',
443
+ '(c) snapshot of KEY_ROLE keys + ROLE_FIELDS values from packages/sdk/src/blocks.ts',
444
+ ],
445
+ commonNames: COMMON_CANDIDATES.length,
446
+ extra: EXTRA,
447
+ },
448
+ members: raw.members,
449
+ probed: probed.length,
450
+ couldNotProbe: rows.length - probed.length,
451
+ couldNotProbeBy: skippedBy,
452
+ membersWithUndeclaredProps: withFindings.length,
453
+ findings: findings.length,
454
+ findingsVisibleAsText: visible.length,
455
+ highTrafficAffected: [...new Set(withFindings.map((r) => r.block).filter((b) => HIGH_TRAFFIC.has(b)))].sort(),
456
+ results: rows,
457
+ }
458
+
459
+ out.mode = CONTROL ? 'control' : 'gap'
460
+ out.candidatesProbed = probed.reduce((n, r) => n + (r.candidates ?? 0), 0)
461
+ // Field-level three-state. The member-level buckets say nothing about an
462
+ // array field that was skipped inside an otherwise-probed member, and every
463
+ // finding so far has been an array ITEM key — so an unreported skip here is
464
+ // exactly where a missed case would hide.
465
+ out.arrayFieldsProbed = probed.reduce((n, r) => n + (r.arrayFieldsProbed ?? 0), 0)
466
+ out.arrayFieldsSkipped = {}
467
+ for (const r of probed) {
468
+ for (const [why, n] of Object.entries(r.arrayFieldsSkipped ?? {})) {
469
+ out.arrayFieldsSkipped[why] = (out.arrayFieldsSkipped[why] ?? 0) + n
470
+ }
471
+ }
472
+ if (CONTROL) {
473
+ out.controlDetectionRate = out.candidatesProbed
474
+ ? Number((findings.length / out.candidatesProbed).toFixed(3))
475
+ : 0
476
+ }
477
+
478
+ if (JSON_OUT) {
479
+ process.stdout.write(JSON.stringify(out, null, 2) + '\n')
480
+ return
481
+ }
482
+
483
+ if (CONTROL) {
484
+ const blind = probed
485
+ .filter((r) => (r.candidates ?? 0) > 0 && r.findings.length < r.candidates)
486
+ .sort((a, b) => (b.candidates - b.findings.length) - (a.candidates - a.findings.length))
487
+ const lines = []
488
+ lines.push('blocks-render-probe --control — can the probe see a DECLARED text field?')
489
+ lines.push('')
490
+ lines.push('probed members ' + probed.length)
491
+ lines.push('declared text fields ' + out.candidatesProbed)
492
+ lines.push('detected in the markup ' + findings.length + ' (rate ' + out.controlDetectionRate + ')')
493
+ lines.push('')
494
+ lines.push('members the probe is partly blind to (declared field never reached the markup):')
495
+ for (const r of blind.slice(0, 40)) {
496
+ const found = new Set(r.findings.map((f) => f.prop))
497
+ lines.push(' ' + r.key.padEnd(34) + (r.candidates - r.findings.length) + ' of ' + r.candidates + ' unseen')
498
+ }
499
+ if (blind.length > 40) lines.push(' … ' + (blind.length - 40) + ' more')
500
+ process.stdout.write(lines.join('\n') + '\n')
501
+ return
502
+ }
503
+
504
+ const lines = []
505
+ lines.push('blocks-render-probe — props a block RENDERS but never DECLARES')
506
+ lines.push('')
507
+ lines.push('members seen ' + raw.members)
508
+ lines.push('probed ' + probed.length)
509
+ lines.push('could not probe ' + out.couldNotProbe + ' ' + JSON.stringify(skippedBy))
510
+ lines.push('candidates probed ' + out.candidatesProbed)
511
+ lines.push('array fields probed ' + out.arrayFieldsProbed + ' skipped ' + JSON.stringify(out.arrayFieldsSkipped))
512
+ lines.push('members with a finding ' + withFindings.length + ' of ' + probed.length + ' probed')
513
+ lines.push('undeclared props found ' + findings.length + ' (' + visible.length + ' visible as text, rest attribute-only)')
514
+ lines.push('high-traffic families ' + (out.highTrafficAffected.join(' ') || 'none'))
515
+ lines.push('')
516
+ for (const r of withFindings.sort((a, b) => b.findings.length - a.findings.length)) {
517
+ const body = r.findings
518
+ .map((f) => f.prop + (f.visible ? '' : '~attr') + (f.declaredElsewhereInFamily ? '*' : ''))
519
+ .join(' ')
520
+ lines.push(r.key.padEnd(34) + body)
521
+ }
522
+ lines.push('')
523
+ lines.push('* = the prop IS declared by a sibling variant (a fields entry closes it);')
524
+ lines.push(' no star = absent from the published vocabulary entirely.')
525
+ lines.push('~attr = the sentinel reached an HTML attribute, not visible text.')
526
+ process.stdout.write(lines.join('\n') + '\n')
527
+ }
528
+
529
+ report(runProbe())