176 lines
5.0 KiB
JavaScript
176 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
type WorkerConfig = {
|
|
workers: {
|
|
max: number;
|
|
reserve_for_interactive: number;
|
|
minimum_background: number;
|
|
};
|
|
};
|
|
|
|
type AutomationLimits = {
|
|
review_shards: {
|
|
normal_default: number;
|
|
normal_active_floor: number;
|
|
hot_intake_default: number;
|
|
exact_item_default: number;
|
|
hard_cap: number;
|
|
};
|
|
commit_review: {
|
|
page_size_default: number;
|
|
page_size_hard_cap: number;
|
|
};
|
|
repair_live_runs: {
|
|
default: number;
|
|
hard_cap: number;
|
|
automerge_default: number;
|
|
issue_implementation_default: number;
|
|
};
|
|
issue_implementation: {
|
|
dispatches_per_sweep_default: number;
|
|
};
|
|
};
|
|
|
|
const root = process.cwd();
|
|
const config = JSON.parse(
|
|
fs.readFileSync(path.join(root, "config", "automation-limits.json"), "utf8"),
|
|
) as WorkerConfig;
|
|
const limits = deriveAutomationLimits(config);
|
|
|
|
const expectations: { file: string; label: string; pattern: RegExp }[] = [
|
|
{
|
|
file: ".github/workflows/sweep.yml",
|
|
label: "manual workflow_dispatch shard_count default",
|
|
pattern: new RegExp(
|
|
`shard_count:[\\s\\S]{0,180}default: "${limits.review_shards.normal_default}"`,
|
|
),
|
|
},
|
|
{
|
|
file: "README.md",
|
|
label: "manual plan shard-count example",
|
|
pattern: new RegExp(`--shard-count ${limits.review_shards.normal_default}\\b`),
|
|
},
|
|
{
|
|
file: "docs/commit-dispatcher.md",
|
|
label: "commit review page size env example",
|
|
pattern: new RegExp(
|
|
`CLAWSWEEPER_COMMIT_REVIEW_PAGE_SIZE=${limits.commit_review.page_size_default}\\b`,
|
|
),
|
|
},
|
|
{
|
|
file: "docs/commit-sweeper.md",
|
|
label: "commit review page size default",
|
|
pattern: new RegExp(`defaults to ${limits.commit_review.page_size_default}\\b`),
|
|
},
|
|
{
|
|
file: "docs/repair/README.md",
|
|
label: "repair live run default",
|
|
pattern: new RegExp(`CLAWSWEEPER_MAX_LIVE_WORKERS=${limits.repair_live_runs.default}\\b`),
|
|
},
|
|
{
|
|
file: "docs/scheduler.md",
|
|
label: "normal review shard default",
|
|
pattern: new RegExp(`${limits.review_shards.normal_default} concurrent Codex\\s+review shards`),
|
|
},
|
|
{
|
|
file: "docs/scheduler.md",
|
|
label: "normal active shard floor",
|
|
pattern: new RegExp(`fewer than ${limits.review_shards.normal_active_floor} items are due`),
|
|
},
|
|
{
|
|
file: "docs/scheduler.md",
|
|
label: "hot intake shard default",
|
|
pattern: new RegExp(
|
|
`broad hot intake: up to ${limits.review_shards.hot_intake_default} shards`,
|
|
),
|
|
},
|
|
{
|
|
file: "docs/limits.md",
|
|
label: "limits documentation references source file",
|
|
pattern: /config\/automation-limits\.json/,
|
|
},
|
|
];
|
|
|
|
for (const [limitPath, value] of Object.entries(flattenLimits(limits))) {
|
|
expectations.push({
|
|
file: "docs/limits.md",
|
|
label: `${limitPath} documented current value`,
|
|
pattern: new RegExp(`\\| \`${escapeRegExp(limitPath)}\` \\| ${value} \\|`),
|
|
});
|
|
}
|
|
for (const [limitPath, value] of Object.entries(flattenLimits(config))) {
|
|
expectations.push({
|
|
file: "docs/limits.md",
|
|
label: `${limitPath} documented worker config value`,
|
|
pattern: new RegExp(`\\| \`${escapeRegExp(limitPath)}\` \\| ${value} \\|`),
|
|
});
|
|
}
|
|
|
|
const missing: string[] = [];
|
|
for (const expectation of expectations) {
|
|
const text = fs.readFileSync(path.join(root, expectation.file), "utf8");
|
|
if (!expectation.pattern.test(text)) {
|
|
missing.push(`${expectation.file}: ${expectation.label}`);
|
|
}
|
|
}
|
|
|
|
if (missing.length > 0) {
|
|
console.error("Automation limits drift check failed:");
|
|
for (const item of missing) console.error(`- ${item}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
function flattenLimits(value: unknown, prefix = ""): Record<string, number> {
|
|
const out: Record<string, number> = {};
|
|
if (!isRecord(value)) return out;
|
|
for (const [key, child] of Object.entries(value)) {
|
|
const childPath = prefix ? `${prefix}.${key}` : key;
|
|
if (Number.isInteger(child)) {
|
|
out[childPath] = child;
|
|
} else {
|
|
Object.assign(out, flattenLimits(child, childPath));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function deriveAutomationLimits(workerConfig: WorkerConfig): AutomationLimits {
|
|
const max = workerConfig.workers.max;
|
|
return {
|
|
review_shards: {
|
|
normal_default: percent(max, 70),
|
|
normal_active_floor: percent(max, 30),
|
|
hot_intake_default: percent(max, 35),
|
|
exact_item_default: 1,
|
|
hard_cap: max,
|
|
},
|
|
commit_review: {
|
|
page_size_default: percent(max, 5),
|
|
page_size_hard_cap: max,
|
|
},
|
|
repair_live_runs: {
|
|
default: percent(max, 40),
|
|
hard_cap: max,
|
|
automerge_default: percent(max, 40),
|
|
issue_implementation_default: percent(max, 40),
|
|
},
|
|
issue_implementation: {
|
|
dispatches_per_sweep_default: percent(max, 4),
|
|
},
|
|
};
|
|
}
|
|
|
|
function percent(max: number, value: number): number {
|
|
return Math.max(1, Math.floor((max * value) / 100));
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function escapeRegExp(value: string): string {
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|