Orgchain v1.11: charters, a ladder of legal forms; agents graduate

A new lineage/v0 op, charter, records a node's legal form: sole proprietor,
LLC, series LLC, series or corporation, with a jurisdiction (ISO 3166), a
legal name, who formed it and a SHA-256 of the filings. Legal form is an
attribute, so a node keeps its inscription, keys and history whatever form it
takes, and the newest charter counts. A node charters itself; a series is
chartered by its master, an ancestor chartered as a series LLC. Whether a
charter stands is derived from the tree, never stored: a series lapses if it
graduates or its master closes or stops holding series, so charters commute
with graduations and closes in any block order.

Agents can graduate: a seat goes independent as a holding of its own, with
its own root and keys and its money along, and can then birth, award and
incorporate. Charter is the tenth verb, and the tree tags each node with its
form. Charter hashes a filing from a file in the tab. The sample charters
yolopreneur as a Wyoming series LLC with its ventures as series, and lineage
graduates into Lineage LLC.

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 12:15:48 -07:00
1 parent 6998ae47aa
commit 4ad75f0970
14 files changed
+389 -50

No files matched your search

+4 -1
View File
@@ -20,7 +20,10 @@ Orgchain is lineage's app for creating and operating lineage holdings on Super T
that predate lab keys.
- Deals live in `src/orgchain/offers.js`: the buyer pre-signs the whole transaction and the holder countersigns.
An offer is signatures kept in the lab, never chain state, and who holds an item is read off the chain, never
from metadata. Inscription metadata is public and permanent, so never put a secret in it (item notes included).
from metadata. Inscription metadata is public and permanent, so never put a secret in it (item notes included), and a
charter carries a hash of the filings, never the filings.
- Charters (`src/orgchain/lineage.js`): legal form is an attribute of a node, and whether a charter stands is derived
from the tree (`standing`), never stored, so charters commute with graduations and closes.
- Keys follow the tree (`src/orgchain/keys.js`): store roots only (one per holding, one per graduated org) and derive
every other key from its position. Every act is signed by whichever key holds the authorizing inscription.
- A node's chain key is its account's BIP-86 Taproot key (`m/86'/1'/account'/0/0`); SegWit is BIP-84 at the same
+14 -4
View File
@@ -9,9 +9,10 @@ chain itself records who did what, and when.
- **Found** a holding (a root: the LLC). **Birth** orgs and agents under any org. **Award** a belt (one skill, one seat,
in any holding: a registry like well-known certifies agents it doesn't hold). **Decide** (a decision on a node's
record). **Make** an item (an inscription its maker holds, and can sell). **Keyring** (a node publishes its keys on
each chain, each key signing the same statement). **Graduate** an org: its edge flips from derived to adopted, and it
becomes a holding of its own, with its own tree, timeline and share of the mining, while its parent's tree keeps a row
for it. **Close** a node (derived children close with it; graduated ones hold their own roots and stay open).
each chain, each key signing the same statement). **Charter** (a node's legal form, from sole proprietor to series
LLC). **Graduate** an org or an agent: its edge flips from derived to adopted, and it becomes a holding of its own,
with its own tree, timeline and share of the mining, while its parent's tree keeps a row for it. **Close** a node
(derived children close with it; graduated ones hold their own roots and stay open).
- **Time.** A beat is a block. At the default tempo each act is its own beat; at 10, 30 or 60 seconds, acts are cued and
land together on the next beat; on manual they wait for *Beat now*. The slider and *Replay* show a holding as it
stood at any beat.
@@ -46,6 +47,14 @@ chain itself records who did what, and when.
check one. The newest keyring counts, no key is bound to two nodes at once, and a graduation retires the keyrings it
re-keys, so the graduate publishes its new keys (the sample shows lineage's). Inspect shows each node's keyring and
the statement its keys signed (`src/orgchain/keyring.js`).
- **Charters.** **Charter** records a node's legal form, which is an attribute, not an identity: a freelancer's holding
starts with no form, takes one when it needs to (sole proprietor, LLC, series LLC or corporation), and keeps its
inscription, keys and history throughout. A series LLC holds its ventures as series, each chartered by the master; a
series lapses if it graduates or its master closes or stops holding series, which the tree shows by striking its tag.
A graduated venture charters itself as an LLC. An agent can graduate too: a seat goes independent as a holding of its
own, free to birth and award, then incorporate. A charter carries a jurisdiction, a legal name, who formed it and a
SHA-256 of the filings, which Charter can compute from a file without the file leaving the tab. The sample shows
yolopreneur as a Wyoming series LLC with its ventures as series, and lineage graduating into Lineage LLC.
- **One interpretation.** `src/orgchain/lineage.js` folds `lineage/v0` events into trees, and vets every act before
anything is signed. Events it rejects stay on the chain and show as rejected; they just don't change the tree.
- **Keys.** Each holding gets its own 24-word lab root, and every node has lineage's keys at its place in the tree
@@ -84,7 +93,8 @@ keyring's is the statement its keys signed).
| `decide` | the deciding node | `title`, `why` |
| `make` | the making node, which then holds the item | `name`, `note` |
| `keyring` | the node itself; the newest counts, and a graduation retires it | `taproot`, `segwit`, `nostr`, `ethereum`, and `proof` (each key's signature) |
| `graduate` | the child's parent (then the handoff to the child's fresh root); the child becomes a holding | `of` |
| `charter` | the node itself, or a series' master; the newest counts | `form`; `jurisdiction`, `legal`, `provider`, `filing`, or `of` for a series |
| `graduate` | the child's parent (then the handoff to the child's fresh root); the child, org or agent, becomes a holding | `of` |
| `close` | the node itself | none |
Names are domain labels (lowercase letters, digits, hyphens). Org and agent indices count separately and are never
+37 -13
View File
@@ -53,28 +53,52 @@ the inscription moves the authority.
| op | parent | metadata | effect |
| --- | --- | --- | --- |
| `found` | none | `name` | a holding; no active holding may already have that name |
| `birth` | an active holding or org | `name`, `kind` (`org` or `agent`, default `org`) | a child; the parent may not already have an active child of that name |
| `award` | an active holding or org | `to`, `skill`, `belt` (`white`, `green` or `black`) | `to` (any other active node, in any holding) holds that belt in that skill, replacing its earlier belt in it |
| `birth` | an active holding, org or graduated agent | `name`, `kind` (`org` or `agent`, default `org`) | a child; the parent may not already have an active child of that name |
| `award` | an active holding, org or graduated agent | `to`, `skill`, `belt` (`white`, `green` or `black`) | `to` (any other active node, in any holding) holds that belt in that skill, replacing its earlier belt in it |
| `decide` | an active node | `title` (required), `why` (optional) | a decision on the node's record |
| `make` | an active node | `name`, `note` (optional) | an item, made and first held by the node; a node makes one item per name |
| `keyring` | the node itself, active | `taproot`, `segwit`, `nostr`, `ethereum`, and `proof`: each key's signature | the node's keys are bound to it, replacing its earlier keyring (section 8) |
| `graduate` | the child's parent, active | `of` (an active org child, not yet adopted) | the edge becomes adopted, and the org becomes a holding of its own (section 5) |
| `charter` | the node itself, active; for a series, its master | `form`; `jurisdiction`, `legal`, `provider` and `filing`, or `of` for a series | the node's legal form, replacing its earlier charter (section 5) |
| `graduate` | the child's parent, active | `of` (an active child, org or agent, not yet adopted) | the edge becomes adopted, and the child becomes a holding of its own (section 5) |
| `close` | the node itself, active | none | the node closes, with every active derived descendant; adopted ones stay open |
Names (holdings, children, skills, items) are 1 to 40 lowercase letters, digits or hyphens, starting and ending with a
letter or digit, because they become domains. Text fields are trimmed, and only their first characters count: 140
for a title, 400 for a why, 280 for a note. Agents do not birth or award. An event that
breaks a rule is rejected: it stays on the chain and changes nothing. Any other `op` is rejected.
letter or digit, because they become domains. Text fields are trimmed, and only their first characters count: 140 for a
title, 400 for a why, 280 for a note, 120 for a legal name. Agents birth and award only once they graduate. An event
that breaks a rule is rejected: it stays on the chain and changes nothing. Any other `op` is rejected.
## 5. Holdings and graduation
## 5. Holdings, graduation and charters
A holding is a root: a `found`, or an org that graduated. When an org graduates, it and its descendants still in the
old holding re-root under it, and it joins the holdings. Its parent stays its parent, so where it came from stays on the
record, and its old holding's tree keeps a row for it. A graduation event belongs to both holdings. Its keys change with
its new root, and so do its derived descendants', so their keyrings retire (section 8).
A holding is a root: a `found`, or a child that graduated. When an org graduates, it and its descendants still in the
old holding re-root under it, and it joins the holdings. An agent can graduate too: it goes independent, and as a holding
of its own it can birth and award. A graduate's parent stays its parent, so where it came from stays on the record, and
its old holding's tree keeps a row for it. A graduation event belongs to both holdings. Its keys change with its new
root, and so do its derived descendants', so their keyrings retire (section 8).
In Orgchain a graduation is followed by a handoff: the org's fresh root claims its inscription, and the derived key that
held it countersigns. After that, only the new root can act for the org.
In Orgchain a graduation is followed by a handoff: the graduate's fresh root claims its inscription, and the derived key
that held it countersigns. After that, only the new root can act for it.
A `charter` records a node's legal form: what it is off chain. Legal form is an attribute, not an identity, so a node
keeps its inscription, history and keys whatever form it takes, and the newest charter counts.
| `form` | what it is | who may take it | chartered by |
| --- | --- | --- | --- |
| `sole` | a person trading under their own name | a holding or an agent | the node |
| `llc` | a company of its own (for a derived org, one its parent's line controls) | a holding, an org or a graduated agent | the node |
| `series-llc` | an LLC that can hold series | a holding, an org or a graduated agent | the node |
| `series` | a cell of an ancestor's series LLC | an org | that ancestor, its master |
| `corp` | a corporation | a holding, an org or a graduated agent | the node |
`jurisdiction` is an ISO 3166 code such as `US-WY`, required for an LLC, a series LLC or a corporation and optional for
a sole proprietor; a series takes its master's. `legal` is the legal name, `provider` the name of whoever formed it (by
the name rule), and `filing` the SHA-256 of the filed documents, in hex: metadata is public, so a charter carries a hash,
never the documents. A series charter names the series in `of`; its master must be an ancestor whose charter is a series
LLC at the time.
Whether a charter stands is read off the tree as it is, not stored: a charter stands while its node is active, and a
series also needs its master active, still a series LLC, and in the same holding as the series. A series that graduates,
or whose master closes or stops holding series, lapses. Because standing is derived, a charter and a graduation in the
same block land the same way in either order. Control follows the inscription, whatever the form.
## 6. Order-sensitive conflicts
+22 -4
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Orgchain · Lineage</title>
<meta name="description" content="Found and operate lineage holdings on a toy chain in your browser: birth orgs and agents, award belts, decide, make and trade items, publish keyrings, send test BTC, graduate and close, one beat at a time. Test coins only.">
<meta name="description" content="Found and operate lineage holdings on a toy chain in your browser: birth orgs and agents, award belts, decide, make and trade items, publish keyrings, charter legal forms, send test BTC, graduate and close, one beat at a time. Test coins only.">
<meta name="color-scheme" content="light dark">
<link rel="icon" href="./favicon.svg" type="image/svg+xml">
<script src="./theme.js"></script>
@@ -16,7 +16,7 @@
<div>
<p class="kicker"><a href="../">Lineage</a> /</p>
<h1 class="wordmark">Orgchain</h1>
<p class="lede">Found a <b>holding</b>, then birth its orgs and agents, award belts, record decisions, make and trade items, publish keyrings, send test BTC, graduate and close. Each act is an inscription whose parent is the node that authorizes it, paid for with the actor's own gas, and each <b>beat</b> is a block whose 50 test BTC the holdings mine. Lineage gives the structure; the chain adds time.</p>
<p class="lede">Found a <b>holding</b>, then birth its orgs and agents, award belts, record decisions, make and trade items, publish keyrings, charter legal forms, send test BTC, graduate and close. Each act is an inscription whose parent is the node that authorizes it, paid for with the actor's own gas, and each <b>beat</b> is a block whose 50 test BTC the holdings mine. Lineage gives the structure; the chain adds time.</p>
</div>
<div class="flags">
<span class="flag hz">Test coins only</span>
@@ -88,7 +88,7 @@
<p class="pad note" id="act-none">Select a holding, or a node in its tree, to act through it.</p>
<div class="pad note" id="act-past" hidden><p>You are looking at beat <b id="past-beat"></b>. History is read-only.</p><button type="button" class="btn sm" id="past-now">Back to now</button></div>
<div id="act-on" hidden>
<div class="verbs" role="group" aria-label="Verb"><button type="button" data-verb="birth" aria-pressed="true">Birth</button><button type="button" data-verb="award" aria-pressed="false">Award</button><button type="button" data-verb="decide" aria-pressed="false">Decide</button><button type="button" data-verb="make" aria-pressed="false">Make</button><button type="button" data-verb="send" aria-pressed="false">Send</button><button type="button" data-verb="offer" aria-pressed="false">Offer</button><button type="button" data-verb="keyring" aria-pressed="false">Keyring</button><button type="button" data-verb="graduate" aria-pressed="false">Graduate</button><button type="button" data-verb="close" aria-pressed="false">Close</button></div>
<div class="verbs" role="group" aria-label="Verb"><button type="button" data-verb="birth" aria-pressed="true">Birth</button><button type="button" data-verb="award" aria-pressed="false">Award</button><button type="button" data-verb="decide" aria-pressed="false">Decide</button><button type="button" data-verb="make" aria-pressed="false">Make</button><button type="button" data-verb="send" aria-pressed="false">Send</button><button type="button" data-verb="offer" aria-pressed="false">Offer</button><button type="button" data-verb="keyring" aria-pressed="false">Keyring</button><button type="button" data-verb="charter" aria-pressed="false">Charter</button><button type="button" data-verb="graduate" aria-pressed="false">Graduate</button><button type="button" data-verb="close" aria-pressed="false">Close</button></div>
<p class="keyline" id="act-key"></p>
<p class="keyline" id="act-gas"></p>
<p class="why pad" id="act-why"></p>
@@ -154,8 +154,26 @@
<p class="hint">Each key signs the way its own wallets sign a message: BIP-322 for the Bitcoin addresses, a nostr event for the npub, EIP-191 for Ethereum. No other node can list these keys while this keyring stands. A newer keyring replaces it, and a graduation retires it.</p>
<button class="btn primary go" type="submit">Publish keyring</button>
</form>
<form class="vf pad" id="f-charter" data-form="charter" autocomplete="off" hidden>
<p class="vh"><b data-name></b> takes a legal form: an on-chain record of what it is off chain. <span id="charter-now"></span></p>
<fieldset class="kinds" id="charter-forms"><legend class="lab">Form</legend></fieldset>
<div id="charter-where">
<label class="lab" for="charter-jurisdiction">Jurisdiction <span>an ISO code, like US-WY</span></label>
<input id="charter-jurisdiction" name="jurisdiction" list="jurisdictions" maxlength="6" spellcheck="false" placeholder="US-WY">
<datalist id="jurisdictions"><option value="US-WY"><option value="US-DE"><option value="US-NV"><option value="US-TX"><option value="US-FL"><option value="US-NY"><option value="US-CA"><option value="CH"><option value="MH"></datalist>
<label class="lab" for="charter-legal">Legal name <span>optional</span></label>
<input id="charter-legal" name="legal" maxlength="120" placeholder="Yolopreneur Holdings LLC">
<label class="lab" for="charter-provider">Formed through <span>optional</span></label>
<input id="charter-provider" name="provider" list="providers" maxlength="40" spellcheck="false" placeholder="doola">
<datalist id="providers"><option value="doola"><option value="otoco"><option value="self"></datalist>
<label class="lab" for="charter-filing">Filing <span>optional: the SHA-256 of the filed documents</span></label>
<div class="line amt"><input id="charter-filing" name="filing" maxlength="64" spellcheck="false" placeholder="SHA-256, in hex"><label class="btn" for="charter-file">Hash a file</label><input type="file" id="charter-file" hidden></div>
</div>
<p class="hint">Public and permanent: a form, a code, a name and a hash, never the documents. Hash a file reads it here, and nothing leaves this tab. The newest charter counts. A series is chartered by its master, and it lapses if the master closes, stops holding series, or the series leaves its holding. Whoever holds the inscription holds control, whatever the form.</p>
<button class="btn primary go" type="submit">Charter</button>
</form>
<form class="vf pad" id="f-graduate" data-form="graduate" hidden>
<p class="vh"><b data-parent></b> vouches for <b data-name></b>, and the edge flips from derived to adopted. A graduated org holds its own root and becomes a holding of its own, so it mines, and it stays open if <b data-parent></b> closes. The event spends <b data-parent></b>'s inscription.</p>
<p class="vh"><b data-parent></b> vouches for <b data-name></b>, and the edge flips from derived to adopted. A graduate holds its own root and becomes a holding of its own, so it mines, and it stays open if <b data-parent></b> closes: an org takes its derived subtree along, and an agent goes independent, free to birth and award. The event spends <b data-parent></b>'s inscription.</p>
<button class="btn primary go" type="submit" id="graduate-go">Graduate</button>
</form>
<form class="vf pad" id="f-close" data-form="close" hidden>
+86 -6
View File
@@ -2,7 +2,7 @@
// Every change goes through the lab's lock (orgchain/store.js) and the reducer (orgchain/lineage.js); this file draws and asks.
import './orgchain.css';
import { MetadataIndex } from './adapters/metadata-index.js';
import { census, lineageEvents, reduce, rows, slug, summary, trail } from './orgchain/lineage.js';
import { FORM_NAMES, census, isSeat, lineageEvents, reduce, rows, slug, standing, summary, trail } from './orgchain/lineage.js';
import { LAB_KEY, beat, decodeLab, emptyLab, encodeLab, mutate, readLab, setRigs, setRule, setTempo, tick } from './orgchain/store.js';
import { SAMPLE, accept, act, beatClash, deposit, gasCheck, gasCost, lastHolding, minerOf, offer, send, seedSample, withdraw } from './orgchain/actions.js';
import { KEYS, statement } from './orgchain/keyring.js';
@@ -193,6 +193,7 @@ function marks(n) {
const list = [
n.kind === 'agent' && h('span', { class: 'mk', text: 'agent' }),
n.edge === 'adopted' && h('span', { class: 'mk ok', text: 'graduated' }),
n.charter && h('span', { class: `mk form${standing(view, n)?.ok ? '' : ' lapsed'}`, title: charterLine(n), text: FORM_SHORT[n.charter.form] }),
n.status === 'closed' && h('span', { class: 'mk bad', text: 'closed' }),
n.born === 'cued' && h('span', { class: 'mk', text: 'cued' }),
...n.badges.map(belt),
@@ -201,6 +202,55 @@ function marks(n) {
return list.flatMap((mark, i) => (i ? [' ', mark] : [mark])); // spaces keep accessible names readable
}
const belt = b => h('span', { class: `belt ${b.belt}`, title: `${b.skill}: ${b.belt} belt` }, b.skill, h('span', { class: 'sr', text: ` (${b.belt} belt)` }));
// Charters (orgchain/lineage.js): a node's legal form, as of the viewed beat.
const FORM_SHORT = { sole: 'sole', llc: 'llc', 'series-llc': 'series llc', series: 'series', corp: 'corp' };
const FORM_TITLE = { sole: 'Sole proprietor', llc: 'LLC', 'series-llc': 'Series LLC', series: 'Series', corp: 'Corporation' };
const FORM_A = { sole: 'a sole proprietor', llc: 'an LLC', 'series-llc': 'a series LLC', corp: 'a corporation' };
// "Series LLC · US-WY · Yolopreneur Holdings LLC · via doola · since beat 3"; a series names its master and its jurisdiction.
function charterLine(n, state = view) {
const c = n.charter, master = c.master && node(c.master, state), where = c.form === 'series' ? master?.charter?.jurisdiction : c.jurisdiction;
return [c.form === 'series' ? `Series of ${master?.name ?? '?'}` : FORM_TITLE[c.form], where, c.legal, c.provider && `via ${c.provider}`, `since ${at(c.beat)}`]
.filter(Boolean).join(' · ');
}
// The nearest ancestor in n's own holding chartered as a series LLC: the master n can become a series of.
function masterOf(n) {
for (let p = node(n.parent); p && p.root === n.root; p = node(p.parent)) if (p.status === 'active' && p.charter?.form === 'series-llc') return p;
return null;
}
// The forms n can take now, as [form, label, hint]. A person can be a sole proprietor; an agent incorporates once it
// graduates; an org in a series LLC's holding can become one of its series.
function charterForms(n) {
const master = n.kind === 'org' && masterOf(n), derived = n.kind === 'org' && n.edge === 'derived';
return [
(n.kind === 'holding' || n.kind === 'agent') && ['sole', 'Sole proprietor', 'a person trading under their own name'],
master && ['series', `Series of ${master.name}`, `a cell of ${master.name}'s LLC, chartered by ${master.name}`],
!isSeat(n) && ['llc', 'LLC', derived ? `a company of its own, owned through ${node(n.parent)?.name} while it is derived` : 'a company of its own'],
!isSeat(n) && ['series-llc', 'Series LLC', 'an LLC that can hold series, one for each venture'],
!isSeat(n) && ['corp', 'Corporation', 'a company with shares'],
].filter(Boolean);
}
// Each form with who signs it (a series' master, otherwise the node itself) and whether that key can pay the gas.
function charterOptions(n) {
return charterForms(n).map(([form, label, hint]) => {
const signer = form === 'series' ? masterOf(n) : n, why = gasCheck(lab, live, 'charter', signer.id);
return { form, label, hint: why ? `${why}.` : hint, off: !!why };
});
}
// '' when some form is open to n, or why none is.
const charterBlock = n => { const list = charterOptions(n); return list.some(o => !o.off) ? '' : list[0]?.hint.replace(/\.$/, '') ?? 'No legal form fits'; };
const charterChoice = () => document.querySelector('#charter-forms input:checked')?.value ?? null;
function renderCharter(n) {
const box = $('charter-forms'), list = charterOptions(n), sig = list.map(o => `${o.form}=${o.label}=${o.off}`).join('|');
if (box.dataset.sig !== sig) {
const keep = charterChoice(), open = list.filter(o => !o.off), pick = open.some(o => o.form === keep) ? keep : open[0]?.form;
box.dataset.sig = sig;
box.replaceChildren(h('legend', { class: 'lab', text: 'Form' }), ...list.map(o =>
h('label', { class: o.off ? 'off' : null }, h('input', { type: 'radio', name: 'form', value: o.form, checked: o.form === pick, disabled: o.off }), ` ${o.label} `, h('span', { text: o.hint }))));
}
const series = charterChoice() === 'series', now = standing(live, n);
$('charter-where').hidden = series;
$('charter-now').textContent = !n.charter ? 'It has no legal form yet.' : `Now: ${charterLine(n, live)}${now.ok ? '' : `, lapsed: ${now.why}`}.`;
}
const below = (x, n) => { for (let p = node(x.parent); p; p = node(p.parent)) if (p.id === n.id) return true; return false; };
// Who n can award: anyone else active, in any holding (a registry certifies outsiders); its own seats first, then its
@@ -262,14 +312,15 @@ function blocked(n) {
const parent = node(n.parent), closed = n.status === 'closed' ? `${n.name} is closed` : '';
const gas = (op, by = n.id, fields = {}) => gasCheck(lab, live, op, by, fields);
return {
birth: closed || (n.kind === 'agent' ? 'Agents do not give birth' : gas('birth')),
award: closed || (n.kind === 'agent' ? 'Agents do not award belts' : recipients(n).length ? gas('award') : 'There is nobody else to award yet'),
birth: closed || (isSeat(n) ? 'Agents give birth only once they graduate' : gas('birth')),
award: closed || (isSeat(n) ? 'Agents award belts only once they graduate' : recipients(n).length ? gas('award') : 'There is nobody else to award yet'),
charter: closed || charterBlock(n),
decide: closed || gas('decide'),
make: closed || gas('make'),
keyring: closed || keyringBlock(n) || gas('keyring'),
send: closed || sendBlock(n),
offer: closed || offerBlock(n),
graduate: closed || (!parent ? 'A holding has no parent to graduate from' : n.kind !== 'org' ? 'Only orgs graduate'
graduate: closed || (!parent ? 'A holding has no parent to graduate from'
: n.edge === 'adopted' ? `${n.name} has already graduated` : parent.status !== 'active' ? `${parent.name} is closed`
: beatClash(live, 'graduate', parent.id, { of: n.id }) || gas('graduate', parent.id, { of: n.id })),
close: closed || gas('close'),
@@ -302,9 +353,10 @@ function renderAct() {
}
for (const id of ['send-has', 'offer-has']) $(id).textContent = btc(held(n.id));
$('graduate-go').textContent = `Graduate ${n.name}`;
if (ui.verb === 'charter' && !why.charter) renderCharter(n);
const keys = ui.verb === 'keyring' && !why.keyring && ringKeys(n);
if (keys) $('keyring-keys').replaceChildren(...[['Taproot', keys.taproot], ['SegWit', keys.segwit], ['nostr', keys.nostr], ['Ethereum', keys.ethereum]].map(fact));
const signer = ui.verb === 'graduate' ? parent : n, k = signer && keyOf(lab.keys, live, signer);
const signer = ui.verb === 'graduate' ? parent : ui.verb === 'charter' && charterChoice() === 'series' ? masterOf(n) : n, k = signer && keyOf(lab.keys, live, signer);
const pays = ui.verb === 'send' || ui.verb === 'offer', path = k && (pays ? `${k.from} · ${payerOf(lab.keys, live, signer).path}` : k.path);
$('act-key').textContent = !signer || why[ui.verb] ? '' : k ? `Signs as ${signer.name}: ${path}` : 'Signs with the lab wallet: this holding predates lab keys.';
const moving = ui.verb === 'graduate' && keyOf(lab.keys, live, n) ? derivedSubtree(live, n.id).length : 0;
@@ -341,6 +393,10 @@ function nodeView(n) {
parent && n.root === n.id && ['Holding', h('span', {}, 'its own, since it graduated',
n.id !== ui.holding && h('button', { type: 'button', class: 'btn sm open', onclick: () => { pick({ holding: n.id, node: n.id }); showTree(); } }, `Open ${n.name}`))],
n.closed !== null && ['Closed', at(n.closed)],
['Charter', n.charter ? charterLine(n) : 'none yet', !n.charter ? 'Charter gives it a legal form: sole proprietor, LLC, series LLC, series or corporation.'
: standing(view, n).ok ? (n.charter.filing ? `Filing SHA-256 ${n.charter.filing.slice(0, 16)}…` : '') : `Lapsed: ${standing(view, n).why}.`],
n.charter?.form === 'series-llc' && ['Series', [...view.nodes.values()].filter(x => x.charter?.master === n.id)
.map(x => `${x.name}${standing(view, x).ok ? '' : ' (lapsed)'}`).join(', ') || 'none yet'],
['Children', n.children.length ? n.children.map(id => node(id, view)?.name).join(', ') : 'none yet'],
payerOf(lab.keys, view, n) && ['Balance', btc(purse(n.id)), `Test BTC at its SegWit address${ui.beat === null ? '' : `, as of beat ${ui.beat}`}.`],
ui.beat === null && keyOf(lab.keys, live, n) && ['Gas tank', btc(tankOf(lab.chain, keyOf(lab.keys, live, n).wallet)),
@@ -449,7 +505,7 @@ function keysView(n) {
const facts = [['Taproot', a.taproot.address, taproot], ['SegWit', a.segwit.address, `BIP-84 at ${a.segwit.path}. Its money: ${n.kind === 'holding' ? 'the beats it earns land here, and ' : ''}sends come and go here.`],
['Tapkey', a.tapkey.address, 'Its npub as a Taproot address (its nostr key with the BIP-86 tweak), so the npub alone finds it. Nostr secrets get pasted into clients, so treat it as an inbox.'],
['npub', a.npub.address, `NIP-06 at ${a.npub.path}. Its nostr name; the leaves below hang off it.`],
['Ethereum', a.ethereum.address, agent ? `BIP-44 at ${a.ethereum.path}: account ${k.account} of its org's seed, the way Ledger Live numbers Ethereum accounts.`
['Ethereum', a.ethereum.address, agent && n.edge !== 'adopted' ? `BIP-44 at ${a.ethereum.path}: account ${k.account} of its org's seed, the way Ledger Live numbers Ethereum accounts.`
: `BIP-44 at ${a.ethereum.path}. Its legal-control key, the one that holds the entity NFT, as on the lineage key tree page.`],
['From', `${k.from} · account ${k.account}`], held, ringFact(n)];
return h('div', { class: 'keys' },
@@ -633,6 +689,30 @@ $('f-make').addEventListener('submit', async event => {
if (!name) return say('Name the item: lowercase letters, digits and hyphens.', true);
if (await perform('make', n.id, { name, note }, (id, where) => `${n.name} made ${name}, ${where}.`)) form.reset();
});
$('f-charter').addEventListener('change', event => { if (event.target.name === 'form') renderAct(); });
// A filing's hash is computed here from the file the person picks; the file itself never leaves the tab.
$('charter-file').addEventListener('change', async event => {
const file = event.target.files?.[0];
if (!file) return;
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', await file.arrayBuffer()));
$('charter-filing').value = [...digest].map(b => b.toString(16).padStart(2, '0')).join('');
event.target.value = '';
say(`Hashed ${file.name} in this tab: only its SHA-256 would go on chain.`);
});
$('f-charter').addEventListener('submit', async event => {
event.preventDefault();
const form = event.currentTarget, n = node(ui.node), kind = charterChoice();
if (!kind) return say('Pick a legal form.', true);
if (kind === 'series') {
const master = masterOf(n);
await perform('charter', master.id, { form: 'series', of: n.id }, (id, where) => `${n.name} is a series of ${master.name}, ${where}.`);
return;
}
const field = name => form.querySelector(`[name="${name}"]`).value.trim();
const fields = { form: kind, jurisdiction: field('jurisdiction').toUpperCase(), legal: field('legal'), provider: slug(field('provider')), filing: field('filing').toLowerCase() };
const done = (id, where) => `${n.name} is chartered as ${FORM_A[kind]}${fields.jurisdiction ? ` in ${fields.jurisdiction}` : ''}, ${where}.`;
if (await perform('charter', n.id, fields, done)) for (const name of ['legal', 'filing']) form.querySelector(`[name="${name}"]`).value = '';
});
$('f-keyring').addEventListener('submit', async event => {
event.preventDefault();
const n = node(ui.node);
+10
View File
@@ -330,3 +330,13 @@ textarea { resize: vertical; line-height: 1.5; }
.ring .f { padding-top: 8px; margin-top: 8px; }
.ring .f:first-child { margin-top: 0; padding-top: 0; }
.ring .f dd { font: var(--t-xs)/1.5 var(--mono); }
.verbs button:nth-child(-n+9) { border-bottom: 2px solid var(--ink); }
.verbs button:last-child:nth-child(3n+1) { grid-column: span 3; }
.mk.lapsed { color: var(--ink-3); text-decoration: line-through; }
#charter-where { display: grid; gap: 6px; }
#charter-where[hidden] { display: none; }
#charter-now { color: var(--ink-2); }
#charter-forms label.off { color: var(--ink-3); }
#charter-forms label { display: grid; grid-template-columns: auto 1fr; column-gap: 8px; row-gap: 2px; }
#charter-forms label span { grid-column: 2; font-size: var(--t-xs); }
#charter-forms label.off { cursor: default; }
+14 -8
View File
@@ -1,4 +1,4 @@
// Orgchain's verbs as lineage/v0 inscriptions: found, birth, award, decide, make, keyring, graduate, close. Beside them, money
// Orgchain's verbs as lineage/v0 inscriptions: found, birth, award, decide, make, keyring, charter, graduate, close. Beside them, money
// moves: sends, offers on items and their acceptance, and funding a holding from the lab wallet.
// Each act is signed by the key that holds its authorizing node's inscription (lineage's key tree, ./keys.js).
// Gas: every act is paid by the acting node's own keys, topped up from its own money (./money.js); the lab wallet
@@ -18,10 +18,11 @@ import { DUST, REWARD, SAT, canSpend, covers, pay, payouts, payerOf, spend, swee
import { acceptOffer, makeOffer, withdrawOffer } from './offers.js';
const FIELDS = { found: ['name'], birth: ['name', 'kind'], award: ['to', 'skill', 'belt'], decide: ['title', 'why'], make: ['name', 'note'],
keyring: KEYS, graduate: ['of'], close: [] };
keyring: KEYS, charter: ['form', 'of', 'jurisdiction', 'legal', 'provider', 'filing'], graduate: ['of'], close: [] };
// A keyring's body is its statement, so an explorer shows what every key signed. The rules rebuild it from the metadata.
const BODY = { found: m => `found ${m.name}`, birth: m => `birth ${m.name}`, award: m => `award ${m.skill} ${m.belt}`,
decide: m => `decide ${m.title}`, make: m => `make ${m.name}`, keyring: (m, by) => statement(by, m), graduate: () => 'graduate', close: () => 'close' };
decide: m => `decide ${m.title}`, make: m => `make ${m.name}`, keyring: (m, by) => statement(by, m),
charter: m => `charter ${m.form}${m.jurisdiction ? ` ${m.jurisdiction}` : ''}`, graduate: () => 'graduate', close: () => 'close' };
export function metadataFor(op, fields = {}) {
if (!Object.hasOwn(FIELDS, op)) throw new Error('Unknown verb.');
@@ -238,18 +239,22 @@ export function deposit(lab, holdingId, amount, now = Date.now()) {
// The drawing in docs/lineage.md, rehearsed over six beats. Names stand in for inscription ids until those exist.
// Its beats pay yolopreneur, and sends pass money down the tree before each org acts, since every act pays its gas.
// Three nodes publish keyrings, and lineage's retires when it graduates (its keys change with its root). It ends on a
// standing offer: lineage's bid for the launch kit sol made, there for the holder to accept.
// Three nodes publish keyrings, and lineage's retires when it graduates (its keys change with its root). yolopreneur is
// chartered as a series LLC with its ventures as series; lineage leaves as a series and charters itself as an LLC once it
// graduates. It ends on a standing offer: lineage's bid for the launch kit sol made, there for the holder to accept.
export const SAMPLE = 'yolopreneur';
const PLAN = [
[['found', null, { name: 'yolopreneur' }]],
[['birth', 'yolopreneur', { name: 'well-known', kind: 'org' }], ['birth', 'yolopreneur', { name: 'time', kind: 'org' }], ['keyring', 'yolopreneur', {}]],
[['birth', 'yolopreneur', { name: 'well-known', kind: 'org' }], ['birth', 'yolopreneur', { name: 'time', kind: 'org' }], ['keyring', 'yolopreneur', {}],
['charter', 'yolopreneur', { form: 'series-llc', jurisdiction: 'US-WY', legal: 'Yolopreneur Holdings LLC', provider: 'doola' }]],
[['send', 'yolopreneur', { to: 'time', amount: 40 }], ['send', 'yolopreneur', { to: 'well-known', amount: 10 }],
['birth', 'well-known', { name: 'mailr', kind: 'org' }], ['birth', 'well-known', { name: 'lineage', kind: 'org' }],
['birth', 'time', { name: 'luna', kind: 'agent' }], ['birth', 'time', { name: 'sol', kind: 'agent' }], ['birth', 'time', { name: 'astra', kind: 'agent' }]],
['birth', 'time', { name: 'luna', kind: 'agent' }], ['birth', 'time', { name: 'sol', kind: 'agent' }], ['birth', 'time', { name: 'astra', kind: 'agent' }],
['charter', 'yolopreneur', { form: 'series', of: 'well-known' }], ['charter', 'yolopreneur', { form: 'series', of: 'time' }]],
[['send', 'well-known', { to: 'lineage', amount: 2 }],
['birth', 'time', { name: 'docscope', kind: 'org' }], ['birth', 'time', { name: 'dollhouse', kind: 'org' }],
['birth', 'time', { name: 'doodleloop', kind: 'org' }], ['birth', 'time', { name: 'hearthframe', kind: 'org' }]],
['birth', 'time', { name: 'doodleloop', kind: 'org' }], ['birth', 'time', { name: 'hearthframe', kind: 'org' }],
['charter', 'yolopreneur', { form: 'series', of: 'lineage' }]],
[['send', 'time', { to: 'sol', amount: 1 }], ['send', 'time', { to: 'luna', amount: 0.5 }],
['award', 'time', { to: 'sol', skill: 'launch', belt: 'green' }], ['award', 'time', { to: 'luna', skill: 'post', belt: 'white' }],
['decide', 'yolopreneur', { title: 'Every venture gets a page', why: '<venture>.yolopreneur.lol, served from one pages branch.' }],
@@ -257,6 +262,7 @@ const PLAN = [
['keyring', 'sol', {}], ['keyring', 'lineage', {}]],
[['graduate', 'well-known', { of: 'lineage' }],
['decide', 'lineage', { title: 'Lineage gets its own org', why: 'Its own repo on Teahub now, lineage.llc later: adopted, not derived.' }],
['charter', 'lineage', { form: 'llc', jurisdiction: 'US-WY', legal: 'Lineage LLC', provider: 'doola' }],
['offer', 'lineage', { item: 'launch-kit', price: 0.1 }]],
];
export function seedSample(lab) {
+4 -2
View File
@@ -32,12 +32,13 @@ export function seedOf(keys, state, node) {
if (!node) return null;
const own = mnemonic => (mnemonic ? { mnemonic, account: 0, origin: `${node.name} root`, steps: [] } : null);
if (node.kind === 'holding') return own(keys.roots[node.id]);
// A graduate, org or agent, holds its own root and is account 0 of it.
if (node.edge === 'adopted') return own(keys.adopted[node.id]);
const parent = state.nodes.get(node.parent);
if (node.kind === 'agent') {
const org = seedOf(keys, state, parent);
return org && { ...org, account: node.index + 1 };
}
if (node.edge === 'adopted') return own(keys.adopted[node.id]);
const up = seedOf(keys, state, parent);
return up && { mnemonic: childMnemonic(up.mnemonic, node.index), account: 0, origin: up.origin, steps: [...up.steps, `${node.index}'`] };
}
@@ -154,7 +155,8 @@ export function secretsOf(keys, state, node) {
const taproot = { bip86: `tr(${wif(m86)})`, tapkey: `tr(${wif(nostr)})`, wallet: `rawtr(${wif(m86)})` }[keys.scheme];
const agent = node.kind === 'agent', domain = domainOf(state, agent ? state.nodes.get(node.parent) : node);
return {
seed: agent ? null : { from: [seed.origin, ...seed.steps].join(' › '), mnemonic: seed.mnemonic },
// A seat is an account of its org's seed; a graduated agent holds a root of its own.
seed: agent && node.edge !== 'adopted' ? null : { from: [seed.origin, ...seed.steps].join(' › '), mnemonic: seed.mnemonic },
account: [['nsec', 'NIP-06', nsecOf(nostr)], ['taproot', 'descriptor', descsum(taproot)],
['segwit', 'descriptor', descsum(`wpkh(${wif(segwitSecret(seed.mnemonic, seed.account))})`)], ['tapkey', 'descriptor', descsum(`tr(${wif(nostr)})`)],
['ethereum', 'private key', `0x${ethSecret(seed.mnemonic, seed.account)}`]],
+57 -4
View File
@@ -5,6 +5,14 @@ import { KEYS, keyringWhy, slots } from './keyring.js';
export const PROTOCOL = 'lineage/v0';
export const BELTS = ['white', 'green', 'black'];
// Legal forms a charter records (SPEC.md section 5): what a node is off chain. A series is a cell of an ancestor's
// series LLC and is chartered by that master; every other form is chartered by the node itself.
export const FORMS = ['sole', 'llc', 'series-llc', 'series', 'corp'];
export const FORM_NAMES = { sole: 'sole proprietor', llc: 'LLC', 'series-llc': 'series LLC', series: 'series', corp: 'corporation' };
const JURISDICTION = /^[A-Z]{2}(-[A-Z0-9]{1,3})?$/;
const HASH = /^[0-9a-f]{64}$/;
// A seat is an agent inside an org's tree. One that graduates holds its own root and acts like a holding.
export const isSeat = n => n.kind === 'agent' && n.root !== n.id;
export const NAME_RULE = 'names are 1-40 lowercase letters, digits or hyphens (they become domains)';
const NAME = /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/;
const isName = v => typeof v === 'string' && NAME.test(v);
@@ -47,7 +55,7 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
case 'birth': {
const kind = m.kind === 'agent' ? 'agent' : 'org';
if ((why = authority('parent'))) fail(why);
else if (by.kind === 'agent') fail('agents do not give birth');
else if (isSeat(by)) fail('agents give birth only once they graduate');
else if (!isName(m.name)) fail(NAME_RULE);
else if (by.children.some(id => active(nodes.get(id)) && nodes.get(id).name === m.name)) fail(`${by.name} already has an active ${m.name}`);
else {
@@ -60,7 +68,7 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
case 'award': {
const to = nodes.get(m.to);
if ((why = authority('issuer'))) fail(why);
else if (by.kind === 'agent') fail('agents do not award belts');
else if (isSeat(by)) fail('agents award belts only once they graduate');
else if (!to || to.id === by.id) fail('the recipient must be another node in this lineage');
else if (!active(to)) fail(`the recipient is ${to.status}`);
else if (!isName(m.skill) || !BELTS.includes(m.belt)) fail('an award needs a skill name and a belt (white, green or black)');
@@ -86,7 +94,6 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
const of = nodes.get(m.of);
if ((why = authority('parent'))) fail(why);
else if (!of || of.parent !== by.id) fail('only its parent can graduate a child');
else if (of.kind !== 'org') fail('only orgs graduate');
else if (!active(of)) fail(`the child is ${of.status}`);
else if (of.edge === 'adopted') fail('already graduated');
else {
@@ -113,6 +120,32 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
shut(by); entry.node = by.id;
}
break;
case 'charter': {
// A node charters itself; a series is chartered by its master, an ancestor chartered as a series LLC. Whether a
// charter still stands (standing below) is read off the tree as it is, so its order against a graduation or a
// close in the same block changes nothing.
const series = m.form === 'series', of = series ? nodes.get(m.of) : by, person = by && (by.kind === 'holding' || by.kind === 'agent');
if ((why = authority(series ? 'master' : 'node'))) fail(why);
else if (!FORMS.includes(m.form)) fail(`a charter's form is ${FORMS.join(', ')}`);
else if (!series && m.of !== undefined && m.of !== by.id) fail('a node charters itself; only a series is chartered by its master');
else if (series && (!of || !descends(nodes, of, by))) fail('a series must descend from its master');
else if (series && !active(of)) fail(`the series is ${of.status}`);
else if (series && of.kind !== 'org') fail('only orgs become series');
else if (series && by.charter?.form !== 'series-llc') fail(`${by.name} is not chartered as a series LLC`);
else if (series && m.jurisdiction !== undefined) fail("a series takes its master's jurisdiction");
else if (m.form === 'sole' && !person) fail('a sole proprietor is a person: a holding or an agent');
else if (m.form !== 'sole' && !series && isSeat(by)) fail('an agent graduates before it incorporates');
else if (m.form !== 'sole' && !series && !JURISDICTION.test(m.jurisdiction ?? '')) fail('a jurisdiction is an ISO code such as US-WY');
else if (m.form === 'sole' && m.jurisdiction !== undefined && !JURISDICTION.test(m.jurisdiction)) fail('a jurisdiction is an ISO code such as US-WY');
else if (m.provider !== undefined && !isName(m.provider)) fail(`a provider's name follows the name rule: ${NAME_RULE}`);
else if (m.filing !== undefined && !HASH.test(m.filing)) fail('a filing is the SHA-256 of the filed documents, in hex');
else {
of.charter = { id: e.id, beat: when, form: m.form, jurisdiction: series ? null : m.jurisdiction ?? null, legal: text(m.legal, 120) || null,
provider: m.provider ?? null, filing: m.filing ?? null, master: series ? by.id : null };
entry.node = of.id;
}
break;
}
case 'keyring': {
if ((why = authority('node'))) fail(why);
else if ((why = keyringWhy(by.id, m))) fail(why);
@@ -144,7 +177,25 @@ export function reduce(events, { upTo = Infinity, pending = false } = {}) {
function make(e, name, parent, kind) {
return { id: e.id, name, kind, parent: parent?.id ?? null, root: parent ? parent.root : e.id, edge: parent ? 'derived' : null,
status: 'active', index: null, born: e.status === 'confirmed' ? e.height : 'cued',
children: [], badges: [], decisions: [], made: [], keyring: null, graduated: null, closed: null };
children: [], badges: [], decisions: [], made: [], keyring: null, charter: null, graduated: null, closed: null };
}
// Is node a descendant of ancestor, by parent links (a graduate keeps its parent)?
function descends(nodes, node, ancestor) {
for (let n = nodes.get(node.parent); n; n = nodes.get(n.parent)) if (n.id === ancestor.id) return true;
return false;
}
// Whether a node's charter stands now: the node is active, and a series' master is too, is still a series LLC, and still
// holds the series in its holding (a series that graduates leaves it). Read off the tree, so event order doesn't matter.
export function standing(state, node) {
const c = node?.charter;
if (!c) return null;
if (node.status !== 'active') return { ok: false, why: `${node.name} is closed` };
if (c.form !== 'series') return { ok: true, why: '' };
const master = state.nodes.get(c.master);
if (master.status !== 'active') return { ok: false, why: `${master.name} is closed` };
if (master.charter?.form !== 'series-llc') return { ok: false, why: `${master.name} no longer holds series` };
if (node.root !== master.root) return { ok: false, why: `${node.name} is no longer in ${master.name}'s holding` };
return { ok: true, why: '' };
}
// Depth-first rows for one holding, drawn like docs/lineage.md: agents, then orgs, under every node. A child that
@@ -194,6 +245,8 @@ export function summary(entry, state) {
case 'decide': return `decide “${text(m.title, 140)}” · ${name(entry.parent)}`;
case 'make': return `make ${m.name} · ${name(entry.parent)}`;
case 'keyring': return `keyring · ${name(entry.parent)}`;
case 'charter': return m.form === 'series' ? `charter ${name(m.of)} as a series of ${name(entry.parent)}`
: `charter ${name(entry.parent)} · ${FORM_NAMES[m.form]}${m.jurisdiction ? ` (${m.jurisdiction})` : ''}`;
case 'graduate': return `graduate ${name(m.of)} from ${name(entry.parent)}`;
case 'close': return `close ${name(entry.parent)}${entry.also.length ? ` and ${entry.also.length} derived` : ''}`;
default: return entry.op;
+101
View File
@@ -0,0 +1,101 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { PROTOCOL, isSeat, reduce, standing, summary } from '../src/orgchain/lineage.js';
import { emptyLab, setTempo } from '../src/orgchain/store.js';
import { act, lineageState, send } from '../src/orgchain/actions.js';
import { holderOf, keyOf, secretsOf } from '../src/orgchain/keys.js';
import { SAT, payerOf } from '../src/orgchain/money.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 charter = (by, meta, height = 2) => ev('charter', by.id, meta, height);
test('a charter records a legal form: a node charters itself, and a series is chartered by its master', () => {
const root = ev('found', null, { name: 'acme' }), ops = ev('birth', root.id, { name: 'ops' }), lab = ev('birth', ops.id, { name: 'lab' }),
bot = ev('birth', ops.id, { name: 'bot', kind: 'agent' }), other = ev('found', null, { name: 'other' });
const filing = 'ab'.repeat(32), tries = [
[charter(root, { form: 'series', of: ops.id }), 'acme is not chartered as a series LLC'],
[charter(root, { form: 'series-llc', jurisdiction: 'US-WY', legal: 'Acme Holdings LLC', provider: 'doola', filing }), 'ok'],
[charter(root, { form: 'series', of: ops.id }), 'ok'],
[charter(root, { form: 'series', of: lab.id }), 'ok'],
[charter(root, { form: 'series', of: bot.id }), 'only orgs become series'],
[charter(other, { form: 'series', of: ops.id }), 'a series must descend from its master'],
[charter(root, { form: 'series', of: ops.id, jurisdiction: 'US-DE' }), "a series takes its master's jurisdiction"],
[charter(ops, { form: 'llc' }), 'a jurisdiction is an ISO code such as US-WY'],
[charter(ops, { form: 'llc', jurisdiction: 'wyoming' }), 'a jurisdiction is an ISO code such as US-WY'],
[charter(ops, { form: 'sole' }), 'a sole proprietor is a person: a holding or an agent'],
[charter(bot, { form: 'llc', jurisdiction: 'US-DE' }), 'an agent graduates before it incorporates'],
[charter(bot, { form: 'sole' }), 'ok'],
[charter(ops, { form: 'gmbh', jurisdiction: 'DE' }), "a charter's form is sole, llc, series-llc, series, corp"],
[charter(ops, { form: 'llc', jurisdiction: 'US-WY', filing: 'nope' }), 'a filing is the SHA-256 of the filed documents, in hex'],
[charter(ops, { form: 'llc', jurisdiction: 'US-WY', provider: 'Doola Inc' }), /^a provider's name follows the name rule/],
[charter(ops, { form: 'llc', jurisdiction: 'US-WY', of: root.id }), 'a node charters itself; only a series is chartered by its master'],
];
const s = reduce([root, ops, lab, bot, other, ...tries.map(([e]) => e)]);
tries.forEach(([e, want]) => {
const entry = s.log.find(x => x.id === e.id), got = entry.ok ? 'ok' : entry.why;
if (want instanceof RegExp) assert.match(got, want); else assert.equal(got, want);
});
const node = e => s.nodes.get(e.id);
assert.deepEqual(node(root).charter, { id: tries[1][0].id, beat: 2, form: 'series-llc', jurisdiction: 'US-WY', legal: 'Acme Holdings LLC',
provider: 'doola', filing, master: null });
assert.deepEqual([node(ops).charter.form, node(ops).charter.master, node(lab).charter.master], ['series', root.id, root.id]);
assert.equal(node(bot).charter.form, 'sole');
assert.deepEqual([ops, lab, bot].map(e => standing(s, node(e))), [{ ok: true, why: '' }, { ok: true, why: '' }, { ok: true, why: '' }]);
assert.equal(summary(s.log.find(x => x.id === tries[1][0].id), s), 'charter acme · series LLC (US-WY)');
assert.equal(summary(s.log.find(x => x.id === tries[2][0].id), s), 'charter ops as a series of acme');
});
test("a series' standing follows the tree: graduating, its master dropping series, or a close lapses it, in any order", () => {
const root = ev('found', null, { name: 'acme' }), ops = ev('birth', root.id, { name: 'ops' }), lab = ev('birth', ops.id, { name: 'lab' });
const master = charter(root, { form: 'series-llc', jurisdiction: 'US-WY' }), series = charter(root, { form: 'series', of: ops.id }, 3);
const kid = charter(root, { form: 'series', of: lab.id }, 3), grad = ev('graduate', root.id, { of: ops.id }, 3);
const base = [root, ops, lab, master];
for (const s of [reduce([...base, series, kid, grad]), reduce([...base, grad, series, kid])]) {
assert.equal(s.nodes.get(ops.id).charter.form, 'series', 'a graduate keeps its parent, so it still descends from its master');
assert.deepEqual(standing(s, s.nodes.get(ops.id)), { ok: false, why: "ops is no longer in acme's holding" });
assert.deepEqual(standing(s, s.nodes.get(lab.id)), { ok: false, why: "lab is no longer in acme's holding" });
}
// Once it graduates, ops charters itself as an LLC, and that stands.
const own = reduce([...base, series, grad, charter(ops, { form: 'llc', jurisdiction: 'US-DE', legal: 'Ops LLC' }, 4)]);
assert.deepEqual(standing(own, own.nodes.get(ops.id)), { ok: true, why: '' });
// The master drops series, so its series lapse; a closed series is closed.
const s = reduce([...base, series, kid, charter(root, { form: 'llc', jurisdiction: 'US-WY' }, 4), ev('close', lab.id, {}, 4)]);
assert.deepEqual(standing(s, s.nodes.get(ops.id)), { ok: false, why: 'acme no longer holds series' });
assert.deepEqual(standing(s, s.nodes.get(lab.id)), { ok: false, why: 'lab is closed' });
});
test('a seat graduates into a holding of its own, and can then birth, award and incorporate', () => {
const root = ev('found', null, { name: 'guild' }), ada = ev('birth', root.id, { name: 'ada', kind: 'agent' }), bo = ev('birth', root.id, { name: 'bo', kind: 'agent' });
const s = reduce([root, ada, bo, ev('graduate', root.id, { of: ada.id }, 2), ev('birth', ada.id, { name: 'studio' }, 3),
ev('award', ada.id, { to: bo.id, skill: 'pair', belt: 'white' }, 3), charter(ada, { form: 'llc', jurisdiction: 'US-WY', legal: 'Ada LLC' }, 3)]);
assert.ok(s.log.every(e => e.ok), s.log.filter(e => !e.ok).map(e => e.why).join('; '));
const node = s.nodes.get(ada.id);
assert.deepEqual([node.kind, node.edge, node.root === node.id, isSeat(node), isSeat(s.nodes.get(bo.id))], ['agent', 'adopted', true, false, true]);
assert.deepEqual(s.roots.map(r => r.name), ['guild', 'ada']);
assert.equal(s.nodes.get(bo.id).badges[0].by, ada.id);
});
test('in the lab a seat goes independent: its own root and keys, its money along, and acts of its own', () => {
const lab = emptyLab();
const root = act(lab, 'found', null, { name: 'guild' }), ada = act(lab, 'birth', root, { name: 'ada', kind: 'agent' });
send(lab, root, ada, SAT);
const before = payerOf(lab.keys, lineageState(lab), lineageState(lab).nodes.get(ada)).address;
act(lab, 'graduate', root, { of: ada });
const s = lineageState(lab), node = s.nodes.get(ada), k = keyOf(lab.keys, s, node);
assert.deepEqual([k.account, k.from, holderOf(lab, s, ada).who], [0, 'ada root', 'own']);
assert.equal(secretsOf(lab.keys, s, node).seed.mnemonic, lab.keys.adopted[ada]);
const purse = payerOf(lab.keys, s, node);
assert.notEqual(purse.address, before);
assert.ok(lab.chain.balance(purse.script) > 0.99 * SAT, 'its money moved to its new SegWit address');
setTempo(lab, 'each');
const studio = act(lab, 'birth', ada, { name: 'studio' });
act(lab, 'charter', ada, { form: 'llc', jurisdiction: 'US-WY', legal: 'Ada LLC', provider: 'doola' });
act(lab, 'charter', ada, { form: 'series-llc', jurisdiction: 'US-WY', legal: 'Ada LLC' });
act(lab, 'charter', ada, { form: 'series', of: studio });
const after = lineageState(lab);
assert.deepEqual([after.nodes.get(ada).charter.form, after.nodes.get(studio).charter.form, standing(after, after.nodes.get(studio)).ok],
['series-llc', 'series', true]);
assert.throws(() => act(lab, 'charter', studio, { form: 'sole' }), /A sole proprietor is a person/);
});
+2 -2
View File
@@ -54,10 +54,10 @@ test('the reducer vets an act before anything is signed', () => {
const lab = emptyLab();
const root = act(lab, 'found', null, { name: 'acme' }), bot = act(lab, 'birth', root, { name: 'bot', kind: 'agent' });
const before = encodeLab(lab);
assert.throws(() => act(lab, 'birth', bot, { name: 'x' }), /^Error: Agents do not give birth\.$/);
assert.throws(() => act(lab, 'birth', bot, { name: 'x' }), /^Error: Agents give birth only once they graduate\.$/);
assert.throws(() => act(lab, 'found', null, { name: 'acme' }), /already named acme/);
assert.throws(() => act(lab, 'award', root, { to: bot, skill: 'launch', belt: 'gold' }), /belt/);
assert.throws(() => act(lab, 'graduate', root, { of: bot }), /Only orgs graduate/);
assert.throws(() => act(lab, 'graduate', bot, { of: root }), /Only its parent can graduate a child/);
assert.throws(() => act(lab, 'sing', root, {}), /Unknown verb/);
assert.equal(encodeLab(lab), before);
});
+4 -4
View File
@@ -29,18 +29,18 @@ test('every rule rejects with a reason, and rejected events change nothing', ()
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('birth', bot.id, { name: 'x' }), /agents give birth only once they graduate/], [ev('birth', root.id, { name: 'ops' }), /already has an active ops/],
[ev('award', bot.id, { to: ops.id, skill: 'launch', belt: 'green' }), /agents award belts only once they graduate/],
[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/],
[ev('graduate', ops.id, { of: bot.id }), /only its parent/], [ev('graduate', root.id, { of: root.id }), /only its parent/],
[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$/);
assert.match(summary(s.log.find(x => x.id === bad[4][0].id), s), /^birth: agents give birth only once they graduate$/);
});
test('graduation flips the edge; closing cascades through derived children only', () => {
+1 -1
View File
@@ -162,7 +162,7 @@ test('graduation moves money to the new keys, and the graduate pays its own clai
const { now, log, live } = money(lab);
// lineage graduates at beat 7 into a holding of its own, so it mines half of that beat. Its keyring at beat 6 filled
// its old gas tank, which stays with its old keys; its acts after graduating fill a new one.
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
assert.deepEqual(now, { yolopreneur: 275 * SAT - 50 * SAT - 2 * FEE - 2 * TOPUP, 'well-known': 8 * SAT - FEE - TOPUP,
lineage: 27 * SAT - FEE - 2 * 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 - TOPUP - FEE], ['gas', 7, 'lineage', 'lineage', 10_000]]);
const lineage = [...live.nodes.values()].find(n => n.name === 'lineage');
+33 -1
View File
@@ -75,7 +75,7 @@ test('found a holding, then birth, award, decide, graduate and close at the defa
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.');
await expect(page.locator('#act-why')).toHaveText('Agents give birth only once they graduate.');
expect(problems).toEqual([]);
});
@@ -357,3 +357,35 @@ test('a node publishes its keyring; a graduation retires it, and the graduate pu
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});
test('a holding is a series LLC with its ventures as series, and a seat graduates to go independent', async ({ page }) => {
const problems = await open(page), inspect = page.locator('#inspect-body');
await page.locator('#sample').click();
await expect(say(page)).toContainText('Loaded the yolopreneur sample', { timeout: 20_000 });
await expect(row(page, 'yolopreneur')).toContainText('series llc');
await row(page, 'lineage').click();
await expect(inspect).toContainText('LLC · US-WY · Lineage LLC · via doola');
await row(page, 'docscope').click();
await verb(page, 'charter');
await expect(page.locator('#charter-forms input[value="series"]')).toBeChecked();
await expect(page.locator('#act-key')).toContainText('Signs as yolopreneur');
await submit(page, 'charter');
await expect(say(page)).toHaveText('docscope is a series of yolopreneur, at beat 8.');
await row(page, 'yolopreneur').click();
await expect(inspect.locator('.f', { has: page.locator('dt', { hasText: /^Series$/ }) })).toContainText('well-known, time, docscope');
await row(page, 'sol').click();
await verb(page, 'graduate');
await submit(page, 'graduate');
await expect(say(page)).toContainText('sol graduates from time');
await expect(row(page, 'sol')).toContainText('graduated');
await verb(page, 'charter');
await page.locator('#charter-forms input[value="llc"]').check();
await page.locator('#charter-jurisdiction').fill('US-DE');
await page.locator('#charter-legal').fill('Sol LLC');
await submit(page, 'charter');
await expect(say(page)).toContainText('sol is chartered as an LLC in US-DE');
await verb(page, 'birth');
await expect(page.locator('#f-birth')).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
expect(problems).toEqual([]);
});