@mcptoolshop/promo-kit 0.1.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.
@@ -0,0 +1,184 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Trust Receipt Generator
5
+ *
6
+ * Generates site/public/trust.json — a machine-readable provenance artifact.
7
+ * Emitted at build time so every deploy carries verifiable metadata.
8
+ *
9
+ * Usage:
10
+ * node scripts/gen-trust-receipt.mjs [--dry-run]
11
+ *
12
+ * Reads:
13
+ * site/src/data/marketir/marketir.snapshot.json (lock hash)
14
+ * site/src/data/marketir/data/tools/*.json (proven claims)
15
+ * site/src/data/worthy.json (worthy stats)
16
+ * site/src/data/projects.json (artifact hash)
17
+ * site/src/data/overrides.json (artifact hash)
18
+ * site/src/data/promo.json (artifact hash)
19
+ * site/src/data/baseline.json (artifact hash)
20
+ * site/src/data/ops-history.json (artifact hash)
21
+ * site/src/data/promo-decisions.json (artifact hash)
22
+ * site/src/data/experiment-decisions.json (artifact hash)
23
+ *
24
+ * Writes:
25
+ * site/public/trust.json
26
+ */
27
+
28
+ import { readFileSync, writeFileSync, readdirSync, existsSync } from "node:fs";
29
+ import { resolve, join } from "node:path";
30
+ import { execSync } from "node:child_process";
31
+ import { createHash } from "node:crypto";
32
+ import { getConfig, getRoot } from "./lib/config.mjs";
33
+
34
+ const ROOT = getRoot();
35
+ const config = getConfig();
36
+ const DATA_DIR = join(ROOT, config.paths.dataDir);
37
+ const PUBLIC_DIR = join(ROOT, config.paths.publicDir);
38
+
39
+ // ── Helpers ─────────────────────────────────────────────────
40
+
41
+ function safeParseJson(filePath, fallback = null) {
42
+ try {
43
+ return JSON.parse(readFileSync(filePath, "utf8"));
44
+ } catch {
45
+ return fallback;
46
+ }
47
+ }
48
+
49
+ function hashFile(filePath) {
50
+ try {
51
+ const content = readFileSync(filePath, "utf8");
52
+ return "sha256:" + createHash("sha256").update(content).digest("hex");
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ // ── Core ────────────────────────────────────────────────────
59
+
60
+ /**
61
+ * Build a trust receipt object from available data.
62
+ *
63
+ * @param {{ dataDir?: string, root?: string }} opts
64
+ * @returns {object} Trust receipt
65
+ */
66
+ export function buildTrustReceipt(opts = {}) {
67
+ const { dataDir = DATA_DIR, root = ROOT } = opts;
68
+
69
+ // 1. Git SHA
70
+ let commit = "unknown";
71
+ try {
72
+ commit = execSync("git rev-parse --short HEAD", { cwd: root, encoding: "utf8" }).trim();
73
+ } catch { /* fail soft */ }
74
+
75
+ // 2. MarketIR lock hash
76
+ const snapshot = safeParseJson(join(dataDir, "marketir", "marketir.snapshot.json"), {});
77
+ const marketirLockHash = snapshot.lockSha256 || null;
78
+
79
+ // 3. COE version
80
+ let coeVersion = "unknown";
81
+ try {
82
+ const rootPkg = safeParseJson(join(root, "package.json"), {});
83
+ const deps = { ...rootPkg.dependencies, ...rootPkg.devDependencies };
84
+ if (deps["@mcptoolshop/clearance-opinion-engine"]) {
85
+ coeVersion = deps["@mcptoolshop/clearance-opinion-engine"].replace(/^\^|~/, "");
86
+ }
87
+ } catch { /* fail soft */ }
88
+
89
+ // 4. Proven claim count
90
+ let provenClaims = 0;
91
+ const toolsDir = join(dataDir, "marketir", "data", "tools");
92
+ try {
93
+ if (existsSync(toolsDir)) {
94
+ const files = readdirSync(toolsDir).filter((f) => f.endsWith(".json"));
95
+ for (const file of files) {
96
+ const tool = safeParseJson(join(toolsDir, file), {});
97
+ if (tool.claims) {
98
+ provenClaims += tool.claims.filter((c) => c.status === "proven").length;
99
+ }
100
+ }
101
+ }
102
+ } catch { /* fail soft */ }
103
+
104
+ // 5. Worthy stats
105
+ const worthy = safeParseJson(join(dataDir, "worthy.json"), {});
106
+ const repos = worthy.repos || {};
107
+ const repoEntries = Object.values(repos);
108
+ const worthyStats = {
109
+ total: repoEntries.length,
110
+ worthy: repoEntries.filter((r) => r.worthy).length,
111
+ notWorthy: repoEntries.filter((r) => !r.worthy).length,
112
+ };
113
+
114
+ // 6. Artifact manifest — SHA-256 of key data files
115
+ const MANIFEST_FILES = [
116
+ "projects.json",
117
+ "overrides.json",
118
+ "worthy.json",
119
+ "promo.json",
120
+ "baseline.json",
121
+ "ops-history.json",
122
+ "promo-decisions.json",
123
+ "experiment-decisions.json",
124
+ ];
125
+
126
+ const artifactManifest = {};
127
+ for (const file of MANIFEST_FILES) {
128
+ const hash = hashFile(join(dataDir, file));
129
+ if (hash) {
130
+ artifactManifest[file] = hash;
131
+ }
132
+ }
133
+
134
+ return {
135
+ generatedAt: new Date().toISOString(),
136
+ commit,
137
+ marketirLockHash,
138
+ coeVersion,
139
+ provenClaims,
140
+ worthyStats,
141
+ artifactManifest,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Full pipeline: build receipt and write to site/public/trust.json.
147
+ *
148
+ * @param {{ dataDir?: string, publicDir?: string, root?: string, dryRun?: boolean }} opts
149
+ * @returns {object} Trust receipt
150
+ */
151
+ export function generateTrustReceipt(opts = {}) {
152
+ const { publicDir = PUBLIC_DIR, dryRun = false, ...buildOpts } = opts;
153
+
154
+ const receipt = buildTrustReceipt(buildOpts);
155
+
156
+ if (dryRun) {
157
+ console.log(` [dry-run] Would write trust.json (commit: ${receipt.commit})`);
158
+ console.log(` [dry-run] MarketIR lock: ${receipt.marketirLockHash || "N/A"}`);
159
+ console.log(` [dry-run] Proven claims: ${receipt.provenClaims}`);
160
+ console.log(` [dry-run] Artifacts: ${Object.keys(receipt.artifactManifest).length} files hashed`);
161
+ return receipt;
162
+ }
163
+
164
+ writeFileSync(join(publicDir, "trust.json"), JSON.stringify(receipt, null, 2) + "\n", "utf8");
165
+ console.log(` Wrote trust.json (commit: ${receipt.commit}, ${Object.keys(receipt.artifactManifest).length} artifacts)`);
166
+
167
+ return receipt;
168
+ }
169
+
170
+ // ── Entry point ─────────────────────────────────────────────
171
+
172
+ const isMain = process.argv[1] &&
173
+ resolve(process.argv[1]).endsWith("gen-trust-receipt.mjs");
174
+
175
+ if (isMain) {
176
+ const dryRun = process.argv.includes("--dry-run");
177
+ console.log("Generating trust receipt...");
178
+ if (dryRun) console.log(" Mode: DRY RUN");
179
+
180
+ const receipt = generateTrustReceipt({ dryRun });
181
+ console.log(` Commit: ${receipt.commit}`);
182
+ console.log(` Proven claims: ${receipt.provenClaims}`);
183
+ console.log(` Worthy: ${receipt.worthyStats.worthy}/${receipt.worthyStats.total}`);
184
+ }
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Kit Bootstrap
5
+ *
6
+ * Validates the environment and creates zero-state seed files
7
+ * required by the portable core. Idempotent — skips existing files.
8
+ *
9
+ * Usage:
10
+ * node scripts/kit-bootstrap.mjs
11
+ */
12
+
13
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
14
+ import { resolve, join, dirname } from "node:path";
15
+ import { loadKitConfig, KIT_VERSION_SUPPORTED } from "./lib/config.mjs";
16
+
17
+ const SCRIPT_ROOT = resolve(import.meta.dirname, "..");
18
+
19
+ // ── Environment checks ──────────────────────────────────────
20
+
21
+ function checkEnvironment(root) {
22
+ const errors = [];
23
+
24
+ // Node 22+
25
+ const major = parseInt(process.versions.node.split(".")[0], 10);
26
+ if (major < 22) {
27
+ errors.push(`Node 22+ required (found ${process.versions.node})`);
28
+ }
29
+
30
+ // kit.config.json exists
31
+ const configPath = join(root, "kit.config.json");
32
+ if (!existsSync(configPath)) {
33
+ errors.push(
34
+ `kit.config.json not found at ${configPath}. ` +
35
+ `Fix: create it or set KIT_CONFIG=/path/to/kit.config.json`
36
+ );
37
+ }
38
+
39
+ return errors;
40
+ }
41
+
42
+ // ── Seed file definitions ───────────────────────────────────
43
+
44
+ function getSeedFiles(config, root) {
45
+ const dataDir = join(root, config.paths.dataDir);
46
+ const publicDir = join(root, config.paths.publicDir);
47
+
48
+ return [
49
+ // Human-owned governance
50
+ {
51
+ path: join(dataDir, "governance.json"),
52
+ content: {
53
+ schemaVersion: 2,
54
+ decisionsFrozen: false,
55
+ experimentsFrozen: false,
56
+ maxPromosPerWeek: 3,
57
+ cooldownDaysPerSlug: 14,
58
+ cooldownDaysPerPartner: 14,
59
+ minCoverageScore: 80,
60
+ minExperimentDataThreshold: 10,
61
+ hardRules: ["never push directly to main"],
62
+ },
63
+ },
64
+ // Promo queue
65
+ {
66
+ path: join(dataDir, "promo-queue.json"),
67
+ content: {
68
+ week: new Date().toISOString().slice(0, 10),
69
+ slugs: [],
70
+ promotionType: "own",
71
+ notes: "",
72
+ },
73
+ },
74
+ // Experiments
75
+ {
76
+ path: join(dataDir, "experiments.json"),
77
+ content: { schemaVersion: 1, experiments: [] },
78
+ },
79
+ // Submissions
80
+ {
81
+ path: join(dataDir, "submissions.json"),
82
+ content: { schemaVersion: 1, submissions: [] },
83
+ },
84
+ // Overrides (empty object)
85
+ {
86
+ path: join(dataDir, "overrides.json"),
87
+ content: {},
88
+ },
89
+ // Ops history
90
+ {
91
+ path: join(dataDir, "ops-history.json"),
92
+ content: { schemaVersion: 1, runs: [] },
93
+ },
94
+ // Feedback (empty JSONL)
95
+ {
96
+ path: join(dataDir, "feedback.jsonl"),
97
+ content: null, // empty file
98
+ },
99
+ // Worthy rubric
100
+ {
101
+ path: join(dataDir, "worthy.json"),
102
+ content: {
103
+ rubric: {
104
+ criteria: [
105
+ "README exists with purpose, install, quickstart",
106
+ "Has at least one release or tag",
107
+ "Tests exist and pass in CI",
108
+ "License file present",
109
+ "No critical security issues",
110
+ ],
111
+ minimumScore: 3,
112
+ },
113
+ scores: {},
114
+ },
115
+ },
116
+ // Generated outputs (zero-state seeds)
117
+ {
118
+ path: join(dataDir, "promo-decisions.json"),
119
+ content: { generatedAt: null, decisions: [] },
120
+ },
121
+ {
122
+ path: join(dataDir, "experiment-decisions.json"),
123
+ content: { generatedAt: null, decisions: [] },
124
+ },
125
+ {
126
+ path: join(dataDir, "baseline.json"),
127
+ content: { generatedAt: null, totalRuns: 0 },
128
+ },
129
+ {
130
+ path: join(dataDir, "feedback-summary.json"),
131
+ content: { generatedAt: null, channels: {}, slugs: {} },
132
+ },
133
+ {
134
+ path: join(dataDir, "queue-health.json"),
135
+ content: { generatedAt: null },
136
+ },
137
+ {
138
+ path: join(dataDir, "recommendations.json"),
139
+ content: { generatedAt: null, recommendations: [] },
140
+ },
141
+ {
142
+ path: join(dataDir, "recommendation-patch.json"),
143
+ content: { generatedAt: null, patches: [], advisoryNotes: [], riskNotes: [], frozenActions: [] },
144
+ },
145
+ {
146
+ path: join(dataDir, "decision-drift.json"),
147
+ content: {
148
+ generatedAt: null,
149
+ entrants: [],
150
+ exits: [],
151
+ scoreDeltas: [],
152
+ reasonChanges: [],
153
+ summary: { totalChanged: 0, newEntrants: 0, exits: 0, scoreChanges: 0, actionOnlyChanges: 0 },
154
+ },
155
+ },
156
+ // Telemetry directories
157
+ {
158
+ path: join(dataDir, "telemetry", "rollup.json"),
159
+ content: { generatedAt: null, eventCounts: {}, dailySummaries: [] },
160
+ },
161
+ ];
162
+ }
163
+
164
+ // ── Main ─────────────────────────────────────────────────────
165
+
166
+ export function bootstrap(root = SCRIPT_ROOT) {
167
+ console.log("Kit Bootstrap");
168
+ console.log("=".repeat(40));
169
+
170
+ // 1. Environment check
171
+ const envErrors = checkEnvironment(root);
172
+ if (envErrors.length > 0) {
173
+ console.error("\nEnvironment errors:");
174
+ envErrors.forEach((e) => console.error(` ✗ ${e}`));
175
+ return { success: false, errors: envErrors, created: [], skipped: [] };
176
+ }
177
+ console.log("✓ Environment OK");
178
+
179
+ // 2. Load config
180
+ const config = loadKitConfig(root);
181
+ const v = config.kitVersion;
182
+ if (v < KIT_VERSION_SUPPORTED[0] || v > KIT_VERSION_SUPPORTED[1]) {
183
+ const msg = `kitVersion ${v} not in supported range [${KIT_VERSION_SUPPORTED.join(", ")}]`;
184
+ console.error(` ✗ ${msg}`);
185
+ return { success: false, errors: [msg], created: [], skipped: [] };
186
+ }
187
+ console.log(`✓ Config loaded (kitVersion: ${v})`);
188
+
189
+ // 3. Create seed files
190
+ const seeds = getSeedFiles(config, root);
191
+ const created = [];
192
+ const skipped = [];
193
+
194
+ for (const seed of seeds) {
195
+ if (existsSync(seed.path)) {
196
+ skipped.push(seed.path);
197
+ continue;
198
+ }
199
+
200
+ // Ensure parent directory exists
201
+ mkdirSync(dirname(seed.path), { recursive: true });
202
+
203
+ if (seed.content === null) {
204
+ writeFileSync(seed.path, "");
205
+ } else {
206
+ writeFileSync(seed.path, JSON.stringify(seed.content, null, 2) + "\n");
207
+ }
208
+ created.push(seed.path);
209
+ }
210
+
211
+ // Ensure telemetry events dir exists
212
+ const eventsDir = join(root, config.paths.dataDir, "telemetry", "events");
213
+ if (!existsSync(eventsDir)) {
214
+ mkdirSync(eventsDir, { recursive: true });
215
+ created.push(eventsDir);
216
+ }
217
+
218
+ // Ensure telemetry daily dir exists
219
+ const dailyDir = join(root, config.paths.dataDir, "telemetry", "daily");
220
+ if (!existsSync(dailyDir)) {
221
+ mkdirSync(dailyDir, { recursive: true });
222
+ created.push(dailyDir);
223
+ }
224
+
225
+ console.log(`\n✓ Created: ${created.length} files/dirs`);
226
+ created.forEach((f) => console.log(` + ${f.replace(root, ".")}`));
227
+ console.log(`✓ Skipped: ${skipped.length} (already exist)`);
228
+
229
+ console.log("\nNext steps:");
230
+ console.log(" 1. Edit kit.config.json with your org details");
231
+ console.log(" 2. Run: npm run kit:selftest");
232
+ console.log(" 3. Commit and push");
233
+
234
+ return { success: true, errors: [], created, skipped };
235
+ }
236
+
237
+ // ── CLI entry point ──────────────────────────────────────────
238
+
239
+ const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename);
240
+ if (isMain) {
241
+ const root = process.env.KIT_CONFIG
242
+ ? dirname(resolve(process.env.KIT_CONFIG))
243
+ : SCRIPT_ROOT;
244
+ const result = bootstrap(root);
245
+ process.exit(result.success ? 0 : 1);
246
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Kit Migrate
5
+ *
6
+ * Reads kitVersion from kit.config.json and applies schema transforms
7
+ * to bring data files up to the current version.
8
+ *
9
+ * v1 → v1 is a no-op (current).
10
+ *
11
+ * Usage:
12
+ * node scripts/kit-migrate.mjs
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from "node:fs";
16
+ import { resolve, join, dirname } from "node:path";
17
+ import { loadKitConfig, KIT_VERSION_SUPPORTED } from "./lib/config.mjs";
18
+
19
+ const SCRIPT_ROOT = resolve(import.meta.dirname, "..");
20
+
21
+ // ── Migration registry ──────────────────────────────────────
22
+
23
+ /**
24
+ * Each migration transforms from version N to N+1.
25
+ * Add entries here when kitVersion increments.
26
+ * @type {Map<number, { label: string, migrate: (root: string, config: object) => void }>}
27
+ */
28
+ const MIGRATIONS = new Map([
29
+ // Example for future:
30
+ // [2, {
31
+ // label: "v1 → v2: Add guardrails.maxRecommendations",
32
+ // migrate(root, config) {
33
+ // // transform data files for v2
34
+ // },
35
+ // }],
36
+ ]);
37
+
38
+ // ── Main ─────────────────────────────────────────────────────
39
+
40
+ export function migrate(root = SCRIPT_ROOT) {
41
+ console.log("Kit Migrate");
42
+ console.log("=".repeat(40));
43
+
44
+ const config = loadKitConfig(root);
45
+ const currentVersion = config.kitVersion;
46
+ const targetVersion = KIT_VERSION_SUPPORTED[1];
47
+
48
+ console.log(` Current kitVersion: ${currentVersion}`);
49
+ console.log(` Target kitVersion: ${targetVersion}`);
50
+ console.log(` Supported range: [${KIT_VERSION_SUPPORTED.join(", ")}]`);
51
+
52
+ // Validate range
53
+ if (currentVersion < KIT_VERSION_SUPPORTED[0]) {
54
+ console.error(`\n✗ kitVersion ${currentVersion} is below minimum supported (${KIT_VERSION_SUPPORTED[0]})`);
55
+ console.error(" Manual migration required. See docs/portable-core.md.");
56
+ return { success: false, from: currentVersion, to: targetVersion, migrationsApplied: 0 };
57
+ }
58
+
59
+ if (currentVersion > targetVersion) {
60
+ console.error(`\n✗ kitVersion ${currentVersion} is above maximum supported (${targetVersion})`);
61
+ console.error(" Update the kit code to a newer version.");
62
+ return { success: false, from: currentVersion, to: targetVersion, migrationsApplied: 0 };
63
+ }
64
+
65
+ if (currentVersion === targetVersion) {
66
+ console.log(`\n✓ Already up to date (v${currentVersion}).`);
67
+ return { success: true, from: currentVersion, to: targetVersion, migrationsApplied: 0 };
68
+ }
69
+
70
+ // Apply sequential migrations
71
+ let applied = 0;
72
+ for (let v = currentVersion + 1; v <= targetVersion; v++) {
73
+ const migration = MIGRATIONS.get(v);
74
+ if (!migration) {
75
+ console.error(`\n✗ No migration found for v${v - 1} → v${v}`);
76
+ return { success: false, from: currentVersion, to: targetVersion, migrationsApplied: applied };
77
+ }
78
+
79
+ console.log(`\n Applying: ${migration.label}`);
80
+ try {
81
+ migration.migrate(root, config);
82
+ applied++;
83
+ console.log(` ✓ Done.`);
84
+ } catch (err) {
85
+ console.error(` ✗ Failed: ${err.message}`);
86
+ return { success: false, from: currentVersion, to: targetVersion, migrationsApplied: applied };
87
+ }
88
+ }
89
+
90
+ // Update kitVersion in config file
91
+ const configPath = join(root, "kit.config.json");
92
+ const raw = JSON.parse(readFileSync(configPath, "utf8"));
93
+ raw.kitVersion = targetVersion;
94
+ writeFileSync(configPath, JSON.stringify(raw, null, 2) + "\n");
95
+
96
+ console.log(`\n✓ Migrated from v${currentVersion} to v${targetVersion} (${applied} migration${applied !== 1 ? "s" : ""}).`);
97
+ console.log(" Run: npm run kit:selftest");
98
+
99
+ return { success: true, from: currentVersion, to: targetVersion, migrationsApplied: applied };
100
+ }
101
+
102
+ // ── CLI entry point ──────────────────────────────────────────
103
+
104
+ const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(import.meta.filename);
105
+ if (isMain) {
106
+ const root = process.env.KIT_CONFIG
107
+ ? dirname(resolve(process.env.KIT_CONFIG))
108
+ : SCRIPT_ROOT;
109
+ const result = migrate(root);
110
+ process.exit(result.success ? 0 : 1);
111
+ }