Extends the #1074 scheme to tests/agent-size-budget.test.cjs, which used the same assertTightCeiling tier ratchet but was still line-based (never rebased in #717). Completes the migration — the last part of the #1074 epic. - Rebase agent sizing from lines to LF-normalized bytes (#717/#683). - Delete the 'SIZE: tier anti-creep' describe (3 assertTightCeiling tests); add a per-agent baseline (tests/agent-size-baseline.json) as the primary anti-creep, and byte hard caps (XL 56 KiB / LARGE 48 KiB / DEFAULT 24 KiB), each above its tier high-water with real headroom. No separate new-file cap: a net-new agent is DEFAULT-tier, already bounded by the DEFAULT cap. - Keep the agent-classification tests verbatim. - scripts/workflow-size.cjs: add generic measureMdFiles(dir, predicate) (workflows + agents share one byte-measurement path); measureWorkflows now delegates to it. - scripts/update-size-baseline.cjs: one 'npm run size:baseline' now regenerates BOTH the workflow and agent baselines (gsd-* filter for agents). Rebased onto next after PR 2/3 (#1096) merged: replicate the scripts/lib/workflow-size.cjs -> scripts/workflow-size.cjs move (PR 1/3) across the generator and the agent test's require; regenerate the agent baseline against current agents (a uniform +170 B preamble drift on all 33 since authoring). Addresses the #1097 review (trek-e): - BLOCKER (acceptance criterion 5): document the agent contract in CONTEXT.md. Adds RULESET.AGENT_SIZE_BUDGET (caps 57344/49152/24576, per-agent baseline, dual size:baseline, shared measureMdFiles seam) and disambiguates it from the separate DEFECT.AGENT-FILE-SIZE-CAP-BREACH 45K-CHAR guard (two units, two purposes). - Docs: now that #1096's docs/TESTING-SUITES.md "Workflow size budget" section is in next, fold in the agent coverage here (renamed to "Workflow & agent size budget"): agent caps + per-agent baseline + the how-to + reference rows, and the disambiguation from the 45K-char guard. - Minor (negative proof): add a boundary-fixture test exercising the hard-cap comparison at cap-1/cap/cap+1 through the real lfByteCount path, so a future threshold/operator edit can't silently neuter a cap. - Nit: align the tier test name wording ("stays within") with the <= operator. Negative proof on a real tracked agent (gsd-planner): baseline catches +10 B; XL hard cap catches 57,516 > 57,344 with the baseline current. Closes #1095 (PR 3/3 child); landing this completes the #1074 epic.
This commit is contained in:
@@ -16,9 +16,12 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { measureWorkflows, WORKFLOWS_DIR } = require('./workflow-size.cjs');
|
||||
const { measureMdFiles, WORKFLOWS_DIR } = require('./workflow-size.cjs');
|
||||
|
||||
const BASELINE_PATH = path.join(__dirname, '..', 'tests', 'workflow-size-baseline.json');
|
||||
const AGENTS_DIR = path.join(__dirname, '..', 'agents');
|
||||
const AGENT_BASELINE_PATH = path.join(__dirname, '..', 'tests', 'agent-size-baseline.json');
|
||||
const isGsdAgent = (f) => f.startsWith('gsd-');
|
||||
|
||||
/**
|
||||
* Serialize a size map to the on-disk baseline format: keys sorted, 2-space
|
||||
@@ -34,25 +37,32 @@ function serializeBaseline(sizes) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the baseline file from the measured workflow sizes.
|
||||
* Write a baseline file from the measured `.md` sizes in a directory.
|
||||
*
|
||||
* @param {object} [opts]
|
||||
* @param {string} [opts.dir] - Workflows dir to measure (default canonical).
|
||||
* @param {string} [opts.outPath] - Baseline file to write (default canonical).
|
||||
* @param {string} [opts.dir] - Directory to measure (default: workflows).
|
||||
* @param {string} [opts.outPath] - Baseline file to write (default: workflow).
|
||||
* @param {function(string): boolean} [opts.predicate] - Filename filter.
|
||||
* @returns {{ outPath: string, count: number, content: string }}
|
||||
*/
|
||||
function generateBaseline({ dir = WORKFLOWS_DIR, outPath = BASELINE_PATH } = {}) {
|
||||
const sizes = measureWorkflows(dir);
|
||||
function generateBaseline({ dir = WORKFLOWS_DIR, outPath = BASELINE_PATH, predicate } = {}) {
|
||||
const sizes = measureMdFiles(dir, predicate);
|
||||
const content = serializeBaseline(sizes);
|
||||
fs.writeFileSync(outPath, content);
|
||||
return { outPath, count: Object.keys(sizes).length, content };
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const { outPath, count } = generateBaseline();
|
||||
process.stdout.write(
|
||||
`Wrote ${count} workflow sizes to ${path.relative(process.cwd(), outPath)}\n`
|
||||
);
|
||||
const targets = [
|
||||
{ label: 'workflow', dir: WORKFLOWS_DIR, outPath: BASELINE_PATH },
|
||||
{ label: 'agent', dir: AGENTS_DIR, outPath: AGENT_BASELINE_PATH, predicate: isGsdAgent },
|
||||
];
|
||||
for (const t of targets) {
|
||||
const { outPath, count } = generateBaseline(t);
|
||||
process.stdout.write(
|
||||
`Wrote ${count} ${t.label} sizes to ${path.relative(process.cwd(), outPath)}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { generateBaseline, serializeBaseline, BASELINE_PATH };
|
||||
module.exports = { generateBaseline, serializeBaseline, BASELINE_PATH, AGENT_BASELINE_PATH };
|
||||
|
||||
@@ -51,18 +51,40 @@ function listWorkflowStems(dir = WORKFLOWS_DIR) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure every top-level workflow file, keyed by filename (`<stem>.md`).
|
||||
* Measure every top-level `.md` file in `dir`, keyed by filename, byte sizes.
|
||||
* Generic over directory and an optional filename predicate — used for both
|
||||
* workflows (`gsd-core/workflows/*.md`) and agents (`agents/gsd-*.md`) so the
|
||||
* size guards and the baseline generator share one measurement path (#1074).
|
||||
* Non-recursive by design.
|
||||
*
|
||||
* @param {string} [dir] - Workflows directory (defaults to the canonical one).
|
||||
* @returns {Object<string, number>} Map of `<stem>.md` → LF byte size, with
|
||||
* keys inserted in sorted order.
|
||||
* @param {string} dir - Directory to scan.
|
||||
* @param {function(string): boolean} [predicate] - Filename filter (default: all `.md`).
|
||||
* @returns {Object<string, number>} Map of filename → LF byte size, keys sorted.
|
||||
*/
|
||||
function measureWorkflows(dir = WORKFLOWS_DIR) {
|
||||
function measureMdFiles(dir, predicate = () => true) {
|
||||
const out = {};
|
||||
for (const stem of listWorkflowStems(dir)) {
|
||||
out[`${stem}.md`] = lfByteCount(path.join(dir, `${stem}.md`));
|
||||
}
|
||||
const names = fs
|
||||
.readdirSync(dir)
|
||||
.filter((f) => f.endsWith('.md') && predicate(f))
|
||||
.sort();
|
||||
for (const name of names) out[name] = lfByteCount(path.join(dir, name));
|
||||
return out;
|
||||
}
|
||||
|
||||
module.exports = { WORKFLOWS_DIR, lfByteCount, listWorkflowStems, measureWorkflows };
|
||||
/**
|
||||
* Measure every top-level workflow file, keyed by filename (`<stem>.md`).
|
||||
*
|
||||
* @param {string} [dir] - Workflows directory (defaults to the canonical one).
|
||||
* @returns {Object<string, number>} Map of `<stem>.md` → LF byte size, sorted.
|
||||
*/
|
||||
function measureWorkflows(dir = WORKFLOWS_DIR) {
|
||||
return measureMdFiles(dir);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WORKFLOWS_DIR,
|
||||
lfByteCount,
|
||||
listWorkflowStems,
|
||||
measureMdFiles,
|
||||
measureWorkflows,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user