Files
elizaos--eliza/packages/scripts/launch-qa/check-model-data.mjs
T
wehub-resource-sync 426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

574 lines
16 KiB
JavaScript

#!/usr/bin/env node
// Runs launch QA launch qa check model data automation for release-readiness checks.
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const here = path.dirname(fileURLToPath(import.meta.url));
const defaultRepoRoot = path.resolve(here, "../../..");
const SKIP_DIRS = new Set([
".git",
".turbo",
"coverage",
"dist",
"node_modules",
"target",
]);
const SECRET_PATTERNS = [
{ label: "openai-key", pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{16,}\b/g },
{ label: "anthropic-key", pattern: /\bsk-ant-[A-Za-z0-9_-]{16,}\b/g },
{ label: "bearer-token", pattern: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}\b/g },
{
label: "github-token",
pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g,
},
{ label: "aws-access-key", pattern: /\bAKIA[0-9A-Z]{16}\b/g },
{ label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{30,}\b/g },
{
label: "stripe-secret-key",
pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/g,
},
{ label: "slack-token", pattern: /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/g },
{
label: "credential-assignment",
pattern:
/\b(?:api[_-]?key|access[_-]?token|auth[_-]?token|secret|password)\b\s*[:=]\s*["']?[A-Za-z0-9._~+/=-]{16,}/gi,
},
];
function rel(repoRoot, filePath) {
return path.relative(repoRoot, filePath).split(path.sep).join("/");
}
function exists(filePath) {
try {
fs.accessSync(filePath);
return true;
} catch {
return false;
}
}
function isDirectory(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch {
return false;
}
}
function safeReadJson(filePath) {
try {
return { ok: true, value: JSON.parse(fs.readFileSync(filePath, "utf8")) };
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
function walkFiles(dir) {
if (!exists(dir) || !isDirectory(dir)) {
return [];
}
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
continue;
}
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...walkFiles(fullPath));
} else if (entry.isFile()) {
files.push(fullPath);
}
}
return files;
}
function isArtifactJson(filePath) {
const name = path.basename(filePath).toLowerCase();
return (
name.endsWith(".json") &&
(name.includes("trajectory") ||
name.includes("finetune") ||
name.includes("fine-tune") ||
name.includes("dataset"))
);
}
function collectCandidateFiles(repoRoot, inputPaths) {
const scanRoots =
inputPaths.length > 0
? inputPaths.map((inputPath) => path.resolve(repoRoot, inputPath))
: [
path.join(repoRoot, "plugins", "app-training", "datasets"),
path.join(repoRoot, "launchdocs", "artifacts"),
];
const files = new Set();
for (const scanRoot of scanRoots) {
if (!exists(scanRoot)) {
continue;
}
const candidates = isDirectory(scanRoot) ? walkFiles(scanRoot) : [scanRoot];
for (const filePath of candidates) {
if (filePath.endsWith(".jsonl") || isArtifactJson(filePath)) {
files.add(filePath);
}
}
}
return [...files].sort((left, right) => left.localeCompare(right));
}
function approxTokens(text) {
return Math.ceil(text.length / 4);
}
function addError(errors, repoRoot, filePath, fields) {
errors.push({
file: rel(repoRoot, filePath),
...fields,
});
}
function findSecretHits(text) {
const hits = [];
for (const { label, pattern } of SECRET_PATTERNS) {
pattern.lastIndex = 0;
for (const match of text.matchAll(pattern)) {
hits.push({ label, offset: match.index ?? 0 });
}
}
return hits;
}
function validateMessagesRow(row) {
const errors = [];
if (typeof row !== "object" || row === null || Array.isArray(row)) {
return ["row must be a JSON object"];
}
if (!Array.isArray(row.messages)) {
return ["row.messages must be an array"];
}
if (row.messages.length === 0) {
errors.push("row.messages must not be empty");
}
let hasUser = false;
let hasAssistant = false;
row.messages.forEach((message, index) => {
if (
typeof message !== "object" ||
message === null ||
Array.isArray(message)
) {
errors.push(`messages[${index}] must be an object`);
return;
}
const role = message.role;
if (!["system", "user", "model", "assistant", "tool"].includes(role)) {
errors.push(
`messages[${index}].role must be system, user, model, assistant, or tool`,
);
}
if (typeof message.content !== "string" || message.content.trim() === "") {
errors.push(`messages[${index}].content must be a non-empty string`);
}
if (role === "user") hasUser = true;
if (role === "model" || role === "assistant") hasAssistant = true;
});
if (!hasUser) {
errors.push("row.messages must include at least one user message");
}
if (!hasAssistant) {
errors.push(
"row.messages must include at least one model or assistant message",
);
}
if (
"metadata" in row &&
(typeof row.metadata !== "object" || row.metadata === null)
) {
errors.push("row.metadata must be an object when present");
}
if ("reward" in row && typeof row.reward !== "number") {
errors.push("row.reward must be a number when present");
}
return errors;
}
function checkJsonlFile({ repoRoot, filePath }) {
const errors = [];
const raw = fs.readFileSync(filePath, "utf8");
const lines = raw.split(/\r?\n/);
let rowCount = 0;
let secretHitCount = 0;
let contentChars = 0;
const roles = {};
lines.forEach((line, index) => {
if (line.trim() === "") {
return;
}
rowCount += 1;
let row;
try {
row = JSON.parse(line);
} catch (error) {
addError(errors, repoRoot, filePath, {
type: "invalid-jsonl",
line: index + 1,
message: `line ${index + 1} is not valid JSON: ${
error instanceof Error ? error.message : String(error)
}`,
});
return;
}
for (const rowError of validateMessagesRow(row)) {
addError(errors, repoRoot, filePath, {
type: "invalid-row-schema",
line: index + 1,
message: `line ${index + 1}: ${rowError}`,
});
}
for (const message of Array.isArray(row.messages) ? row.messages : []) {
if (message && typeof message === "object") {
if (typeof message.role === "string") {
roles[message.role] = (roles[message.role] ?? 0) + 1;
}
if (typeof message.content === "string") {
contentChars += message.content.length;
}
}
}
const secretHits = findSecretHits(line);
secretHitCount += secretHits.length;
for (const hit of secretHits) {
addError(errors, repoRoot, filePath, {
type: "secret-like-string",
line: index + 1,
label: hit.label,
message: `line ${index + 1} contains secret-like string (${hit.label})`,
});
}
});
if (rowCount === 0) {
addError(errors, repoRoot, filePath, {
type: "empty-jsonl",
message: "JSONL file has no non-empty rows",
});
}
return {
kind: "jsonl",
file: rel(repoRoot, filePath),
rows: rowCount,
bytes: fs.statSync(filePath).size,
approxTokens: approxTokens(raw),
contentApproxTokens: approxTokens("x".repeat(contentChars)),
roles,
secretHits: secretHitCount,
errors,
};
}
function metadataCandidatesFor(jsonlPath) {
const dir = path.dirname(jsonlPath);
const ext = path.extname(jsonlPath);
const baseWithoutExt = path.basename(jsonlPath, ext);
return [
path.join(dir, `${baseWithoutExt}.meta.json`),
path.join(dir, `${path.basename(jsonlPath)}.meta.json`),
];
}
function checkMetadataForJsonl({ repoRoot, jsonlPath, rowCount }) {
const errors = [];
const metadataPath = metadataCandidatesFor(jsonlPath).find((candidate) =>
exists(candidate),
);
if (!metadataPath) {
return { metadataFile: null, errors };
}
const parsed = safeReadJson(metadataPath);
if (!parsed.ok) {
addError(errors, repoRoot, metadataPath, {
type: "invalid-metadata-json",
message: `metadata JSON did not parse: ${parsed.error}`,
});
return { metadataFile: rel(repoRoot, metadataPath), errors };
}
const metadata = parsed.value;
if (
typeof metadata !== "object" ||
metadata === null ||
Array.isArray(metadata)
) {
addError(errors, repoRoot, metadataPath, {
type: "invalid-metadata-schema",
message: "metadata JSON must be an object",
});
return { metadataFile: rel(repoRoot, metadataPath), errors };
}
for (const field of [
"rowCount",
"caseCount",
"exampleCount",
"sampleCount",
]) {
if (
field in metadata &&
typeof metadata[field] === "number" &&
metadata[field] !== rowCount
) {
addError(errors, repoRoot, metadataPath, {
type: "metadata-row-count-mismatch",
field,
expected: rowCount,
actual: metadata[field],
message: `metadata ${field}=${metadata[field]} does not match JSONL rows=${rowCount}`,
});
}
}
const outcomeFields = ["passCount", "failCount", "rejectedCount"];
if (outcomeFields.every((field) => typeof metadata[field] === "number")) {
const total = outcomeFields.reduce(
(sum, field) => sum + metadata[field],
0,
);
if (total !== rowCount) {
addError(errors, repoRoot, metadataPath, {
type: "metadata-outcome-count-mismatch",
expected: rowCount,
actual: total,
message: `metadata pass/fail/rejected total=${total} does not match JSONL rows=${rowCount}`,
});
}
}
const raw = fs.readFileSync(metadataPath, "utf8");
for (const hit of findSecretHits(raw)) {
addError(errors, repoRoot, metadataPath, {
type: "secret-like-string",
label: hit.label,
message: `metadata contains secret-like string (${hit.label})`,
});
}
return { metadataFile: rel(repoRoot, metadataPath), errors };
}
function looksLikeTrajectoryArtifact(value) {
if (typeof value !== "object" || value === null) {
return false;
}
if (Array.isArray(value)) {
return value.every((item) => typeof item === "object" && item !== null);
}
return (
Array.isArray(value.trajectories) ||
Array.isArray(value.steps) ||
Array.isArray(value.messages) ||
typeof value.trajectoryId === "string" ||
typeof value.datasetId === "string" ||
typeof value.model === "string"
);
}
function checkJsonArtifact({ repoRoot, filePath }) {
const errors = [];
const raw = fs.readFileSync(filePath, "utf8");
const parsed = safeReadJson(filePath);
if (!parsed.ok) {
addError(errors, repoRoot, filePath, {
type: "invalid-json-artifact",
message: `JSON artifact did not parse: ${parsed.error}`,
});
} else if (!looksLikeTrajectoryArtifact(parsed.value)) {
addError(errors, repoRoot, filePath, {
type: "invalid-artifact-schema",
message:
"artifact JSON must include trajectories, steps, messages, trajectoryId, datasetId, or model",
});
}
let secretHitCount = 0;
for (const hit of findSecretHits(raw)) {
secretHitCount += 1;
addError(errors, repoRoot, filePath, {
type: "secret-like-string",
label: hit.label,
message: `artifact contains secret-like string (${hit.label})`,
});
}
return {
kind: "json",
file: rel(repoRoot, filePath),
rows: null,
bytes: fs.statSync(filePath).size,
approxTokens: approxTokens(raw),
secretHits: secretHitCount,
errors,
};
}
export function checkModelData(options = {}) {
const repoRoot = path.resolve(options.repoRoot ?? defaultRepoRoot);
const inputPaths = options.paths ?? [];
const files = collectCandidateFiles(repoRoot, inputPaths);
const summaries = [];
const errors = [];
for (const filePath of files) {
if (filePath.endsWith(".jsonl")) {
const summary = checkJsonlFile({ repoRoot, filePath });
const metadata = checkMetadataForJsonl({
repoRoot,
jsonlPath: filePath,
rowCount: summary.rows,
});
summary.metadataFile = metadata.metadataFile;
summary.errors.push(...metadata.errors);
summaries.push(summary);
errors.push(...summary.errors);
} else {
const summary = checkJsonArtifact({ repoRoot, filePath });
summaries.push(summary);
errors.push(...summary.errors);
}
}
const totals = summaries.reduce(
(acc, summary) => {
acc.files += 1;
acc.bytes += summary.bytes;
acc.approxTokens += summary.approxTokens;
acc.rows += summary.rows ?? 0;
acc.secretHits += summary.secretHits;
if (summary.kind === "jsonl") acc.jsonlFiles += 1;
if (summary.kind === "json") acc.jsonFiles += 1;
return acc;
},
{
files: 0,
jsonlFiles: 0,
jsonFiles: 0,
rows: 0,
bytes: 0,
approxTokens: 0,
secretHits: 0,
},
);
return {
ok: errors.length === 0,
checkedFiles: summaries.map((summary) => summary.file),
errorCount: errors.length,
totals,
files: summaries,
errors,
};
}
function printHuman(result) {
const status = result.ok ? "PASS" : "FAIL";
const summary = `files=${result.totals.files} jsonl=${result.totals.jsonlFiles} json=${result.totals.jsonFiles} rows=${result.totals.rows} bytes=${result.totals.bytes} approxTokens=${result.totals.approxTokens}`;
const write = result.ok ? console.log : console.error;
write(`[model-data-gate] ${status} ${summary}`);
for (const file of result.files) {
const rowText = file.rows === null ? "" : ` rows=${file.rows}`;
const metadataText = file.metadataFile
? ` metadata=${file.metadataFile}`
: "";
write(
`- ${file.file} kind=${file.kind}${rowText} bytes=${file.bytes} approxTokens=${file.approxTokens}${metadataText}`,
);
}
if (!result.ok) {
for (const error of result.errors) {
const where = error.line ? `${error.file}:${error.line}` : error.file;
console.error(` ! ${where} ${error.message}`);
}
}
}
function parseArgs(argv) {
const args = {
json: false,
repoRoot: process.env.MODEL_DATA_GATE_REPO_ROOT,
paths: [],
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === "--json") {
args.json = true;
} else if (arg === "--repo-root") {
args.repoRoot = argv[++i];
} else if (arg.startsWith("--repo-root=")) {
args.repoRoot = arg.slice("--repo-root=".length);
} else if (arg === "--path") {
args.paths.push(argv[++i]);
} else if (arg.startsWith("--path=")) {
args.paths.push(arg.slice("--path=".length));
} else if (arg === "-h" || arg === "--help") {
args.help = true;
} else {
args.paths.push(arg);
}
}
return args;
}
function usage() {
return `Usage: node packages/scripts/launch-qa/check-model-data.mjs [--json] [--repo-root <path>] [--path <file-or-dir> ...]
Validates app-training JSONL datasets and trajectory/fine-tune artifacts offline.
`;
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
try {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
console.log(usage());
process.exit(0);
}
const result = checkModelData({
repoRoot: args.repoRoot,
paths: args.paths.filter(Boolean),
});
if (args.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printHuman(result);
}
process.exit(result.ok ? 0 : 1);
} catch (error) {
console.error(
`[model-data-gate] ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}