* test(#3674): characterize existing wave-dispatch output and add tests for the extracted partitioner Pins resolveWaveDispatch's and emitWorkflowScript's current, unextracted output (chain-overlap, disjoint-empty-set, and a multi-wave/multi-stage golden script) as a regression safety net ahead of extracting partitionStages into a standalone module. Also adds the new module's unit and property tests (test matrix rows 1-11) against its expected public API, which does not exist yet and is added in the next commit. * feat(#3674): extract file-overlap partitioner into a shared, generic module Moves partitionStages' greedy first-fit file-overlap algorithm into a new, dependency-free src/file-overlap-partitioner.cts module (partitionByFileOverlap), generalized over a plain {id, files}[] shape rather than claude-orchestration.cts's Plan/Wave interfaces. partitionStages becomes a thin adapter mapping its own Plan[] shape onto the generic input and back — behavior-preserving, no dependency ordering, no path normalization, no filesystem access moved or added. Enables a future consumer (quick-batch, #3675 / ADR-1239) to reuse the same primitive without pulling in orchestration internals. * docs(#3674): register the file-overlap-partitioner module bookkeeping New src/*.cts -> bin/lib/*.cjs modules need four hand-maintained registrations beyond the code itself: .gitignore (compiled artifact), eslint.config.mjs (ADR-457: lint the .cts, not the emitted .cjs), docs/INVENTORY.md's CLI Modules roster row (regenerated via gen-inventory-manifest.cjs --write), and a CONTEXT.md glossary entry matching the convention set by similarly-scoped leaf modules (text-lines.cts, plan-dependency-graph.cts, spec-section.cts). * fix(#3674): alphabetize INVENTORY.md row, manifest regen no-op, fast-check import already correct - docs/INVENTORY.md: move file-overlap-partitioner.cjs row to alphabetical position - docs/INVENTORY-MANIFEST.json: regenerated via gen-inventory-manifest.cjs --write, produced no diff (manifest is keyed by content, not row order) - tests/claude-orchestration.test.cjs's direct require('fast-check') is correct as-is: tests/helpers/fast-check-setup.cjs's own docstring scopes the shared-seed wrapper to "every *.property.test.cjs file"; claude-orchestration.test.cjs is not a .property.test.cjs file, and every .property.test.cjs file sampled uses the wrapper consistently. No outlier. * fix(#3674): constrain the no-overlap property test to unique ids, fixing an ambiguous duplicate-id reconstruction The `no two plans in the same stage share a modified file` property reconstructs which physical item produced each output id via `remaining.findIndex(r => r.id === id)`. Under duplicate ids (an explicitly-supported input shape for `partitionByFileOverlap`) that reconstruction can pick the wrong physical occurrence, producing a false-positive overlap failure (observed counterexample: p0(f1), p208(f1), p208([]) — correctly staged as [[p0,p208#2],[p208#1]], but misread by id-order as [[p0,p208#1],...], which do overlap). Properties (a) determinism and (b) totality already exercise duplicate ids correctly and are left unchanged; only this property's generated items are now constrained to unique ids via `fc.uniqueArray`, where the reconstruction is unambiguous. --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -43,6 +43,10 @@
|
||||
* Zero external dependencies. Pure functions. Never throws on bad input.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- file-overlap-partitioner.cjs is an export= CommonJS module
|
||||
import fileOverlapPartitionerMod = require('./file-overlap-partitioner.cjs');
|
||||
const { partitionByFileOverlap } = fileOverlapPartitionerMod;
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -330,29 +334,15 @@ interface EmitErr {
|
||||
*
|
||||
* This is the same overlap rule execute-phase applies inline — the only difference
|
||||
* is the execution vehicle (Workflow `parallel()` vs one-agent-per-message).
|
||||
*
|
||||
* #3674: the algorithm itself now lives in the generic, dependency-free
|
||||
* `file-overlap-partitioner.cts` module (`partitionByFileOverlap`) — this is a
|
||||
* thin adapter mapping this module's own `Plan[]` shape onto that module's
|
||||
* generic `{ id, files }[]` input and back. Behavior-preserving extraction:
|
||||
* same greedy first-fit algorithm, same output, only the implementation moved.
|
||||
*/
|
||||
function partitionStages(plans: Plan[]): string[][] {
|
||||
const stages: { plans: Plan[]; files: Set<string> }[] = [];
|
||||
for (const plan of plans) {
|
||||
const fileSet = new Set(plan.files_modified);
|
||||
let placed = false;
|
||||
for (const stage of stages) {
|
||||
let overlap = false;
|
||||
for (const f of fileSet) {
|
||||
if (stage.files.has(f)) { overlap = true; break; }
|
||||
}
|
||||
if (!overlap) {
|
||||
stage.plans.push(plan);
|
||||
for (const f of fileSet) stage.files.add(f);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
stages.push({ plans: [plan], files: new Set(fileSet) });
|
||||
}
|
||||
}
|
||||
return stages.map((s) => s.plans.map((p) => p.id));
|
||||
return partitionByFileOverlap(plans.map((p) => ({ id: p.id, files: p.files_modified })));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
77
src/file-overlap-partitioner.cts
Normal file
77
src/file-overlap-partitioner.cts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* File-overlap partitioner — shared greedy first-fit stage assignment (#3674).
|
||||
*
|
||||
* Extracted from `claude-orchestration.cts`'s `partitionStages` (#1143), which
|
||||
* emits sequential Workflow `parallel()` stage barriers so that no two plans
|
||||
* sharing a `files_modified` entry ever cohabit a stage. This module is the
|
||||
* SAME algorithm, generalized: it depends on no `Plan`/`Wave` interface from
|
||||
* `claude-orchestration.cts` (or any other caller-specific shape), so a future
|
||||
* consumer (quick-batch, #3675 / ADR-1239 "Quick-batch binding") can partition
|
||||
* its own planned-path items without pulling in orchestration internals.
|
||||
*
|
||||
* This is a pure, behavior-preserving extraction (#3674's acceptance bar is
|
||||
* byte-identical output for `claude-orchestration.cts`'s existing callers —
|
||||
* not an improvement). Explicitly OUT of scope, per the #3674 design lock:
|
||||
* - dependency-DAG ordering — this module only ever sees a flat item list
|
||||
* and file-overlap; a caller resolves dependency order before calling in
|
||||
* (same contract `partitionStages` already had);
|
||||
* - path normalization — file entries are compared by exact string equality
|
||||
* only. `Foo.ts` vs `foo.ts`, or `src/a.ts` vs `src\a.ts`, are treated as
|
||||
* DISTINCT files. This is deliberate, not a gap to "fix" during extraction;
|
||||
* - filesystem access — this module never reads a path off disk.
|
||||
*
|
||||
* Zero external dependencies. Pure function. Never throws on well-typed input.
|
||||
*/
|
||||
|
||||
/** A generic overlap-partitioned item: an id plus the files it touches. */
|
||||
interface OverlapItem {
|
||||
id: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Partition `items` into a near-minimal number of sequential stages (via
|
||||
* greedy first-fit — not guaranteed optimal for arbitrary overlap graphs, but
|
||||
* correct: no two items sharing a file ever cohabit a stage) such that no two
|
||||
* items in the same stage share a file. Each item goes into the earliest
|
||||
* stage where it does not overlap any item already there, in input order.
|
||||
*
|
||||
* An item with an EMPTY `files` array declares no files; it overlaps nothing
|
||||
* and coalesces into stage 0 (mirrors `partitionStages`' original behavior —
|
||||
* this module cannot guard against undeclared concurrent writes; a caller
|
||||
* must declare `files` accurately).
|
||||
*
|
||||
* File comparison is EXACT STRING EQUALITY — no path normalization, no
|
||||
* case-folding, no separator canonicalization. Duplicate `id`s in the input
|
||||
* are NOT deduplicated; each item is placed independently, in input order.
|
||||
*
|
||||
* Deterministic: identical input (including input order) always yields an
|
||||
* identical partition.
|
||||
*/
|
||||
function partitionByFileOverlap(items: OverlapItem[]): string[][] {
|
||||
const stages: { items: OverlapItem[]; files: Set<string> }[] = [];
|
||||
for (const item of items) {
|
||||
const fileSet = new Set(item.files);
|
||||
let placed = false;
|
||||
for (const stage of stages) {
|
||||
let overlap = false;
|
||||
for (const f of fileSet) {
|
||||
if (stage.files.has(f)) { overlap = true; break; }
|
||||
}
|
||||
if (!overlap) {
|
||||
stage.items.push(item);
|
||||
for (const f of fileSet) stage.files.add(f);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed) {
|
||||
stages.push({ items: [item], files: new Set(fileSet) });
|
||||
}
|
||||
}
|
||||
return stages.map((s) => s.items.map((i) => i.id));
|
||||
}
|
||||
|
||||
export = {
|
||||
partitionByFileOverlap,
|
||||
};
|
||||
Reference in New Issue
Block a user