@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.
- package/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/bin/promo-kit.mjs +150 -0
- package/index.mjs +9 -0
- package/kit.config.example.json +19 -0
- package/package.json +45 -0
- package/scripts/apply-control-patch.mjs +205 -0
- package/scripts/apply-submission-status.mjs +225 -0
- package/scripts/gen-baseline.mjs +402 -0
- package/scripts/gen-decision-drift.mjs +253 -0
- package/scripts/gen-experiment-decisions.mjs +282 -0
- package/scripts/gen-feedback-summary.mjs +278 -0
- package/scripts/gen-promo-decisions.mjs +507 -0
- package/scripts/gen-queue-health.mjs +223 -0
- package/scripts/gen-recommendation-patch.mjs +352 -0
- package/scripts/gen-recommendations.mjs +409 -0
- package/scripts/gen-telemetry-aggregate.mjs +266 -0
- package/scripts/gen-trust-receipt.mjs +184 -0
- package/scripts/kit-bootstrap.mjs +246 -0
- package/scripts/kit-migrate.mjs +111 -0
- package/scripts/kit-selftest.mjs +207 -0
- package/scripts/lib/config.mjs +124 -0
|
@@ -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
|
+
}
|