Orgchain v1.8: belts cross holdings, and a graduate becomes its own holding

From the operator's notes on trying v1.6.1. An award's recipient may sit in
any holding, so a registry like well-known can certify an agent it doesn't
hold (say, after the agent pays it). A graduated org already held its own
root; now it is a holding of its own too: it and its subtree re-root under
it, it joins the Holdings list with its own tree and timeline, mines its
share and can be funded, and its parent's tree keeps a row for it that opens
it. Events that touch two holdings (a belt across them, a graduation) show in
both timelines, and the holding that acted gets the last-act credit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H4upF5LVwmPDDqrhdUfnXh
This commit is contained in:
teahubxyzandClaude Opus 5.5 committed 2026-10-15 00:44:59 -07:00
1 parent 69a97544da
commit 7d981341ff
11 files changed
+122 -37

No files matched your search

+2 -1
View File
@@ -85,8 +85,9 @@ test('the yolopreneur sample is six beats of the drawing in docs/lineage.md', ()
const s = state(lab, {}), named = name => [...s.nodes.values()].find(x => x.name === name);
assert.ok(s.log.every(e => e.ok));
assert.equal(s.nodes.get(root).name, SAMPLE);
assert.deepEqual([...s.nodes.values()].filter(x => x.root === root).length, 12);
assert.deepEqual([...s.nodes.values()].filter(x => x.root === root).length, 11);
assert.equal(named('lineage').edge, 'adopted');
assert.deepEqual(s.roots.map(r => r.name), ['yolopreneur', 'lineage']); // graduated: a holding of its own
assert.deepEqual(named('sol').badges.map(b => `${b.skill}:${b.belt}`), ['launch:green']);
assert.deepEqual(named('time').children.map(id => s.nodes.get(id).name), ['luna', 'sol', 'astra', 'docscope', 'dollhouse', 'doodleloop', 'hearthframe']);
assert.deepEqual([2, 3, 4, 5].map(upTo => state(lab, { upTo }).nodes.size), [1, 3, 8, 12]);
+21 -2
View File
@@ -31,7 +31,7 @@ test('every rule rejects with a reason, and rejected events change nothing', ()
[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: 'f'.repeat(64) + 'i0', skill: 'launch', belt: 'green' }), /another node in this lineage/],
[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/],
@@ -59,7 +59,26 @@ test('graduation flips the edge; closing cascades through derived children only'
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 });
// A graduate is a holding of its own: its parent's tree keeps a row for it, and its own tree starts at it.
assert.deepEqual(s.roots.map(r => r.id), [root.id, grad.id]);
assert.equal(s.nodes.get(grad.id).root, grad.id);
assert.deepEqual(s.log.find(e => e.id === up.id).roots, [root.id, grad.id]);
assert.deepEqual(s.log.find(e => e.id === still.id).roots, [grad.id]);
assert.deepEqual(rows(s, root.id).filter(r => r.away).map(r => r.node.name), ['grad']);
assert.deepEqual(draw(s, grad.id), ['● grad']);
assert.equal(trail(s, grad.id), 'grad');
assert.deepEqual(census(s, root.id), { orgs: 2, agents: 1, belts: 1, decisions: 0, closed: 3 });
assert.deepEqual(census(s, grad.id), { orgs: 0, agents: 0, belts: 0, decisions: 1, closed: 0 });
});
test('a belt can cross holdings: the event shows in both, and the issuer acted', () => {
const wk = ev('found', null, { name: 'well-known' }), other = ev('found', null, { name: 'acme' }), bot = ev('birth', other.id, { name: 'bot', kind: 'agent' });
const award = ev('award', wk.id, { to: bot.id, skill: 'listen', belt: 'white' }, 2);
const s = reduce([wk, other, bot, award]), entry = s.log.at(-1);
assert.equal(entry.ok, true);
assert.deepEqual([entry.root, entry.roots], [wk.id, [wk.id, other.id]]);
assert.deepEqual(s.nodes.get(bot.id).badges.map(b => [b.skill, b.belt, b.by]), [['listen', 'white', wk.id]]);
assert.equal(summary(entry, s), 'award listen · white → bot, from well-known');
});
test('upTo shows a past beat; pending adds cued events', () => {
+4 -3
View File
@@ -160,11 +160,12 @@ test('graduation moves money to the new keys, and the graduate pays its own clai
const lab = emptyLab();
seedSample(lab);
const { now, log, live } = money(lab);
assert.deepEqual(now, { yolopreneur: 300 * SAT - 50 * SAT - 2 * FEE - TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 2 * SAT - FEE - TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 27 * SAT - FEE - TOPUP, time: 38.5 * SAT - 2 * FEE - 3 * TOPUP, sol: SAT - TOPUP, luna: 0.5 * SAT });
assert.deepEqual(log.slice(-2), [['move', 7, 'lineage', 'lineage', 2 * SAT - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
const lineage = [...live.nodes.values()].find(n => n.name === 'lineage');
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 2 * SAT - FEE - TOPUP);
assert.equal(lab.chain.balance(payerOf(lab.keys, live, lineage).script), 27 * SAT - FEE - TOPUP);
assert.equal(money(lab, 5).now.lineage, 2 * SAT); // before graduating, at its derived address
const solo = emptyLab();
const root = act(solo, 'found', null, { name: 'acme' }), kid = act(solo, 'birth', root, { name: 'kid' });
+37 -4
View File
@@ -54,8 +54,10 @@ test('found a holding, then birth, award, decide, graduate and close at the defa
await expect(say(page)).toHaveText('studio decided “Ship weekly”, at beat 7.');
await verb(page, 'graduate');
await page.locator('#graduate-go').click();
await expect(say(page)).toHaveText('studio graduates from acme-holdings, at beat 8. Its edge is adopted now.');
await expect(say(page)).toHaveText("studio graduates from acme-holdings, at beat 8. It's a holding of its own now.");
await expect(row(page, 'studio')).toContainText('graduated');
await expect(page.locator('#holdings-list')).toContainText('from acme-holdings at beat 8');
await expect(row(page, 'bot')).toHaveCount(0); // its seats went with it
await row(page, 'acme-holdings').click();
await verb(page, 'close');
await page.locator('#close-go').click();
@@ -64,12 +66,16 @@ test('found a holding, then birth, award, decide, graduate and close at the defa
await expect(say(page)).toHaveText('acme-holdings is closed, at beat 9.');
await expect(row(page, 'acme-holdings')).toContainText('closed');
await expect(row(page, 'studio')).not.toContainText('closed'); // graduated: it holds its own root
await row(page, 'bot').click();
await verb(page, 'birth');
await expect(page.locator('#act-why')).toHaveText('Agents do not give birth.');
await page.locator('#timeline-list .ev', { hasText: 'Ship weekly' }).click();
await expect(page.locator('#inspect-body')).toContainText('"op": "decide"');
await expect(page.locator('#inspect-body')).toContainText('applied');
await row(page, 'studio').click();
await page.locator('#inspect-body').getByRole('button', { name: 'Open studio' }).click();
await expect(page.locator('#tree-sub')).toContainText('studio');
await expect(page.locator('#timeline-list')).toContainText('graduate studio from acme-holdings');
await row(page, 'bot').click();
await verb(page, 'birth');
await expect(page.locator('#act-why')).toHaveText('Agents do not give birth.');
expect(problems).toEqual([]);
});
@@ -300,3 +306,30 @@ test('the sample ends on a standing offer, and its holder accepts it', async ({
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});
test('an agent pays a registry in another holding, and the registry awards it a belt', async ({ page }) => {
const problems = await open(page);
const hold = name => page.locator('#holdings-list .hold', { has: page.locator('.hn', { hasText: new RegExp(`^${name}$`) }) });
await found(page, 'well-known');
await expect(say(page)).toHaveText('well-known is founded, at beat 2.');
await found(page, 'acme');
await expect(say(page)).toHaveText('acme is founded, at beat 3.');
await birth(page, 'scout', 'agent');
await expect(say(page)).toHaveText('scout is born to acme, at beat 4.');
await pay(page, 'acme / scout (agent)', '1');
await expect(say(page)).toHaveText('acme sent 1 tBTC to scout, at beat 5.');
await row(page, 'scout').click();
await pay(page, 'well-known', '0.1');
await expect(say(page)).toHaveText('scout sent 0.1 tBTC to well-known, at beat 6.');
await hold('well-known').click();
await verb(page, 'award');
await page.locator('#award-to').selectOption({ label: 'acme / scout (agent)' });
await page.locator('#award-skill').fill('listen');
await submit(page, 'award');
await expect(say(page)).toHaveText('scout holds a white belt in listen, at beat 7.');
await expect(page.locator('#timeline-list')).toContainText('award listen · white → scout, from well-known');
await hold('acme').click();
await expect(page.locator('#timeline-list')).toContainText('award listen · white → scout, from well-known');
await expect(row(page, 'scout')).toContainText('listen');
expect(problems).toEqual([]);
});