#!/usr/bin/env node // fjl_walk.js — ARION's independent implementation of the Facet Judgment // Layer walk (Artifact Council draft cadb7e7af62a089c6f1d4b7bf4509e7d, v0.1, // queue copy incl. walked_at). Written cold from the spec text — no reference // implementation consulted. // // node fjl_walk.js '' -> prints output JSON (walk result) // node fjl_walk.js --conditions -> runs the section-10 vectors // node fjl_walk.js --fixtures -> runs hash fixtures {input,output_hash} // // Output object follows the spec's published shape; walked_at is taken from // input.walked_at when present (deterministic replay) else current UTC. 'use strict'; const crypto = require('crypto'); const EDGE_KEYS = new Set(['witness', 'facet', 'subject', 'evidence', 'witness_weight', 'id']); const ROOT_KEYS = new Set(['subject', 'witness_weight', 'id']); const BRIDGE_KEYS = new Set(['witness', 'their_facet', 'my_facet', 'weight', 'evidence', 'id']); const INPUT_KEYS = new Set(['reader', 'roots', 'edges', 'bridges', 'facet', 'subject', 'floor', 'depth', 'decay', 'edge_budget', 'path_budget', 'loss', 'id', 'walked_at']); const err = e => ({ error: e }); const isNum = n => typeof n === 'number' && isFinite(n); const codepoints = s => [...s].length; function validFacet(f) { return typeof f === 'string' && f !== '' && f !== '*' && f.normalize('NFC') === f && codepoints(f) >= 1 && codepoints(f) <= 64; } function validEvidence(ev) { if (typeof ev !== 'object' || ev === null) return false; const ks = Object.keys(ev); if (ev.kind === 'aer1') return ks.every(k => k === 'kind' || k === 'id') && typeof ev.id === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(ev.id); if (ev.kind === 'url-hash') return ks.every(k => k === 'kind' || k === 'url' || k === 'hash') && typeof ev.url === 'string' && /^[a-z]+:\/\//i.test(ev.url) && typeof ev.hash === 'string' && /^sha256:[0-9a-f]{64}$/.test(ev.hash); return false; } // ---------- input / record validation ---------- function validate(inp) { if (typeof inp !== 'object' || inp === null) return err('bad_shape'); for (const k of Object.keys(inp)) if (!INPUT_KEYS.has(k)) return err('bad_shape'); if (!validFacet(inp.facet)) return err('bad_facet'); if (typeof inp.subject !== 'string' || inp.subject === '') return err('bad_shape'); if (typeof inp.reader !== 'string' || inp.reader === '') return err('bad_shape'); if (!Number.isInteger(inp.floor) || inp.floor < 2) return err('bad_floor'); if (!Number.isInteger(inp.depth) || inp.depth < 1) return err('bad_depth'); if (!isNum(inp.decay) || inp.decay <= 0 || inp.decay > 1) return err('bad_decay'); for (const b of ['edge_budget', 'path_budget']) if (inp[b] !== undefined && (!Number.isInteger(inp[b]) || inp[b] < 1)) return err('bad_budget'); if (inp.walked_at !== undefined && !(typeof inp.walked_at === 'string' && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z$/.test(inp.walked_at))) return err('bad_time'); if (!Array.isArray(inp.roots) || !Array.isArray(inp.edges) || !Array.isArray(inp.bridges)) return err('bad_shape'); for (const r of inp.roots) { if (typeof r !== 'object' || r === null) return err('bad_shape'); for (const k of Object.keys(r)) if (!ROOT_KEYS.has(k)) return err('bad_shape'); if (typeof r.subject !== 'string' || r.subject === '') return err('bad_shape'); if (r.witness_weight !== undefined && !(isNum(r.witness_weight) && r.witness_weight > 0 && r.witness_weight <= 1)) return err('bad_shape'); } for (const e of inp.edges) { if (typeof e !== 'object' || e === null) return err('bad_shape'); for (const k of Object.keys(e)) if (!EDGE_KEYS.has(k)) return err('bad_shape'); if (!validFacet(e.facet)) return err('bad_facet'); if (typeof e.witness !== 'string' || e.witness === '') return err('bad_shape'); if (typeof e.subject !== 'string' || e.subject === '') return err('bad_shape'); if (!(isNum(e.witness_weight) && e.witness_weight > 0 && e.witness_weight <= 1)) return err('bad_shape'); if (!validEvidence(e.evidence)) return err('bad_evidence'); // dropped-not-error rules are applied during the walk (self-edge) } for (const b of inp.bridges) { if (typeof b !== 'object' || b === null) return err('bad_shape'); for (const k of Object.keys(b)) if (!BRIDGE_KEYS.has(k)) return err('bad_shape'); if (!validFacet(b.their_facet) || !validFacet(b.my_facet)) return err('bad_facet'); if (b.their_facet === b.my_facet) return err('bad_shape'); if (typeof b.witness !== 'string' || b.witness === '') return err('bad_shape'); if (!(isNum(b.weight) && b.weight > 0 && b.weight <= 1)) return err('bad_shape'); if (b.evidence !== undefined && !validEvidence(b.evidence)) return err('bad_evidence'); } if (inp.loss !== undefined && typeof inp.loss !== 'string') return err('bad_shape'); return null; } // ---------- the walk ---------- function walk(inp) { const e = validate(inp); if (e) return e; const { reader, facet, subject, floor, depth, decay } = inp; const edgeBudget = inp.edge_budget, pathBudget = inp.path_budget; const walkedAt = inp.walked_at || new Date().toISOString().replace(/\.\d+Z$/, 'Z'); // usable records: drop self-edges; bridges must be reader-signed, non-composed. const edges = inp.edges.filter(x => x.witness !== x.subject); const bridges = inp.bridges.filter(x => x.witness === reader && x.my_facet === facet); // out-edge candidates at a node, in caller order. For each edge return the // applied {weight, bridgeWeight, sourceFacet} view or null. const candidates = node => edges.map(ed => { if (ed.witness !== node) return null; if (ed.facet === facet) return { ed, bw: 1, src: undefined }; const br = bridges.find(b => b.their_facet === ed.facet); if (!br) return null; return { ed, bw: br.weight, src: ed.facet }; }); const paths = []; // every recorded path (array of hops) const accepted = []; // internally node-disjoint subset, in discovery order let inspections = 0, stop = null; const nodesOf = hops => { const s = new Set(); for (const h of hops) { s.add(h.witness); s.add(h.subject); } return s; }; const intermediatesOf = hops => { const s = nodesOf(hops); s.delete(reader); s.delete(subject); return s; }; function dfs(hops, nodes) { if (stop) return; const cur = hops[hops.length - 1].subject; for (const c of candidates(cur)) { if (!c) continue; // not an out-edge on this facet // budget check: the next inspection would pass edge_budget -> capped if (edgeBudget !== undefined && inspections >= edgeBudget) { if (!stop) stop = 'capped'; return; } inspections++; const ed = c.ed; if (nodes.has(ed.subject)) continue; // simple paths only if (hops.length + 1 > depth) continue; // hop cap incl. root hop const hop = { role: 'edge', witness: ed.witness, facet, subject: ed.subject, witness_weight: ed.witness_weight, applied_discount: ed.witness_weight * c.bw * decay, evidence: ed.evidence, }; if (c.src !== undefined) { hop.source_facet = c.src; hop.bridge_weight = c.bw; } const path = hops.concat([hop]); if (ed.subject === subject) { // the next path would pass path_budget -> stop before recording if (pathBudget !== undefined && paths.length >= pathBudget) { if (!stop) stop = 'capped'; return; } paths.push(path); const inter = intermediatesOf(path); const ok = accepted.every(p => { const o = intermediatesOf(p); for (const n of inter) if (o.has(n)) return false; return true; }); if (ok) { accepted.push(path); if (accepted.length >= floor) { stop = 'floor_met'; return; } } continue; // do not extend past subject } dfs(path, new Set([...nodes, ed.subject])); if (stop) return; } } for (const root of inp.roots) { if (stop) break; if (typeof root.subject !== 'string') continue; const rh = { role: 'root', witness: reader, facet, subject: root.subject, witness_weight: root.witness_weight === undefined ? 1 : root.witness_weight, applied_discount: root.witness_weight === undefined ? 1 : root.witness_weight, }; if (root.subject === subject) { if (pathBudget !== undefined && paths.length >= pathBudget) { stop = 'capped'; break; } paths.push([rh]); if (accepted.length + 1 <= floor) { accepted.push([rh]); if (accepted.length >= floor) { stop = 'floor_met'; break; } } continue; } if (nodesOf([rh]).has(subject) === false) dfs([rh], new Set([reader, root.subject])); } if (!stop) stop = 'complete'; // published chains + min_cut let published; if (stop === 'floor_met') published = accepted; else published = paths; // complete: all; capped: in hand // exact maximum internally node-disjoint subset (small cones only) const inters = published.map(intermediatesOf); const compat = (i, j) => { for (const n of inters[i]) if (inters[j].has(n)) return false; return true; }; let best = 0; (function bt(i, chosen) { if (chosen + (published.length - i) <= best) return; if (i === published.length) { best = Math.max(best, chosen); return; } bt(i + 1, chosen); let ok = true; for (let j = 0; j < i; j++) if (chosenMask[j] && !compat(i, j)) { ok = false; break; } if (ok) { chosenMask[i] = true; bt(i + 1, chosen + 1); chosenMask[i] = false; } }); const chosenMask = new Array(published.length).fill(false); function btRun() { (function bt(i, chosen) { if (chosen + (published.length - i) <= best) return; if (i === published.length) { best = Math.max(best, chosen); return; } let ok = true; for (let j = 0; j < i; j++) if (chosenMask[j] && !compat(i, j)) { ok = false; break; } if (ok) { chosenMask[i] = true; bt(i + 1, chosen + 1); chosenMask[i] = false; } bt(i + 1, chosen); })(0, 0); } btRun(); const min_cut = stop === 'floor_met' ? accepted.length : best; const verdict = stop === 'floor_met' ? 'admitted' : stop === 'complete' ? 'insufficient_evidence' : 'unresolved'; const out = { reader, facet, subject, floor, depth, decay, min_cut, stop, verdict, chains: published.map(hops => ({ hops, chain_discount: hops.reduce((a, h) => a * h.applied_discount, 1), })), loss: inp.loss === undefined ? 'unassigned' : inp.loss, walked_at: walkedAt, }; if (inp.id !== undefined) out.id = inp.id; if (edgeBudget !== undefined) out.edge_budget = edgeBudget; if (pathBudget !== undefined) out.path_budget = pathBudget; return out; } // ---------- RFC 8785 JCS (minimal, own implementation) ---------- function jcs(v) { if (v === null) return 'null'; const t = typeof v; if (t === 'boolean') return v ? 'true' : 'false'; if (t === 'number') { if (!isFinite(v)) throw new Error('non-finite'); if (Number.isInteger(v) && Math.abs(v) < 1e21) return String(v); let s = v.toString(); if (!/[.eE]/.test(s)) s += '.0'; return s; } if (t === 'string') { let r = '"'; for (const ch of v) { const c = ch.codePointAt(0); if (ch === '"') r += '\\"'; else if (ch === '\\') r += '\\\\'; else if (ch === '\b') r += '\\b'; else if (ch === '\f') r += '\\f'; else if (ch === '\n') r += '\\n'; else if (ch === '\r') r += '\\r'; else if (ch === '\t') r += '\\t'; else if (c < 0x20) r += '\\u' + c.toString(16).padStart(4, '0'); else r += ch; } return r + '"'; } if (Array.isArray(v)) return '[' + v.map(jcs).join(',') + ']'; const ks = Object.keys(v).sort((a, b) => { const ua = [...a].map(c => c.codePointAt(0)), ub = [...b].map(c => c.codePointAt(0)); for (let i = 0; i < Math.min(ua.length, ub.length); i++) { if (ua[i] !== ub[i]) return ua[i] - ub[i]; } return ua.length - ub.length; }); return '{' + ks.map(k => jcs(k) + ':' + jcs(v[k])).join(',') + '}'; } const outHash = o => 'sha256:' + crypto.createHash('sha256').update(jcs(o), 'utf8').digest('hex'); module.exports = { walk, jcs, outHash }; // ---------- conformance conditions (section 10) ---------- const E = (w, s, f, wgt = 1, ev) => ({ witness: w, subject: s, facet: f, witness_weight: wgt, evidence: ev }); const AER = id => ({ kind: 'aer1', id }); const EV = AER('00000000-0000-4000-8000-000000000001'); function conditions() { const T = []; const base = over => Object.assign({ reader: 'R', facet: 'f', subject: 'S', floor: 2, depth: 4, decay: 1, roots: [], edges: [], bridges: [], walked_at: '2026-10-09T00:00:00Z', }, over); const run = (name, input, expect) => { const out = walk(input); const fails = []; for (const [k, v] of Object.entries(expect)) { if (k === 'chain_subjects') { const got = out.chains ? out.chains.map(c => c.hops[c.hops.length - 1].subject) : []; if (JSON.stringify(got) !== JSON.stringify(v)) fails.push(`chains=${JSON.stringify(got)} want ${JSON.stringify(v)}`); } else if (k === 'chain_count') { if (!out.chains || out.chains.length !== v) fails.push(`chain_count=${out.chains ? out.chains.length : 'n/a'} want ${v}`); } else if (k === 'has_source_facet') { const all = (out.chains || []).every(c => c.hops.slice(1).every(h => h.source_facet !== undefined)); if (!all) fails.push('missing source_facet'); } else if (k === 'chain_discount_approx') { const d = out.chains && out.chains[0] && out.chains[0].chain_discount; if (d === undefined || Math.abs(d - v) > 1e-9) fails.push(`chain_discount=${d} want ~${v}`); } else if (JSON.stringify(out[k]) !== JSON.stringify(v)) { fails.push(`${k}=${JSON.stringify(out[k])} want ${JSON.stringify(v)}`); } } T.push({ name, ok: fails.length === 0, fails }); }; // ring-outside: mutual edges, no root run('ring-outside', base({ edges: [E('A', 'B', 'f', 1, EV), E('B', 'A', 'f', 1, EV)] }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 0, chain_count: 0 }); // one-chain: one root, weights 1 run('one-chain', base({ roots: [{ subject: 'A', witness_weight: 1 }], edges: [E('A', 'S', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 1 }); // two-disjoint run('two-disjoint', base({ roots: [{ subject: 'A' }, { subject: 'B' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'S', 'f', 1, EV)], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, chain_count: 2 }); // shared-node run('shared-node', base({ roots: [{ subject: 'A' }, { subject: 'B' }], edges: [E('A', 'X', 'f', 1, EV), E('B', 'X', 'f', 1, EV), E('X', 'S', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 2 }); // floor-met: 3 disjoint, floor 2 -> first two only run('floor-met', base({ roots: [{ subject: 'A' }, { subject: 'B' }, { subject: 'C' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'S', 'f', 1, EV), E('C', 'S', 'f', 1, EV)], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, chain_count: 2, chain_subjects: ['S', 'S'] }); // caller-order: roots C,B,A -> C,B only run('caller-order', base({ roots: [{ subject: 'C' }, { subject: 'B' }, { subject: 'A' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'S', 'f', 1, EV), E('C', 'S', 'f', 1, EV)], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, chain_count: 2 }); // shared-then-disjoint run('shared-then-disjoint', base({ roots: [{ subject: 'A' }, { subject: 'B' }, { subject: 'C' }], edges: [E('A', 'X', 'f', 1, EV), E('X', 'S', 'f', 1, EV), E('B', 'X', 'f', 1, EV), E('C', 'S', 'f', 1, EV)], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, chain_count: 2 }); // capped: three disjoint, path_budget 1 run('capped', base({ path_budget: 1, roots: [{ subject: 'A' }, { subject: 'B' }, { subject: 'C' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'S', 'f', 1, EV), E('C', 'S', 'f', 1, EV)], }), { stop: 'capped', verdict: 'unresolved', min_cut: 1, chain_count: 1 }); // floor-before-budget: floor met inside path_budget 3 run('floor-before-budget', base({ path_budget: 3, roots: [{ subject: 'A' }, { subject: 'B' }, { subject: 'C' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'S', 'f', 1, EV), E('C', 'S', 'f', 1, EV)], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, chain_count: 2 }); // other-facet run('other-facet', base({ roots: [{ subject: 'A' }], edges: [E('A', 'S', 'g', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 0, chain_count: 0 }); // star-facet / overall-rating / floor-one / bad-budget run('star-facet', base({ facet: '*' }), { error: 'bad_facet' }); run('overall-rating', base({ facet: undefined, rating: 1, facets: { f: 1 } }), { error: 'bad_shape' }); run('floor-one', base({ floor: 1 }), { error: 'bad_floor' }); run('bad-budget', base({ path_budget: 0 }), { error: 'bad_budget' }); // reader-bridge: R bridges wages->labor 0.5 across two disjoint paths run('reader-bridge', base({ facet: 'labor', roots: [{ subject: 'A' }, { subject: 'B' }], edges: [E('A', 'S', 'wages', 1, EV), E('B', 'S', 'wages', 1, EV)], bridges: [{ witness: 'R', their_facet: 'wages', my_facet: 'labor', weight: 0.5 }], }), { stop: 'floor_met', verdict: 'admitted', min_cut: 2, has_source_facet: true, chain_discount_approx: 0.5 }); // witness-bridge: same bridge signed by someone else -> ignored run('witness-bridge', base({ facet: 'labor', roots: [{ subject: 'A' }, { subject: 'B' }], edges: [E('A', 'S', 'wages', 1, EV), E('B', 'S', 'wages', 1, EV)], bridges: [{ witness: 'Q', their_facet: 'wages', my_facet: 'labor', weight: 0.5 }], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 0, chain_count: 0 }); // bridge-compose: a->b and b->c; a-edge does not enter walk on c run('bridge-compose', base({ facet: 'c', roots: [{ subject: 'A' }], edges: [E('A', 'S', 'a', 1, EV)], bridges: [{ witness: 'R', their_facet: 'a', my_facet: 'b', weight: 0.5 }, { witness: 'R', their_facet: 'b', my_facet: 'c', weight: 0.5 }], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 0, chain_count: 0 }); // self-edge dropped run('self-edge', base({ roots: [{ subject: 'A' }], edges: [E('A', 'A', 'f', 1, EV), E('A', 'S', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 1 }); // cycle behind one bridge: cycle on the bridged facet must not loop; cut 1 run('cycle', base({ facet: 'labor', roots: [{ subject: 'A' }], edges: [E('A', 'B', 'wages', 1, EV), E('B', 'A', 'wages', 1, EV), E('A', 'S', 'wages', 1, EV)], bridges: [{ witness: 'R', their_facet: 'wages', my_facet: 'labor', weight: 0.5 }], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 1 }); // depth: second disjoint path needs one hop more than depth run('depth', base({ depth: 2, roots: [{ subject: 'A' }, { subject: 'B' }], edges: [E('A', 'S', 'f', 1, EV), E('B', 'M', 'f', 1, EV), E('M', 'S', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 1 }); // dark-cone: root reaching nobody on subject stays unpublished run('dark-cone', base({ roots: [{ subject: 'A' }, { subject: 'Z' }], edges: [E('A', 'S', 'f', 1, EV), E('Z', 'Q', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_count: 1 }); // sum-field: score on the verdict -> invalid (input carries an aggregate key) run('sum-field', base({ score: 5 }), { error: 'bad_shape' }); // bad-pointer: url-hash missing hash -> record fails validation run('bad-pointer', base({ roots: [{ subject: 'A' }], edges: [{ witness: 'A', subject: 'S', facet: 'f', witness_weight: 1, evidence: { kind: 'url-hash', url: 'https://x.example/a' } }], }), { error: 'bad_evidence' }); // decay: two hops decay 0.5 -> chain_discount = product run('decay', base({ depth: 3, decay: 0.5, roots: [{ subject: 'A', witness_weight: 1 }], edges: [E('A', 'B', 'f', 1, EV), E('B', 'S', 'f', 1, EV)], }), { stop: 'complete', verdict: 'insufficient_evidence', min_cut: 1, chain_discount_approx: 0.25 }); return T; } if (require.main === module) { const arg = process.argv[2]; if (arg === '--conditions') { const T = conditions(); let pass = 0; for (const t of T) { if (t.ok) { pass++; console.log('PASS', t.name); } else console.log('FAIL', t.name, '-', t.fails.join('; ')); } console.log(`${pass}/${T.length} conditions pass`); process.exit(pass === T.length ? 0 : 1); } else if (arg === '--fixtures') { const fx = JSON.parse(require('fs').readFileSync(process.argv[3], 'utf8')); let pass = 0; for (const f of fx) { const out = walk(f.input); const h = outHash(out); const ok = h === f.output_hash; if (ok) pass++; console.log((ok ? 'PASS' : 'FAIL'), f.name, h, ok ? '' : `want ${f.output_hash}`); } console.log(`${pass}/${fx.length} fixtures pass`); process.exit(pass === fx.length ? 0 : 1); } else if (arg) { const inp = JSON.parse(require('fs').existsSync(arg) ? require('fs').readFileSync(arg, 'utf8') : arg); console.log(JSON.stringify(walk(inp), null, 1)); } else { console.error('usage: fjl_walk.js '); process.exit(2); } }