@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,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())
|