Files
msd-core/tests/helpers/compact-content-split.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

593 lines
25 KiB
JavaScript

'use strict';
/**
* Shared library for the compact-content partition guard (ADR-4139 Decision 5,
* epic #4139, Phase 3 #4403). See `docs/PARTITION-RULES.md` for the operational
* spec this module implements — it is the source of truth for behavior; this
* file is the mechanics.
*
* A "registered split" is any `msd-core/workflows/<name>/detail/*.md` path
* paired with the spine at `msd-core/workflows/<name>.md`. There is no
* separate registry — a pair is registered by existing on disk
* (`discoverRegisteredSplits`). Everything else here is one of the five
* checks PARTITION-RULES.md describes, or a primitive those checks share:
*
* 1. Completeness — NOT implemented here (fires once, at split time, and
* needs the merge-base spine content the way the pilot test already
* reads it; `normalizeNonTrivialLines` is the shared primitive it needs).
* 2. Disjointness — `checkDisjointness`.
* 3. Registration — `checkRegistration`.
* 4. Protected content — `extractProtectedBlocks` extracts the sentinel-
* wrapped spans; the guard test compares their presence itself.
* 5. Boundary moves — `readBoundaryMoveTrailers`.
*
* `checkDetailFileSizeCap` is the pilot's size-cap assertion, generalized.
*
* This module only reads (filesystem + `git log`/`git merge-base`, both
* read-only). No writes, no network.
*/
const fs = require('node:fs');
const path = require('node:path');
const {
REPO_ROOT,
GIT_TIMEOUT_MS,
safeDirArgs,
} = require('./emitted-runtime.cjs');
const { NEW_FILE_CAP, ACK_TRAILER_DELIM, normalizeAckReason } = require('./emitted-diff.cjs');
const { runGit, OUTCOME } = require('./process-seam.cjs');
/** Default scan root: `msd-core/workflows/` at repo root. */
const DEFAULT_WORKFLOWS_DIR = path.join(__dirname, '..', '..', 'msd-core', 'workflows');
/**
* Discover every registered spine/detail split under `workflowsDir`.
*
* A split is registered by a `<workflowsDir>/<name>/detail/` DIRECTORY (never
* a `<name>/detail.md` FILE — only an actual `detail/` subdirectory counts)
* containing at least one `*.md` file, where `<name>` is exactly ONE path
* segment directly under `workflowsDir` — a `detail/` dir nested two or more
* segments deep (e.g. `<name>/sub/detail/`) is walked (so its own children
* are still found) but is never itself registered, per PARTITION-RULES.md's
* "no nested split names" contract.
*
* Pure enumeration: a missing spine is reported here as `spineExists: false`,
* never thrown — deciding that's a failure is check 3's (registration) job,
* not discovery's.
*
* @param {string} [workflowsDir]
* @returns {Array<{name: string, spinePath: string, detailPaths: string[], spineExists: boolean}>}
*/
function discoverRegisteredSplits(workflowsDir = DEFAULT_WORKFLOWS_DIR) {
const found = new Map(); // name -> split record, first registration wins
function walk(dir) {
let entries;
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return; // unreadable directory: nothing to discover under it
}
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const full = path.join(dir, entry.name);
if (entry.name === 'detail') {
let mdFiles = [];
try {
mdFiles = fs.readdirSync(full, { withFileTypes: true })
.filter((e) => e.isFile() && e.name.endsWith('.md'))
.map((e) => e.name);
} catch {
mdFiles = [];
}
if (mdFiles.length > 0) {
// `dir` is detail/'s PARENT — the segment(s) between workflowsDir
// and dir are the candidate `<name>`. Only a single segment counts.
const segments = path.relative(workflowsDir, dir).split(path.sep).filter(Boolean);
if (segments.length === 1) {
const name = segments[0];
if (!found.has(name)) {
const spinePath = path.join(workflowsDir, `${name}.md`);
const detailPaths = mdFiles.map((f) => path.join(full, f)).sort();
found.set(name, { name, spinePath, detailPaths, spineExists: fs.existsSync(spinePath) });
}
}
}
}
walk(full);
}
}
walk(workflowsDir);
return [...found.values()].sort((a, b) => a.name.localeCompare(b.name));
}
/**
* "Trivial" lines (fences, `---` rules, bare headings, bare `Label:` lines)
* are boilerplate that legitimately repeats throughout any markdown file with
* code blocks — excluding them from the completeness/disjointness checks is
* what keeps those checks from flaring on structure rather than content.
* Ported verbatim from the pilot's `tests/plan-phase-compact-split.test.cjs`,
* which this module supersedes as Phase 3's generalized version of the same
* check.
*
* Bare shell block-closer/reopener keywords (#4405) are the same class of
* problem in a bash-heavy workflow like execute-phase.md: a lone `fi` or
* `done` line carries no content of its own — it is pure block-structure
* syntax that recurs once per `if`/`for`/`while` anywhere in the file. A
* split that extracts even one `if...fi` block will otherwise always collide
* with some unrelated `if...fi` block left in the spine, exactly the
* structure-not-content false positive this function exists to suppress.
*
* A bare XML-ish tag line (#4405) — `<step name="x">`, `</doc_assignment>`,
* `<verify_assignment>` — is the markup equivalent of the same problem: every
* workflow in this corpus repeats these tags once per step/template block, so
* any split that extracts even one such block collides with an unrelated one
* left in the spine. The tag NAME and attributes carry structure, never prose
* content, so treating the whole line as trivial is the same judgment call
* `isKnownSanctionedBoilerplate` already makes for the shared launcher line —
* generalized here since it recurs for any tag, not one specific string.
*/
function isTrivial(line) {
if (/^`{3,}/.test(line)) return true;
if (/^-{3,}$/.test(line)) return true;
if (/^#+\s*$/.test(line)) return true;
if (/^[A-Za-z][A-Za-z ]*:$/.test(line)) return true; // bare label lines like "Options:"
if (/^(fi|done|esac|else|then|do|\{|\})\s*;?\s*$/.test(line)) return true; // bare shell block syntax
if (/^<\/?[A-Za-z][\w-]*(\s+[^<>]*)?>$/.test(line)) return true; // bare open/close tag, alone on its own line
return false;
}
/**
* Normalize file content into the comparable unit the completeness and
* disjointness checks both operate on: trimmed, non-empty, non-trivial
* lines, in order, duplicates preserved (callers choose Set vs array
* depending whether they need membership or a count).
*
* @param {string} content
* @returns {string[]}
*/
function normalizeNonTrivialLines(content) {
return content
.split(/\r?\n/)
.map((l) => l.trim())
.filter((l) => l.length > 0 && !isTrivial(l));
}
/**
* Known-boilerplate line prefixes (#4405), beyond the launcher preamble below:
* an exact, verbatim paragraph or call-opener this codebase repeats at every
* agent-spawn callsite across the ENTIRE corpus, not just within one file.
* `execute-phase.md`, `docs-update.md`, and `new-project.md` (at minimum) each
* spawn multiple agents and each carries its own copy of these — the same
* sanctioned-duplication shape as the launcher preamble, just keyed on a set
* of known strings instead of one.
*/
const KNOWN_BOILERPLATE_PREFIXES = [
'> **ORCHESTRATOR RULE — CODEX RUNTIME**:',
'Agent(prompt="',
];
/**
* Is `line` one of the corpus's known sanctioned cross-file duplicates: the
* canonical `msd_run` launcher bootstrap preamble (see
* `msd-core/workflows/_runtime-launcher.snippet.sh`,
* `tests/runtime-launcher-parity.test.cjs`), or one of the `KNOWN_BOILERPLATE_PREFIXES`
* above?
*
* The launcher preamble's own guard mandates exactly one inlined copy in every
* workflow/detail file that calls `msd_run` — spine and detail both call it,
* so both legitimately carry their own copy. That is sanctioned
* cross-file duplication, not something the disjointness check should ever
* flag, so it is filtered out wherever this module compares lines across a
* spine/detail pair.
*
* @param {string} line
* @returns {boolean}
*/
function isKnownSanctionedBoilerplate(line) {
if (line.startsWith('_MSD_SHIM_NAME="msd-tools.cjs";')) return true;
return KNOWN_BOILERPLATE_PREFIXES.some((prefix) => line.startsWith(prefix));
}
const PROTECTED_START = '<!-- msd:protected:start -->';
const PROTECTED_END = '<!-- msd:protected:end -->';
const PROTECTED_SINGLE = '<!-- msd:protected -->';
/**
* Extract every `<!-- msd:protected -->` (single-line) and
* `<!-- msd:protected:start -->`/`<!-- msd:protected:end -->` (paired)
* sentinel-wrapped block from `content`.
*
* Never throws on a malformed sentinel (unclosed start, orphan end, a single
* marker with no following content, a paired block enclosing nothing) —
* those are reported best-effort in `blocks` (covering whatever content is
* actually present, or an empty span) AND named explicitly in `malformed`,
* mirroring the well-formedness rules `tests/plan-phase-compact-split.test.cjs`'s
* `'every msd:protected sentinel...'` test already enforced for the one
* existing split. The check-4 caller decides what to do with a malformed
* sentinel; this function's job is only to describe the file accurately.
*
* Single-marker coverage: from the marker line, skip blank lines, then
* collect (trimmed) lines until a blank line, another sentinel line, or EOF.
* Paired coverage: every non-blank (trimmed) line strictly between the start
* and end sentinel lines.
*
* @param {string} content
* @returns {{
* blocks: Array<{kind: 'single'|'paired', lines: string[], firstLine: string}>,
* malformed: Array<{line: number, message: string}>,
* }}
*/
function extractProtectedBlocks(content) {
const lines = content.split(/\r?\n/);
const blocks = [];
const malformed = [];
const singleMarkerLines = [];
let openStart = -1;
for (let i = 0; i < lines.length; i++) {
const trimmed = lines[i].trim();
if (trimmed === PROTECTED_START) {
if (openStart !== -1) {
// A new start while one is already open: the earlier open span is
// abandoned (best-effort — it never gets a `blocks` entry) and this
// one becomes the tracked open start.
malformed.push({ line: i + 1, message: `nested/unclosed msd:protected:start at line ${i + 1}` });
}
openStart = i;
} else if (trimmed === PROTECTED_END) {
if (openStart === -1) {
malformed.push({ line: i + 1, message: `msd:protected:end with no matching start at line ${i + 1}` });
continue;
}
const covered = lines.slice(openStart + 1, i).map((l) => l.trim()).filter((l) => l.length > 0);
if (covered.length === 0) {
malformed.push({
line: openStart + 1,
message: `protected block at lines ${openStart + 1}-${i + 1} encloses no content`,
});
}
blocks.push({ kind: 'paired', lines: covered, firstLine: covered[0] || '' });
openStart = -1;
} else if (trimmed === PROTECTED_SINGLE) {
singleMarkerLines.push(i);
}
}
if (openStart !== -1) {
malformed.push({ line: openStart + 1, message: 'a msd:protected:start sentinel was never closed' });
const covered = lines.slice(openStart + 1).map((l) => l.trim()).filter((l) => l.length > 0);
blocks.push({ kind: 'paired', lines: covered, firstLine: covered[0] || '' });
}
for (const idx of singleMarkerLines) {
let j = idx + 1;
while (j < lines.length && lines[j].trim() === '') j++;
if (j >= lines.length || lines[j].trim().length === 0) {
malformed.push({ line: idx + 1, message: `single protected marker at line ${idx + 1} has no following content` });
blocks.push({ kind: 'single', lines: [], firstLine: '' });
continue;
}
const covered = [];
for (let k = j; k < lines.length; k++) {
const trimmed = lines[k].trim();
if (trimmed === '' || trimmed === PROTECTED_START || trimmed === PROTECTED_END || trimmed === PROTECTED_SINGLE) break;
covered.push(trimmed);
}
blocks.push({ kind: 'single', lines: covered, firstLine: covered[0] || '' });
}
return { blocks, malformed };
}
/** Trailer key for check 5 (boundary moves), PARTITION-RULES.md check 5. */
const ACK_TRAILER_BOUNDARY_MOVE = 'Boundary-Move-Declared';
// Record/value separators for the `git log --format` trailer extraction
// below — same ASCII control characters, same escaped-hex-in-`separator=`
// gotcha, as `readAckTrailers` (`tests/helpers/emitted-runtime.cjs`) and
// `ship.md:312`'s own `%(trailers:...)` extraction. Only one trailer key
// space here (unlike the hash/growth pair `readAckTrailers` reads), so no
// field separator is needed — one placeholder per commit record.
const BOUNDARY_RECORD_SEP = '\x1e'; // between commits
const BOUNDARY_VALUE_SEP = '\x1d'; // between multiple values of the SAME trailer key on one commit
/**
* Bounded git invocation for the boundary-move trailer reader, built on the
* never-throws process seam (`runGit`) so a PER-CALL `timeoutMs` override is
* honored (mirrors `emitted-runtime.cjs`'s private `ackTrailerGit`, which
* this reimplements locally since it is not exported). A git failure THROWS
* — never an empty result — same "a git failure is a hard error" law as
* every other git-touching helper in this test suite.
*/
function boundaryTrailerGit(args, { cwd = REPO_ROOT, timeoutMs = GIT_TIMEOUT_MS } = {}) {
const result = runGit([...safeDirArgs(cwd), ...args], { cwd, timeoutMs });
if (result.outcome === OUTCOME.EXITED && result.exitCode === 0) {
return result.stdout;
}
throw new Error(
`boundary-move-trailer: \`git ${args.join(' ')}\` failed — outcome=${result.outcome} `
+ `exitCode=${result.exitCode} stderr=${(result.stderr || '').trim()}`,
);
}
/**
* Parse raw `Boundary-Move-Declared` trailer VALUES (no git I/O) into the
* declarations map, applying the SAME rules `parseAckTrailers`
* (`tests/helpers/emitted-diff.cjs`) applies to its two spaces, narrowed to
* one: empty key -> error, empty reason -> error, two declarations of the
* SAME key with the SAME reason (after `normalizeAckReason`) -> silent
* dedupe (keep the first), two declarations of the same key with DIFFERENT
* reasons -> error and drop the key entirely (never guess a winner).
*
* Keys here are deliberately NOT validated against `<`/`>`/whitespace or the
* `__proto__`/`constructor`/`prototype` reserved set the ack-trailer parser
* rejects — those defenses exist because `parseAckTrailers`' output keys
* plain OBJECTS (`ackHash`/`ackGrowth` consumers). `declarations` here is a
* `Map`, which has no prototype-pollution surface, so narrowing to only the
* rules PARTITION-RULES.md's check 5 actually specifies avoids inventing
* validation the spec never asked for.
*
* @param {string[]} values
* @returns {{declarations: Map<string, {reason: string}>, errors: string[]}}
*/
function parseBoundaryMoveTrailerValues(values) {
const errors = [];
const declarations = new Map();
const conflicted = new Set(); // keys already reported ambiguous — never resurrected
for (const raw of values) {
const delimIndex = raw.indexOf(ACK_TRAILER_DELIM);
if (delimIndex === -1) {
errors.push(
`${ACK_TRAILER_BOUNDARY_MOVE}: trailer value ${JSON.stringify(raw)} has no `
+ `"${ACK_TRAILER_DELIM}" delimiter — expected "<spine-path> — <reason>"`,
);
continue;
}
const key = raw.slice(0, delimIndex).trim();
const reason = raw.slice(delimIndex + ACK_TRAILER_DELIM.length).trim();
if (key === '') {
errors.push(`${ACK_TRAILER_BOUNDARY_MOVE}: trailer value ${JSON.stringify(raw)} has an empty key`);
continue;
}
if (reason === '') {
errors.push(
`${ACK_TRAILER_BOUNDARY_MOVE}: trailer value ${JSON.stringify(raw)} has an empty reason — `
+ 'name it and say why',
);
continue;
}
if (conflicted.has(key)) continue;
const existing = declarations.get(key);
if (existing === undefined) {
declarations.set(key, { reason });
} else if (normalizeAckReason(existing.reason) === normalizeAckReason(reason)) {
// Identical after normalization — dedupe silently, keep the first declaration.
} else {
errors.push(
`${ACK_TRAILER_BOUNDARY_MOVE}: trailer key "${key}" is declared twice with ambiguous, `
+ 'conflicting reasons — an ambiguous declaration cannot silently pick a winner',
);
declarations.delete(key);
conflicted.add(key);
}
}
return { declarations, errors };
}
/**
* Read `Boundary-Move-Declared` commit trailers over `<merge-base>..<headRef>`
* (never `<baseRef>..<headRef>` — a two-dot range would let the diff and the
* commit range being checked disagree about what "this PR" means, the same
* correction ADR-3942 made for its own ack trailers).
*
* Fails closed: an uncomputable merge-base (shallow clone, unrelated
* histories, a bad ref) THROWS — never returns an empty Map, which would
* silently read as "no boundary moves to excuse" and disarm check 5 exactly
* the way an empty `readAckTrailers` result would disarm ADR-3942's gate.
*
* @param {{baseRef: string, headRef?: string, cwd?: string, timeoutMs?: number}} opts
* @returns {{declarations: Map<string, {reason: string}>, errors: string[]}}
*/
function readBoundaryMoveTrailers({ baseRef, headRef = 'HEAD', cwd = REPO_ROOT, timeoutMs = GIT_TIMEOUT_MS } = {}) {
let mergeBaseOut;
try {
mergeBaseOut = boundaryTrailerGit(['merge-base', baseRef, headRef], { cwd, timeoutMs });
} catch (err) {
throw new Error(
`boundary-move-trailer: could not compute a merge-base range for "${baseRef}..${headRef}" `
+ '(a bad ref, a shallow clone with no common ancestor, or another git failure): '
+ err.message,
);
}
const mergeBase = mergeBaseOut.trim();
if (!/^[0-9a-f]{40}$/.test(mergeBase)) {
throw new Error(
`boundary-move-trailer: git merge-base for "${baseRef}..${headRef}" returned no usable `
+ `commit (${JSON.stringify(mergeBase)}) — the range is structurally uncomputable `
+ '(possibly a shallow clone with no common ancestor).',
);
}
const valueSepHex = `%x${BOUNDARY_VALUE_SEP.codePointAt(0).toString(16).padStart(2, '0')}`;
const format = `${BOUNDARY_RECORD_SEP}%(trailers:key=${ACK_TRAILER_BOUNDARY_MOVE},valueonly,separator=${valueSepHex})`;
let raw;
try {
raw = boundaryTrailerGit(['log', `${mergeBase}..${headRef}`, `--format=${format}`], { cwd, timeoutMs });
} catch (err) {
throw new Error(`boundary-move-trailer: could not read commit trailers over the range: ${err.message}`);
}
const normalized = raw.replace(/\r/g, ''); // a CRLF commit message must parse identically to LF
const values = [];
// index 0 is the (empty) text before the FIRST record separator — every
// real record starts with one, by construction of the `--format` string.
const records = normalized.split(BOUNDARY_RECORD_SEP).slice(1);
for (const record of records) {
const field = record.replace(/\n+$/, ''); // git's own between-commit newline
for (const v of field.split(BOUNDARY_VALUE_SEP)) if (v !== '') values.push(v);
}
return parseBoundaryMoveTrailerValues(values);
}
/**
* Check 2 (disjointness): no non-trivial line may appear in both a spine and
* any of its detail parts. Checked on every registered pair regardless of
* what a given PR touched — this is what keeps duplication from creeping
* back in after a split is made.
*
* The canonical `msd_run` launcher preamble is excluded from both sides
* before comparing (`isKnownSanctionedBoilerplate`) — it is sanctioned
* cross-file duplication under a different guard's contract, not a
* violation of this one.
*
* Capped at the first 20 reported violations PER SPLIT, to avoid a
* runaway report on a badly-drifted pair; this is a reporting cap only,
* not a detection cap — `ok` (computed by the caller from array length) is
* unaffected either way since any violation at all fails the check.
*
* @param {ReturnType<typeof discoverRegisteredSplits>} splits - already
* filtered by the caller to `spineExists: true` entries.
* @returns {Array<{splitName: string, detailPath: string, line: string}>}
*/
function checkDisjointness(splits) {
const violations = [];
const PER_SPLIT_CAP = 20;
for (const split of splits) {
const spineLines = normalizeNonTrivialLines(fs.readFileSync(split.spinePath, 'utf8'))
.filter((l) => !isKnownSanctionedBoilerplate(l));
const spineSet = new Set(spineLines);
let reported = 0;
for (const detailPath of split.detailPaths) {
if (reported >= PER_SPLIT_CAP) break;
const detailLines = normalizeNonTrivialLines(fs.readFileSync(detailPath, 'utf8'))
.filter((l) => !isKnownSanctionedBoilerplate(l));
for (const line of detailLines) {
if (reported >= PER_SPLIT_CAP) break;
if (spineSet.has(line)) {
violations.push({ splitName: split.name, detailPath, line });
reported++;
}
}
}
}
return violations;
}
/** A detail-path-shaped substring in spine prose: `<segments>/detail/<segments>.md`. */
const DETAIL_REFERENCE_PATTERN = /[\w./-]+\/detail\/[\w./-]+\.md/g;
/**
* Check 3 (registration): a `<name>/detail/*.md` with no `<name>.md` spine
* fails as an orphan; a spine that never mentions one of its own detail
* paths fails as unreferenced; and any detail-path-SHAPED substring in the
* spine's prose that does not resolve to a real file fails as a dangling
* reference — three independent ways the spine/detail pairing can rot.
*
* Runs uncritically over every split `discoverRegisteredSplits` found,
* including `spineExists: false` entries — that is exactly the orphan case
* this check exists to name.
*
* @param {ReturnType<typeof discoverRegisteredSplits>} splits
* @param {string} workflowsDir
* @returns {Array<object>} violations, one of:
* `{kind: 'orphan_detail', name, detailPaths}`
* `{kind: 'unreferenced_detail', name, detailPath}`
* `{kind: 'dangling_reference', name, referencedPath}`
*/
function checkRegistration(splits, workflowsDir) {
const violations = [];
for (const split of splits) {
if (!split.spineExists) {
violations.push({ kind: 'orphan_detail', name: split.name, detailPaths: split.detailPaths });
continue;
}
const spineContent = fs.readFileSync(split.spinePath, 'utf8');
for (const detailPath of split.detailPaths) {
const rel = path.relative(workflowsDir, detailPath).split(path.sep).join('/');
if (!spineContent.includes(rel)) {
violations.push({ kind: 'unreferenced_detail', name: split.name, detailPath });
}
}
// The prose spells these out in full — e.g. `msd-core/workflows/plan-phase/
// detail/elaboration.md` — i.e. relative to the REPO ROOT (`workflowsDir`'s
// grandparent: `workflowsDir` = `<root>/msd-core/workflows`), not relative
// to `workflowsDir` itself (verified against plan-phase.md's real
// references; resolving against `workflowsDir` directly double-prefixes
// `msd-core/workflows/` and false-positives every real reference).
const repoRoot = path.join(workflowsDir, '..', '..');
const referenced = new Set(spineContent.match(DETAIL_REFERENCE_PATTERN) || []);
for (const referencedPath of referenced) {
const resolved = path.join(repoRoot, referencedPath);
// Containment check: DETAIL_REFERENCE_PATTERN allows `.` and `/` freely, so
// spine prose shaped like `../../../etc/detail/passwd.md` would otherwise
// resolve outside repoRoot and turn this existence check into a path-traversal
// oracle (security review finding, #4403). A resolved path outside repoRoot
// can never be a real detail file this repo ships, so it is reported the same
// as any other dangling reference rather than probed on disk.
const relFromRoot = path.relative(repoRoot, resolved);
const escapesRoot = relFromRoot === '' || relFromRoot.startsWith('..') || path.isAbsolute(relFromRoot);
if (escapesRoot || !fs.existsSync(resolved)) {
violations.push({ kind: 'dangling_reference', name: split.name, referencedPath });
}
}
}
return violations;
}
/**
* Every detail file is a NEW shipped file (it did not exist before its
* split), so it must stay under the same `NEW_FILE_CAP` (32768 bytes,
* `tests/helpers/emitted-diff.cjs`, ADR-1610 Decision point 3) any other
* brand-new workflow/agent file is held to — generalizes the pilot's
* `tests/plan-phase-compact-split.test.cjs` size assertion to every split.
*
* @param {ReturnType<typeof discoverRegisteredSplits>} splits
* @returns {Array<{splitName: string, detailPath: string, size: number}>}
*/
function checkDetailFileSizeCap(splits) {
const violations = [];
for (const split of splits) {
for (const detailPath of split.detailPaths) {
const size = fs.statSync(detailPath).size;
if (size >= NEW_FILE_CAP) {
violations.push({ splitName: split.name, detailPath, size });
}
}
}
return violations;
}
module.exports = {
DEFAULT_WORKFLOWS_DIR,
discoverRegisteredSplits,
normalizeNonTrivialLines,
isKnownSanctionedBoilerplate,
extractProtectedBlocks,
ACK_TRAILER_BOUNDARY_MOVE,
readBoundaryMoveTrailers,
parseBoundaryMoveTrailerValues,
checkDisjointness,
checkRegistration,
checkDetailFileSizeCap,
NEW_FILE_CAP,
};