ini-source
ini-source reads sectioned INI / .gitconfig-style config text (a FILE arg, or stdin) and emits one uniform JSON object per assignment — {"section":S,"key":K,"value":V}, all strings — tagged with the section it lives under, the front of a pipe you feed into the fold/filter/transform gifts (ini-source app.ini | pluck --fields key,value). Lines split on the FIRST '=', full-line ; and # comments and blank lines skipped, section headers set the tag, outer quotes stripped, CRLF==LF, BOM stripped. Zero dependencies, runs unchanged in Node or a browser, same input yields byte-identical output every run.
The honest edge
ini-source parses sectioned INI text into uniform string records only — it does not coerce types ("8080" stays a string), does not interpolate ${VAR}, does not honor inline comments, and does not process escapes inside quotes. Its own honesty axis: a DUPLICATE key within a section, or a REPEATED [section] header, is REFUSED (exit 2, line-numbered) — never a silent overwrite (the common INI-parser behaviour that changes your data without telling you). A malformed line, an illegal/empty key, or a field-name collision is exit 2. An honest, pinnable parse — not a full INI runtime.
Run it
printf '[s]\nk=v\n' | node ini-source.js # -> {"section":"s","key":"k","value":"v"}
test_ini-source.js (43/43, independent regex-route oracle) + Plumb conformance GREEN (28/28, signed 2026-09-12, clock-independent, 2 mutation-bites non-vacuous)
Node / browser, no dependencies
The code — every file that ships
ini-source.js301 lineson GitHub →
#!/usr/bin/env node
/* ini-source.js — parse INI / .gitconfig-style sectioned config text into a uniform
JSONL record stream. Dependency-free, deterministic, pure. Runs in Node or a browser. MIT.
WHAT IT IS. A SOURCE: it turns non-JSONL sectioned config text (an .ini, a
.gitconfig-style file, an editorconfig-shaped file) into the front of a pipe — one JSON
object per line (JSONL) that the fold/filter/transform gifts consume. Give it a file (or
pipe text on stdin) and it emits one uniform record per assignment, tagged with the
section it lives under:
[server] -> (sets the current section)
host = localhost -> {"section":"server","key":"host","value":"localhost"}
port = 8080 -> {"section":"server","key":"port","value":"8080"}
; a comment -> (skipped)
[client]
host = 10.0.0.1 -> {"section":"client","key":"host","value":"10.0.0.1"}
Every record has the same three string fields, so it drops straight into a generic
filter or fold: ini-source app.ini | range-filter ... / ini-source app.ini | pluck ...
THE SHAPE (the design decision that makes it compose). One record per key-assignment,
the FLAT uniform triple {section, key, value} — NOT one nested record per section. A
nested {section, entries:{...}} record would carry the section's structure but would not
compose with the flat fold/filter/transform gifts (they cannot reach into a per-record
sub-object with a generic --field). The flat triple lets a downstream gift filter by
section, pluck the key/value, or fold across sections without knowing the file's shape.
Keys that appear BEFORE any [section] header (a global preamble, as in .gitconfig) are
emitted with section "" — the honest "no section" marker, never an invented default name.
STRINGS ONLY (inherited honesty axis). Values are emitted as LITERAL STRINGS —
ini-source does not guess types. "8080" stays the string "8080"; "true" stays "true".
The moment a parser coerces (is 08 octal? is 1e3 a number? is TRUE a boolean?) the SAME
file parses to different bytes under different tools, and a source you cannot pin is not
a source. For the same reason it does NOT interpolate ${VAR}/%VAR%, does NOT honor inline
(trailing) comments, and does NOT process backslash escapes inside a quoted value — each
is a context-dependent guess that would break byte determinism. It parses the grammar it
was given and REFUSES the rest, loudly.
THE HONESTY AXIS THAT IS ITS OWN (fail-closed on a duplicated key). A key that appears
TWICE within the same section is a HARD ERROR (exit 2), naming the line, the key, and the
section. This is the whole reason ini-source exists as its own gift rather than a flag on
kv-source: the common INI-parser behaviour is to silently keep the last (or the first) of
a duplicated key — which changes the caller's data without telling them. ini-source
refuses. The same key in DIFFERENT sections is fine (that is what sections are for); a
key repeated in the global preamble (section "") is a duplicate. A repeated [section]
HEADER is likewise refused (exit 2): merging or overriding two blocks of the same name is
the same silent guess one level up.
THE GRAMMAR (declared, so it is pinnable)
* A leading UTF-8 BOM on the first line is stripped (Windows INI files often carry one).
* Lines split on newline; a trailing CR (CRLF files) is stripped, so a file parses
identically whether it uses LF or CRLF.
* A whitespace-only line emits nothing.
* A line whose first non-whitespace character is ';' or '#' is a full-line COMMENT and
emits nothing. (There are no inline/trailing comments — a ';' inside a value is part
of the value.)
* A line that, trimmed, is [ NAME ] — begins with '[' and ends with ']' — is a SECTION
HEADER. NAME is the inner text trimmed; it must be non-empty and contain no '[' or
']'. An empty [] or a malformed header is a hard error (exit 2). The section it names
becomes the current section. A section name may appear only once.
* Any other non-blank line MUST be KEY=VALUE. It is split on the FIRST '='; everything
before is the key, everything after is the value.
- KEY is trimmed and MUST match [A-Za-z_][A-Za-z0-9_.-]* — a non-empty name. An
empty key, or a key with spaces/illegal characters, is a hard error (exit 2).
- VALUE is trimmed. If, after trimming, it both begins and ends with a matching
'"' or "'" (length >= 2), those OUTER quotes are stripped and the inner text is
the literal value (no escape processing). Otherwise the trimmed remainder is the
literal value.
* A non-blank, non-comment, non-header line with no '=' is a MALFORMED line (exit 2).
THE MODEL
[FILE] The INI file to parse. If omitted, read stdin.
--section-field NAME The object key the SECTION is emitted under. Default "section".
--key-field NAME The object key the parsed KEY is emitted under. Default "key".
--value-field NAME The object key the parsed VALUE is emitted under. Default "value".
The three field names must be pairwise distinct (else the record
would collapse) — a collision is exit 2.
--global NAME The section name given to keys that appear before any header.
Default "" (empty string).
DETERMINISM. parse(text, opts) is a pure function — no clock, no randomness, no network —
so the same text yields byte-identical output every run and every machine.
USAGE
node ini-source.js app.ini
cat app.ini | node ini-source.js
node ini-source.js app.ini --section-field s --key-field k --value-field v
node ini-source.js --help
Exit codes: 0 success (including an empty stream from empty input) · 2 input error
(malformed line, illegal/empty key, empty/malformed header, DUPLICATE key in a section,
REPEATED section header, field-name collision, empty field name, unknown option,
unreadable file). Always a clean one-line message on stderr, never a stack trace.
Released under MIT.
*/
"use strict";
/* ---- the pure core ------------------------------------------------ */
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_.\-]*$/;
// Strip one pair of matching outer quotes from an already-trimmed value, if present.
function stripQuotes(v) {
if (v.length >= 2) {
var a = v.charAt(0), b = v.charAt(v.length - 1);
if ((a === '"' && b === '"') || (a === "'" && b === "'")) {
return v.slice(1, v.length - 1);
}
}
return v;
}
function trim(s) { return s.replace(/^\s+/, "").replace(/\s+$/, ""); }
// Parse INI text into an array of uniform three-field records. Throws a clean Error on any
// malformed input — the CLI turns that into exit 2. Pure; no side effects.
function parse(text, opts) {
opts = opts || {};
var sectionField = opts.sectionField === undefined ? "section" : opts.sectionField;
var keyField = opts.keyField === undefined ? "key" : opts.keyField;
var valueField = opts.valueField === undefined ? "value" : opts.valueField;
var globalName = opts.global === undefined ? "" : opts.global;
if (typeof sectionField !== "string" || sectionField.length === 0) throw new Error("--section-field must be a non-empty name");
if (typeof keyField !== "string" || keyField.length === 0) throw new Error("--key-field must be a non-empty name");
if (typeof valueField !== "string" || valueField.length === 0) throw new Error("--value-field must be a non-empty name");
if (sectionField === keyField || sectionField === valueField || keyField === valueField) {
throw new Error("--section-field, --key-field and --value-field must be pairwise distinct");
}
if (typeof globalName !== "string") throw new Error("--global must be a string");
if (typeof text !== "string") throw new Error("input must be text");
// strip a leading UTF-8 BOM on the whole document
if (text.charAt(0) === "\uFEFF") text = text.slice(1);
var out = [];
var current = globalName;
var seenSections = {}; // section name -> true (a section may appear once)
var seenKeys = {}; // section name -> { key -> true } (dup key in a section)
seenKeys[current] = {};
var lines = text.split("\n");
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.charAt(line.length - 1) === "\r") line = line.slice(0, line.length - 1); // CRLF==LF
var t = trim(line);
if (t.length === 0) continue; // blank line
var c0 = t.charAt(0);
if (c0 === ";" || c0 === "#") continue; // full-line comment
// section header?
if (c0 === "[" && t.charAt(t.length - 1) === "]") {
var name = trim(t.slice(1, t.length - 1));
if (name.length === 0) throw new Error("empty section header on line " + (i + 1) + ": " + JSON.stringify(line));
if (name.indexOf("[") !== -1 || name.indexOf("]") !== -1) {
throw new Error("malformed section header on line " + (i + 1) + " (name contains '[' or ']'): " + JSON.stringify(line));
}
if (seenSections[name]) throw new Error("repeated section header [" + name + "] on line " + (i + 1) + " (a section may appear only once)");
seenSections[name] = true;
current = name;
if (seenKeys[current] === undefined) seenKeys[current] = {};
continue;
}
// key = value
var eq = line.indexOf("=");
if (eq === -1) throw new Error("malformed line " + (i + 1) + " (not a comment, section header, or KEY=VALUE): " + JSON.stringify(line));
var key = trim(line.slice(0, eq));
var value = trim(line.slice(eq + 1));
if (!KEY_RE.test(key)) throw new Error("illegal key on line " + (i + 1) + ": " + JSON.stringify(key) + " (keys must match [A-Za-z_][A-Za-z0-9_.-]*)");
if (seenKeys[current][key]) {
throw new Error("duplicate key " + JSON.stringify(key) + " in section [" + current + "] on line " + (i + 1) + " (a key may appear once per section; refusing to silently overwrite)");
}
seenKeys[current][key] = true;
value = stripQuotes(value);
var rec = {};
rec[sectionField] = current;
rec[keyField] = key;
rec[valueField] = value;
out.push(rec);
}
return out;
}
// Render records as JSONL text (one JSON object per line, trailing newline if any).
function toJSONL(records) {
var s = "";
for (var i = 0; i < records.length; i++) s += JSON.stringify(records[i]) + "\n";
return s;
}
/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.iniSource = { parse: parse, toJSONL: toJSONL };
}
if (typeof module !== "undefined" && module.exports) {
module.exports = { parse: parse, toJSONL: toJSONL };
}
/* ---- CLI (runs only when invoked directly, never on require) ------ */
function parseArgs(args) {
var opts = {};
var file;
var i = 0;
while (i < args.length) {
var a = args[i];
if (a === "--section-field") {
if (args[i + 1] === undefined) throw new Error("--section-field requires a name");
opts.sectionField = args[i + 1]; i += 2;
} else if (a === "--key-field") {
if (args[i + 1] === undefined) throw new Error("--key-field requires a name");
opts.keyField = args[i + 1]; i += 2;
} else if (a === "--value-field") {
if (args[i + 1] === undefined) throw new Error("--value-field requires a name");
opts.valueField = args[i + 1]; i += 2;
} else if (a === "--global") {
if (args[i + 1] === undefined) throw new Error("--global requires a name");
opts.global = args[i + 1]; i += 2;
} else if (a.charAt(0) === "-" && a !== "-") {
throw new Error("unknown option " + a);
} else {
if (file !== undefined) throw new Error("only one input file may be given (got a second: " + JSON.stringify(a) + ")");
file = a; i += 1;
}
}
return { file: file, opts: opts };
}
function readAll(stream) {
return new Promise(function (resolve, reject) {
var chunks = [];
stream.on("data", function (c) { chunks.push(c); });
stream.on("end", function () { resolve(Buffer.concat(chunks).toString("utf8")); });
stream.on("error", reject);
});
}
function helpText() {
return (
"ini-source.js — parse INI / .gitconfig-style sectioned config into a uniform JSONL stream.\n\n" +
" node ini-source.js app.ini\n" +
" cat app.ini | node ini-source.js\n" +
" node ini-source.js app.ini --section-field s --key-field k --value-field v\n" +
" node ini-source.js --help\n\n" +
" [FILE] INI file to parse (default: read stdin)\n" +
" --section-field NAME field the SECTION is emitted under (default \"section\")\n" +
" --key-field NAME field the parsed KEY is emitted under (default \"key\")\n" +
" --value-field NAME field the parsed VALUE is emitted under (default \"value\")\n" +
" --global NAME section name for keys before any header (default \"\")\n\n" +
"Emits one JSON object per assignment: { section: S, key: K, value: V }, one per line.\n" +
"Full-line ; and # comments and blank lines are skipped; lines split on the first '='.\n\n" +
"Edge: STRINGS ONLY — no type coercion, no ${VAR} interpolation, no inline comments, no\n" +
"escapes. A duplicate key within a section, or a repeated [section], is REFUSED (exit 2) —\n" +
"ini-source never silently overwrites. An honest, pinnable parse.\n"
);
}
function main(argv) {
var args = argv.slice(2);
if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
process.stdout.write(helpText());
return Promise.resolve(0);
}
var parsed;
try { parsed = parseArgs(args); }
catch (e) { process.stderr.write("ini-source: " + e.message + "\n"); return Promise.resolve(2); }
var getText;
if (parsed.file !== undefined) {
getText = new Promise(function (resolve, reject) {
require("fs").readFile(parsed.file, "utf8", function (err, data) {
if (err) reject(new Error("cannot read " + JSON.stringify(parsed.file) + ": " + err.code));
else resolve(data);
});
});
} else {
getText = readAll(process.stdin);
}
return getText.then(function (text) {
var records = parse(text, parsed.opts); // throws -> caught below
process.stdout.write(toJSONL(records));
return 0;
}).catch(function (e) {
process.stderr.write("ini-source: " + e.message + "\n");
return 2;
});
}
if (typeof require !== "undefined" && require.main === module) {
main(process.argv).then(function (code) { process.exitCode = code; });
}
test_ini-source.js172 lineson GitHub →
#!/usr/bin/env node
/* test_ini-source.js — executable battery for ini-source. `node test_ini-source.js` ->
exit 0 PASS / non-zero FAIL. Zero dependencies.
Coverage: frozen hand goldens; a differential grid cross-checked against an
INDEPENDENT regex-route oracle (a different parse route than the gift's indexOf-split);
the {section,key,value} uniform + strings-only contract; custom field names + --global;
every fail-closed throw (malformed line, illegal key, empty/malformed header, DUP key in
a section, REPEATED section, field collision, empty field, non-string input); CRLF==LF,
BOM strip, quote strip, first-'=' split, '#'/';' inside a value literal; determinism
across two parses; toJSONL shape; and in-battery MUTATION TRIPWIRES (a sabotaged parse
must go RED, so a green run is non-vacuous).
*/
"use strict";
var G = require("./ini-source.js");
var pass = 0, fail = 0;
function ok(name, cond) { if (cond) { pass++; } else { fail++; console.error("FAIL: " + name); } }
function eq(name, a, b) { ok(name, JSON.stringify(a) === JSON.stringify(b)); }
function throws(name, fn) {
var threw = false;
try { fn(); } catch (e) { threw = true; }
ok(name, threw);
}
/* ---- INDEPENDENT ORACLE (regex-route, from spec, shares no code with the gift) ---- */
function oracle(text, opts) {
opts = opts || {};
var sf = opts.sectionField || "section", kf = opts.keyField || "key", vf = opts.valueField || "value";
var gl = opts.global === undefined ? "" : opts.global;
if (sf === kf || sf === vf || kf === vf) throw new Error("field collision");
if (text.charCodeAt(0) === 0xFEFF) text = text.slice(1);
var HDR = /^\[([^\[\]]+)\]$/, KV = /^([^=]*)=([\s\S]*)$/, KEY = /^[A-Za-z_][A-Za-z0-9_.\-]*$/;
var out = [], cur = gl, secs = {}, keys = {}; keys[cur] = {};
var ls = text.split("\n");
for (var i = 0; i < ls.length; i++) {
var line = ls[i].replace(/\r$/, "");
var t = line.replace(/^\s+|\s+$/g, "");
if (!t) continue;
if (t[0] === ";" || t[0] === "#") continue;
var hm = t.match(HDR);
if (t[0] === "[" && t[t.length - 1] === "]") {
if (!hm) throw new Error("bad header");
var nm = hm[1].replace(/^\s+|\s+$/g, "");
if (!nm) throw new Error("empty header");
if (secs[nm]) throw new Error("repeat section");
secs[nm] = 1; cur = nm; if (!keys[cur]) keys[cur] = {}; continue;
}
var m = line.match(KV);
if (!m) throw new Error("no =");
var k = m[1].replace(/^\s+|\s+$/g, ""), v = m[2].replace(/^\s+|\s+$/g, "");
if (!KEY.test(k)) throw new Error("bad key");
if (keys[cur][k]) throw new Error("dup key");
keys[cur][k] = 1;
if (v.length >= 2) { var a = v[0], b = v[v.length - 1]; if ((a === '"' && b === '"') || (a === "'" && b === "'")) v = v.slice(1, -1); }
var r = {}; r[sf] = cur; r[kf] = k; r[vf] = v; out.push(r);
}
return out;
}
/* ---- frozen hand goldens ---- */
eq("basic section", G.parse("[s]\na=1\nb=2\n"),
[{section:"s",key:"a",value:"1"},{section:"s",key:"b",value:"2"}]);
eq("two sections, same key ok",
G.parse("[server]\nhost=a\n[client]\nhost=b\n"),
[{section:"server",key:"host",value:"a"},{section:"client",key:"host",value:"b"}]);
eq("global preamble -> section ''",
G.parse("x=1\n[s]\ny=2\n"),
[{section:"",key:"x",value:"1"},{section:"s",key:"y",value:"2"}]);
eq("comments (; and #) + blank lines skipped",
G.parse("; a comment\n[s]\n# another\n\nk=v\n"),
[{section:"s",key:"k",value:"v"}]);
eq("first-'=' split (value contains '=')",
G.parse("[s]\nurl=a=b=c\n"), [{section:"s",key:"url",value:"a=b=c"}]);
eq("value trim + double-quote strip",
G.parse('[s]\nk = " hi there " \n'), [{section:"s",key:"k",value:" hi there "}]);
eq("single-quote strip", G.parse("[s]\nk='x'\n"), [{section:"s",key:"k",value:"x"}]);
eq("empty value", G.parse("[s]\nk=\n"), [{section:"s",key:"k",value:""}]);
eq("'#'/';' inside a value are literal",
G.parse("[s]\nk=a#b;c\n"), [{section:"s",key:"k",value:"a#b;c"}]);
eq("CRLF == LF", G.parse("[s]\r\nk=v\r\n"), G.parse("[s]\nk=v\n"));
eq("leading BOM stripped", G.parse("\uFEFF[s]\nk=v\n"), [{section:"s",key:"k",value:"v"}]);
eq("empty input -> empty stream", G.parse(""), []);
eq("section name trimmed", G.parse("[ srv ]\nk=v\n"), [{section:"srv",key:"k",value:"v"}]);
eq("custom fields + --global",
G.parse("g=1\n[s]\nk=v\n", {sectionField:"S",keyField:"K",valueField:"V",global:"root"}),
[{S:"root",K:"g",V:"1"},{S:"s",K:"k",V:"v"}]);
/* ---- the honesty axis: fail-closed edges ---- */
throws("DUP key in a section -> throw", function(){ G.parse("[s]\nk=1\nk=2\n"); });
throws("DUP key in global -> throw", function(){ G.parse("k=1\nk=2\n"); });
throws("REPEATED section header -> throw", function(){ G.parse("[s]\na=1\n[s]\nb=2\n"); });
throws("malformed line (no '=') -> throw", function(){ G.parse("[s]\nnope\n"); });
throws("unclosed header is a no-'=' line -> throw", function(){ G.parse("[s\nk=v\n"); });
throws("empty section [] -> throw", function(){ G.parse("[]\nk=v\n"); });
throws("illegal key (space) -> throw", function(){ G.parse("[s]\nbad key=v\n"); });
throws("illegal key (digit lead) -> throw", function(){ G.parse("[s]\n1k=v\n"); });
throws("empty key -> throw", function(){ G.parse("[s]\n=v\n"); });
throws("field collision -> throw", function(){ G.parse("[s]\nk=v\n", {keyField:"section"}); });
throws("empty field name -> throw", function(){ G.parse("[s]\nk=v\n", {keyField:""}); });
throws("non-string input -> throw", function(){ G.parse(42); });
/* ---- strings-only contract (no coercion) ---- */
eq("numbers stay strings", G.parse("[s]\nn=8080\nf=1e3\nb=true\n"),
[{section:"s",key:"n",value:"8080"},{section:"s",key:"f",value:"1e3"},{section:"s",key:"b",value:"true"}]);
eq("no ${VAR} interpolation", G.parse("[s]\nk=${HOME}\n"), [{section:"s",key:"k",value:"${HOME}"}]);
(function(){
var recs = G.parse("[s]\nk=v\n");
ok("record has exactly the 3 declared fields", Object.keys(recs[0]).length === 3 &&
"section" in recs[0] && "key" in recs[0] && "value" in recs[0]);
})();
/* ---- differential grid vs the independent oracle ---- */
var grid = [
"[s]\nk=v\n",
"a=1\n[x]\nb=2\n[y]\nb=3\n",
"[s]\nurl=http://a=b\n",
'[s]\nk="quoted"\n',
"; c\n[s]\nk = v \n\n",
"\uFEFF[s]\nk=v\n",
"[s]\r\nk=v\r\n",
"",
];
for (var gi = 0; gi < grid.length; gi++) {
eq("grid[" + gi + "] gift==oracle", G.parse(grid[gi]), oracle(grid[gi]));
}
// grid with custom opts
var o2 = {sectionField:"S",keyField:"K",valueField:"V",global:"root"};
eq("grid custom-opts gift==oracle", G.parse("g=1\n[s]\nk=v\n", o2), oracle("g=1\n[s]\nk=v\n", o2));
/* ---- determinism ---- */
eq("two parses byte-identical",
G.toJSONL(G.parse("[a]\nk=1\n[b]\nk=2\n")), G.toJSONL(G.parse("[a]\nk=1\n[b]\nk=2\n")));
/* ---- toJSONL shape ---- */
eq("toJSONL one object per line + trailing NL",
G.toJSONL([{section:"s",key:"k",value:"v"}]), '{"section":"s","key":"k","value":"v"}\n');
eq("toJSONL empty -> empty", G.toJSONL([]), "");
/* ---- MUTATION TRIPWIRES: a sabotaged parse must diverge from the honest oracle ---- */
// Mutant A: dup-key check removed (silent last-wins). Must NOT match oracle on a dup vector.
function mutantA(text) {
var out = [], cur = "", secs = {};
var ls = text.split("\n");
for (var i = 0; i < ls.length; i++) {
var line = ls[i].replace(/\r$/, ""), t = line.replace(/^\s+|\s+$/g, "");
if (!t || t[0] === ";" || t[0] === "#") continue;
if (t[0] === "[" && t[t.length - 1] === "]") { cur = t.slice(1, -1).replace(/^\s+|\s+$/g, ""); continue; }
var eq2 = line.indexOf("="); if (eq2 === -1) throw new Error("no =");
out.push({section:cur,key:line.slice(0,eq2).replace(/^\s+|\s+$/g,""),value:line.slice(eq2+1).replace(/^\s+|\s+$/g,"")});
}
return out; // NO dup check
}
(function(){
var dupText = "[s]\nk=1\nk=2\n";
var honestThrew = false; try { oracle(dupText); } catch(e){ honestThrew = true; }
var mutantDidNot = false; try { mutantA(dupText); mutantDidNot = true; } catch(e){}
ok("mutation bite: dropping the dup-key check diverges from the honest oracle", honestThrew && mutantDidNot);
})();
// Mutant B: split on LAST '=' instead of first. Must diverge on a value containing '='.
function mutantB(text) {
var line = "k=a=b", last = line.lastIndexOf("=");
return line.slice(last + 1); // "b" — wrong; honest first-split value is "a=b"
}
(function(){
var honest = G.parse("[s]\nk=a=b\n")[0].value; // "a=b"
ok("mutation bite: last-'=' split diverges from first-'=' honest value", honest === "a=b" && mutantB() !== honest);
})();
/* ---- summary ---- */
console.log((fail === 0 ? "PASS" : "FAIL") + " — " + pass + "/" + (pass + fail));
process.exit(fail === 0 ? 0 : 1);