Contract
Give it a stream of JSON Lines and a schema you declare, and it walks the records in order and stops at the FIRST one that breaks the contract — naming the record's line, the field, and what was wrong. The schema is plain data (fields → {type, required}), never code: nothing is executed or eval'd, so it is safe to accept from an untrusted source. Types are string, number, boolean, object, array, null, any; a bare "id":"number" is sugar for a required field of that type; --closed rejects the first unexpected field. Same records + same schema → the same first failure, byte-identical every run, so it drops straight into a CI gate on a model's output.
The honest edge
contract checks STRUCTURE (which fields, at which types), not MEANING. A record that is structurally perfect and semantically nonsense passes — "age": -3 with type number passes, an email that is not an email passes. It makes the declared shape enforceable and names the first record that breaks it; it cannot tell you a structurally-valid value is correct, sensible, in range, or true.
Run it
node contract.js --schema schema.json records.jsonl # exit 0 all pass | 1 first violating record (line+field+reason) | 2 unusable input/ill-formed schema; --closed rejects unexpected fields; stdin supported
test_contract.js battery (all pass) + --selftest (15/15: schema compile, per-type match, required/optional, shorthand sugar, closed-mode reject, first-bad-record-wins, cross-process byte-identical)
Zero dependencies, Node or browser, deterministic
The code — every file that ships
contract.js332 lineson GitHub →
#!/usr/bin/env node
/* SPDX-License-Identifier: MIT */
/* contract.js — assert JSONL output matches a declared record schema, at runtime.
*
* A tiny, dependency-free schema check for JSON Lines (one JSON value per line).
* Give it a stream of records and a SCHEMA you declare, and it walks the records
* in order and stops at the FIRST one that breaks the contract — naming the
* record, the field, and what was wrong. Same records + same schema → the same
* first failure, byte-identical, every run. It drops straight into a CI gate.
*
* records.jsonl, schema ──▶ check(records, schema)
* ──▶ { ok:true }
* | { ok:false, line, field, reason } (first bad record)
*
* THE SCHEMA (declared data, never code). A schema is a plain object naming the
* fields a record must carry and the STRUCTURE each must have — nothing executes,
* nothing is eval'd, so a schema is safe to accept from an untrusted source:
*
* { "id": { "type": "number", "required": true },
* "name": { "type": "string", "required": true },
* "tags": { "type": "array" }, // present-optional, typed if present
* "meta": { "type": "object", "required": false } }
*
* type one of: string number boolean object array null any
* ("object" means a non-array, non-null object; "array" means Array;
* "null" means the JSON null; "any" accepts any present value)
* required true (default) → the field must be present; false → may be absent,
* but if present it must match `type`.
*
* Shorthand: a bare string value is sugar for a required field of that type —
* { "id": "number" } === { "id": { "type": "number", "required": true } }.
*
* By default a record may carry EXTRA fields the schema does not mention; pass
* { closed: true } (CLI --closed) to reject the first unexpected field instead.
*
* DETERMINISM (the promise, and the self-test that proves it). Records are read
* in file order; fields are checked in the schema's declared key order; the FIRST
* violation wins and nothing after it is read. There is no map iteration whose
* order could vary, no clock, no randomness — the verdict is a pure function of
* (records, schema). `--selftest` proves a conforming stream passes, a broken one
* fails at the expected line+field, and the check is idempotent.
*
* THE LINE IT WILL NOT CROSS (the honest ceiling — printed every --help/--list).
* contract checks that each record HAS the declared fields at the declared TYPES.
* It does NOT check the values are correct, sensible, in range, or true. A record
* that is structurally perfect and semantically nonsense passes. "age": -3 with
* type number passes; an email that is not an email passes. Structure, not
* meaning — a human owns the meaning; contract only makes the shape enforceable.
*
* EXIT CODES (filter convention, gate-friendly).
* 0 every record satisfies the schema
* 1 a record violated the schema — the FIRST one, with line + field + reason
* 2 the input could not be used (missing file, unreadable, bad JSON in the
* records, or an ill-formed SCHEMA) — always a clean message, never a stack.
*
* Runs identically in Node and in a browser (no DOM, no deps). MIT.
*/
var CEILING =
'ceiling: contract checks STRUCTURE (which fields, which types), not MEANING.\n' +
' A structurally-valid record with nonsense values passes — "age":-3 is a valid\n' +
' number. A human owns whether the values are right; contract only makes the\n' +
' declared shape enforceable and names the first record that breaks it.';
/* ---- errors: a clean, tagged failure, never a stack trace ------------------ */
function ContractError(msg) { var e = new Error(msg); e.contract = true; return e; }
/* ---- the closed type vocabulary (data, not code) -------------------------- */
var TYPES = { string: 1, number: 1, boolean: 1, object: 1, array: 1, 'null': 1, any: 1 };
/* structural type of a JSON value, in this gift's closed vocabulary */
function typeOf(v) {
if (v === null) return 'null';
if (Array.isArray(v)) return 'array';
var t = typeof v;
if (t === 'string' || t === 'number' || t === 'boolean' || t === 'object') return t;
return t; // undefined/function can't come from JSON.parse; guarded by schema validation
}
/* ---- normalize + validate a schema (throws ContractError if ill-formed) ---- */
/* returns an ordered list of {field, type, required} so field order is the
* schema's declared key order — the source of the deterministic field sequence. */
function compileSchema(schema) {
if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
throw ContractError('schema: must be a JSON object mapping field names to rules');
}
var fields = Object.keys(schema);
var out = [];
for (var i = 0; i < fields.length; i++) {
var name = fields[i];
var rule = schema[name];
var type, required;
if (typeof rule === 'string') { // shorthand: "number" → required number
type = rule; required = true;
} else if (rule !== null && typeof rule === 'object' && !Array.isArray(rule)) {
type = Object.prototype.hasOwnProperty.call(rule, 'type') ? rule.type : 'any';
required = Object.prototype.hasOwnProperty.call(rule, 'required') ? rule.required : true;
if (typeof required !== 'boolean') {
throw ContractError('schema: field ' + JSON.stringify(name) +
': "required" must be true or false');
}
} else {
throw ContractError('schema: field ' + JSON.stringify(name) +
': rule must be a type string or an object like {"type":"string"}');
}
if (typeof type !== 'string' || !Object.prototype.hasOwnProperty.call(TYPES, type)) {
throw ContractError('schema: field ' + JSON.stringify(name) +
': unknown type ' + JSON.stringify(type) +
' (allowed: string number boolean object array null any)');
}
out.push({ field: name, type: type, required: required });
}
return out;
}
/* ---- parse a JSONL string into records (throws ContractError on a bad line) */
function parseJsonl(text, label) {
var out = [];
var lines = String(text).split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
if (line.replace(/\s+/g, '') === '') continue; // skip blank lines
var obj;
try { obj = JSON.parse(line); }
catch (e) {
throw ContractError(label + ':' + (i + 1) + ': not valid JSON (' +
((e && e.message) || 'parse error') + ')');
}
out.push({ value: obj, line: i + 1 });
}
return out;
}
/* ---- check ONE record against the compiled schema; return null | {field,reason} */
function checkRecord(value, compiled, closed) {
// a record must be a JSON object (not array, not scalar, not null)
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
return { field: null, reason: 'record is not a JSON object (got ' + typeOf(value) + ')' };
}
// fields, in schema-declared order (deterministic)
for (var i = 0; i < compiled.length; i++) {
var r = compiled[i];
var present = Object.prototype.hasOwnProperty.call(value, r.field);
if (!present) {
if (r.required) return { field: r.field, reason: 'missing required field' };
continue; // optional + absent → fine
}
if (r.type === 'any') continue; // present + any type accepted
var actual = typeOf(value[r.field]);
if (actual !== r.type) {
return { field: r.field, reason: 'expected ' + r.type + ', got ' + actual };
}
}
// closed mode: reject the first extra field not named by the schema
if (closed) {
var declared = {};
for (var j = 0; j < compiled.length; j++) declared[compiled[j].field] = 1;
var keys = Object.keys(value); // insertion order from JSON.parse — deterministic per input
for (var k = 0; k < keys.length; k++) {
if (!Object.prototype.hasOwnProperty.call(declared, keys[k])) {
return { field: keys[k], reason: 'unexpected field (schema is closed)' };
}
}
}
return null;
}
/* ---- the gift: check a JSONL string against a schema ---------------------- */
/* opts: { closed:false, label:'records' }
* returns { ok:true } | { ok:false, line, field, reason } — the FIRST violation. */
function check(recordsText, schema, opts) {
opts = opts || {};
var closed = !!opts.closed;
var label = opts.label || 'records';
var compiled = compileSchema(schema); // throws on ill-formed schema (→ exit 2)
var recs = parseJsonl(recordsText, label); // throws on bad JSON (→ exit 2)
for (var i = 0; i < recs.length; i++) {
var bad = checkRecord(recs[i].value, compiled, closed);
if (bad) return { ok: false, line: recs[i].line, field: bad.field, reason: bad.reason };
}
return { ok: true };
}
/* ================= self-test (proves the promises; no deps) ================= */
function selftest() {
var fails = [];
function ok(name, cond) { if (!cond) fails.push(name); }
var schema = {
id: { type: 'number', required: true },
name: 'string', // shorthand → required string
tags: { type: 'array', required: false } // optional, typed if present
};
// conforming stream (extra field allowed in default open mode) → ok
var good = '{"id":1,"name":"a","tags":[]}\n{"id":2,"name":"b"}\n{"id":3,"name":"c","extra":9}\n';
ok('conforming passes', check(good, schema).ok === true);
// missing required field → first bad record named
var miss = '{"id":1,"name":"a"}\n{"id":2}\n{"id":3,"name":"c"}\n';
var r1 = check(miss, schema);
ok('missing field caught', r1.ok === false && r1.line === 2 && r1.field === 'name' &&
/missing required/.test(r1.reason));
// wrong type → named with expected/got
var wrong = '{"id":"1","name":"a"}\n';
var r2 = check(wrong, schema);
ok('type mismatch caught', r2.ok === false && r2.line === 1 && r2.field === 'id' &&
/expected number, got string/.test(r2.reason));
// FIRST bad record wins — the second violation is never reported
var two = '{"id":1,"name":"a"}\n{"id":2}\n{"name":"x"}\n';
var r3 = check(two, schema);
ok('first-bad-record wins', r3.ok === false && r3.line === 2 && r3.field === 'name');
// optional present-but-wrong-type → caught
var badopt = '{"id":1,"name":"a","tags":"oops"}\n';
var r4 = check(badopt, schema);
ok('optional typed-if-present', r4.ok === false && r4.field === 'tags' &&
/expected array, got string/.test(r4.reason));
// non-object record → structural reject
var scal = '{"id":1,"name":"a"}\n[1,2,3]\n';
var r5 = check(scal, schema);
ok('non-object record rejected', r5.ok === false && r5.line === 2 && r5.field === null);
// closed mode → first extra field rejected; open mode → same record passes
var extra = '{"id":1,"name":"a","surprise":1}\n';
ok('closed rejects extra', check(extra, schema, { closed: true }).ok === false);
ok('open allows extra', check(extra, schema).ok === true);
var rc = check(extra, schema, { closed: true });
ok('closed names the extra field', rc.field === 'surprise' && /unexpected field/.test(rc.reason));
// 'null' type and 'any' type
var s2 = { x: { type: 'null' }, y: { type: 'any' } };
ok('null type matches null', check('{"x":null,"y":0}\n', s2).ok === true);
ok('null type rejects non-null', check('{"x":0,"y":0}\n', s2).ok === false);
ok('any accepts anything present', check('{"x":null,"y":{"deep":[1]}}\n', s2).ok === true);
// ill-formed schema → thrown ContractError (would be exit 2)
var threw = false;
try { check('{}\n', { f: { type: 'weird' } }); } catch (e) { threw = !!e.contract; }
ok('bad schema throws', threw);
// determinism / idempotence — same inputs, same verdict object twice
ok('idempotent verdict',
JSON.stringify(check(miss, schema)) === JSON.stringify(check(miss, schema)));
// empty stream (no records) → vacuously ok
ok('empty stream ok', check('\n\n', schema).ok === true);
if (fails.length) { print('SELFTEST FAIL: ' + fails.join(', ')); return 1; }
print('SELFTEST OK (' + 15 + ' checks)');
return 0;
}
/* ================= tiny I/O shim (Node CLI; browser export) ================ */
function print(s) {
if (typeof process !== 'undefined' && process.stdout) process.stdout.write(s + '\n');
else if (typeof console !== 'undefined') console.log(s);
}
function eprint(s) {
if (typeof process !== 'undefined' && process.stderr) process.stderr.write(s + '\n');
else if (typeof console !== 'undefined') console.error(s);
}
var HELP =
'contract — assert JSONL records match a declared structural schema.\n\n' +
'usage:\n' +
' contract --schema <schema.json> [<records.jsonl>] [--closed]\n' +
' contract --selftest\n' +
' contract --help | --list\n\n' +
'reads records from the file argument or stdin. exit 0 = all pass,\n' +
'1 = first record that violates the schema (line + field + reason on stderr),\n' +
'2 = unusable input or ill-formed schema.\n\n' +
' --schema F the declared schema (a JSON object; see below)\n' +
' --closed reject the first field a record carries that the schema omits\n' +
' --selftest run the built-in proofs and exit\n\n' +
'schema: { "field": {"type":"string","required":true}, ... }\n' +
' type: string number boolean object array null any · required: true|false\n' +
' shorthand: "field":"number" === required field of that type\n\n' +
CEILING;
function readAll(fd) {
var fs = require('fs');
return fs.readFileSync(fd === undefined ? 0 : fd, 'utf8');
}
function main(argv) {
var args = argv.slice(2);
if (args.indexOf('--help') >= 0 || args.indexOf('-h') >= 0) { print(HELP); return 0; }
if (args.indexOf('--list') >= 0) { print(HELP); return 0; }
if (args.indexOf('--selftest') >= 0) return selftest();
var closed = false, schemaPath = null, recPath = null, i;
for (i = 0; i < args.length; i++) {
if (args[i] === '--closed') closed = true;
else if (args[i] === '--schema') { schemaPath = args[++i]; }
else if (args[i].slice(0, 9) === '--schema=') { schemaPath = args[i].slice(9); }
else if (args[i].charAt(0) === '-') { eprint('contract: unknown option ' + args[i]); return 2; }
else recPath = args[i];
}
if (!schemaPath) { eprint('contract: --schema <file> is required (see --help)'); return 2; }
var fs = require('fs'), schema, records;
try { schema = JSON.parse(fs.readFileSync(schemaPath, 'utf8')); }
catch (e) { eprint('contract: cannot read schema ' + JSON.stringify(schemaPath) +
': ' + ((e && e.message) || 'error')); return 2; }
try { records = recPath ? fs.readFileSync(recPath, 'utf8') : readAll(0); }
catch (e) { eprint('contract: cannot read records: ' + ((e && e.message) || 'error')); return 2; }
var res;
try { res = check(records, schema, { closed: closed, label: recPath || 'stdin' }); }
catch (e) {
if (e && e.contract) { eprint('contract: ' + e.message); return 2; }
throw e;
}
if (res.ok) return 0;
eprint('contract: ' + (recPath || 'stdin') + ':' + res.line + ': ' +
(res.field === null ? '' : 'field ' + JSON.stringify(res.field) + ': ') + res.reason);
return 1;
}
/* ---- exports (browser) + CLI (Node) --------------------------------------- */
var API = { check: check, compileSchema: compileSchema, typeOf: typeOf, selftest: selftest, CEILING: CEILING };
if (typeof module !== 'undefined' && module.exports) {
module.exports = API;
if (require.main === module) process.exit(main(process.argv));
} else if (typeof window !== 'undefined') {
window.ForestGifts = window.ForestGifts || {};
window.ForestGifts.contract = API;
}
test_contract.js106 lineson GitHub →
#!/usr/bin/env node
/* SPDX-License-Identifier: MIT */
/* test_contract.js — out-of-band battery for the contract gift.
*
* Oracles are hand-computed here, independent of contract.js's own --selftest:
* the test asserts WHAT the answer must be, the gift computes it, we compare.
* A green battery + a green --selftest are two witnesses, not one restated.
*/
var C = require('./contract.js');
var fails = [];
function eq(name, got, want) {
var g = JSON.stringify(got), w = JSON.stringify(want);
if (g !== w) fails.push(name + '\n got : ' + g + '\n want: ' + w);
}
function truthy(name, cond) { if (!cond) fails.push(name); }
var schema = {
id: { type: 'number', required: true },
name: 'string',
tags: { type: 'array', required: false }
};
/* 1. a fully-conforming stream → ok:true (extra field allowed, open mode) */
eq('T1 conforming', C.check(
'{"id":1,"name":"a","tags":[]}\n{"id":2,"name":"b"}\n{"id":3,"name":"c","x":9}\n', schema),
{ ok: true });
/* 2. missing required field on record 2 → line 2, field name, missing */
eq('T2 missing required', C.check('{"id":1,"name":"a"}\n{"id":2}\n', schema),
{ ok: false, line: 2, field: 'name', reason: 'missing required field' });
/* 3. wrong type on id (string not number), record 1 */
eq('T3 wrong type', C.check('{"id":"1","name":"a"}\n', schema),
{ ok: false, line: 1, field: 'id', reason: 'expected number, got string' });
/* 4. FIRST bad record wins — record 2 fails (missing name), record 3 also would */
eq('T4 first wins', C.check('{"id":1,"name":"a"}\n{"id":2}\n{"name":"z"}\n', schema),
{ ok: false, line: 2, field: 'name', reason: 'missing required field' });
/* 5. optional field present but wrong type → caught at that field */
eq('T5 optional typed', C.check('{"id":1,"name":"a","tags":"nope"}\n', schema),
{ ok: false, line: 1, field: 'tags', reason: 'expected array, got string' });
/* 6. a record that is not an object (an array) → field null, structural reason */
eq('T6 non-object', C.check('{"id":1,"name":"a"}\n[1,2]\n', schema),
{ ok: false, line: 2, field: null, reason: 'record is not a JSON object (got array)' });
/* 7. closed mode rejects the first unexpected field; open mode passes it */
eq('T7 closed rejects', C.check('{"id":1,"name":"a","surprise":1}\n', schema, { closed: true }),
{ ok: false, line: 1, field: 'surprise', reason: 'unexpected field (schema is closed)' });
eq('T7 open allows', C.check('{"id":1,"name":"a","surprise":1}\n', schema),
{ ok: true });
/* 8. null type + any type */
var s2 = { x: { type: 'null' }, y: { type: 'any' } };
eq('T8 null ok', C.check('{"x":null,"y":0}\n', s2), { ok: true });
eq('T8 null bad', C.check('{"x":5,"y":0}\n', s2),
{ ok: false, line: 1, field: 'x', reason: 'expected null, got number' });
eq('T8 any ok', C.check('{"x":null,"y":{"z":[1,2]}}\n', s2), { ok: true });
/* 9. shorthand string rule === {type,required:true} */
eq('T9 shorthand', C.check('{"name":5}\n', { name: 'string' }),
{ ok: false, line: 1, field: 'name', reason: 'expected string, got number' });
/* 10. boolean + object types */
var s3 = { flag: 'boolean', meta: { type: 'object' } };
eq('T10 bool+object ok', C.check('{"flag":true,"meta":{}}\n', s3), { ok: true });
eq('T10 object rejects array', C.check('{"flag":false,"meta":[]}\n', s3),
{ ok: false, line: 1, field: 'meta', reason: 'expected object, got array' });
/* 11. empty stream (blank lines only) → vacuously ok */
eq('T11 empty ok', C.check('\n \n\n', schema), { ok: true });
/* 12. ill-formed schema throws a tagged ContractError (→ exit 2 path) */
var threw = false;
try { C.check('{}\n', { f: { type: 'nope' } }); } catch (e) { threw = e && e.contract === true; }
truthy('T12 bad schema throws tagged', threw);
var threw2 = false;
try { C.check('{}\n', [1, 2, 3]); } catch (e) { threw2 = e && e.contract === true; }
truthy('T12 non-object schema throws', threw2);
/* 13. bad JSON in records throws (→ exit 2), with line number */
var threwJ = false, msgJ = '';
try { C.check('{"id":1,"name":"a"}\n{oops\n', schema); } catch (e) { threwJ = e && e.contract === true; msgJ = e.message; }
truthy('T13 bad json throws', threwJ && /:2:/.test(msgJ));
/* 14. determinism — identical verdict object across repeated calls */
truthy('T14 idempotent',
JSON.stringify(C.check('{"id":2}\n', schema)) === JSON.stringify(C.check('{"id":2}\n', schema)));
/* 15. required:false absent is fine; required:false present-and-right is fine */
var s4 = { a: { type: 'number', required: false } };
eq('T15 optional absent', C.check('{}\n', s4), { ok: true });
eq('T15 optional present ok', C.check('{"a":7}\n', s4), { ok: true });
/* 16. field order determinism — id checked before name (schema-declared order);
* a record missing BOTH reports id first, not name */
eq('T16 field order', C.check('{}\n', schema),
{ ok: false, line: 1, field: 'id', reason: 'missing required field' });
if (fails.length) {
console.error('BATTERY FAIL (' + fails.length + '):\n ' + fails.join('\n '));
process.exit(1);
}
console.log('BATTERY OK — all checks passed');
process.exit(0);