base-themes 0.1.2 → 0.1.3
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/CHANGELOG.md +25 -0
- package/CODE_OF_CONDUCT.md +22 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +21 -0
- package/README.md +316 -3
- package/RELEASE.md +80 -0
- package/SECURITY.md +49 -0
- package/bin/base-themes.mjs +143 -0
- package/dist/base-themes.css +1 -1
- package/dist/base-themes.js +857 -302
- package/dist/llms-full.txt +288 -0
- package/dist/llms.txt +79 -0
- package/dist/types/blocks/AuthCard.d.ts +2 -0
- package/dist/types/blocks/CommandPaletteBlock.d.ts +2 -0
- package/dist/types/blocks/DashboardShell.d.ts +2 -0
- package/dist/types/blocks/DataTableBlock.d.ts +2 -0
- package/dist/types/blocks/PricingPanel.d.ts +2 -0
- package/dist/types/blocks/SettingsForm.d.ts +2 -0
- package/dist/types/blocks/TeamActivityFeed.d.ts +2 -0
- package/dist/types/blocks/ThemeShowcaseCard.d.ts +2 -0
- package/dist/types/blocks/index.d.ts +8 -0
- package/dist/types/components/ui/Input.d.ts +3 -0
- package/dist/types/components/ui/index.d.ts +1 -1
- package/dist/types/lib.d.ts +1 -0
- package/docs/adoption-dashboard.md +149 -0
- package/docs/analytics-setup.md +145 -0
- package/docs/community-gallery-proposal.md +64 -0
- package/docs/community-proof-telemetry.md +47 -0
- package/docs/contributor-issue-seeds.md +240 -0
- package/docs/registry-access-telemetry.md +87 -0
- package/docs/release-announcement-kit.md +229 -0
- package/docs/search-console-setup.md +111 -0
- package/docs/theme-token-contract.md +113 -0
- package/examples/dashboard/README.md +24 -0
- package/examples/dashboard/index.html +12 -0
- package/examples/dashboard/package-lock.json +1212 -0
- package/examples/dashboard/package.json +24 -0
- package/examples/dashboard/src/App.tsx +115 -0
- package/examples/dashboard/src/main.tsx +11 -0
- package/examples/dashboard/src/styles.css +129 -0
- package/examples/dashboard/src/vite-env.d.ts +4 -0
- package/examples/dashboard/tsconfig.app.json +23 -0
- package/examples/dashboard/tsconfig.json +7 -0
- package/examples/dashboard/tsconfig.node.json +15 -0
- package/examples/dashboard/vite.config.ts +6 -0
- package/examples/next/README.md +29 -0
- package/examples/next/app/base-themes-demo.tsx +70 -0
- package/examples/next/app/layout.tsx +16 -0
- package/examples/next/app/page.tsx +9 -0
- package/examples/next/app/styles.css +106 -0
- package/examples/next/next-env.d.ts +6 -0
- package/examples/next/next.config.ts +5 -0
- package/examples/next/package-lock.json +1199 -0
- package/examples/next/package.json +27 -0
- package/examples/next/tsconfig.json +36 -0
- package/examples/registry-copy/README.md +73 -0
- package/examples/registry-copy/plan-copy.mjs +130 -0
- package/examples/theme-customization/README.md +26 -0
- package/examples/theme-customization/index.html +12 -0
- package/examples/theme-customization/package-lock.json +1212 -0
- package/examples/theme-customization/package.json +24 -0
- package/examples/theme-customization/src/App.tsx +138 -0
- package/examples/theme-customization/src/main.tsx +11 -0
- package/examples/theme-customization/src/styles.css +138 -0
- package/examples/theme-customization/src/vite-env.d.ts +4 -0
- package/examples/theme-customization/tsconfig.app.json +23 -0
- package/examples/theme-customization/tsconfig.json +7 -0
- package/examples/theme-customization/tsconfig.node.json +15 -0
- package/examples/theme-customization/vite.config.ts +6 -0
- package/examples/vite/README.md +32 -0
- package/examples/vite/index.html +12 -0
- package/examples/vite/package-lock.json +1200 -0
- package/examples/vite/package.json +24 -0
- package/examples/vite/src/App.tsx +101 -0
- package/examples/vite/src/main.tsx +11 -0
- package/examples/vite/src/styles.css +125 -0
- package/examples/vite/src/vite-env.d.ts +4 -0
- package/examples/vite/tsconfig.app.json +23 -0
- package/examples/vite/tsconfig.json +7 -0
- package/examples/vite/tsconfig.node.json +15 -0
- package/examples/vite/vite.config.ts +6 -0
- package/llms-full.txt +288 -0
- package/llms.txt +79 -0
- package/package.json +157 -14
- package/registry/items/accordion.json +101 -0
- package/registry/items/alert-dialog.json +107 -0
- package/registry/items/autocomplete.json +106 -0
- package/registry/items/avatar.json +101 -0
- package/registry/items/block-auth-card.json +105 -0
- package/registry/items/block-command-palette.json +99 -0
- package/registry/items/block-dashboard-shell.json +101 -0
- package/registry/items/block-data-table.json +99 -0
- package/registry/items/block-pricing-panel.json +99 -0
- package/registry/items/block-settings-form.json +107 -0
- package/registry/items/block-team-activity-feed.json +99 -0
- package/registry/items/block-theme-showcase-card.json +99 -0
- package/registry/items/button.json +102 -0
- package/registry/items/checkbox-group.json +106 -0
- package/registry/items/checkbox.json +102 -0
- package/registry/items/collapsible.json +101 -0
- package/registry/items/combobox.json +101 -0
- package/registry/items/context-menu.json +106 -0
- package/registry/items/csp-provider.json +96 -0
- package/registry/items/dialog.json +102 -0
- package/registry/items/direction-provider.json +101 -0
- package/registry/items/drawer.json +101 -0
- package/registry/items/field.json +101 -0
- package/registry/items/fieldset.json +101 -0
- package/registry/items/form.json +101 -0
- package/registry/items/input.json +102 -0
- package/registry/items/menu.json +101 -0
- package/registry/items/menubar.json +106 -0
- package/registry/items/meter.json +101 -0
- package/registry/items/navigation-menu.json +101 -0
- package/registry/items/number-field.json +101 -0
- package/registry/items/otp-field.json +101 -0
- package/registry/items/popover.json +102 -0
- package/registry/items/preview-card.json +101 -0
- package/registry/items/progress.json +101 -0
- package/registry/items/radio-group.json +102 -0
- package/registry/items/radio.json +101 -0
- package/registry/items/scroll-area.json +101 -0
- package/registry/items/select.json +102 -0
- package/registry/items/separator.json +101 -0
- package/registry/items/slider.json +102 -0
- package/registry/items/switch.json +102 -0
- package/registry/items/tabs.json +101 -0
- package/registry/items/theme-bauhaus.json +107 -0
- package/registry/items/theme-bento.json +107 -0
- package/registry/items/theme-calm.json +107 -0
- package/registry/items/theme-cyberpunk.json +108 -0
- package/registry/items/theme-data-dense.json +107 -0
- package/registry/items/theme-editorial.json +107 -0
- package/registry/items/theme-enterprise.json +108 -0
- package/registry/items/theme-fluent.json +107 -0
- package/registry/items/theme-glass.json +107 -0
- package/registry/items/theme-linear.json +107 -0
- package/registry/items/theme-luxury.json +107 -0
- package/registry/items/theme-material.json +107 -0
- package/registry/items/theme-minimal.json +107 -0
- package/registry/items/theme-mono.json +107 -0
- package/registry/items/theme-neo-brutalism.json +107 -0
- package/registry/items/theme-playful.json +107 -0
- package/registry/items/theme-retro.json +107 -0
- package/registry/items/theme-shadcn.json +107 -0
- package/registry/items/theme-soft-ui.json +107 -0
- package/registry/items/theme-terminal.json +107 -0
- package/registry/items/toast.json +106 -0
- package/registry/items/toggle-group.json +101 -0
- package/registry/items/toggle.json +101 -0
- package/registry/items/toolbar.json +101 -0
- package/registry/items/tooltip.json +102 -0
- package/registry/registry.json +564 -49
- package/registry/shadcn-registry.json +415 -0
- package/research/telemetry-fixtures/analytics-events.jsonl +9 -0
- package/research/telemetry-fixtures/bundle-report.json +44 -0
- package/research/telemetry-fixtures/community-proof.csv +5 -0
- package/research/telemetry-fixtures/registry-access.jsonl +10 -0
- package/research/telemetry-fixtures/search-console-export.csv +4 -0
- package/scripts/registry-plan.mjs +434 -0
- package/scripts/render-launch-actions.mjs +405 -0
- package/scripts/render-launch-status.mjs +373 -0
- package/scripts/render-release-announcement.mjs +329 -0
- package/scripts/verify-launch-readiness.mjs +415 -0
- package/scripts/verify-telemetry-fixtures.mjs +85 -0
- package/scripts/verify-telemetry-report.mjs +89 -0
- package/skills/base-themes/SKILL.md +151 -47
- package/src/blocks/AuthCard.tsx +29 -0
- package/src/blocks/Blocks.css +182 -0
- package/src/blocks/CommandPaletteBlock.tsx +32 -0
- package/src/blocks/DashboardShell.tsx +36 -0
- package/src/blocks/DataTableBlock.tsx +44 -0
- package/src/blocks/PricingPanel.tsx +28 -0
- package/src/blocks/SettingsForm.tsx +37 -0
- package/src/blocks/TeamActivityFeed.tsx +38 -0
- package/src/blocks/ThemeShowcaseCard.tsx +32 -0
- package/src/blocks/index.ts +8 -0
- package/src/components/ui/Accordion.css +42 -0
- package/src/components/ui/Accordion.tsx +41 -0
- package/src/components/ui/AlertDialog.css +40 -0
- package/src/components/ui/AlertDialog.tsx +52 -0
- package/src/components/ui/Autocomplete.css +3 -0
- package/src/components/ui/Autocomplete.tsx +50 -0
- package/src/components/ui/Avatar.css +45 -0
- package/src/components/ui/Avatar.tsx +36 -0
- package/src/components/ui/Button.css +79 -0
- package/src/components/ui/Button.tsx +20 -0
- package/src/components/ui/Checkbox.css +37 -0
- package/src/components/ui/Checkbox.tsx +32 -0
- package/src/components/ui/CheckboxGroup.tsx +21 -0
- package/src/components/ui/Collapsible.css +34 -0
- package/src/components/ui/Collapsible.tsx +29 -0
- package/src/components/ui/Combobox.css +75 -0
- package/src/components/ui/Combobox.tsx +53 -0
- package/src/components/ui/ContextMenu.css +9 -0
- package/src/components/ui/ContextMenu.tsx +47 -0
- package/src/components/ui/CspProvider.tsx +10 -0
- package/src/components/ui/Dialog.css +41 -0
- package/src/components/ui/Dialog.tsx +45 -0
- package/src/components/ui/DirectionProvider.tsx +17 -0
- package/src/components/ui/Drawer.css +77 -0
- package/src/components/ui/Drawer.tsx +56 -0
- package/src/components/ui/Field.css +19 -0
- package/src/components/ui/Field.tsx +24 -0
- package/src/components/ui/Fieldset.css +16 -0
- package/src/components/ui/Fieldset.tsx +19 -0
- package/src/components/ui/Form.css +5 -0
- package/src/components/ui/Form.tsx +12 -0
- package/src/components/ui/Input.css +50 -0
- package/src/components/ui/Input.tsx +62 -0
- package/src/components/ui/Menu.css +59 -0
- package/src/components/ui/Menu.tsx +50 -0
- package/src/components/ui/Menubar.css +26 -0
- package/src/components/ui/Menubar.tsx +42 -0
- package/src/components/ui/Meter.css +45 -0
- package/src/components/ui/Meter.tsx +37 -0
- package/src/components/ui/NavigationMenu.css +103 -0
- package/src/components/ui/NavigationMenu.tsx +64 -0
- package/src/components/ui/NumberField.css +38 -0
- package/src/components/ui/NumberField.tsx +28 -0
- package/src/components/ui/OtpField.css +28 -0
- package/src/components/ui/OtpField.tsx +24 -0
- package/src/components/ui/Popover.css +25 -0
- package/src/components/ui/Popover.tsx +37 -0
- package/src/components/ui/PreviewCard.css +33 -0
- package/src/components/ui/PreviewCard.tsx +43 -0
- package/src/components/ui/Progress.css +33 -0
- package/src/components/ui/Progress.tsx +28 -0
- package/src/components/ui/Radio.tsx +22 -0
- package/src/components/ui/RadioGroup.css +42 -0
- package/src/components/ui/RadioGroup.tsx +29 -0
- package/src/components/ui/ScrollArea.css +42 -0
- package/src/components/ui/ScrollArea.tsx +22 -0
- package/src/components/ui/Select.css +86 -0
- package/src/components/ui/Select.tsx +39 -0
- package/src/components/ui/Separator.css +14 -0
- package/src/components/ui/Separator.tsx +12 -0
- package/src/components/ui/Slider.css +39 -0
- package/src/components/ui/Slider.tsx +21 -0
- package/src/components/ui/Switch.css +45 -0
- package/src/components/ui/Switch.tsx +29 -0
- package/src/components/ui/Tabs.css +72 -0
- package/src/components/ui/Tabs.tsx +44 -0
- package/src/components/ui/Toast.css +75 -0
- package/src/components/ui/Toast.tsx +48 -0
- package/src/components/ui/Toggle.tsx +12 -0
- package/src/components/ui/ToggleGroup.css +35 -0
- package/src/components/ui/ToggleGroup.tsx +30 -0
- package/src/components/ui/Toolbar.css +60 -0
- package/src/components/ui/Toolbar.tsx +36 -0
- package/src/components/ui/Tooltip.css +14 -0
- package/src/components/ui/Tooltip.tsx +31 -0
- package/src/components/ui/index.ts +83 -0
- package/src/components/ui/useDirection.ts +1 -0
- package/src/components/ui/useToastManager.ts +11 -0
- package/src/docs/blockMeta.json +66 -0
- package/src/docs/componentMeta.json +322 -0
- package/src/docs/staticPageMeta.json +143 -0
- package/src/docs/themeMeta.json +22 -0
- package/src/styles/tokenContract.json +61 -0
- package/workers/analytics-receiver.mjs +170 -0
- package/wrangler.analytics.jsonc +12 -0
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import packageJson from '../package.json' with { type: 'json' }
|
|
4
|
+
|
|
5
|
+
const requiredScripts = [
|
|
6
|
+
'registry:check',
|
|
7
|
+
'tokens:check',
|
|
8
|
+
'seo:check',
|
|
9
|
+
'analytics:check',
|
|
10
|
+
'community:check',
|
|
11
|
+
'community:issues',
|
|
12
|
+
'release:announce',
|
|
13
|
+
'telemetry:collect',
|
|
14
|
+
'telemetry:check',
|
|
15
|
+
'telemetry:fixtures',
|
|
16
|
+
'package:smoke',
|
|
17
|
+
'bundle:report',
|
|
18
|
+
'launch:status',
|
|
19
|
+
'launch:actions',
|
|
20
|
+
'launch:campaign',
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const requiredReleaseLinks = {
|
|
24
|
+
repo: 'https://github.com/markbang/base-themes',
|
|
25
|
+
docs: 'https://base-themes.bangwu.me',
|
|
26
|
+
registry: 'https://base-themes.bangwu.me/registry/registry.json',
|
|
27
|
+
cli: 'https://base-themes.bangwu.me/docs/cli',
|
|
28
|
+
blocks: 'https://base-themes.bangwu.me/blocks',
|
|
29
|
+
llms: 'https://base-themes.bangwu.me/llms.txt',
|
|
30
|
+
llmsFull: 'https://base-themes.bangwu.me/llms-full.txt',
|
|
31
|
+
fork: 'https://github.com/markbang/base-themes/fork',
|
|
32
|
+
showAndTell: 'https://github.com/markbang/base-themes/discussions/new?category=show-and-tell',
|
|
33
|
+
featureRequest: 'https://github.com/markbang/base-themes/issues/new?template=feature_request.yml',
|
|
34
|
+
gallerySubmission: 'https://github.com/markbang/base-themes/issues/new?template=gallery_submission.yml',
|
|
35
|
+
goodFirstIssues: 'https://github.com/markbang/base-themes/issues?q=is%3Aissue+state%3Aopen+label%3A%22type%3A+good+first+issue%22',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function fail(message, details = []) {
|
|
39
|
+
console.error(`Launch readiness invalid: ${message}`)
|
|
40
|
+
for (const detail of details) console.error(`- ${detail}`)
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const missingScripts = requiredScripts.filter((script) => !packageJson.scripts?.[script])
|
|
45
|
+
if (missingScripts.length > 0) {
|
|
46
|
+
fail('package.json is missing required release/adoption scripts', missingScripts)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const releaseChecklist = readFileSync('RELEASE.md', 'utf8')
|
|
50
|
+
const releaseKit = readFileSync('docs/release-announcement-kit.md', 'utf8')
|
|
51
|
+
const adoptionDashboard = readFileSync('docs/adoption-dashboard.md', 'utf8')
|
|
52
|
+
|
|
53
|
+
const requiredReleaseChecklistPhrases = [
|
|
54
|
+
'npm run launch:check',
|
|
55
|
+
'npm run release:announce -- --json',
|
|
56
|
+
'npm run community:issues -- --json',
|
|
57
|
+
'npm run telemetry:collect',
|
|
58
|
+
'npm run telemetry:check',
|
|
59
|
+
'npm run telemetry:check -- --live',
|
|
60
|
+
'npm run telemetry:fixtures',
|
|
61
|
+
'npm run launch:status',
|
|
62
|
+
'npm run launch:status -- --live',
|
|
63
|
+
'npm run launch:actions',
|
|
64
|
+
'npm run launch:actions -- --live',
|
|
65
|
+
'npm run launch:campaign',
|
|
66
|
+
'public adoption gate',
|
|
67
|
+
]
|
|
68
|
+
const missingReleaseChecklistPhrases = requiredReleaseChecklistPhrases.filter((phrase) => !releaseChecklist.includes(phrase))
|
|
69
|
+
if (missingReleaseChecklistPhrases.length > 0) {
|
|
70
|
+
fail('RELEASE.md is missing launch/adoption checklist coverage', missingReleaseChecklistPhrases)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const requiredReleaseKitPhrases = [
|
|
74
|
+
'npm run launch:check',
|
|
75
|
+
'npm run launch:actions',
|
|
76
|
+
'npm run launch:campaign',
|
|
77
|
+
'at least three external channels',
|
|
78
|
+
'star the repo',
|
|
79
|
+
'fork it',
|
|
80
|
+
'Show and tell Discussion',
|
|
81
|
+
'community gallery issue template',
|
|
82
|
+
'24 hours, 7 days, and 30 days',
|
|
83
|
+
'T+1 day',
|
|
84
|
+
'T+7 days',
|
|
85
|
+
'T+30 days',
|
|
86
|
+
'shareAssets',
|
|
87
|
+
'signalTrends',
|
|
88
|
+
'refuses to overwrite',
|
|
89
|
+
'--force',
|
|
90
|
+
'refuses to save a campaign pack when telemetry is incomplete',
|
|
91
|
+
'--allow-incomplete',
|
|
92
|
+
]
|
|
93
|
+
const missingReleaseKitPhrases = requiredReleaseKitPhrases.filter((phrase) => !releaseKit.includes(phrase))
|
|
94
|
+
if (missingReleaseKitPhrases.length > 0) {
|
|
95
|
+
fail('release announcement kit is missing launch/adoption guidance', missingReleaseKitPhrases)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const requiredForkWorkflowPhrases = [
|
|
99
|
+
'Fork-To-First-Change',
|
|
100
|
+
'Fork-to-first-change',
|
|
101
|
+
'npm run example:theme-customization:build',
|
|
102
|
+
'npm run example:registry-copy -- plan button select block:dashboard-shell theme:enterprise --json',
|
|
103
|
+
]
|
|
104
|
+
const readme = readFileSync('README.md', 'utf8')
|
|
105
|
+
const staticDocs = readFileSync('src/docs/StaticDocsPages.tsx', 'utf8')
|
|
106
|
+
const missingForkWorkflowPhrases = requiredForkWorkflowPhrases.filter((phrase) => ![readme, staticDocs, releaseKit].some((source) => source.includes(phrase)))
|
|
107
|
+
if (missingForkWorkflowPhrases.length > 0) {
|
|
108
|
+
fail('fork-to-first-change workflow must be documented before asking users for forks', missingForkWorkflowPhrases)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requiredGatePhrases = [
|
|
112
|
+
'Current public adoption score: **1/4**',
|
|
113
|
+
'at least three of the four public signals pass',
|
|
114
|
+
'Do not mark the strategy complete while the adoption score is below `3/4`',
|
|
115
|
+
'generated under `research/`',
|
|
116
|
+
'intentionally not required for CI or release-readiness checks',
|
|
117
|
+
'record fields for external post URLs plus T+1, T+7, and T+30 telemetry evidence',
|
|
118
|
+
]
|
|
119
|
+
const missingGatePhrases = requiredGatePhrases.filter((phrase) => !adoptionDashboard.includes(phrase))
|
|
120
|
+
if (missingGatePhrases.length > 0) {
|
|
121
|
+
fail('adoption dashboard is missing explicit non-completion gate language', missingGatePhrases)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const requiredDashboardReleaseChecks = [
|
|
125
|
+
'npm run registry:check',
|
|
126
|
+
'npm run lint',
|
|
127
|
+
'npm run test',
|
|
128
|
+
'npm run build',
|
|
129
|
+
'npm run seo:check',
|
|
130
|
+
'npm run bundle:report',
|
|
131
|
+
'npm run analytics:check',
|
|
132
|
+
'npm run community:check',
|
|
133
|
+
'npm run telemetry:check',
|
|
134
|
+
'npm run telemetry:check -- --live',
|
|
135
|
+
'npm run telemetry:fixtures',
|
|
136
|
+
'npm run launch:check',
|
|
137
|
+
'npm run launch:status',
|
|
138
|
+
'npm run launch:status -- --live',
|
|
139
|
+
'npm run launch:actions',
|
|
140
|
+
'npm run launch:actions -- --live',
|
|
141
|
+
'npm run launch:campaign',
|
|
142
|
+
'npm run package:smoke',
|
|
143
|
+
'npm pack --dry-run',
|
|
144
|
+
]
|
|
145
|
+
const missingDashboardReleaseChecks = requiredDashboardReleaseChecks.filter((check) => !adoptionDashboard.includes(check))
|
|
146
|
+
if (missingDashboardReleaseChecks.length > 0) {
|
|
147
|
+
fail('adoption dashboard release-readiness commands are not aligned with the current launch gate', missingDashboardReleaseChecks)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let announcement
|
|
151
|
+
try {
|
|
152
|
+
announcement = JSON.parse(execFileSync('node', ['scripts/render-release-announcement.mjs', '--json'], { encoding: 'utf8' }))
|
|
153
|
+
} catch (error) {
|
|
154
|
+
fail('release announcement must render valid JSON', [error.message])
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let launchStatus
|
|
158
|
+
try {
|
|
159
|
+
launchStatus = JSON.parse(execFileSync('node', ['scripts/render-launch-status.mjs', '--json'], { encoding: 'utf8' }))
|
|
160
|
+
} catch (error) {
|
|
161
|
+
fail('launch status must render valid JSON from the latest telemetry report', [error.message])
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let launchActions
|
|
165
|
+
try {
|
|
166
|
+
launchActions = JSON.parse(execFileSync('node', ['scripts/render-launch-actions.mjs', '--json'], { encoding: 'utf8' }))
|
|
167
|
+
} catch (error) {
|
|
168
|
+
fail('launch actions must render valid JSON from the latest telemetry and announcement data', [error.message])
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (launchStatus.completionThreshold !== 3 || launchStatus.signalCount !== 4) {
|
|
172
|
+
fail('launch status must preserve the public adoption gate shape', [`got ${launchStatus.completionThreshold}/${launchStatus.signalCount}`])
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof launchStatus.externallyValidated !== 'boolean' || typeof launchStatus.publicTelemetryComplete !== 'boolean' || !Array.isArray(launchStatus.telemetryErrors) || !Array.isArray(launchStatus.missingSignals) || !Array.isArray(launchStatus.signalTrends)) {
|
|
176
|
+
fail('launch status JSON must include externallyValidated, publicTelemetryComplete, telemetryErrors, missingSignals, and signalTrends fields')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (launchActions.completionThreshold !== 3 || launchActions.signalCount !== 4 || typeof launchActions.publicTelemetryComplete !== 'boolean' || !Array.isArray(launchActions.telemetryErrors) || !Array.isArray(launchActions.actions) || !Array.isArray(launchActions.signalTrends)) {
|
|
180
|
+
fail('launch actions JSON must preserve the public adoption gate and expose telemetry completeness, signal trends, plus an action list')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!Array.isArray(launchActions.shareAssets) || launchActions.shareAssets.length < 4) {
|
|
184
|
+
fail('launch actions JSON must include share assets from the release announcement pack')
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (!Array.isArray(launchActions.channelChecklist) || launchActions.channelChecklist.length < 4) {
|
|
188
|
+
fail('launch actions JSON must include the release channel checklist')
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!Array.isArray(launchActions.promotionWave) || launchActions.promotionWave.length < 4) {
|
|
192
|
+
fail('launch actions JSON must include a channel promotion wave')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!Array.isArray(launchActions.campaignChecklist) || launchActions.campaignChecklist.length < 7) {
|
|
196
|
+
fail('launch actions JSON must include a campaign execution checklist')
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const launchActionShareAssetIds = new Set(launchActions.shareAssets.map((asset) => asset.id))
|
|
200
|
+
const launchActionChannelsMissingShareAssets = launchActions.channelChecklist.filter((item) => !Array.isArray(item.shareAssetIds) || item.shareAssetIds.length === 0)
|
|
201
|
+
if (launchActionChannelsMissingShareAssets.length > 0) {
|
|
202
|
+
fail('launch action channel checklist entries must include shareAssetIds', launchActionChannelsMissingShareAssets.map((item) => item.channel ?? '(unknown channel)'))
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const incompletePromotionWave = launchActions.promotionWave.filter((item) => !item.channel || !item.copy || !item.primaryLink || !item.action || !item.measure || !Array.isArray(item.targetSignals) || item.targetSignals.length === 0 || !Array.isArray(item.shareAssets) || item.shareAssets.length === 0)
|
|
206
|
+
if (incompletePromotionWave.length > 0) {
|
|
207
|
+
fail('launch action promotion wave entries must include copy, target signals, primary links, share assets, actions, and measures', incompletePromotionWave.map((item) => item.channel ?? '(unknown channel)'))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const unknownLaunchActionShareAssetIds = launchActions.channelChecklist.flatMap((item) => item.shareAssetIds ?? []).filter((id) => !launchActionShareAssetIds.has(id))
|
|
211
|
+
if (unknownLaunchActionShareAssetIds.length > 0) {
|
|
212
|
+
fail('launch action channel checklist references unknown share asset ids', unknownLaunchActionShareAssetIds)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const unknownPromotionWaveShareAssetIds = launchActions.promotionWave.flatMap((item) => item.shareAssetIds ?? []).filter((id) => !launchActionShareAssetIds.has(id))
|
|
216
|
+
if (unknownPromotionWaveShareAssetIds.length > 0) {
|
|
217
|
+
fail('launch action promotion wave references unknown share asset ids', unknownPromotionWaveShareAssetIds)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const incompleteCampaignChecklist = launchActions.campaignChecklist.filter((item) => !item.phase || !item.task || !item.evidence || !Array.isArray(item.recordFields) || item.recordFields.length === 0)
|
|
221
|
+
if (incompleteCampaignChecklist.length > 0) {
|
|
222
|
+
fail('launch action campaign checklist entries must include phase, task, evidence, and record fields', incompleteCampaignChecklist.map((item) => item.phase ?? '(unknown phase)'))
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const missingCampaignChannels = launchActions.promotionWave.map((item) => item.channel).filter((channel) => !launchActions.campaignChecklist.some((item) => item.channel === channel))
|
|
226
|
+
if (missingCampaignChannels.length > 0) {
|
|
227
|
+
fail('launch action campaign checklist must include every promotion wave channel', missingCampaignChannels)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const campaignCommandText = launchActions.campaignChecklist.flatMap((item) => item.commands ?? []).join('\n')
|
|
231
|
+
const missingCampaignCommands = ['npm run launch:check', 'npm run launch:status -- --live', 'npm run telemetry:collect', 'npm run launch:actions'].filter((command) => !campaignCommandText.includes(command))
|
|
232
|
+
if (missingCampaignCommands.length > 0) {
|
|
233
|
+
fail('launch action campaign checklist is missing required launch and measurement commands', missingCampaignCommands)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const missingLaunchActionIds = launchStatus.missingSignals
|
|
237
|
+
.map((signal) => signal.id)
|
|
238
|
+
.filter((id) => !launchActions.actions.some((action) => action.signalId === id))
|
|
239
|
+
if (missingLaunchActionIds.length > 0) {
|
|
240
|
+
fail('launch actions must include one actionable block for each missing public signal', missingLaunchActionIds)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const forkAction = launchActions.actions.find((action) => action.signalId === 'forks')
|
|
244
|
+
if (forkAction && !forkAction.commands?.includes('npm run example:theme-customization:build')) {
|
|
245
|
+
fail('launch actions must route the missing fork signal to the Fork-to-first-change workflow')
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const missingAnnouncementLinks = Object.entries(requiredReleaseLinks)
|
|
249
|
+
.filter(([key, value]) => announcement.links?.[key] !== value)
|
|
250
|
+
.map(([key]) => key)
|
|
251
|
+
if (missingAnnouncementLinks.length > 0) {
|
|
252
|
+
fail('release announcement JSON is missing direct launch/adoption links', missingAnnouncementLinks)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const announcementText = [
|
|
256
|
+
announcement.githubRelease,
|
|
257
|
+
announcement.social,
|
|
258
|
+
announcement.forum,
|
|
259
|
+
announcement.directory,
|
|
260
|
+
...(announcement.callsToAction ?? []),
|
|
261
|
+
].join('\n')
|
|
262
|
+
const missingAnnouncementText = Object.values(requiredReleaseLinks).filter((link) => !announcementText.includes(link))
|
|
263
|
+
if (missingAnnouncementText.length > 0) {
|
|
264
|
+
fail('release announcement copy is missing direct launch/adoption URLs', missingAnnouncementText)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const requiredAnnouncementWorkflowText = [
|
|
268
|
+
'Fork-to-first-change',
|
|
269
|
+
'npm run example:theme-customization:build',
|
|
270
|
+
'npm run example:registry-copy -- plan button select block:dashboard-shell theme:enterprise --json',
|
|
271
|
+
'Good-first issues to publish with this release',
|
|
272
|
+
]
|
|
273
|
+
const missingAnnouncementWorkflowText = requiredAnnouncementWorkflowText.filter((text) => !announcementText.includes(text))
|
|
274
|
+
if (missingAnnouncementWorkflowText.length > 0) {
|
|
275
|
+
fail('release announcement copy must turn fork asks into a verifiable first-change workflow', missingAnnouncementWorkflowText)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const requiredCommands = [
|
|
279
|
+
'npm install base-themes @base-ui/react react react-dom',
|
|
280
|
+
'npx base-themes plan button select block:dashboard-shell theme:enterprise --json',
|
|
281
|
+
'npx base-themes add button select block:dashboard-shell theme:enterprise --target . --dry-run --json',
|
|
282
|
+
'npx base-themes doctor . --json',
|
|
283
|
+
]
|
|
284
|
+
const missingCommands = requiredCommands.filter((command) => !announcement.commands?.includes(command))
|
|
285
|
+
if (missingCommands.length > 0) {
|
|
286
|
+
fail('release announcement commands are missing install, source-copy, or doctor actions', missingCommands)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const requiredChannels = ['GitHub Release', 'X / Bluesky', 'Hacker News / Reddit', 'Product / devtool directories']
|
|
290
|
+
const missingChannels = requiredChannels.filter((channel) => !announcement.channelChecklist?.some((item) => item.channel === channel))
|
|
291
|
+
if (missingChannels.length > 0) {
|
|
292
|
+
fail('release announcement JSON is missing channel tracking checklist entries', missingChannels)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const incompleteChannels = announcement.channelChecklist.filter((item) => !item.action || !item.measure || !item.primaryLink)
|
|
296
|
+
if (incompleteChannels.length > 0) {
|
|
297
|
+
fail('release channel checklist entries must include action, measure, and primaryLink', incompleteChannels.map((item) => item.channel ?? '(unknown channel)'))
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const channelsMissingShareAssets = announcement.channelChecklist.filter((item) => !Array.isArray(item.shareAssetIds) || item.shareAssetIds.length === 0)
|
|
301
|
+
if (channelsMissingShareAssets.length > 0) {
|
|
302
|
+
fail('release channel checklist entries must include shareAssetIds for execution-ready promotion', channelsMissingShareAssets.map((item) => item.channel ?? '(unknown channel)'))
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const unattributedChannels = announcement.channelChecklist.filter((item) => !['utm_campaign', 'utm_source', 'utm_medium', 'utm_content'].every((param) => item.primaryLink.includes(`${param}=`)))
|
|
306
|
+
if (unattributedChannels.length > 0) {
|
|
307
|
+
fail('release channel checklist primary links must include launch attribution parameters', unattributedChannels.map((item) => item.channel ?? '(unknown channel)'))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const channelCopyByName = {
|
|
311
|
+
'GitHub Release': announcement.githubRelease,
|
|
312
|
+
'X / Bluesky': announcement.social,
|
|
313
|
+
'Hacker News / Reddit': announcement.forum,
|
|
314
|
+
'Product / devtool directories': announcement.directory,
|
|
315
|
+
}
|
|
316
|
+
const channelsMissingAttributedCopy = announcement.channelChecklist.filter((item) => !channelCopyByName[item.channel]?.includes(item.primaryLink))
|
|
317
|
+
if (channelsMissingAttributedCopy.length > 0) {
|
|
318
|
+
fail('release channel copy must include its attributed primaryLink, not only the checklist', channelsMissingAttributedCopy.map((item) => item.channel ?? '(unknown channel)'))
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const channelsMissingStarAsk = announcement.channelChecklist.filter((item) => !/star (the )?repo/i.test(channelCopyByName[item.channel] ?? ''))
|
|
322
|
+
if (channelsMissingStarAsk.length > 0) {
|
|
323
|
+
fail('release channel copy must ask for a repo star after trying the package', channelsMissingStarAsk.map((item) => item.channel ?? '(unknown channel)'))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (!announcement.attribution?.campaign || !announcement.attribution?.parameters?.includes('utm_campaign')) {
|
|
327
|
+
fail('release announcement JSON must expose launch attribution metadata')
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!Array.isArray(announcement.shareAssets) || announcement.shareAssets.length < 4) {
|
|
331
|
+
fail('release announcement JSON must include share assets for screenshots, block routes, and docs routes')
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const invalidShareAssets = announcement.shareAssets.filter((asset) => !asset.id || !asset.title || !asset.type || !asset.url?.includes('utm_campaign=') || !asset.imageUrl?.startsWith('https://base-themes.bangwu.me/previews/') || !asset.use)
|
|
335
|
+
if (invalidShareAssets.length > 0) {
|
|
336
|
+
fail('release share assets must include id, type, attributed URL, preview image URL, and usage guidance', invalidShareAssets.map((asset) => asset.id ?? asset.title ?? '(untitled)'))
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const requiredShareAssetIds = ['dashboard-shell-block', 'enterprise-theme-preview', 'base-ui-vs-shadcn', 'cli-doctor-workflow']
|
|
340
|
+
const missingShareAssets = requiredShareAssetIds.filter((id) => !announcement.shareAssets.some((asset) => asset.id === id))
|
|
341
|
+
if (missingShareAssets.length > 0) {
|
|
342
|
+
fail('release share assets must cover block, theme, forum, and directory launch surfaces', missingShareAssets)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const shareAssetIds = new Set(announcement.shareAssets.map((asset) => asset.id))
|
|
346
|
+
const unknownChecklistShareAssets = announcement.channelChecklist.flatMap((item) => item.shareAssetIds ?? []).filter((id) => !shareAssetIds.has(id))
|
|
347
|
+
if (unknownChecklistShareAssets.length > 0) {
|
|
348
|
+
fail('release channel checklist references unknown share asset ids', unknownChecklistShareAssets)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const requiredChannelMeasures = ['stars', 'forks', 'issues', 'registry', 'npm']
|
|
352
|
+
const channelMeasureText = announcement.channelChecklist.map((item) => item.measure).join('\n').toLowerCase()
|
|
353
|
+
const missingChannelMeasures = requiredChannelMeasures.filter((measure) => !channelMeasureText.includes(measure))
|
|
354
|
+
if (missingChannelMeasures.length > 0) {
|
|
355
|
+
fail('release channel checklist must map posts back to public adoption and registry signals', missingChannelMeasures)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let seedIssues
|
|
359
|
+
try {
|
|
360
|
+
seedIssues = JSON.parse(execFileSync('node', ['scripts/render-contributor-issues.mjs', '--json'], { encoding: 'utf8' }))
|
|
361
|
+
} catch (error) {
|
|
362
|
+
fail('contributor seed issues must render valid JSON', [error.message])
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (!Array.isArray(seedIssues) || seedIssues.length < 6) {
|
|
366
|
+
fail('launch should have at least six contributor seed issues ready', [`got ${Array.isArray(seedIssues) ? seedIssues.length : 'non-array output'}`])
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const goodFirstIssues = seedIssues.filter((issue) => issue.labels?.includes('type: good first issue'))
|
|
370
|
+
if (goodFirstIssues.length < 4) {
|
|
371
|
+
fail('launch should have several good-first issue URLs ready before asking for contributors', [`got ${goodFirstIssues.length}`])
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (!Array.isArray(announcement.recommendedGoodFirstIssues) || announcement.recommendedGoodFirstIssues.length < 2) {
|
|
375
|
+
fail('release announcement JSON must include at least two recommended good-first issues to publish')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const invalidRecommendedIssues = announcement.recommendedGoodFirstIssues.filter((issue) => !issue.title || !issue.url?.startsWith('https://github.com/markbang/base-themes/issues/new?') || !issue.labels?.includes('type: good first issue'))
|
|
379
|
+
if (invalidRecommendedIssues.length > 0) {
|
|
380
|
+
fail('recommended good-first issues must include title, prefilled URL, and good-first label', invalidRecommendedIssues.map((issue) => issue.title ?? '(untitled)'))
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const missingRecommendedIssueCopy = announcement.recommendedGoodFirstIssues.filter((issue) => !announcement.githubRelease.includes(issue.url))
|
|
384
|
+
if (missingRecommendedIssueCopy.length > 0) {
|
|
385
|
+
fail('GitHub release copy must include recommended good-first issue URLs', missingRecommendedIssueCopy.map((issue) => issue.title))
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const githubReleaseChannel = announcement.channelChecklist.find((item) => item.channel === 'GitHub Release')
|
|
389
|
+
const missingRecommendedIssueChecklistUrls = announcement.recommendedGoodFirstIssues.filter((issue) => !githubReleaseChannel?.recommendedIssueUrls?.includes(issue.url))
|
|
390
|
+
if (missingRecommendedIssueChecklistUrls.length > 0) {
|
|
391
|
+
fail('GitHub Release channel checklist must include recommended good-first issue URLs', missingRecommendedIssueChecklistUrls.map((issue) => issue.title))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const externalIssueAction = launchActions.actions.find((action) => action.signalId === 'external-human-issue-or-pr')
|
|
395
|
+
const missingRecommendedLaunchActionUrls = announcement.recommendedGoodFirstIssues.filter((issue) => !externalIssueAction?.links?.includes(issue.url))
|
|
396
|
+
if (missingRecommendedLaunchActionUrls.length > 0) {
|
|
397
|
+
fail('launch actions must include recommended good-first issue URLs for the missing external issue/PR signal', missingRecommendedLaunchActionUrls.map((issue) => issue.title))
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (!externalIssueAction?.commands?.some((command) => command.includes('gh issue create --repo markbang/base-themes'))) {
|
|
401
|
+
fail('launch actions must include GitHub CLI commands for recommended good-first issues')
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const externalIssueStatus = launchStatus.missingSignals.find((signal) => signal.id === 'external-human-issue-or-pr')
|
|
405
|
+
const missingRecommendedStatusUrls = announcement.recommendedGoodFirstIssues.filter((issue) => !externalIssueStatus?.recommendedGoodFirstIssues?.some((statusIssue) => statusIssue.url === issue.url))
|
|
406
|
+
if (missingRecommendedStatusUrls.length > 0) {
|
|
407
|
+
fail('launch status must include recommended good-first issue URLs for the missing external issue/PR signal', missingRecommendedStatusUrls.map((issue) => issue.title))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const invalidSeedUrls = seedIssues.filter((issue) => !issue.url?.startsWith('https://github.com/markbang/base-themes/issues/new?'))
|
|
411
|
+
if (invalidSeedUrls.length > 0) {
|
|
412
|
+
fail('contributor seed issues must include prefilled GitHub issue URLs', invalidSeedUrls.map((issue) => issue.title ?? '(untitled)'))
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
console.log(`Launch readiness valid: ${Object.keys(requiredReleaseLinks).length} adoption links, ${seedIssues.length} seed issues, ${goodFirstIssues.length} good-first issues`)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const fixtureEnv = {
|
|
5
|
+
SEARCH_CONSOLE_EXPORT: resolve('research/telemetry-fixtures/search-console-export.csv'),
|
|
6
|
+
ANALYTICS_EXPORT: resolve('research/telemetry-fixtures/analytics-events.jsonl'),
|
|
7
|
+
REGISTRY_ACCESS_EXPORT: resolve('research/telemetry-fixtures/registry-access.jsonl'),
|
|
8
|
+
COMMUNITY_PROOF_EXPORT: resolve('research/telemetry-fixtures/community-proof.csv'),
|
|
9
|
+
BUNDLE_REPORT_EXPORT: resolve('research/telemetry-fixtures/bundle-report.json'),
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fail(message, details = []) {
|
|
13
|
+
console.error(`Telemetry fixture import invalid: ${message}`)
|
|
14
|
+
for (const detail of details) console.error(`- ${detail}`)
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let payload
|
|
19
|
+
try {
|
|
20
|
+
payload = JSON.parse(execFileSync('node', ['scripts/collect-telemetry.mjs', '--json', '--no-write'], {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
env: { ...process.env, ...fixtureEnv },
|
|
23
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
24
|
+
}))
|
|
25
|
+
} catch (error) {
|
|
26
|
+
fail('collect-telemetry --json must import fixture exports and emit valid JSON', [error.message])
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function expectEqual(label, actual, expected) {
|
|
30
|
+
if (actual !== expected) fail(`${label} should be ${expected}`, [`got ${actual}`])
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
expectEqual('Search Console row count', payload.searchConsole?.rowCount, 3)
|
|
34
|
+
expectEqual('Search Console clicks', payload.searchConsole?.clicks, 23)
|
|
35
|
+
expectEqual('Search Console impressions', payload.searchConsole?.impressions, 490)
|
|
36
|
+
expectEqual('Website analytics event count', payload.analytics?.eventCount, 9)
|
|
37
|
+
expectEqual('Website analytics install command copies', payload.analytics?.installCopies, 1)
|
|
38
|
+
expectEqual('Website analytics GitHub outbound clicks', payload.analytics?.githubOutboundClicks, 4)
|
|
39
|
+
expectEqual('Website analytics theme snippet copies', payload.analytics?.themeSnippetCopies, 1)
|
|
40
|
+
expectEqual('Website analytics top campaigns', payload.analytics?.topCampaigns?.[0]?.value, 'launch-0-1-2')
|
|
41
|
+
expectEqual('Website analytics top sources', payload.analytics?.topSources?.[0]?.value, 'github-release')
|
|
42
|
+
expectEqual('Website analytics top mediums', payload.analytics?.topMediums?.[0]?.value, 'release-notes')
|
|
43
|
+
expectEqual('Website analytics top GitHub click sources', payload.analytics?.topGithubClickSources?.[0]?.value, 'github-release')
|
|
44
|
+
expectEqual('Website analytics top install-copy sources', payload.analytics?.topInstallCopySources?.[0]?.value, 'github-release')
|
|
45
|
+
expectEqual('Website analytics campaign funnel value', payload.analytics?.campaignFunnels?.[0]?.value, 'launch-0-1-2')
|
|
46
|
+
expectEqual('Website analytics campaign funnel route views', payload.analytics?.campaignFunnels?.[0]?.routeViews, 1)
|
|
47
|
+
expectEqual('Website analytics campaign funnel install copies', payload.analytics?.campaignFunnels?.[0]?.installCopies, 1)
|
|
48
|
+
expectEqual('Website analytics campaign funnel GitHub clicks', payload.analytics?.campaignFunnels?.[0]?.githubClicks, 4)
|
|
49
|
+
expectEqual('Website analytics source funnel value', payload.analytics?.sourceFunnels?.[0]?.value, 'github-release')
|
|
50
|
+
expectEqual('Website analytics source funnel install copies', payload.analytics?.sourceFunnels?.[0]?.installCopies, 1)
|
|
51
|
+
expectEqual('Website analytics source funnel GitHub clicks', payload.analytics?.sourceFunnels?.[0]?.githubClicks, 4)
|
|
52
|
+
expectEqual('Registry access imported rows', payload.registryAccess?.rowCount, 10)
|
|
53
|
+
expectEqual('Registry access matched requests', payload.registryAccess?.matchedRequests, 9)
|
|
54
|
+
expectEqual('Registry access component item requests', payload.registryAccess?.componentItemRequests, 1)
|
|
55
|
+
expectEqual('Registry access block item requests', payload.registryAccess?.blockItemRequests, 1)
|
|
56
|
+
expectEqual('Registry access theme item requests', payload.registryAccess?.themeItemRequests, 1)
|
|
57
|
+
expectEqual('Community proof signals', payload.communityProof?.signalCount, 4)
|
|
58
|
+
expectEqual('Community proof discussions', payload.communityProof?.discussions, 1)
|
|
59
|
+
expectEqual('Community proof permissioned gallery', payload.communityProof?.permissionedGallery, 2)
|
|
60
|
+
expectEqual('Community proof external repos', payload.communityProof?.externalRepos, 1)
|
|
61
|
+
expectEqual('Community proof external URLs', payload.communityProof?.externalUrls, 1)
|
|
62
|
+
expectEqual('Bundle report budget status', payload.bundleReport?.ok, true)
|
|
63
|
+
expectEqual('Bundle report app JS gzip bytes', payload.bundleReport?.appJs?.gzipBytes, 21000)
|
|
64
|
+
expectEqual('Bundle report largest JS bytes', payload.bundleReport?.largestJs?.bytes, 350000)
|
|
65
|
+
expectEqual('Bundle report budget check count', payload.bundleReport?.budgetChecks?.length, 3)
|
|
66
|
+
|
|
67
|
+
if (!payload.bundleReport?.budgetChecks?.every((check) => check.passed)) {
|
|
68
|
+
fail('Bundle report fixture budget checks should all pass')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const githubTargets = payload.analytics?.topGithubClickTargets?.map((row) => row.value) ?? []
|
|
72
|
+
for (const target of ['repo-star', 'repo-fork', 'feature-request', 'good-first-issues']) {
|
|
73
|
+
if (!githubTargets.includes(target)) fail('GitHub outbound click fixture targets were not summarized', [target])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const topRegistryItems = payload.registryAccess?.topRegistryItems?.map((row) => row.value) ?? []
|
|
77
|
+
for (const itemPath of ['/registry/items/button.json', '/registry/items/block-dashboard-shell.json', '/registry/items/theme-enterprise.json']) {
|
|
78
|
+
if (!topRegistryItems.includes(itemPath)) fail('Registry item fixture paths were not summarized', [itemPath])
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (payload.adoption?.completionThreshold !== 3 || payload.adoption?.signalCount !== 4) {
|
|
82
|
+
fail('fixture import must preserve the public adoption gate shape')
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('Telemetry fixture import valid: search, analytics, registry, community, and bundle exports summarized correctly')
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
3
|
+
import { basename, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function fail(message, details = []) {
|
|
6
|
+
console.error(`Telemetry report invalid: ${message}`)
|
|
7
|
+
for (const detail of details) console.error(`- ${detail}`)
|
|
8
|
+
process.exit(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function latestTelemetryPath() {
|
|
12
|
+
const files = readdirSync(resolve('research'))
|
|
13
|
+
.filter((file) => /^telemetry-\d{4}-\d{2}-\d{2}\.json$/.test(file))
|
|
14
|
+
.sort()
|
|
15
|
+
|
|
16
|
+
return files.length ? resolve('research', files.at(-1)) : undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function loadTelemetryReport() {
|
|
20
|
+
if (process.argv.includes('--live')) {
|
|
21
|
+
try {
|
|
22
|
+
return {
|
|
23
|
+
source: 'live collect-telemetry',
|
|
24
|
+
payload: JSON.parse(execFileSync('node', ['scripts/collect-telemetry.mjs', '--json', '--no-write'], {
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
27
|
+
})),
|
|
28
|
+
}
|
|
29
|
+
} catch (error) {
|
|
30
|
+
fail('collect-telemetry --json must emit valid JSON', [error.message])
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const source = latestTelemetryPath()
|
|
35
|
+
if (!source) fail('no research/telemetry-YYYY-MM-DD.json report exists', ['Run npm run telemetry:collect first, or use --live.'])
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return {
|
|
39
|
+
source: basename(source),
|
|
40
|
+
payload: JSON.parse(readFileSync(source, 'utf8')),
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
fail('latest telemetry JSON could not be parsed', [`${source}: ${error.message}`])
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { source, payload } = loadTelemetryReport()
|
|
48
|
+
const telemetryErrors = Array.isArray(payload.errors) ? payload.errors : []
|
|
49
|
+
if (telemetryErrors.length > 0) {
|
|
50
|
+
fail('telemetry collection errors must be resolved before treating the report as valid', telemetryErrors)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const requiredSignalIds = ['npm-weekly-downloads', 'github-stars', 'external-human-issue-or-pr', 'forks']
|
|
54
|
+
const signalIds = payload.adoption?.signals?.map((signal) => signal.id) ?? []
|
|
55
|
+
const missingSignalIds = requiredSignalIds.filter((id) => !signalIds.includes(id))
|
|
56
|
+
if (missingSignalIds.length > 0) {
|
|
57
|
+
fail('adoption JSON is missing required public signal ids', missingSignalIds)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (payload.adoption?.completionThreshold !== 3) {
|
|
61
|
+
fail('completion threshold must stay at three public signals', [`got ${payload.adoption?.completionThreshold}`])
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (payload.adoption?.signalCount !== 4 || payload.adoption?.signals?.length !== 4) {
|
|
65
|
+
fail('adoption JSON must track exactly four public completion signals', [`got ${payload.adoption?.signalCount ?? 'missing'} / ${payload.adoption?.signals?.length ?? 'missing'}`])
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (payload.adoption?.passedSignals !== payload.adoption.signals.filter((signal) => signal.passed).length) {
|
|
69
|
+
fail('passedSignals must equal the number of passed signal entries')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (payload.adoption?.score !== `${payload.adoption.passedSignals}/${payload.adoption.signalCount}`) {
|
|
73
|
+
fail('adoption score string must match passedSignals/signalCount', [`got ${payload.adoption?.score}`])
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (payload.adoption?.externallyValidated !== (payload.adoption.passedSignals >= payload.adoption.completionThreshold)) {
|
|
77
|
+
fail('externallyValidated must be derived from passedSignals and completionThreshold')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const missingCurrentValues = payload.adoption.signals.filter((signal) => !('current' in signal) || !('passed' in signal) || !signal.threshold)
|
|
81
|
+
if (missingCurrentValues.length > 0) {
|
|
82
|
+
fail('every adoption signal must include current, passed, and threshold fields', missingCurrentValues.map((signal) => signal.id))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (payload.adoption.externallyValidated) {
|
|
86
|
+
console.log(`Telemetry report valid (${source}): adoption externally validated at ${payload.adoption.score}`)
|
|
87
|
+
} else {
|
|
88
|
+
console.log(`Telemetry report valid (${source}): adoption not externally validated at ${payload.adoption.score}`)
|
|
89
|
+
}
|