Super Testnet's kitchen sink becomes lineage's app. Lineage gives the structure; the chain adds time. - Holdings home, a tree drawn like docs/lineage.md, act and inspect panels, and a timeline grouped by beat. - Verbs: found, birth, award, decide, graduate, close. Each is a lineage/v0 inscription whose parent is the node that authorizes it. - One reducer (src/orgchain/lineage.js) folds the events into trees and vets every act before it is signed. Closing cascades through derived children; graduated (adopted) ones stay open. - Time: tempo each act / 10s / 30s / 60s / manual, Beat now, and a view slider with replay. A beat is a block; cued acts wait in the mempool. - Its own lab (orgchain:lab:v1) with save, load and reset; a yolopreneur sample of six beats. - Removed: the recursive preview, explorer UI, server, Docker and Fly files. The engine (src/adapters, src/upstream, storage, miner) is unchanged. - Tests: reducer and lab (node --test), plus a Playwright spec for desktop and mobile that also runs against a deployed copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
84 lines
6.3 KiB
JavaScript
84 lines
6.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { PROTOCOL, census, reduce, rows, slug, summary, trail } from '../src/orgchain/lineage.js';
|
|
|
|
let n = 0;
|
|
const ev = (op, parent = null, meta = {}, height = 1) => ({ id: `${String(++n).padStart(64, '0')}i0`, status: height === null ? 'pending' : 'confirmed',
|
|
height, metadata: { op, ...meta }, parent, protocol: PROTOCOL });
|
|
const draw = (s, root) => rows(s, root).map(r => r.guide + (r.type === 'group' ? r.label : `${r.path} ${r.node.name}`));
|
|
|
|
test('found and birth build the tree; orgs and agents are indexed separately', () => {
|
|
const root = ev('found', null, { name: 'yolopreneur' }), wk = ev('birth', root.id, { name: 'well-known' }, 2),
|
|
time = ev('birth', root.id, { name: 'time', kind: 'org' }, 2), sol = ev('birth', time.id, { name: 'sol', kind: 'agent' }, 3),
|
|
doc = ev('birth', time.id, { name: 'docscope' }, 3);
|
|
const s = reduce([root, wk, time, sol, doc]);
|
|
assert.ok(s.log.every(e => e.ok));
|
|
assert.deepEqual([time, sol, doc].map(e => s.nodes.get(e.id).index), [1, 0, 0]);
|
|
assert.equal(s.nodes.get(sol.id).edge, 'derived');
|
|
assert.equal(s.nodes.get(root.id).kind, 'holding');
|
|
assert.deepEqual(draw(s, root.id), ['● yolopreneur', '└─ orgs', " ├─ 0' well-known", " └─ 1' time",
|
|
' ├─ agents', " │ └─ 0' sol", ' └─ orgs', " └─ 0' docscope"]);
|
|
assert.equal(trail(s, sol.id), 'yolopreneur / time / sol');
|
|
assert.deepEqual(census(s, root.id), { orgs: 3, agents: 1, belts: 0, decisions: 0, closed: 0 });
|
|
assert.equal(summary(s.log[3], s), 'birth sol (agent) ← time');
|
|
});
|
|
|
|
test('every rule rejects with a reason, and rejected events change nothing', () => {
|
|
const root = ev('found', null, { name: 'acme' }), bot = ev('birth', root.id, { name: 'bot', kind: 'agent' }),
|
|
ops = ev('birth', root.id, { name: 'ops' }), other = ev('found', null, { name: 'other' });
|
|
const bad = [
|
|
[ev('found', root.id, { name: 'x' }), /no parent/], [ev('found', null, { name: 'Acme Co' }), /lowercase/],
|
|
[ev('found', null, { name: 'acme' }), /already named acme/], [ev('birth', 'f'.repeat(64) + 'i0', { name: 'x' }), /not in this lineage/],
|
|
[ev('birth', bot.id, { name: 'x' }), /agents do not give birth/], [ev('birth', root.id, { name: 'ops' }), /already has an active ops/],
|
|
[ev('award', bot.id, { to: ops.id, skill: 'launch', belt: 'green' }), /agents do not award/],
|
|
[ev('award', root.id, { to: other.id, skill: 'launch', belt: 'green' }), /another node in this holding/],
|
|
[ev('award', root.id, { to: root.id, skill: 'launch', belt: 'green' }), /another node/],
|
|
[ev('award', root.id, { to: bot.id, skill: 'launch', belt: 'gold' }), /belt/], [ev('decide', root.id, { title: ' ' }), /title/],
|
|
[ev('graduate', ops.id, { of: bot.id }), /only its parent/], [ev('graduate', root.id, { of: bot.id }), /only orgs graduate/],
|
|
[ev('close'), /spends the node/], [ev('nope', root.id), /not a lineage\/v0 op/],
|
|
];
|
|
const s = reduce([root, bot, ops, other, ...bad.map(([e]) => e)]);
|
|
bad.forEach(([e, why]) => { const entry = s.log.find(x => x.id === e.id); assert.equal(entry.ok, false, e.metadata.op); assert.match(entry.why, why); });
|
|
assert.equal(s.nodes.size, 4);
|
|
assert.match(summary(s.log.find(x => x.id === bad[4][0].id), s), /^birth: agents do not give birth$/);
|
|
});
|
|
|
|
test('graduation flips the edge; closing cascades through derived children only', () => {
|
|
const root = ev('found', null, { name: 'acme' }), lab = ev('birth', root.id, { name: 'lab' }), kid = ev('birth', lab.id, { name: 'kid' }),
|
|
seat = ev('birth', kid.id, { name: 'seat', kind: 'agent' }), grad = ev('birth', lab.id, { name: 'grad' }),
|
|
up = ev('graduate', lab.id, { of: grad.id }, 2), again = ev('graduate', lab.id, { of: grad.id }, 2),
|
|
award = ev('award', lab.id, { to: seat.id, skill: 'launch', belt: 'green' }, 2), close = ev('close', lab.id, {}, 3),
|
|
after = ev('birth', lab.id, { name: 'late' }, 4), still = ev('decide', grad.id, { title: 'Carry on', why: 'adopted' }, 4);
|
|
const s = reduce([root, lab, kid, seat, grad, up, again, award, close, after, still]);
|
|
assert.equal(s.nodes.get(grad.id).edge, 'adopted');
|
|
assert.equal(s.nodes.get(grad.id).graduated, 2);
|
|
assert.match(s.log.find(e => e.id === again.id).why, /already graduated/);
|
|
assert.deepEqual(s.nodes.get(seat.id).badges.map(b => `${b.skill}:${b.belt}`), ['launch:green']);
|
|
assert.deepEqual([lab, kid, seat, grad].map(e => s.nodes.get(e.id).status), ['closed', 'closed', 'closed', 'active']);
|
|
assert.deepEqual(s.log.find(e => e.id === close.id).also, [kid.id, seat.id]);
|
|
assert.equal(summary(s.log.find(e => e.id === close.id), s), 'close lab and 2 derived');
|
|
assert.match(s.log.find(e => e.id === after.id).why, /parent is closed/);
|
|
assert.equal(s.nodes.get(grad.id).decisions[0].title, 'Carry on');
|
|
assert.deepEqual(census(s, root.id), { orgs: 3, agents: 1, belts: 1, decisions: 1, closed: 3 });
|
|
});
|
|
|
|
test('upTo shows a past beat; pending adds cued events', () => {
|
|
const root = ev('found', null, { name: 'acme' }, 1), a = ev('birth', root.id, { name: 'a' }, 2), b = ev('birth', root.id, { name: 'b' }, null),
|
|
award = ev('award', root.id, { to: a.id, skill: 'post', belt: 'white' }, null);
|
|
assert.equal(reduce([root, a, b, award], { upTo: 1 }).nodes.size, 1);
|
|
assert.equal(reduce([root, a, b, award]).nodes.size, 2);
|
|
const live = reduce([root, a, b, award], { pending: true });
|
|
assert.equal(live.nodes.get(b.id).born, 'cued');
|
|
assert.equal(live.nodes.get(a.id).badges[0].beat, 'cued');
|
|
assert.equal(live.log.filter(e => e.pending).length, 2);
|
|
});
|
|
|
|
test('summaries and slugs', () => {
|
|
const root = ev('found', null, { name: 'acme' }), ops = ev('birth', root.id, { name: 'ops' }), seat = ev('birth', root.id, { name: 'sol', kind: 'agent' });
|
|
const more = [ev('award', root.id, { to: seat.id, skill: 'launch', belt: 'green' }), ev('decide', ops.id, { title: 'Ship weekly' }), ev('graduate', root.id, { of: ops.id })];
|
|
const s = reduce([root, ops, seat, ...more]);
|
|
assert.deepEqual(s.log.map(e => summary(e, s)), ['found acme', 'birth ops ← acme', 'birth sol (agent) ← acme',
|
|
'award launch · green → sol, from acme', 'decide “Ship weekly” · ops', 'graduate ops from acme']);
|
|
assert.deepEqual(['Acme Holdings LLC', ' --Zoë_lab-- ', 'a'.repeat(50), '!!!'].map(slug), ['acme-holdings-llc', 'zoe-lab', 'a'.repeat(40), '']);
|
|
});
|