Branch Splitter
Split one payload into a fixed, declared, closed set of named branches — one labeled JSONL record per declared branch, in declared order. It is the open half of a parallel-independent compose (the ⊗ product): fanout splits, its pair junction merges, so fanout | junction round-trips. Zero dependencies, pure function — same payload plus the same branch list yield byte-identical output every run. It fails closed (non-zero exit, offending branch named) on an empty, whitespace, duplicate, or charset-invalid branch, and has no default branch: it emits only to branches you declared, never to one it inferred. Runs in Node or the browser (window.ForestGifts.fanout).
The honest edge
fanout splits one input into declared branches; it does not run them, order them by any policy but declared order, or judge whether a branch name is meaningful — it only refuses an undeclared, empty, duplicate, or ill-formed branch.
Run it
node fanout.js --branches a,b,c "the prompt"
test_fanout.js (17/17, golden corpus + determinism + non-vacuity); braid test_braid.js 65/65 proves fanout | junction round-trips
Node / browser, no dependencies
The code — every file that ships
fanout.js237 lineson GitHub →
#!/usr/bin/env node
/* fanout.js — split one input into a fixed, DECLARED set of named branches.
WHY THIS EXISTS. When one base prompt (or any payload) must go to several
named destinations — models, variants, personas, test conditions — the set
of destinations should be EXPLICIT and CLOSED, decided up front, not
discovered at runtime. The usual ad-hoc answer builds the branch list on the
fly from whatever keys happen to be present, and the day a typo or an
unhandled case introduces a branch nobody declared, the payload fans to a
destination no one signed off on — silently. fanout refuses that. You declare
the branches; it emits exactly one labeled record per declared branch, in
declared order, and it FAILS CLOSED the instant it is asked to emit to a
branch that was not declared, is empty, is duplicated, or is ill-formed.
It is the OPEN half of a parallel-independent compose (the ⊗ product): fanout
splits, its pair `junction` merges. Together they give a pipeline the
parallel product to sit beside sequential (`|`) composition.
THE ONE DISCIPLINE (the whole reason to trust it). It emits ONLY to branches
you declared (the ⊢ rule: claim only what you can prove). There is no default
branch and no inferred branch. An undeclared, empty, duplicate, or
charset-invalid branch is not a warning to absorb — it is a non-zero exit with
the offending branch named. That refusal is the feature.
Pure function of its inputs. No dependencies. Same input + same branch list
-> byte-identical JSONL, every run, every seed. Order is DECLARED order,
never hash/set order. Runs in a browser (attach fanout to your namespace) or
on Node (this CLI / require()).
USAGE
node fanout.js --branches a,b,c "the prompt" # payload as argument
echo -n "the prompt" | node fanout.js --branches a,b,c # payload from stdin (exact bytes)
node fanout.js --branches-file branches.txt "the prompt" # one branch per line
node fanout.js --help
OUTPUT (JSONL, one record per declared branch, in declared order):
{"branch":"a","input":"the prompt","seq":0,"of":3}
{"branch":"b","input":"the prompt","seq":1,"of":3}
{"branch":"c","input":"the prompt","seq":2,"of":3}
Released under MIT. Its edge is printed in the README: fanout splits one
input into declared branches; it does not RUN them, order them by any policy
but declared order, or judge whether a branch NAME is meaningful — it only
refuses an undeclared, empty, duplicate, or ill-formed branch.
*/
"use strict";
// The one charset rule: a branch name is a non-empty token of [A-Za-z0-9._-].
// (The callsign-safe alphabet — reuse the charset rule, do not re-invent it.)
var BRANCH_RE = /^[A-Za-z0-9._-]+$/;
// A structured refusal. The CLI turns this into a non-zero exit with the
// message; require() callers get a thrown Error they can catch.
function FanoutError(message) {
var e = new Error(message);
e.name = "FanoutError";
return e;
}
/* parseBranches(spec) -> array of validated branch names, in declared order.
`spec` is an array of raw tokens (already split from a CSV or a file). Fails
closed on: empty list, an empty/whitespace token, a duplicate, a
charset-invalid token. The offending branch is always named. */
function parseBranches(spec) {
if (!Array.isArray(spec)) {
throw FanoutError("branches: expected a list of branch names");
}
var branches = [];
var seen = Object.create(null);
for (var i = 0; i < spec.length; i++) {
var raw = spec[i];
var name = (typeof raw === "string") ? raw.trim() : String(raw).trim();
if (name.length === 0) {
throw FanoutError("branches: empty branch name at position " + i +
" (an undeclared/blank destination is exactly what fanout refuses)");
}
if (!BRANCH_RE.test(name)) {
throw FanoutError("branches: ill-formed branch name " + JSON.stringify(name) +
" (allowed: letters, digits, dot, underscore, hyphen)");
}
if (seen[name]) {
throw FanoutError("branches: duplicate branch name " + JSON.stringify(name) +
" (an ambiguous fan is refused; each declared branch must be unique)");
}
seen[name] = true;
branches.push(name);
}
if (branches.length === 0) {
throw FanoutError("branches: no branches declared " +
"(fanout has no default branch — declare at least one)");
}
return branches;
}
/* fanout(input, branches) -> array of records, one per declared branch, in
declared order. Pure: no I/O, no clock, no randomness. `input` is coerced via
String() exactly like the shipped sha256 gift, so a caller passing a number
or boolean gets deterministic behavior rather than a surprise. */
function fanout(input, branches) {
var validated = parseBranches(branches);
var payload = (typeof input === "string") ? input : String(input);
var of = validated.length;
var records = [];
for (var i = 0; i < of; i++) {
records.push({ branch: validated[i], input: payload, seq: i, of: of });
}
return records;
}
/* fanoutJSONL(input, branches) -> the canonical serialized form: one JSON
object per line, keys in fixed order (branch,input,seq,of), in declared
branch order, terminated by a single trailing newline. This string IS the
gift's canonical output — same input + same branches yields byte-identical
text every run. */
function fanoutJSONL(input, branches) {
var records = fanout(input, branches);
var lines = [];
for (var i = 0; i < records.length; i++) {
var r = records[i];
// Fixed key order, JSON.stringify per-field to get correct escaping.
lines.push(
"{" +
"\"branch\":" + JSON.stringify(r.branch) + "," +
"\"input\":" + JSON.stringify(r.input) + "," +
"\"seq\":" + r.seq + "," +
"\"of\":" + r.of +
"}"
);
}
return lines.join("\n") + "\n";
}
// Browser: attach to a namespace. Node/require: export. CLI: run below.
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.fanout = fanout;
window.ForestGifts.fanoutJSONL = fanoutJSONL;
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { fanout: fanout, fanoutJSONL: fanoutJSONL, parseBranches: parseBranches };
}
// ---- CLI (runs only when invoked directly, never on require) ----------------
function readBranchesArg(args) {
var iCsv = args.indexOf("--branches");
var iFile = args.indexOf("--branches-file");
if (iCsv !== -1 && iFile !== -1) {
throw FanoutError("choose one of --branches or --branches-file, not both");
}
if (iCsv !== -1) {
var csv = args[iCsv + 1];
if (csv === undefined) throw FanoutError("--branches needs a comma-separated value");
return csv.split(",");
}
if (iFile !== -1) {
var path = args[iFile + 1];
if (path === undefined) throw FanoutError("--branches-file needs a path");
var fs = require("fs");
var text = fs.readFileSync(path, "utf8");
// One branch per line; blank lines are dropped before validation so a
// trailing newline in the file is not itself an "empty branch".
return text.split("\n").map(function (s) { return s.replace(/\r$/, ""); })
.filter(function (s) { return s.trim().length > 0; });
}
throw FanoutError("no branches declared: pass --branches a,b,c or --branches-file <path>");
}
function positionalPayload(args) {
// The payload is the first argument that is not a flag and not a flag's value.
var flagsWithValue = { "--branches": true, "--branches-file": true };
for (var i = 0; i < args.length; i++) {
var a = args[i];
if (a.charAt(0) === "-") { // a flag; skip it and its value if it takes one
if (flagsWithValue[a]) i++;
continue;
}
return a;
}
return null; // no positional -> read stdin
}
function main(argv) {
var args = argv.slice(2);
if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
process.stdout.write(
"fanout.js — split one input into a fixed, DECLARED set of named branches.\n\n" +
" node fanout.js --branches a,b,c \"the prompt\" payload as argument\n" +
" echo -n \"the prompt\" | node fanout.js --branches a,b,c payload from stdin (exact bytes)\n" +
" node fanout.js --branches-file branches.txt \"the prompt\" one branch per line\n" +
" node fanout.js --help\n\n" +
"Emits one JSONL record per DECLARED branch, in declared order. Fails closed\n" +
"(non-zero exit, branch named) on an undeclared, empty, duplicate, or ill-formed\n" +
"branch. There is no default branch.\n\n" +
"Edge: fanout SPLITS; it does not run the branches, order them by any policy but\n" +
"declared order, or judge whether a branch name is meaningful.\n"
);
return 0;
}
var branchSpec;
try {
branchSpec = readBranchesArg(args);
} catch (e) {
process.stderr.write("fanout: " + e.message + "\n");
return 2;
}
var payload = positionalPayload(args);
function emit(input) {
try {
process.stdout.write(fanoutJSONL(input, branchSpec));
return 0;
} catch (e) {
process.stderr.write("fanout: " + e.message + "\n");
return 2;
}
}
if (payload !== null) {
return emit(payload);
}
// stdin: use the exact bytes received (no trailing-newline munging).
var chunks = [];
process.stdin.on("data", function (d) { chunks.push(d); });
process.stdin.on("end", function () {
var buf = Buffer.concat(chunks);
process.exitCode = emit(buf.toString("utf8"));
});
return 0;
}
if (typeof require !== "undefined" && require.main === module) {
process.exitCode = main(process.argv);
}
test_fanout.js129 lineson GitHub →
#!/usr/bin/env node
/* test_fanout.js — the golden corpus + determinism self-test + non-vacuity bite
for the fanout gift. Zero dependencies (Node assert only). This file IS the
canonicalizer self-test the Plumb cites (Gift-Works Procedure v1 §2): a gift
whose output is byte-identical across repeated evaluation has proven its
canonical form is idempotent and order-faithful.
Run: node test_fanout.js # exit 0 GREEN / non-zero RED
*/
"use strict";
var assert = require("assert");
var F = require("./fanout.js");
var pass = 0, fail = 0;
function check(name, fn) {
try { fn(); pass++; }
catch (e) { fail++; console.error("FAIL: " + name + " — " + e.message); }
}
// ---- GOLDEN CORPUS: the known-good half -----------------------------------
// Each vector is (payload, branches, expected JSONL). Expected is written out
// literally so the corpus is a fixed oracle, not a re-derivation of the code.
var GOLDEN = [
{
name: "single branch",
input: "p", branches: ["a"],
expect: '{"branch":"a","input":"p","seq":0,"of":1}\n'
},
{
name: "three branches, declared order preserved",
input: "the prompt", branches: ["a", "b", "c"],
expect:
'{"branch":"a","input":"the prompt","seq":0,"of":3}\n' +
'{"branch":"b","input":"the prompt","seq":1,"of":3}\n' +
'{"branch":"c","input":"the prompt","seq":2,"of":3}\n'
},
{
name: "declared order is NOT sorted (z before a)",
input: "x", branches: ["z", "a"],
expect:
'{"branch":"z","input":"x","seq":0,"of":2}\n' +
'{"branch":"a","input":"x","seq":1,"of":2}\n'
},
{
name: "multibyte payload fidelity (accents, emoji, non-latin)",
input: "café🦌日本語", branches: ["m"],
expect: '{"branch":"m","input":"café🦌日本語","seq":0,"of":1}\n'
},
{
name: "payload with quotes/newlines/tabs is JSON-escaped",
input: 'line1\nline2\t"q"', branches: ["e"],
expect: '{"branch":"e","input":"line1\\nline2\\t\\"q\\"","seq":0,"of":1}\n'
},
{
name: "charset-legal names: dot, underscore, hyphen, digits",
input: "p", branches: ["a.1", "b_2", "c-3"],
expect:
'{"branch":"a.1","input":"p","seq":0,"of":3}\n' +
'{"branch":"b_2","input":"p","seq":1,"of":3}\n' +
'{"branch":"c-3","input":"p","seq":2,"of":3}\n'
},
{
name: "String() coercion parity: number payload",
input: 42, branches: ["n"],
expect: '{"branch":"n","input":"42","seq":0,"of":1}\n'
}
];
GOLDEN.forEach(function (v) {
check("golden: " + v.name, function () {
assert.strictEqual(F.fanoutJSONL(v.input, v.branches), v.expect);
});
});
// ---- FAIL-CLOSED: the known-bad half (real defects, not author-invented) ---
// Each is a branch declaration fanout MUST refuse. A refusal that does not
// throw is a silent fan to an undeclared/ambiguous destination — the exact
// fault this gift exists to prevent.
var BAD = [
{ name: "empty branch list", branches: [], match: /no branches declared/ },
{ name: "empty branch token", branches: ["a", ""], match: /empty branch name/ },
{ name: "whitespace-only token", branches: ["a", " "], match: /empty branch name/ },
{ name: "duplicate branch", branches: ["a", "a"], match: /duplicate branch name/ },
{ name: "charset-invalid (space)", branches: ["a", "b c"], match: /ill-formed branch name/ },
{ name: "charset-invalid (slash)", branches: ["a/b"], match: /ill-formed branch name/ }
];
BAD.forEach(function (v) {
check("fail-closed: " + v.name, function () {
assert.throws(function () { F.fanoutJSONL("p", v.branches); }, v.match,
"expected refusal for " + v.name);
});
});
// ---- NON-VACUITY MUTATION BITE --------------------------------------------
// A suite that cannot reject a wrong answer proves nothing. Assert that a
// deliberately-wrong expectation is CAUGHT (the negation must fail).
check("mutation bite: wrong output is rejected", function () {
var wrong = '{"branch":"a","input":"p","seq":9,"of":1}\n'; // seq tampered
assert.notStrictEqual(F.fanoutJSONL("p", ["a"]), wrong,
"the corpus must distinguish a wrong (tampered seq) output");
});
check("mutation bite: a refused case must actually throw", function () {
var threw = false;
try { F.fanoutJSONL("p", ["a", "a"]); } catch (e) { threw = true; }
assert.strictEqual(threw, true, "duplicate must throw — a non-throwing refuser has no teeth");
});
// ---- DETERMINISM SELF-TEST (the canonicalizer self-test, Gift-Works §2) ----
// Evaluate the same input N times; the output must be byte-identical every
// time. This proves the canonical form is idempotent and order-faithful on the
// gift's own output — the property the Plumb's canonicalizer self-test names.
check("determinism: byte-identical across 25 evaluations", function () {
var first = F.fanoutJSONL("det prompt 🦌", ["one", "two", "three"]);
for (var i = 0; i < 25; i++) {
assert.strictEqual(F.fanoutJSONL("det prompt 🦌", ["one", "two", "three"]), first,
"evaluation " + i + " diverged — output is not deterministic");
}
});
// Order-faithfulness: a permuted branch list yields a permuted output (order is
// carried, never normalized away).
check("determinism: order-faithful (permutation changes output)", function () {
var ab = F.fanoutJSONL("p", ["a", "b"]);
var ba = F.fanoutJSONL("p", ["b", "a"]);
assert.notStrictEqual(ab, ba, "declared order must be faithful, not normalized");
});
console.log((fail === 0 ? "GREEN" : "RED") + ": " + pass + " passed, " + fail + " failed");
process.exitCode = fail === 0 ? 0 : 3;