@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,207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kit Self-Test
|
|
5
|
+
*
|
|
6
|
+
* Validates the kit installation: config, seeds, invariants, dry-runs, build.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/kit-selftest.mjs [--skip-build] [--skip-invariants]
|
|
10
|
+
*
|
|
11
|
+
* Environment:
|
|
12
|
+
* KIT_CONFIG=/path/to/kit.config.json — point at an alternate config root
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { resolve, join, dirname } from "node:path";
|
|
17
|
+
import { execSync } from "node:child_process";
|
|
18
|
+
import { loadKitConfig, KIT_VERSION_SUPPORTED } from "./lib/config.mjs";
|
|
19
|
+
|
|
20
|
+
const SCRIPT_ROOT = resolve(import.meta.dirname, ".."); // where scripts live
|
|
21
|
+
const DATA_ROOT = process.env.KIT_CONFIG
|
|
22
|
+
? dirname(resolve(process.env.KIT_CONFIG))
|
|
23
|
+
: SCRIPT_ROOT;
|
|
24
|
+
|
|
25
|
+
// Auto-skip when running from npm package (dirs don't exist)
|
|
26
|
+
const autoSkipInvariants = !existsSync(join(SCRIPT_ROOT, "tests", "invariants"));
|
|
27
|
+
const autoSkipBuild = !existsSync(join(SCRIPT_ROOT, "site"));
|
|
28
|
+
|
|
29
|
+
const skipBuild = process.argv.includes("--skip-build") || autoSkipBuild;
|
|
30
|
+
const skipInvariants = process.argv.includes("--skip-invariants") || autoSkipInvariants;
|
|
31
|
+
|
|
32
|
+
// ── Test runner ──────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const results = [];
|
|
35
|
+
|
|
36
|
+
function check(label, fn) {
|
|
37
|
+
try {
|
|
38
|
+
fn();
|
|
39
|
+
results.push({ label, pass: true });
|
|
40
|
+
console.log(` ✓ ${label}`);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
results.push({ label, pass: false, error: err.message });
|
|
43
|
+
console.log(` ✗ ${label}: ${err.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assert(condition, message) {
|
|
48
|
+
if (!condition) throw new Error(message);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ── Tests ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
console.log("Kit Self-Test");
|
|
54
|
+
console.log("=".repeat(40));
|
|
55
|
+
if (DATA_ROOT !== SCRIPT_ROOT) {
|
|
56
|
+
console.log(` Config root: ${DATA_ROOT}`);
|
|
57
|
+
console.log(` Script root: ${SCRIPT_ROOT}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 1. Config
|
|
61
|
+
console.log("\n[Config]");
|
|
62
|
+
|
|
63
|
+
check("kit.config.json exists", () => {
|
|
64
|
+
const configPath = join(DATA_ROOT, "kit.config.json");
|
|
65
|
+
assert(
|
|
66
|
+
existsSync(configPath),
|
|
67
|
+
`kit.config.json not found at ${configPath}. Fix: create it or set KIT_CONFIG=/path/to/kit.config.json`
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
let config;
|
|
72
|
+
check("kit.config.json is valid JSON with required fields", () => {
|
|
73
|
+
config = loadKitConfig(DATA_ROOT);
|
|
74
|
+
assert(config.kitVersion, "kitVersion missing");
|
|
75
|
+
assert(config.org?.name || config.org?.account, "org name or account missing");
|
|
76
|
+
assert(config.site?.title, "site.title missing");
|
|
77
|
+
assert(config.paths?.dataDir, "paths.dataDir missing");
|
|
78
|
+
assert(config.paths?.publicDir, "paths.publicDir missing");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
check("kitVersion in supported range", () => {
|
|
82
|
+
const v = config.kitVersion;
|
|
83
|
+
assert(
|
|
84
|
+
v >= KIT_VERSION_SUPPORTED[0] && v <= KIT_VERSION_SUPPORTED[1],
|
|
85
|
+
`kitVersion ${v} not in [${KIT_VERSION_SUPPORTED.join(", ")}]`
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// 2. Seed files
|
|
90
|
+
console.log("\n[Seed Files]");
|
|
91
|
+
|
|
92
|
+
const coreSeedFiles = [
|
|
93
|
+
"governance.json",
|
|
94
|
+
"promo-queue.json",
|
|
95
|
+
"experiments.json",
|
|
96
|
+
"submissions.json",
|
|
97
|
+
"overrides.json",
|
|
98
|
+
"ops-history.json",
|
|
99
|
+
"worthy.json",
|
|
100
|
+
"promo-decisions.json",
|
|
101
|
+
"experiment-decisions.json",
|
|
102
|
+
"baseline.json",
|
|
103
|
+
"feedback-summary.json",
|
|
104
|
+
"queue-health.json",
|
|
105
|
+
"recommendations.json",
|
|
106
|
+
"recommendation-patch.json",
|
|
107
|
+
"decision-drift.json",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
for (const file of coreSeedFiles) {
|
|
111
|
+
check(`${file} exists`, () => {
|
|
112
|
+
const fullPath = join(DATA_ROOT, config.paths.dataDir, file);
|
|
113
|
+
assert(existsSync(fullPath), `Missing: ${fullPath}. Fix: run npm run kit:init`);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 3. Invariant tests
|
|
118
|
+
if (!skipInvariants) {
|
|
119
|
+
console.log("\n[Invariant Tests]");
|
|
120
|
+
|
|
121
|
+
check("invariant tests pass", () => {
|
|
122
|
+
try {
|
|
123
|
+
execSync("node --test tests/invariants/*.test.mjs", {
|
|
124
|
+
cwd: SCRIPT_ROOT,
|
|
125
|
+
stdio: "pipe",
|
|
126
|
+
timeout: 60000,
|
|
127
|
+
env: { ...process.env },
|
|
128
|
+
});
|
|
129
|
+
} catch (err) {
|
|
130
|
+
throw new Error(`Invariant tests failed: ${err.stderr?.toString().slice(-200) || err.message}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
console.log("\n[Invariant Tests] Skipped (--skip-invariants)");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Core dry-runs
|
|
138
|
+
console.log("\n[Core Dry-Runs]");
|
|
139
|
+
|
|
140
|
+
const dryRunScripts = [
|
|
141
|
+
"scripts/gen-promo-decisions.mjs",
|
|
142
|
+
"scripts/gen-experiment-decisions.mjs",
|
|
143
|
+
"scripts/gen-baseline.mjs",
|
|
144
|
+
"scripts/gen-feedback-summary.mjs",
|
|
145
|
+
"scripts/gen-queue-health.mjs",
|
|
146
|
+
"scripts/gen-telemetry-aggregate.mjs",
|
|
147
|
+
"scripts/gen-recommendations.mjs",
|
|
148
|
+
"scripts/gen-recommendation-patch.mjs",
|
|
149
|
+
"scripts/gen-decision-drift.mjs",
|
|
150
|
+
"scripts/gen-trust-receipt.mjs",
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
for (const script of dryRunScripts) {
|
|
154
|
+
const name = script.split("/").pop();
|
|
155
|
+
check(`${name} --dry-run`, () => {
|
|
156
|
+
try {
|
|
157
|
+
execSync(`node ${script} --dry-run`, {
|
|
158
|
+
cwd: SCRIPT_ROOT,
|
|
159
|
+
stdio: "pipe",
|
|
160
|
+
timeout: 30000,
|
|
161
|
+
env: { ...process.env },
|
|
162
|
+
});
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`${name} failed. Fix: ensure kit:init has been run and data files exist. ` +
|
|
166
|
+
(err.stderr?.toString().slice(-200) || err.message)
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 5. Site build (optional)
|
|
173
|
+
if (!skipBuild) {
|
|
174
|
+
console.log("\n[Site Build]");
|
|
175
|
+
|
|
176
|
+
check("Astro build succeeds", () => {
|
|
177
|
+
try {
|
|
178
|
+
execSync("npm run build", {
|
|
179
|
+
cwd: join(SCRIPT_ROOT, "site"),
|
|
180
|
+
stdio: "pipe",
|
|
181
|
+
timeout: 120000,
|
|
182
|
+
});
|
|
183
|
+
} catch (err) {
|
|
184
|
+
throw new Error(err.stderr?.toString().slice(-300) || err.message);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
console.log("\n[Site Build] Skipped (--skip-build)");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ── Summary ──────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
console.log("\n" + "=".repeat(40));
|
|
194
|
+
const passed = results.filter((r) => r.pass).length;
|
|
195
|
+
const failed = results.filter((r) => !r.pass).length;
|
|
196
|
+
console.log(`Results: ${passed} passed, ${failed} failed, ${results.length} total`);
|
|
197
|
+
|
|
198
|
+
if (failed > 0) {
|
|
199
|
+
console.log("\nFailed checks:");
|
|
200
|
+
results.filter((r) => !r.pass).forEach((r) => {
|
|
201
|
+
console.log(` ✗ ${r.label}: ${r.error}`);
|
|
202
|
+
});
|
|
203
|
+
process.exit(1);
|
|
204
|
+
} else {
|
|
205
|
+
console.log("\n✓ All checks passed.");
|
|
206
|
+
process.exit(0);
|
|
207
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kit configuration loader.
|
|
3
|
+
*
|
|
4
|
+
* Reads kit.config.json from the repo root and deep-merges with defaults.
|
|
5
|
+
* All scripts in the portable core import from here instead of hardcoding values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
9
|
+
import { resolve, join, dirname } from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
|
|
12
|
+
// ── Defaults ─────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const DEFAULTS = {
|
|
15
|
+
kitVersion: 1,
|
|
16
|
+
org: { name: "", account: "", url: "" },
|
|
17
|
+
site: { title: "", url: "", description: "" },
|
|
18
|
+
repo: { marketing: "" },
|
|
19
|
+
contact: { email: "" },
|
|
20
|
+
paths: { dataDir: "site/src/data", publicDir: "site/public" },
|
|
21
|
+
guardrails: {
|
|
22
|
+
maxDataPatchesPerRun: 5,
|
|
23
|
+
dailyTelemetryCapPerType: 50,
|
|
24
|
+
spikeThreshold: 300,
|
|
25
|
+
maxRecommendations: 20,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const KIT_VERSION_SUPPORTED = [1, 1]; // [min, max]
|
|
30
|
+
|
|
31
|
+
// ── Deep merge ───────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function deepMerge(base, override) {
|
|
34
|
+
const result = { ...base };
|
|
35
|
+
for (const [key, value] of Object.entries(override)) {
|
|
36
|
+
if (value && typeof value === "object" && !Array.isArray(value) && base[key] && typeof base[key] === "object") {
|
|
37
|
+
result[key] = deepMerge(base[key], value);
|
|
38
|
+
} else {
|
|
39
|
+
result[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Loader ───────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Load kit config from a specific root directory.
|
|
49
|
+
* @param {string} root - repo root path
|
|
50
|
+
* @returns {object} merged config
|
|
51
|
+
*/
|
|
52
|
+
export function loadKitConfig(root) {
|
|
53
|
+
const configPath = join(root, "kit.config.json");
|
|
54
|
+
if (!existsSync(configPath)) {
|
|
55
|
+
return { ...DEFAULTS };
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
59
|
+
return deepMerge(DEFAULTS, raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return { ...DEFAULTS };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Cached singleton ─────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
let _cached = null;
|
|
68
|
+
let _cachedRoot = null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find the kit root directory.
|
|
72
|
+
* Priority: KIT_CONFIG env var > walk up from scripts/lib/ > fallback 2 levels up.
|
|
73
|
+
*/
|
|
74
|
+
function findRoot() {
|
|
75
|
+
// 1. KIT_CONFIG env var takes precedence
|
|
76
|
+
if (process.env.KIT_CONFIG) {
|
|
77
|
+
const envPath = resolve(process.env.KIT_CONFIG);
|
|
78
|
+
if (existsSync(envPath)) return dirname(envPath);
|
|
79
|
+
console.warn(
|
|
80
|
+
` \u26A0 KIT_CONFIG="${process.env.KIT_CONFIG}" not found, falling back to auto-discovery`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 2. Walk up from scripts/lib/ looking for kit.config.json
|
|
85
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
86
|
+
let dir = thisDir;
|
|
87
|
+
for (let i = 0; i < 5; i++) {
|
|
88
|
+
if (existsSync(join(dir, "kit.config.json"))) return dir;
|
|
89
|
+
const parent = dirname(dir);
|
|
90
|
+
if (parent === dir) break;
|
|
91
|
+
dir = parent;
|
|
92
|
+
}
|
|
93
|
+
// 3. Fallback: assume scripts/lib/ is 2 levels deep from root
|
|
94
|
+
return resolve(thisDir, "..", "..");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get the kit root directory. Prefers KIT_CONFIG env var, then auto-discovery.
|
|
99
|
+
* Use this for data/config path resolution in portable core scripts.
|
|
100
|
+
* @returns {string} absolute root path
|
|
101
|
+
*/
|
|
102
|
+
export function getRoot() {
|
|
103
|
+
return findRoot();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the kit config singleton. Discovers repo root automatically.
|
|
108
|
+
* @returns {object} merged config
|
|
109
|
+
*/
|
|
110
|
+
export function getConfig() {
|
|
111
|
+
const root = findRoot();
|
|
112
|
+
if (_cached && _cachedRoot === root) return _cached;
|
|
113
|
+
_cached = loadKitConfig(root);
|
|
114
|
+
_cachedRoot = root;
|
|
115
|
+
return _cached;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Reset the cached config (for testing).
|
|
120
|
+
*/
|
|
121
|
+
export function resetConfigCache() {
|
|
122
|
+
_cached = null;
|
|
123
|
+
_cachedRoot = null;
|
|
124
|
+
}
|