@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.
- package/agents/abm-strategist.md +89 -0
- package/agents/ads-meta.md +97 -0
- package/agents/analyst.md +173 -0
- package/agents/architect.md +280 -0
- package/agents/brand-guardian.md +88 -0
- package/agents/brand-strategist.md +92 -0
- package/agents/campaign-content.md +90 -0
- package/agents/campaign-email.md +88 -0
- package/agents/campaign-sms.md +87 -0
- package/agents/campaign-social.md +87 -0
- package/agents/cco.md +99 -0
- package/agents/ceo.md +106 -0
- package/agents/chairman.md +120 -0
- package/agents/cmo.md +101 -0
- package/agents/community-greeter.md +89 -0
- package/agents/community-moderator.md +92 -0
- package/agents/compliance.md +92 -0
- package/agents/copywriter.md +100 -0
- package/agents/creative-strategist.md +95 -0
- package/agents/cro.md +98 -0
- package/agents/cto.md +267 -0
- package/agents/customer-interviewer.md +93 -0
- package/agents/customer-researcher.md +89 -0
- package/agents/customer-success-manager.md +90 -0
- package/agents/customer-trainer.md +90 -0
- package/agents/cxo.md +100 -0
- package/agents/demand-creator.md +89 -0
- package/agents/demo-mover.md +83 -0
- package/agents/demo-specialist.md +89 -0
- package/agents/demo-thai-family-law.md +87 -0
- package/agents/designer.md +112 -0
- package/agents/discovery-caller.md +98 -0
- package/agents/doctor.md +139 -0
- package/agents/educate-coach.md +100 -0
- package/agents/elevate-tutor.md +83 -0
- package/agents/email-lifecycle-marketer.md +91 -0
- package/agents/engage-specialist.md +99 -0
- package/agents/events-coordinator.md +90 -0
- package/agents/foundation-builder.md +94 -0
- package/agents/funnel-architect.md +89 -0
- package/agents/gift-creator.md +100 -0
- package/agents/google-ads.md +103 -0
- package/agents/guide.md +292 -0
- package/agents/helpdesk-dispatcher.md +88 -0
- package/agents/hook-specialist.md +91 -0
- package/agents/identify-optimizer.md +101 -0
- package/agents/implementer.md +305 -0
- package/agents/incident-commander.md +120 -0
- package/agents/insights-lead.md +93 -0
- package/agents/journey-runner.md +113 -0
- package/agents/linkedin-ads.md +123 -0
- package/agents/live-sales-chat.md +90 -0
- package/agents/market-researcher.md +88 -0
- package/agents/media-buyer.md +96 -0
- package/agents/movers-customer-researcher.md +95 -0
- package/agents/movers-foundation-builder.md +96 -0
- package/agents/movers-market-researcher.md +97 -0
- package/agents/movers-pricing-strategist.md +171 -0
- package/agents/nurture-architect.md +99 -0
- package/agents/offer-architect.md +92 -0
- package/agents/onboarder.md +222 -0
- package/agents/onboarding-specialist.md +92 -0
- package/agents/operations-dashboard.md +98 -0
- package/agents/perf-engineer.md +348 -0
- package/agents/playbook-writer.md +71 -0
- package/agents/plg-strategist.md +91 -0
- package/agents/positioning-architect.md +88 -0
- package/agents/press-officer.md +89 -0
- package/agents/pricing-strategist.md +94 -0
- package/agents/privacy-officer.md +92 -0
- package/agents/referral-manager.md +91 -0
- package/agents/refine-analyst.md +102 -0
- package/agents/release-manager.md +261 -0
- package/agents/renewals-upsell-rep.md +90 -0
- package/agents/review-engineer.md +327 -0
- package/agents/rewards-steward.md +82 -0
- package/agents/sales-call-coach.md +94 -0
- package/agents/sales-closer.md +98 -0
- package/agents/security-auditor.md +313 -0
- package/agents/sell-closer.md +101 -0
- package/agents/share-amplifier.md +99 -0
- package/agents/social-media-manager.md +92 -0
- package/agents/storyteller.md +250 -0
- package/agents/strategist.md +83 -0
- package/agents/strategy-aligner.md +94 -0
- package/agents/support-agent.md +90 -0
- package/agents/tagger.md +245 -0
- package/agents/tech-writer.md +216 -0
- package/agents/test-engineer.md +243 -0
- package/agents/tiktok-ads.md +105 -0
- package/agents/tracking-engineer.md +92 -0
- package/agents/upsell-strategist.md +101 -0
- package/agents/voice.md +85 -0
- package/agents/w4-tools.md +153 -0
- package/agents/w4-verify.md +20 -138
- package/agents/workflow-optimiser.md +84 -0
- package/commands/close.md +814 -160
- package/commands/create.md +2 -2
- package/commands/deploy.md +554 -175
- package/commands/do.md +119 -109
- package/commands/fast.md +67 -0
- package/commands/improve.md +2 -2
- package/commands/one.md +418 -0
- package/commands/see.md +1 -1
- package/commands/sync.md +1 -1
- package/commands/tasks.md +222 -0
- package/commands/vespio.md +110 -0
- package/commands/vespio.remote.md +227 -0
- package/hooks/hooks.json +25 -79
- package/hooks/lib/governor-escape-match.sh +88 -0
- package/hooks/lib/hook.sh +4 -13
- package/hooks/lib/signal.sh +9 -2
- package/hooks/scripts/branch-pin.sh +6 -0
- package/hooks/scripts/config-protect.sh +6 -0
- package/hooks/scripts/do-outcome-gate.sh +19 -2
- package/hooks/scripts/git-add-guard.sh +43 -2
- package/hooks/scripts/governor-escape.sh +96 -0
- package/hooks/scripts/load-guard.sh +127 -0
- package/hooks/scripts/post-edit-check.sh +36 -29
- package/hooks/scripts/session-start.sh +34 -80
- package/hooks/scripts/task-complete-verify.sh +46 -40
- package/package.json +1 -1
- package/rules/documentation.md +9 -0
- package/scripts/ad-copy-lint.sh +656 -0
- package/scripts/agentverse-audit.sh +256 -0
- package/scripts/asi-walk.sh +435 -0
- package/scripts/astro-build-cached.sh +143 -0
- package/scripts/blocks-manifest-cached.sh +100 -0
- package/scripts/blocks-manifest.mjs +497 -0
- package/scripts/blocks-ratchet.sh +116 -0
- package/scripts/blocks-render-probe.mjs +529 -0
- package/scripts/blocks-usage.mjs +334 -0
- package/scripts/chat-context-check.sh +89 -0
- package/scripts/close-metrics.sh +558 -0
- package/scripts/close-owner.sh +268 -0
- package/scripts/db-sync-lock-check.sh +116 -0
- package/scripts/deploy-dev.sh +35 -0
- package/scripts/deploy-emit.sh +298 -0
- package/scripts/deploy-gate-check.sh +128 -0
- package/scripts/deploy-ready.sh +78 -0
- package/scripts/deploy-record.sh +605 -0
- package/scripts/deploy.sh +1273 -0
- package/scripts/do-auto.sh +598 -48
- package/scripts/do-brief.sh +113 -0
- package/scripts/do-close.sh +1137 -0
- package/scripts/do-commit.sh +75 -0
- package/scripts/do-consumer-sweep.sh +18 -1
- package/scripts/do-cycle-shape-check.sh +160 -0
- package/scripts/do-decide.sh +476 -0
- package/scripts/do-derive-check.sh +436 -0
- package/scripts/do-fleet.sh +106 -28
- package/scripts/do-folder.sh +10 -1
- package/scripts/do-next.sh +106 -0
- package/scripts/do-orchestrate.sh +17 -5
- package/scripts/do-plan-json.mjs +201 -0
- package/scripts/do-plan-json.sh +8 -0
- package/scripts/do-preflight.sh +117 -0
- package/scripts/do-project.sh +157 -0
- package/scripts/do-prove-selftest.sh +108 -0
- package/scripts/do-prove.sh +295 -23
- package/scripts/do-rank.py +31 -1
- package/scripts/do-recon-cache.sh +7 -1
- package/scripts/do-recon-pack.sh +196 -0
- package/scripts/do-reconcile.sh +121 -4
- package/scripts/do-signal.sh +280 -23
- package/scripts/do-smoke.sh +18 -1
- package/scripts/do-test-gate.sh +80 -0
- package/scripts/do-tick.sh +102 -0
- package/scripts/do-tier.sh +6 -0
- package/scripts/do-triage.sh +182 -0
- package/scripts/do-ui-gate.sh +1 -1
- package/scripts/do-w4-gates.sh +451 -0
- package/scripts/do-walk.sh +12 -1
- package/scripts/env-sync.sh +173 -0
- package/scripts/factory-brief-check.sh +330 -0
- package/scripts/factory-check.sh +68 -14
- package/scripts/factory-close-check.sh +257 -0
- package/scripts/factory-emit.sh +211 -0
- package/scripts/factory-executor-check.mjs +353 -0
- package/scripts/factory-peak.sh +301 -0
- package/scripts/factory-repo.sh +118 -3
- package/scripts/factory-review-check.mjs +61 -0
- package/scripts/factory-ship.sh +61 -0
- package/scripts/factory-tasks-check.sh +18 -1
- package/scripts/factory-turn.sh +326 -0
- package/scripts/factory-walk.sh +396 -0
- package/scripts/factory-width.sh +57 -0
- package/scripts/fade-toxic.sh +4 -3
- package/scripts/fixtures/factory-brief-real.md +44 -0
- package/scripts/fixtures/triage-dupe.md +5 -0
- package/scripts/fleet-manifest.mjs +108 -0
- package/scripts/fleet-status.sh +110 -0
- package/scripts/full-suite-paths-check.sh +144 -0
- package/scripts/gate-reaper-check.sh +98 -0
- package/scripts/gate-reaper.sh +125 -0
- package/scripts/gate-run.sh +73 -0
- package/scripts/gc-content-check.sh +140 -0
- package/scripts/gen-dev-config.py +20 -0
- package/scripts/govern-bound-check.sh +60 -0
- package/scripts/govern-claims-check.sh +233 -0
- package/scripts/govern-mem-check.sh +290 -0
- package/scripts/governor-doors-check.sh +362 -0
- package/scripts/governor-escape-check.sh +171 -0
- package/scripts/health.sh +413 -0
- package/scripts/id-inventory.mjs +418 -0
- package/scripts/land.sh +551 -0
- package/scripts/lib/gc-finished.sh +77 -0
- package/scripts/lib/govern.sh +361 -0
- package/scripts/lib/govern.ts +756 -0
- package/scripts/lighthouse-run.sh +187 -0
- package/scripts/livekit-live-check.sh +61 -0
- package/scripts/livekit-ratchet.sh +46 -0
- package/scripts/load-guard-check.sh +49 -0
- package/scripts/machine-check.sh +102 -0
- package/scripts/machine-watch.sh +177 -0
- package/scripts/one-agents.mjs +415 -0
- package/scripts/one-resume.sh +78 -0
- package/scripts/orphan-baseline.json +182 -0
- package/scripts/orphan-modules.mjs +179 -0
- package/scripts/pr-body.sh +335 -0
- package/scripts/preview-fd-check.sh +289 -0
- package/scripts/promise-manifest.mjs +24 -1
- package/scripts/release.sh +322 -0
- package/scripts/roles-check.sh +946 -0
- package/scripts/sdk-build-cached.sh +64 -0
- package/scripts/signal-watch.sh +241 -0
- package/scripts/skills-publish.sh +94 -0
- package/scripts/speed-cache-check.sh +173 -0
- package/scripts/speed-check.mjs +907 -0
- package/scripts/speed-parity-check.sh +648 -0
- package/scripts/speed-waterfall-check.sh +355 -0
- package/scripts/substrate-env-parity.mjs +156 -0
- package/scripts/tasks-claim-race.mjs +108 -0
- package/scripts/tasks-loop.sh +185 -0
- package/scripts/test-cached.sh +255 -0
- package/scripts/test-full.sh +87 -0
- package/scripts/test-honesty.mjs +137 -0
- package/scripts/test-lanes.sh +166 -0
- package/scripts/test-speed.sh +94 -0
- package/scripts/triage-shape-check.sh +149 -0
- package/scripts/tsc-cached.sh +179 -0
- package/scripts/typedb-flake-check.sh +197 -0
- package/scripts/urls-lint.sh +15 -0
- package/scripts/verify-fast.sh +445 -0
- package/scripts/vespio-sync.sh +149 -0
- package/scripts/wf-check.mjs +104 -1
- package/scripts/worktree-preview.sh +879 -0
- package/scripts/worktree-up.sh +208 -0
- package/skills/livekit-agents/SKILL.md +285 -0
- package/skills/livekit-agents/references/freshness-rules.md +168 -0
- package/skills/shadcn/SKILL.md +1 -1
- package/skills/signal/SKILL.md +0 -1
- package/skills/voice/SKILL.md +94 -6
- package/skills/voice/corpus-check.sh +87 -0
- package/hooks/scripts/compact-hint.sh +0 -35
- package/hooks/scripts/gate-guard.sh +0 -83
- package/hooks/scripts/read-tracker.sh +0 -26
- package/hooks/scripts/session-end-verify.sh +0 -51
- package/hooks/scripts/stop-reflect.sh +0 -140
- package/hooks/scripts/sync-priority-todo.sh +0 -57
- package/hooks/scripts/sync-todo-docs.sh +0 -46
- package/hooks/scripts/tool-signal.sh +0 -48
- 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()
|