Something went wrong. Try again.
A collaborative coding-agent orchestrator for atproto radl.app
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548import { COLLECTIONS, type AddMemberRecord, type AgentRecord, type ArchiveGoalRecord, type ArchiveProjectRecord, type ArtifactRecord, type ArtifactRequestRecord, type ArtifactTypeRecord, type BlessCommentRecord, type CheckrunRecord, type ClaimRecord, type CloseGoalRecord, type EditProjectRecord, type GoalRecord, type ImageRecord, type ImportMessageRecord, type JoinRecord, type LabelGoalRecord, type MergeRecord, type MessageRecord, type ProjectRecord, type RadialRecord, type RemoveMemberRecord, type RetractBlessRecord, type RetractImportRecord, type RetractRequestRecord, type ReviewRecord, type SavedViewRecord, type SetAutoReviewRecord, type SetFeedbackSourceRecord, type SetGuestCommentsRecord, type SetThreadSourceRecord, type SpaceRecord, type StrongRef,} from './generated/records.js'import { claimContractViolation, claimDeadline, claimLive } from './claim-lease.js'import { DEVICE_DIRECTORY_COLLECTIONS, deviceIgnoreReason, deviceViews, readDeviceDirectory, type DeviceDirectory, type DeviceGate, type DeviceView,} from './devices.js'import { compareCodePoints, compareInstant } from './order.js'import type { EditAnnotation, RecordStore, StoredRecord } from './store.js'import { validateRecord } from './validation.js'
// The repo rev and the first-sighting stamp are observer-local ingestion metadata; a convergent// index cannot include them.//// CLAIMS are the one exception, and only because the rule that reads them is observer-local too: a// lease is a duration measured from when THIS observer first saw the version (`claim-lease.ts`), so// a claim in the index carries the stamp that decides its own liveness and the UI can show the// deadline the fold actually applied. `MaterializedIndex.staleAt` is the other surface of that same// exception: it is derived from those stamps solely to say when this observer must refold. Nothing// else does, and nothing convergent — the digest included — reads either value.export type IndexedRecord<T extends RadialRecord = RadialRecord> = Omit< StoredRecord<T>, 'rev' | 'firstSeenAt'> & { authorRole: 'admin' | 'member' | 'agent' firstSeenAt?: string}
export interface MemberView { did: string kind: 'human' | 'agent' role: 'admin' | 'member' | 'agent' active: boolean membership?: StrongRef}
export interface IgnoredRecord { uri: string reason: string}
export interface ArtifactChain { root: string versions: string[] current: string}
export interface TargetView<T extends GoalRecord | ProjectRecord> { target: IndexedRecord<T> requests: Array<IndexedRecord<ArtifactRequestRecord>> openRequests: Array<IndexedRecord<ArtifactRequestRecord>> // Requests withdrawn by a `retractRequest` tombstone the space trusts: authored by the request's // own author or an active admin. They stay in `requests` (rendered with a "retracted" badge, the // way a removed member still shows) but are excluded from `openRequests` — nothing new can attach. retracted: Array<IndexedRecord<ArtifactRequestRecord>> awaitingInput: string[] artifacts: Array<IndexedRecord<ArtifactRecord>> artifactChains: ArtifactChain[] reviews: Array<IndexedRecord<ReviewRecord>> checkruns: Array<IndexedRecord<CheckrunRecord>> // Every trusted merge annotation referencing an artifact under this target. Kept parallel with // checkruns: consumers derive per-artifact merged state by matching `value.artifact` uri#cid // here (author DID = merge.did); there is no derived Set, and multiple merges per artifact // (equivalent annotations from different daemons) are all retained. (design §10) merges: Array<IndexedRecord<MergeRecord>> messages: Array<IndexedRecord<MessageRecord>> winningClaims: Record<string, IndexedRecord<ClaimRecord>>}
export interface GoalView extends TargetView<GoalRecord> { /** Foreign record snapshot that a human explicitly imported as this goal. */ origin?: StrongRef /** * The single predicate for whether this goal has ended — agents and readers both use it, and there * is no second reading of "is this goal live". Any active member may write one (`closeGoal`), and * it is reversible. Resolved from the legacy `goal.closed` base and the ending overlay pool; the * exact rule, including why the two legacy spellings are not symmetric, is on `endingOf` below. */ ended: boolean /** The ending's annotation. Unknown future values remain valid and do not affect `ended`. */ disposition?: string /** The overlay's timestamp; absent for a legacy `goal.closed` edit, which carries none. */ endedAt?: string /** * Comments by NON-MEMBERS that an active member has vouched for, as `blessComment` records: a * strongref pinning the guest's own record plus a SNAPSHOT of the exact body that was read. * * The snapshot is what keeps this in the fold at all. A guest's record lives in a repo no observer * polls — ingestion only ever reads member repos (design §3), which is why an unblessed guest * comment is invisible here by construction — so a bless carrying only a pointer would oblige every * materializer, browser tab included, to go and fetch a non-member's PDS before it could render or * bundle one. The fold cannot do IO, and a guest can rewrite or delete at will; copying the bytes * into the member's own signed record makes the bless self-contained, auditable against the pinned * cid while the original is up, and honest about what a member actually vouched for. * * Non-retracted only, ordered (createdAt, uri) like every other list. What blessing is FOR is * `TurnBundle.guestComments` — the one path by which guest text reaches an agent, and only ever * because a member put it there. */ blessedComments: Array<IndexedRecord<BlessCommentRecord>> /** * Blessings a trusted `retractBless` tombstone has withdrawn — the bless's own author or an active * admin, the same authority rule as `retractRequest`. They stay listed (the way a retracted request * stays in `requests`) so the UI can badge one rather than silently losing it, and they are excluded * from `blessedComments`, so nothing downstream — the bundle above all — can still read them. */ retractedBlessings: Array<IndexedRecord<BlessCommentRecord>> /** Enabled Roomy attachments. Their remote contents are deliberately never folded or bundled. */ threadSources: Array<{ service: string; roomId: string; record: IndexedRecord<SetThreadSourceRecord> }> /** Member-signed snapshots of selected foreign thread messages. */ importedMessages: Array<IndexedRecord<ImportMessageRecord>> /** Imported snapshots withdrawn by their author or an active admin. */ retractedImports: Array<IndexedRecord<ImportMessageRecord>> /** * The goal's labels as the space currently holds them: free-form strings any active member may * attach, and the typed field anything mechanical must read — a filter, a saved view, an agent * scanning for its slice of the work. Never parsed out of prose (design §7). * * Resolved from the `labelGoal` overlay pool: the winning record by (createdAt, uri) carries the * goal's WHOLE set, so a write replaces rather than merges and `labels: []` clears. `[]` when no * record exists. The cost of whole-set latest-wins is that two members relabelling one goal inside * a single ingestion window lose one edit — the same race `closeGoal` already accepts, and undone * by relabelling; the alternative was a second CRDT shape (an OR-set with tombstones) that this * fold does not otherwise have. * * DISPLAY ORDER IS AUTHOR ORDER — the array as the winning record spells it, deduped but never * re-sorted, so a member who leads with the label that matters sees it lead. */ labels: readonly string[]}
export interface ProjectView extends TargetView<ProjectRecord> { /** Enabled admin-authored foreign feedback sources, latest-wins per source URI. */ feedbackSources: Array<{ source: string; record: IndexedRecord<SetFeedbackSourceRecord> }> currentSystemArtifacts: Array<IndexedRecord<ArtifactRecord>> // Per-artifact-type auto-review defaults for this project. The base is the project record's own // `autoReview` field; standalone trusted `setAutoReview` records (admin-authored, latest-wins per // type) overlay it — the same primary-record-plus-standalone-edit divergence as goal archiving via // `archiveGoal`, chosen because rewriting the project record would drop the edit (store keeps the // earliest CID) and break every strongref pinning the project by uri#cid. autoReview: Record<string, boolean> // The project's three mutable facts, as the space currently holds them: the record's own values // overlaid by trusted `editProject` records, latest-wins PER FIELD (so a rename need not restate // the remote, and two edits touching different fields both survive). Same reason as `autoReview`: // the project record is pinned by uri#cid from every goal beneath it and the store keeps its // earliest CID, so an in-place rewrite would be a write no materializer reads. // // READ THESE, NOT `target.value.*`. `target.value` stays the bytes at `target.cid` — that is what // makes a strongref to it honest — so it is the ORIGINAL name, remote and branch, and a consumer // reading it is reading a fact that may have been superseded. `checks` has no overlay and is still // read off the record. name: string gitUrl: string defaultBranch: string // True when the most recent trusted `archiveProject` record targeting this project sets `archived: // true`. Reversible, like goal archiving — a later `archived: false` un-archives — but unlike // `archiveGoal` it is the project AUTHOR's alone, the same rule as `editProject`: the two are one // capability (what this project is, and whether it is still live), and it belongs to whoever wrote // the record rather than to anybody holding a grant. Agents skip an archived project, and every // goal under it. archived: boolean}
/** * One member's named filter over this space's goals, as the fold currently holds it. * * PERSONAL, not shared: a view belongs to whoever authored it, and the fold lists everybody's * because records are public regardless — a client shows its own human's by matching `did`. It * exists on-protocol rather than in browser storage for the reason everything else here does: there * is no central server to sync it, and per-origin storage is single-device and evictable. * * The match is AND across `labels`, plus `state`, plus `project` when present; an absent field does * not constrain, which is what keeps a future additive field compatible with views written today. * Nothing in this fold or the daemon acts on a view — it is UI derivation that happens to be * durable — and `viewMatches` in the UI is the one implementation of the predicate. */export interface SavedViewView { /** The member who owns this view. */ did: string name: string /** Every label a goal must carry (AND). Empty means labels do not constrain. */ labels: readonly string[] state: 'live' | 'ended' | 'all' /** When present, restricts the view to goals under this project. */ project?: StrongRef record: IndexedRecord<SavedViewRecord>}
export interface MaterializedIndex { space: IndexedRecord<SpaceRecord> /** * Whether this space's records travel in private-mode envelopes rather than on public PDSes * (design §18). Set once, at creation, by `space create --private`; a space never changes mode, * so there is no overlay — the space record either says so, or it arrived in an envelope, which * only a private space's does. Either is enough, because reading the flag alone would let a public * copy of the space record disable the gate below. * * What it changes in the fold: guest comments are forced off, and every record outside the public * device directory must have arrived signed (`devices.ts`). The second is what makes the private * path's authorship proof mandatory rather than opportunistic — without it a record sitting in any * member's public repo would fold with no device check at all. */ private: boolean members: MemberView[] /** * Every signing device published by a DID this space knows, with the status the fold gave it. * Convergent: it is a pure function of public `device` records, and it is what the * device-management UI renders and what an operator * compares when two replicas disagree about whose envelopes count. */ devices: DeviceView[] // Agent self-descriptions: handle, harness, models, and the artifact types the agent accepts — // what an assignee picker needs to offer only the agents that can produce a given type. An agent // record carries no space ref, so membership is what binds it here (see `agents` in materialize), // and one agent DID may publish several (the daemon writes one per profile), so this is a flat // list keyed by nothing: consumers group by `did` themselves. agents: Array<IndexedRecord<AgentRecord>> // Members who have opened this space at least once, by their own account of it. A `join` is a // self-assertion and grants nothing — an admin's `addMember` is the only thing that makes a DID's // records count, and nothing in this fold reads a join to decide that. It exists because a grant // lives in the ADMIN's repo, so without it a client cannot find the spaces its human belongs to // without an index; writing one into the joiner's own repo is what makes that a plain // `listRecords`. The only thing it lets this index answer is who has turned up, which is a fact // about people and not about permission. joins: Array<IndexedRecord<JoinRecord>> artifactTypes: Array<IndexedRecord<ArtifactTypeRecord>> goals: GoalView[] /** Imported goals grouped by the foreign record URI (CID changes do not break the badge). */ goalsByOrigin: Map<string, GoalView[]> projects: ProjectView[] // Images a member has uploaded into their own repo, so prose that names one can render it without // a second network round trip. PRESENTATION ONLY, and deliberately not a unit of anything: an // image record is a blob plus its metadata, it carries no space, goal or project reference, and // nothing below reads it. It cannot open, close or fulfil anything; it contributes nothing to a // goal's state, to a timeline, to a claim, or to the convergent digest. A body referring to one // by `radial-image:<at-uri>` resolves it here first and off the author's PDS otherwise (design §4). // // Bound by membership rather than by a reference, the same way an `agent` record is: the record // says nothing about where it is used, so what admits it is that its author's records count here. images: Array<IndexedRecord<ImageRecord>> /** * Whether this space SOLICITS comments from non-members (design §4). Admin-authored * `setGuestComments` records overlay a default of `false`, latest-wins — the same standalone-overlay * shape as `setAutoReview`, and admin-only for the same reason: it is governance, not annotation. * * It governs solicitation and display, and nothing else. It cannot stop a guest writing a `message` * into their own repo — nothing can, atproto repos are self-sovereign — so what turning it off means * is that the UI stops offering the composer and stops querying the backlink index. Blessings * already written stay blessed: a member vouched for those bytes, and a toggle is not a retraction. */ guestCommentsEnabled: boolean /** * Every member's saved views, latest-wins per (author DID, name), deleted ones omitted. Ordered * (did, name) so the list is the same for every observer. See `SavedViewView`. */ savedViews: SavedViewView[] /** Observer-local earliest claim expiry; excluded from convergence comparisons and the digest. */ staleAt?: string edits: EditAnnotation[] ignored: IgnoredRecord[]}
export interface MaterializeOptions { spaceUri: string asOf?: string}
type AnyIndexed = IndexedRecord<RadialRecord>
/** * The request types the turn layer owns rather than the registry. * * `review` asks for a verdict on one artifact version; `answer` asks for a reply to one message. Both * are typed, signed `artifactRequest` records like any other — what makes them built-in is that their * TERMINAL record is not an artifact (a `review`, a thread `message`), so neither can be registry * data: an admin registering a type by either name would be redefining what a turn of that kind is, * and the fold would have to guess which reading a request meant. `type create` refuses both names. */export const REVIEW_TYPE_NAME = 'review'export const ANSWER_TYPE_NAME = 'answer'
const compareRecord = ( left: Pick<StoredRecord, 'uri' | 'value'>, right: Pick<StoredRecord, 'uri' | 'value'>,): number => { const leftCreated = (left.value as { createdAt?: string }).createdAt ?? '' const rightCreated = (right.value as { createdAt?: string }).createdAt ?? '' // (instant, then code points) — the claim tie-break, and the order of every list in the fold. return compareInstant(leftCreated, rightCreated) || compareCodePoints(left.uri, right.uri)}
/** * Exact duplicates out, author order kept. * * Deliberately EXACT and not case-folded. Case-insensitive deduplication is a writer's job (the * sidecar's `goal label`, and the UI through it) because it has to pick a surviving spelling, and a * fold that picked one would be reinterpreting what a member signed. What this removes is only the * literal repeat, which no reader could have meant. */const dedupeLabels = (labels: readonly string[] | undefined): readonly string[] => labels ? [...new Set(labels)] : []
/** * A strongref pinning this exact record — `uri#cid`, which is the whole point of a strongref. * * Goals are the one exception, matched by URI alone. That is legacy, not preference: the old * author-only closure was an in-place edit of the goal record, so it minted a second CID and every * reference into that goal would otherwise dangle the moment somebody closed it. Nothing writes such * an edit any more (an ending is a standalone `closeGoal` overlay, design §7), but the CIDs it minted * have already moved, so the leniency stays. */const exactRef = ( reference: StrongRef | undefined, record: Pick<StoredRecord, 'uri' | 'cid' | 'collection'> | undefined,): boolean => Boolean( reference && record && reference.uri === record.uri && (record.collection === COLLECTIONS.goal || reference.cid === record.cid), )
const refKey = (reference: StrongRef): string => `${reference.uri}#${reference.cid}`const recordRefKey = (record: Pick<StoredRecord, 'uri' | 'cid'>): string => `${record.uri}#${record.cid}`
const sameRef = (left: StrongRef | undefined, right: StrongRef | undefined): boolean => Boolean(left && right && left.uri === right.uri && left.cid === right.cid)
function asIndexed<T extends RadialRecord>( record: StoredRecord, authorRole: 'admin' | 'member' | 'agent',): IndexedRecord<T> { const { rev: _rev, firstSeenAt: _firstSeenAt, ...indexed } = record return { ...indexed, value: record.value as T, authorRole }}
/** * Membership, folded once over the records it is given, in (createdAt, uri) order. A removal revokes * the exact grant it targets and retains nothing: every record authored by a DID with no active * grant is ignored. Grants issued by an admin before their own removal stay folded; a re-added * member's records all count again. * * Kept as a function so the membership rule remains explicit and independently testable. */function foldMembership( records: StoredRecord[], spaceRecord: StoredRecord, rootDid: string, ignored: IgnoredRecord[],): { memberState: Map<string, MemberView>; authorizedEvents: Set<string> } { const memberState = new Map<string, MemberView>() memberState.set(rootDid, { did: rootDid, kind: 'human', role: 'admin', active: true }) const authorizedEvents = new Set<string>() const events = records .filter( (record) => (record.collection === COLLECTIONS.addMember || record.collection === COLLECTIONS.removeMember) && exactRef((record.value as AddMemberRecord).space, spaceRecord), ) .sort(compareRecord)
for (const event of events) { const author = memberState.get(event.did) if (event.did !== rootDid && (!author?.active || author.role !== 'admin')) { ignored.push({ uri: event.uri, reason: 'membership event author is not an active space admin' }) continue }
if (event.collection === COLLECTIONS.addMember) { const add = event.value as AddMemberRecord if (add.did === rootDid) { ignored.push({ uri: event.uri, reason: 'root membership is implicit' }) continue } const current = memberState.get(add.did) if (current?.active) { ignored.push({ uri: event.uri, reason: 'member is already active' }) continue } authorizedEvents.add(event.uri) memberState.set(add.did, { did: add.did, kind: add.kind, role: add.role, active: true, membership: { uri: event.uri, cid: event.cid }, }) continue }
const removal = event.value as RemoveMemberRecord if (removal.did === rootDid) { ignored.push({ uri: event.uri, reason: 'root cannot be removed' }) continue } const current = memberState.get(removal.did) if (!current?.active || !sameRef(removal.membership, current.membership)) { ignored.push({ uri: event.uri, reason: 'removal does not target the current membership grant' }) continue } authorizedEvents.add(event.uri) memberState.set(removal.did, { ...current, active: false }) } return { memberState, authorizedEvents }}
/** * The half of `trustRecords` that answers "whose records count here": locate the space record, run * the device gate, fold membership. Extracted so `everMemberDids` below asks the identical question * `materialize` asks — one membership rule, not a connection-side paraphrase of it — and returning * `undefined` rather than throwing when the space record is absent, because for one caller that * absence is an answer (a replica still bootstrapping) and not an error. */function membershipOver( valid: StoredRecord[], spaceUri: string, ignored: IgnoredRecord[],): | { directory: DeviceDirectory spaceRecord: StoredRecord isPrivate: boolean admissible: StoredRecord[] memberState: Map<string, MemberView> authorizedEvents: Set<string> } | undefined { // Gate 2, first half: which signing keys each DID has published. Read from the public path only, // and independent of membership — see `devices.ts` for // why the directory cannot depend on the fold it feeds. const directory = readDeviceDirectory(valid, ignored)
// The space record is exempt from the device gate (`DeviceGate.spaceUri` says why), so it resolves // before the gate runs — which is also what tells the gate whether this space is private at all. const spaceRecord = valid.find( (record) => record.collection === COLLECTIONS.space && record.uri === spaceUri, ) if (!spaceRecord) return undefined // Signed OR declared: a space record that arrived in an envelope is a private space's by // construction, and reading the flag alone would let a public copy of it turn the gate off. const isPrivate = (spaceRecord.value as SpaceRecord).private === true || spaceRecord.deviceKeyIds !== undefined const gate: DeviceGate = { private: isPrivate, spaceUri, } const admissible = valid.filter((record) => { const reason = deviceIgnoreReason(record, directory, gate) if (reason) ignored.push({ uri: record.uri, reason }) return !reason }) const { memberState, authorizedEvents } = foldMembership( admissible, spaceRecord, spaceRecord.did, ignored, ) return { directory, spaceRecord, isPrivate, admissible, memberState, authorizedEvents }}
/** * Every DID that has ever held membership in this space — the root implicitly, and every grant * recipient whether or not a removal followed — or `undefined` when the record set holds no space * record to fold membership against, which is a replica still bootstrapping and not an empty set. * * This exists for the connection layer (`private/peers.ts`), which must not serve a private corpus * to a DID whose only relationship to the space is a directory entry (ADR §30.2) — and the reason it * lives HERE is the reason `foldMembership` is a named function: the membership rule exists once. * "Ever" rather than "active" is deliberate: a removed member already holds the corpus, so refusing * their connection would disclose the removal without protecting anything (`authorizeEndpoint`). * * Folded over only the collections membership can read — the space record, the grants and removals, * and the device directory the gate verifies them against — so a caller evaluating it per frame * pays for a scan of those records and not a validation of the whole store. */export function everMemberDids( records: readonly StoredRecord[], spaceUri: string,): Set<string> | undefined { const relevant: readonly string[] = [ COLLECTIONS.space, COLLECTIONS.addMember, COLLECTIONS.removeMember, ...DEVICE_DIRECTORY_COLLECTIONS, ] const ignored: IgnoredRecord[] = [] const valid = records.filter( (record) => relevant.includes(record.collection) && validateRecord(record.collection, record.value).success, ) const base = membershipOver([...valid], spaceUri, ignored) return base && new Set(base.memberState.keys())}
function trustRecords( records: StoredRecord[], spaceUri: string,): { space: IndexedRecord<SpaceRecord> private: boolean records: AnyIndexed[] members: MemberView[] devices: DeviceView[] ignored: IgnoredRecord[]} { const ignored: IgnoredRecord[] = [] const valid = records.filter((record) => { const result = validateRecord(record.collection, record.value) if (!result.success) { ignored.push({ uri: record.uri, reason: `invalid record: ${result.issues.map((issue) => `${issue.path} ${issue.message}`).join(', ')}`, }) return false } return true })
const base = membershipOver(valid, spaceUri, ignored) if (!base) throw new Error(`Space record not found: ${spaceUri}`) const { directory, spaceRecord, isPrivate, admissible, memberState, authorizedEvents } = base const space = asIndexed<SpaceRecord>(spaceRecord, 'admin') const rootDid = space.did
const trusted: AnyIndexed[] = [] for (const record of admissible) { if ( record.collection === COLLECTIONS.addMember || record.collection === COLLECTIONS.removeMember ) { if (authorizedEvents.has(record.uri)) trusted.push(asIndexed(record, 'admin')) continue } if (record.did === rootDid) { trusted.push(asIndexed(record, 'admin')) continue } const member = memberState.get(record.did) if (!member?.active) { ignored.push({ uri: record.uri, reason: 'author is not an active space member' }) continue } trusted.push(asIndexed(record, member.role)) }
return { space, private: isPrivate, records: trusted.sort((a, b) => compareCodePoints(a.uri, b.uri)), members: [...memberState.values()].sort((a, b) => compareCodePoints(a.did, b.did)), // Only the devices of DIDs this space has heard of. Binding is membership-free (it has to be, // or no first grant could be verified), but the VIEW is not: a key published by a stranger is // not this space's business, and listing one would invite reading the directory as a roster. devices: deviceViews(directory, (did) => memberState.has(did)), ignored: ignored.sort((a, b) => compareCodePoints(a.uri, b.uri) || compareCodePoints(a.reason, b.reason)), }}
function buildChains(artifacts: Array<IndexedRecord<ArtifactRecord>>): ArtifactChain[] { const byRef = new Map(artifacts.map((artifact) => [recordRefKey(artifact), artifact])) const rootFor = (artifact: IndexedRecord<ArtifactRecord>): IndexedRecord<ArtifactRecord> => { const seen = new Set([recordRefKey(artifact)]) let cursor = artifact while (cursor.value.prev) { const previous = byRef.get(refKey(cursor.value.prev)) if ( !previous || previous.value.type !== artifact.value.type || seen.has(recordRefKey(previous)) ) { break } seen.add(recordRefKey(previous)) cursor = previous } return cursor } const groups = new Map<string, Array<IndexedRecord<ArtifactRecord>>>() for (const artifact of artifacts) { const root = rootFor(artifact) const group = groups.get(recordRefKey(root)) ?? [] group.push(artifact) groups.set(recordRefKey(root), group) } return [...groups.entries()] .map(([root, versions]) => { const ordered = versions.sort(compareRecord) const referenced = new Set( ordered.flatMap((version) => (version.value.prev ? [refKey(version.value.prev)] : [])), ) const leaves = ordered.filter((version) => !referenced.has(recordRefKey(version))) const current = [...leaves].sort(compareRecord).at(-1) as IndexedRecord<ArtifactRecord> return { root: root.slice(0, root.lastIndexOf('#')), versions: ordered.map((version) => version.uri), current: current.uri, } }) .sort((a, b) => compareCodePoints(a.root, b.root))}
interface ViewInput<T extends GoalRecord | ProjectRecord> { target: IndexedRecord<T> requests: Array<IndexedRecord<ArtifactRequestRecord>> artifacts: Array<IndexedRecord<ArtifactRecord>> reviews: Array<IndexedRecord<ReviewRecord>> checkruns: Array<IndexedRecord<CheckrunRecord>> merges: Array<IndexedRecord<MergeRecord>> messages: Array<IndexedRecord<MessageRecord>> claims: Array<IndexedRecord<ClaimRecord>> // Space-global trusted messages, used ONLY for the awaiting-input computation. A question `re`ing a // request can be anchored to a different target than the request itself (a project-scoped capture // turn asks its question anchored to the source goal, so the message lands in the goal view while // the request lives in the project view). Awaiting-input must therefore scan every trusted message, // not just this target's `messages` (which drive the displayed thread). Matching is by exact // request/question strongref, so the wider scan never mis-attributes across targets. allMessages: Array<IndexedRecord<MessageRecord>> // Active agent membership distinguishes a turn's question from an ordinary member comment. // This is convergent fold state, not a claim or observer-local dispatch decision, so open // requests remain blocked for every operator after the agent holding a claim asks for input. agentDids: Set<string> // refKeys (uri#cid) of requests a trusted retract tombstone has withdrawn. Global set, keyed by // strongref; buildView only sees this target's requests, so cross-target keys never collide. retractedRefs: Set<string> asOf: string}
// A request is awaitingInput iff an active agent member has posted an unanswered trusted// question against it: a message `q` whose author is in `agentDids`,// `q.value.re` pointing at the request (exact uri#cid match), and `q.value.declines`// not `true`, for which no trusted message `a` (authored by a different member,// i.e. `a.did !== q.did`) has `a.value.parent` pointing back at `q`. This is purely// structural over the already-trusted records passed in. `requests` is this target's requests, but// `messages` is the space-global trusted set (a question can be anchored to a different target than// the request it answers — see ViewInput.allMessages); exact request/question strongref matching// keeps the wider scan from mis-attributing across targets.function computeAwaitingInput( requests: Array<IndexedRecord<ArtifactRequestRecord>>, messages: Array<IndexedRecord<MessageRecord>>, agentDids: Set<string>,): string[] { const awaiting = new Set<string>() for (const request of requests) { const requestKey = recordRefKey(request) const questions = messages.filter( (message) => agentDids.has(message.did) && message.value.re && refKey(message.value.re) === requestKey && message.value.declines !== true && // A message that is itself a REPLY is not an unanswered question. Every turn type but // `answer` makes this vacuous — a question carries `re` and nothing else — but an answer // turn's terminal record is also a message carrying `re`, and without this its reply would // read as a question nobody had answered, blocking the request it just closed. message.value.parent === undefined, ) const hasUnanswered = questions.some((question) => { const questionKey = recordRefKey(question) return !messages.some( (answer) => answer.value.parent && refKey(answer.value.parent) === questionKey && answer.did !== question.did, ) }) if (hasUnanswered) awaiting.add(request.uri) } return [...awaiting].sort()}
function buildView<T extends GoalRecord | ProjectRecord>(input: ViewInput<T>): TargetView<T> { const requestByRef = new Map(input.requests.map((request) => [recordRefKey(request), request])) const artifactByRef = new Map(input.artifacts.map((artifact) => [recordRefKey(artifact), artifact])) const fulfilled = new Set<string>()
for (const artifact of input.artifacts) { if (requestByRef.has(refKey(artifact.value.request))) fulfilled.add(refKey(artifact.value.request)) } for (const review of input.reviews) { if (!review.value.request) continue const request = requestByRef.get(refKey(review.value.request)) if ( request?.value.type === REVIEW_TYPE_NAME && request.value.subject && refKey(request.value.subject) === refKey(review.value.subject) ) { fulfilled.add(refKey(review.value.request)) } } // The two things a message can do to the request it names, both keyed on the ASSIGNEE — which is // why an answer request requires one in v1: // // `declines: true` refuses it, for any request type. The turn is done and nothing is coming. // a REPLY answers it, for an `answer` request: a message whose `parent` is the exact // message the request asked about. // // The `parent` half is what separates the two things an answer turn can write, because unlike every // other turn type both of them are messages carrying `re`. The reply threads under the subject; the // can't-answer QUESTION does not, and must leave the request open so `computeAwaitingInput` can // block it until a human replies. Closing on `re` alone would let a turn end its own request by // asking a question nobody has answered. for (const message of input.messages) { if (!message.value.re) continue const request = requestByRef.get(refKey(message.value.re)) if (!request || request.value.assignee !== message.did) continue const declined = message.value.declines === true const answered = request.value.type === ANSWER_TYPE_NAME && Boolean(message.value.parent && request.value.subject) && refKey(message.value.parent as StrongRef) === refKey(request.value.subject as StrongRef) if (declined || answered) fulfilled.add(refKey(message.value.re)) }
const retracted = input.requests.filter((request) => input.retractedRefs.has(recordRefKey(request))) const openRequests = input.requests.filter( (request) => !fulfilled.has(recordRefKey(request)) && !input.retractedRefs.has(recordRefKey(request)), ) const openRefs = new Set(openRequests.map(recordRefKey)) const winningClaims: Record<string, IndexedRecord<ClaimRecord>> = {} const claimsByRequest = new Map<string, Array<IndexedRecord<ClaimRecord>>>() for (const claim of input.claims) { const requestKey = refKey(claim.value.request) const request = requestByRef.get(requestKey) if ( !request || request.value.assignee || !openRefs.has(requestKey) || // A duration from first sight when the version declares one, an instant comparison when it // cannot — see `claimLive`. Instant-based either way, never lexical: a lease written as // `…:30Z` by one operator and an `asOf` of `…:30.500Z` on another must agree. !claimLive(claim.value, input.asOf, claim.firstSeenAt) ) { continue } const claims = claimsByRequest.get(requestKey) ?? [] claims.push(claim) claimsByRequest.set(requestKey, claims) } for (const [requestKey, claims] of claimsByRequest) { const winner = claims.sort(compareRecord)[0] if (winner) winningClaims[requestKey.slice(0, requestKey.lastIndexOf('#'))] = winner }
return { target: input.target, requests: input.requests.sort(compareRecord), openRequests: openRequests.sort(compareRecord), retracted: retracted.sort(compareRecord), awaitingInput: computeAwaitingInput(input.requests, input.allMessages, input.agentDids), artifacts: input.artifacts.sort(compareRecord), artifactChains: buildChains(input.artifacts), reviews: input.reviews .filter((review) => artifactByRef.has(refKey(review.value.subject))) .sort(compareRecord), checkruns: input.checkruns .filter((checkrun) => artifactByRef.has(refKey(checkrun.value.artifact))) .sort(compareRecord), merges: input.merges .filter((merge) => artifactByRef.has(refKey(merge.value.artifact))) .sort(compareRecord), messages: input.messages.sort(compareRecord), winningClaims, }}
export function materialize(store: RecordStore, options: MaterializeOptions): MaterializedIndex { const current = store.records() const trust = trustRecords(current, options.spaceUri) const asOf = options.asOf ?? new Date().toISOString() const rootDid = trust.space.did const isPrivate = trust.private
// An agent record is a self-description with no space ref of its own, so it is membership that // binds it to this space: it counts when its author is an active member here whose grant says // `kind: 'agent'`. Records from human members (or from the root DID, whose membership is implicit // and human) fall through to the unassociated diagnostic below rather than being surfaced. const agentDids = new Set( trust.members.filter((member) => member.active && member.kind === 'agent').map((member) => member.did), ) const agents = trust.records .filter( (record): record is IndexedRecord<AgentRecord> => record.collection === COLLECTIONS.agent && agentDids.has(record.did), ) .sort(compareRecord)
// A member saying "I am here" — written by the UI the first time somebody opens a space they hold a // grant in. It confers NOTHING: membership is an admin's `addMember` and only that, so a join from a // DID with no grant is dropped by trust like any other record, and a join is never consulted when // deciding whose records count. What it is for lives in the joiner's own repo, not here: it is how a // client finds the spaces its human belongs to without an index, since a grant is written into // somebody ELSE's repo and atproto has no reverse lookup. Surfaced because the fold can then answer a // question membership alone cannot — who has actually turned up. const joins = trust.records .filter( (record): record is IndexedRecord<JoinRecord> => record.collection === COLLECTIONS.join && exactRef((record.value as JoinRecord).space, trust.space), ) .sort(compareRecord)
// Every trusted member's image records. No filter beyond trust: an image record names no space, // so there is nothing here to resolve against — see `MaterializedIndex.images` for why that is the // whole of the rule, and why nothing downstream may read this. const images = trust.records .filter( (record): record is IndexedRecord<ImageRecord> => record.collection === COLLECTIONS.image, ) .sort(compareRecord)
const artifactTypes = trust.records .filter( (record): record is IndexedRecord<ArtifactTypeRecord> => record.collection === COLLECTIONS.artifactType && exactRef((record.value as ArtifactTypeRecord).space, trust.space) && record.authorRole === 'admin', ) .sort(compareRecord) const typeByName = new Map<string, IndexedRecord<ArtifactTypeRecord>>() for (const type of artifactTypes) { if (!typeByName.has(type.value.name)) typeByName.set(type.value.name, type) }
const projects = trust.records.filter( (record): record is IndexedRecord<ProjectRecord> => record.collection === COLLECTIONS.project && exactRef((record.value as ProjectRecord).space, trust.space), ) const projectByRef = new Map(projects.map((project) => [recordRefKey(project), project])) const goals = trust.records.filter( (record): record is IndexedRecord<GoalRecord> => record.collection === COLLECTIONS.goal && exactRef((record.value as GoalRecord).space, trust.space) && projectByRef.has(refKey((record.value as GoalRecord).project)), ) const goalByUri = new Map(goals.map((goal) => [goal.uri, goal]))
const regularRequests = trust.records.filter( (record): record is IndexedRecord<ArtifactRequestRecord> => { if (record.collection !== COLLECTIONS.artifactRequest) return false const request = record.value as ArtifactRequestRecord // Both built-in turn types are excluded EXPLICITLY rather than left to fall out of the registry // lookup below: a registry record an admin happened to name `answer` (or `review`) would // otherwise make one request materialize twice — once as a registry request, once as a built-in // one — with two different fulfillment rules watching it. `type create` refuses both names, but // the fold cannot depend on a CLI guard for its own determinism. if (request.type === REVIEW_TYPE_NAME || request.type === ANSWER_TYPE_NAME) return false const expectedScope = typeByName.get(request.type)?.value.scope if (!expectedScope) return false return expectedScope === 'goal' ? Boolean(request.goal && goalByUri.has(request.goal.uri)) : Boolean(request.project && projectByRef.has(refKey(request.project))) }, ) const regularRequestByRef = new Map( regularRequests.map((request) => [recordRefKey(request), request]), )
const artifacts = trust.records.filter( (record): record is IndexedRecord<ArtifactRecord> => { if (record.collection !== COLLECTIONS.artifact) return false const artifact = record.value as ArtifactRecord const type = typeByName.get(artifact.type) const request = regularRequestByRef.get(refKey(artifact.request)) if (!type || !request || request.value.type !== artifact.type) return false return type.value.scope === 'goal' ? Boolean( artifact.goal && goalByUri.has(artifact.goal.uri) && request.value.goal?.uri === artifact.goal.uri, ) : Boolean( artifact.project && projectByRef.has(refKey(artifact.project)) && request.value.project?.uri === artifact.project.uri, ) }, ) const artifactByRef = new Map(artifacts.map((artifact) => [recordRefKey(artifact), artifact]))
// Trusted messages are folded HERE, above the request assembly, because an `answer` request names // one as its subject the way a review request names an artifact. Nothing about the filter changed // when it moved: it depends only on `goalByUri` and `artifactByRef`, both already built. const targetUriForArtifact = (artifact: IndexedRecord<ArtifactRecord>): string => (artifact.value.goal ?? artifact.value.project)!.uri const artifactTargetUri = (reference: StrongRef): string | undefined => { const artifact = artifactByRef.get(refKey(reference)) return artifact ? targetUriForArtifact(artifact) : undefined } const messages = trust.records.filter( (record): record is IndexedRecord<MessageRecord> => { if (record.collection !== COLLECTIONS.message) return false const message = record.value as MessageRecord return Boolean( (message.goal && goalByUri.has(message.goal.uri)) || (message.artifact && artifactByRef.has(refKey(message.artifact))), ) }, ) const messageByRef = new Map(messages.map((message) => [recordRefKey(message), message])) const targetUriForMessage = (message: IndexedRecord<MessageRecord>): string | undefined => message.value.goal?.uri ?? (message.value.artifact ? artifactTargetUri(message.value.artifact) : undefined)
const reviewRequests = trust.records.filter( (record): record is IndexedRecord<ArtifactRequestRecord> => { if (record.collection !== COLLECTIONS.artifactRequest) return false const request = record.value as ArtifactRequestRecord if (request.type !== REVIEW_TYPE_NAME || !request.subject) return false const subject = artifactByRef.get(refKey(request.subject)) if (!subject) return false return subject.value.goal ? request.goal?.uri === subject.value.goal.uri : Boolean( request.project && subject.value.project && refKey(request.project) === refKey(subject.value.project), ) }, ) // An `answer` request asks an agent to reply to one exact message version, and its terminal record // is a thread message rather than an artifact. Same shape as a review request one level down: the // `subject` is pinned by uri#cid (a subject repointed at a newer version of the message would be a // request to answer something else), and it must resolve under the request's OWN goal — goal-scoped // only in v1, which covers artifact threads too since a message on a goal's artifact is already in // that goal's view. const answerRequests = trust.records.filter( (record): record is IndexedRecord<ArtifactRequestRecord> => { if (record.collection !== COLLECTIONS.artifactRequest) return false const request = record.value as ArtifactRequestRecord if (request.type !== ANSWER_TYPE_NAME || !request.subject || !request.goal) return false const subject = messageByRef.get(refKey(request.subject)) return Boolean(subject && targetUriForMessage(subject) === request.goal.uri) }, ) const requests = [...regularRequests, ...reviewRequests, ...answerRequests].sort(compareRecord) const requestByRef = new Map(requests.map((request) => [recordRefKey(request), request]))
// A retract tombstone withdraws a request. It is honored only when it targets a request that // resolves into this space and its author is either that request's author or an active admin // (design §7: authors own their records; admins govern). Trust already dropped retracts from // non-members; here we additionally gate on the author-or-admin rule and pin the exact uri#cid. const memberRoleByDid = new Map(trust.members.map((member) => [member.did, member.role])) const retractedRefs = new Set<string>() const associatedRetractUris: string[] = [] for (const retract of trust.records) { if (retract.collection !== COLLECTIONS.retractRequest) continue const targetKey = refKey((retract.value as RetractRequestRecord).request) const request = requestByRef.get(targetKey) if (!request) continue associatedRetractUris.push(retract.uri) if (retract.did === request.did || memberRoleByDid.get(retract.did) === 'admin') { retractedRefs.add(targetKey) } }
// ── blessed guest comments (design §4) ────────────────────────────────────────────────────────── // // A `blessComment` is an ordinary member record that happens to quote a non-member: the guest's own // `message`, pinned by uri#cid, plus the body a member read at the moment they vouched for it. The // guest's record itself is NOT here and cannot be — nothing polls a non-member's repo — which is // exactly why an unblessed comment cannot reach an agent (`buildTurnBundle`) no matter what a // backlink index says about it. // // Attached to the goal by URI rather than uri#cid, like a message and like an ending: a legacy // `closed` edit already minted a second goal cid, so a goal ref that insisted on the cid would // dangle on exactly the goals that have been closed. // // A trusted `retractBless` withdraws one, under `retractRequest`'s authority rule — the bless's own // author or an active admin. It matters more here than it does for a request: a bless is the one // way untrusted prose enters a turn bundle, so a mistaken one has to be revocable by somebody other // than whoever made the mistake. Blessings from a non-member never arrive at all (trust dropped // them upstream); a retract from anybody else is recorded as associated and simply does not fire. const blessByGoal = new Map<string, Array<IndexedRecord<BlessCommentRecord>>>() const blessByRef = new Map<string, IndexedRecord<BlessCommentRecord>>() const associatedBlessUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.blessComment) continue const bless = record as IndexedRecord<BlessCommentRecord> if (!goalByUri.has(bless.value.goal.uri)) continue associatedBlessUris.push(bless.uri) blessByRef.set(recordRefKey(bless), bless) const forGoal = blessByGoal.get(bless.value.goal.uri) ?? [] forGoal.push(bless) blessByGoal.set(bless.value.goal.uri, forGoal) } const retractedBlessRefs = new Set<string>() const associatedRetractBlessUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.retractBless) continue const targetKey = refKey((record.value as RetractBlessRecord).bless) const bless = blessByRef.get(targetKey) if (!bless) continue associatedRetractBlessUris.push(record.uri) if (record.did === bless.did || memberRoleByDid.get(record.did) === 'admin') { retractedBlessRefs.add(targetKey) } }
const importsByGoal = new Map<string, Array<IndexedRecord<ImportMessageRecord>>>() const importsByRef = new Map<string, IndexedRecord<ImportMessageRecord>>() const associatedImportUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.importMessage) continue const imported = record as IndexedRecord<ImportMessageRecord> if (!exactRef(imported.value.space, trust.space) || !goalByUri.has(imported.value.goal.uri)) continue associatedImportUris.push(imported.uri) importsByRef.set(recordRefKey(imported), imported) const entries = importsByGoal.get(imported.value.goal.uri) ?? [] entries.push(imported) importsByGoal.set(imported.value.goal.uri, entries) } const retractedImportRefs = new Set<string>() const associatedRetractImportUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.retractImport) continue const targetKey = refKey((record.value as RetractImportRecord).import) const imported = importsByRef.get(targetKey) if (!imported) continue associatedRetractImportUris.push(record.uri) if (record.did === imported.did || memberRoleByDid.get(record.did) === 'admin') { retractedImportRefs.add(targetKey) } }
const threadSourcesByGoal = new Map<string, Map<string, IndexedRecord<SetThreadSourceRecord>>>() const associatedThreadSourceUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.setThreadSource) continue const source = record.value as SetThreadSourceRecord if (!exactRef(source.space, trust.space) || !goalByUri.has(source.goal.uri)) continue associatedThreadSourceUris.push(record.uri) const sources = threadSourcesByGoal.get(source.goal.uri) ?? new Map() const key = `${source.service}\u0000${source.roomId}` const current = sources.get(key) if (!current || compareRecord(current, record) < 0) sources.set(key, record as IndexedRecord<SetThreadSourceRecord>) threadSourcesByGoal.set(source.goal.uri, sources) }
// Whether the space asks non-members for comments at all. Admin-authored, latest-wins over a // default of `false`, pinned to the space by exact uri#cid — a space record is never rewritten, so // the pin cannot go stale. Non-admin records fall through to the unassociated diagnostic below, // exactly as a non-admin `setAutoReview` does. // // In a PRIVATE space the setting is refused outright, in the fold, and this is the first of the // two independent stops that keep a private space off the public backlink index (design §18). The // UI's `guests.ts` asks Constellation — a public service — for the backlinks to a goal whenever a // space has guest comments enabled, which would hand a public index the space's goal URIs and // rkeys on every view. Turning that off in the UI alone would leave the leak one forgotten branch // away; refusing the record here means no consumer of the fold can even ask. let guestCommentsSetting: IndexedRecord<SetGuestCommentsRecord> | undefined const associatedGuestCommentUris: string[] = [] const refusedInPrivate: IgnoredRecord[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.setGuestComments || record.authorRole !== 'admin') continue if (!exactRef((record.value as SetGuestCommentsRecord).space, trust.space)) continue associatedGuestCommentUris.push(record.uri) if (isPrivate) { refusedInPrivate.push({ uri: record.uri, reason: 'guest comments cannot be enabled in a private space' }) continue } if (!guestCommentsSetting || compareRecord(guestCommentsSetting, record) < 0) { guestCommentsSetting = record as IndexedRecord<SetGuestCommentsRecord> } }
// Goal endings are one latest-wins overlay pool. `archiveGoal` remains readable as the legacy // spelling of parked; new writes use `closeGoal`. Matching by URI survives legacy goal rewrites. // Which record wins is decided here; what winning MEANS is decided where the view is built, and // the two legacy spellings are not symmetric there — see `goalViews` below. type GoalEndingRecord = IndexedRecord<ArchiveGoalRecord | CloseGoalRecord> const endingByGoal = new Map<string, GoalEndingRecord>() const associatedEndingUris: string[] = [] for (const ending of trust.records) { if (ending.collection !== COLLECTIONS.archiveGoal && ending.collection !== COLLECTIONS.closeGoal) continue const goalUri = (ending.value as ArchiveGoalRecord | CloseGoalRecord).goal.uri if (!goalByUri.has(goalUri)) continue associatedEndingUris.push(ending.uri) const current = endingByGoal.get(goalUri) if (!current || compareRecord(current, ending) < 0) { endingByGoal.set(goalUri, ending as GoalEndingRecord) } }
// A goal's labels are one latest-wins overlay pool, the same shape as its ending and for the same // reason: nothing rewrites a goal record (the store keeps the earliest CID, and every strongref // into the goal is pinned), so an editable field has to be a standalone record. Matched by goal URI // alone — the established rule for goal references, see `exactRef`. // // Any active member may write one, the `closeGoal` rule rather than `editProject`'s author-only // one: a label is an annotation about where attention should go, which is precisely the line the // `editProject` comment below draws. Trust has already dropped non-members; there is no role check // here. The winning record carries the WHOLE set — see `GoalView.labels` for what that costs. const labelsByGoal = new Map<string, IndexedRecord<LabelGoalRecord>>() const associatedLabelUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.labelGoal) continue const label = record.value as LabelGoalRecord if (!goalByUri.has(label.goal.uri)) continue associatedLabelUris.push(record.uri) const current = labelsByGoal.get(label.goal.uri) if (!current || compareRecord(current, record) < 0) { labelsByGoal.set(label.goal.uri, record as IndexedRecord<LabelGoalRecord>) } }
// A member's named filters over this space's goals, latest-wins per (author DID, name) — the // `setAutoReview` per-key pool shape. Keyed by NAME rather than by record URI because that is what // makes a view editable at all: records are immutable and a rewrite would be kept-earliest and // flagged as an edit annotation, so re-saving under the same name is the edit, and a record with // `deleted: true` is the delete. Author-scoped, so two members may each hold a view called "Mine" // without colliding. Pinned to the space by exact uri#cid; a space record is never rewritten. const savedViewsByAuthorAndName = new Map<string, IndexedRecord<SavedViewRecord>>() const associatedSavedViewUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.savedView) continue const view = record.value as SavedViewRecord if (!exactRef(view.space, trust.space)) continue associatedSavedViewUris.push(record.uri) const key = `${record.did}\u0000${view.name}` const current = savedViewsByAuthorAndName.get(key) if (!current || compareRecord(current, record) < 0) { savedViewsByAuthorAndName.set(key, record as IndexedRecord<SavedViewRecord>) } } const savedViews: SavedViewView[] = [...savedViewsByAuthorAndName.values()] .filter((record) => record.value.deleted !== true) .map((record) => ({ did: record.did, name: record.value.name, labels: dedupeLabels(record.value.labels), state: record.value.state ?? 'live', ...(record.value.project ? { project: record.value.project } : {}), record, })) .sort((a, b) => compareCodePoints(a.did, b.did) || compareCodePoints(a.name, b.name))
// A project's per-type auto-review default is overlaid by standalone `setAutoReview` records so // the setting can change without rewriting the (strongref-pinned) project record. Unlike // `archiveGoal`, this is admin-only (like the artifactType registry): only an admin governs which // types auto-review. Matched by exact project uri#cid — projects resolve strictly, so an edit that // mints a new project cid simply orphans stale overlays. "Latest" per (project.uri, type) is by // (createdAt, uri). Non-admin records fall through to the unassociated/ignored diagnostic below. const autoReviewByProject = new Map<string, Map<string, IndexedRecord<SetAutoReviewRecord>>>() const associatedAutoReviewUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.setAutoReview || record.authorRole !== 'admin') continue const setting = record.value as SetAutoReviewRecord if (!projectByRef.has(refKey(setting.project))) continue associatedAutoReviewUris.push(record.uri) const byType = autoReviewByProject.get(setting.project.uri) ?? new Map() const current = byType.get(setting.type) if (!current || compareRecord(current, record) < 0) { byType.set(setting.type, record as IndexedRecord<SetAutoReviewRecord>) } autoReviewByProject.set(setting.project.uri, byType) }
const feedbackByProject = new Map<string, Map<string, IndexedRecord<SetFeedbackSourceRecord>>>() const associatedFeedbackUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.setFeedbackSource || record.authorRole !== 'admin') continue const setting = record.value as SetFeedbackSourceRecord if (!exactRef(setting.space, trust.space) || !projectByRef.has(refKey(setting.project))) continue associatedFeedbackUris.push(record.uri) const bySource = feedbackByProject.get(setting.project.uri) ?? new Map() const current = bySource.get(setting.source) if (!current || compareRecord(current, record) < 0) { bySource.set(setting.source, record as IndexedRecord<SetFeedbackSourceRecord>) } feedbackByProject.set(setting.project.uri, bySource) }
// A project's name, remote and default branch are overlaid by standalone `editProject` records for // the same reason auto-review is: rewriting the project record would be dropped by the store (it // keeps the earliest CID) and would orphan every strongref pinning the project by uri#cid. // // Author-only, and deliberately narrower than `archiveGoal`'s any-active-member rule: what a // project IS — which repository the space's turns clone and cut from — is not an annotation about // where attention should go, and a member repointing another member's project at a different remote // would redirect every future turn. So the rule is design §7's other half, unmixed: authors own // their records. Latest-wins is PER FIELD, by (createdAt, uri), so an edit naming only `name` // leaves a previously-edited `gitUrl` standing. Records from anybody else fall through to the // unassociated diagnostic below. const editsByProject = new Map<string, Map<string, IndexedRecord<EditProjectRecord>>>() const associatedEditUris: string[] = [] const EDITABLE = ['name', 'gitUrl', 'defaultBranch'] as const for (const record of trust.records) { if (record.collection !== COLLECTIONS.editProject) continue const edit = record.value as EditProjectRecord const project = projectByRef.get(refKey(edit.project)) if (!project || record.did !== project.did) continue associatedEditUris.push(record.uri) const byField = editsByProject.get(edit.project.uri) ?? new Map() for (const field of EDITABLE) { if (edit[field] === undefined) continue const current = byField.get(field) if (!current || compareRecord(current, record) < 0) { byField.set(field, record as IndexedRecord<EditProjectRecord>) } } editsByProject.set(edit.project.uri, byField) }
// Archiving a project shelves it and everything under it. Same author-only rule and same // latest-wins reversibility as `editProject` — see `ProjectView.archived`. Matched by exact uri#cid // like every other project ref (a project record is never rewritten, so the pin cannot go stale). const archiveByProject = new Map<string, IndexedRecord<ArchiveProjectRecord>>() const associatedProjectArchiveUris: string[] = [] for (const record of trust.records) { if (record.collection !== COLLECTIONS.archiveProject) continue const archive = record.value as ArchiveProjectRecord const project = projectByRef.get(refKey(archive.project)) if (!project || record.did !== project.did) continue associatedProjectArchiveUris.push(record.uri) const current = archiveByProject.get(archive.project.uri) if (!current || compareRecord(current, record) < 0) { archiveByProject.set(archive.project.uri, record as IndexedRecord<ArchiveProjectRecord>) } }
const reviews = trust.records.filter( (record): record is IndexedRecord<ReviewRecord> => record.collection === COLLECTIONS.review && artifactByRef.has(refKey((record.value as ReviewRecord).subject)), ) const checkruns = trust.records.filter( (record): record is IndexedRecord<CheckrunRecord> => record.collection === COLLECTIONS.checkrun && artifactByRef.has(refKey((record.value as CheckrunRecord).artifact)), ) const merges = trust.records.filter( (record): record is IndexedRecord<MergeRecord> => record.collection === COLLECTIONS.merge && artifactByRef.has(refKey((record.value as MergeRecord).artifact)), ) // The horizon clamp (`claim-lease.ts`), applied here rather than inside `foldTarget` so the // refusal can be NAMED. A claim a clock jump pushed out of contract is permanent — the rkey is a // pure function of the request and records are never deleted — so the difference between "the // fold ignores it" and "the fold ignores it and says why" is the difference between an operator // seeing a clamped lease in `radiald index` (and the web app's Diagnostics disclosure) and // watching a request sit open with no explanation. const outOfContract: IgnoredRecord[] = [] const firstSeenByUri = new Map(current.map((record) => [record.uri, record.firstSeenAt])) const claims = trust.records .filter((record): record is IndexedRecord<ClaimRecord> => { if (record.collection !== COLLECTIONS.claim) return false if (!requestByRef.has(refKey((record.value as ClaimRecord).request))) return false const violation = claimContractViolation(record.value as ClaimRecord) if (violation) { outOfContract.push({ uri: record.uri, reason: violation }) return false } return true }) // The one place an observer-local stamp is put back on an indexed record — see `IndexedRecord`. .map((claim) => { const firstSeenAt = firstSeenByUri.get(claim.uri) return firstSeenAt === undefined ? claim : { ...claim, firstSeenAt } }) const staleAt = claims .filter((claim) => claimLive(claim.value, asOf, claim.firstSeenAt)) .map((claim) => { const deadline = claimDeadline(claim.value, claim.firstSeenAt) if (deadline) return deadline // The legacy fallback is live at expiresAt itself; refold one millisecond later so the cache // boundary agrees with claimLive's inclusive comparison. return new Date(Date.parse(claim.value.expiresAt) + 1).toISOString() }) .filter((deadline) => Number.isFinite(Date.parse(deadline)) && Date.parse(asOf) < Date.parse(deadline)) .sort((left, right) => Date.parse(left) - Date.parse(right))[0] const targetUriForRequest = (request: IndexedRecord<ArtifactRequestRecord>): string => (request.value.goal ?? request.value.project)!.uri const targetUriForReview = (review: IndexedRecord<ReviewRecord>): string | undefined => artifactTargetUri(review.value.subject) const targetUriForCheckrun = (checkrun: IndexedRecord<CheckrunRecord>): string | undefined => artifactTargetUri(checkrun.value.artifact) const targetUriForMerge = (merge: IndexedRecord<MergeRecord>): string | undefined => artifactTargetUri(merge.value.artifact) const targetUriForClaim = (claim: IndexedRecord<ClaimRecord>): string | undefined => { const request = requestByRef.get(refKey(claim.value.request)) return request ? targetUriForRequest(request) : undefined }
const makeInput = <T extends GoalRecord | ProjectRecord>( target: IndexedRecord<T>, ): ViewInput<T> => ({ target, requests: requests.filter((request) => targetUriForRequest(request) === target.uri), artifacts: artifacts.filter((artifact) => targetUriForArtifact(artifact) === target.uri), reviews: reviews.filter((review) => targetUriForReview(review) === target.uri), checkruns: checkruns.filter((checkrun) => targetUriForCheckrun(checkrun) === target.uri), merges: merges.filter((merge) => targetUriForMerge(merge) === target.uri), messages: messages.filter((message) => targetUriForMessage(message) === target.uri), claims: claims.filter((claim) => targetUriForClaim(claim) === target.uri), allMessages: messages, agentDids, retractedRefs, asOf, })
// A goal's ending: the legacy `closed` edit as the base, overlaid by the pool above. // // `closeGoal` is the one axis — a member saying what happened to this work — so it decides `ended` // outright, in either direction, and reopening a legacy-closed goal needs no rewrite of the goal // record. The two legacy spellings are NOT one axis and are not read as one: `goal.closed` says the // work is over, `archiveGoal` says stop looking at this, and a writer of either knew nothing about // the other. So an `archiveGoal` winner can only ADD its park — a legacy `archived: false` cannot // cancel a closure it was never talking about, and the goal an author closed stays ended (and stays // `completed`, which is the ending that actually happened) however its archive flag has been // toggled. The asymmetry is the whole reason the winner is not simply read off `value`. const endingOf = (goal: IndexedRecord<GoalRecord>): Pick<GoalView, 'ended' | 'disposition' | 'endedAt'> => { // No timestamp of its own: the goal record's `createdAt` is when the goal was opened, not when it // was closed, so a legacy closure has no `endedAt` to report. Its word is `completed` — what the // old UI's Close button meant. const legacy = goal.value.closed === true ? { ended: true, disposition: 'completed' } : { ended: false } const ending = endingByGoal.get(goal.uri) if (!ending) return legacy if (ending.collection === COLLECTIONS.closeGoal) { const close = ending.value as CloseGoalRecord if (!close.closed) return { ended: false } return { ended: true, ...(close.disposition === undefined ? {} : { disposition: close.disposition }), endedAt: close.createdAt, } } if ((ending.value as ArchiveGoalRecord).archived !== true) return legacy return legacy.ended ? legacy : { ended: true, disposition: 'parked', endedAt: ending.value.createdAt } }
const blessingsOf = ( goal: IndexedRecord<GoalRecord>, ): Pick<GoalView, 'blessedComments' | 'retractedBlessings'> => { const all = (blessByGoal.get(goal.uri) ?? []).sort(compareRecord) return { blessedComments: all.filter((bless) => !retractedBlessRefs.has(recordRefKey(bless))), retractedBlessings: all.filter((bless) => retractedBlessRefs.has(recordRefKey(bless))), } } const importsOf = (goal: IndexedRecord<GoalRecord>): Pick<GoalView, 'importedMessages' | 'retractedImports'> => { const all = (importsByGoal.get(goal.uri) ?? []).sort(compareRecord) return { importedMessages: all.filter((entry) => !retractedImportRefs.has(recordRefKey(entry))), retractedImports: all.filter((entry) => retractedImportRefs.has(recordRefKey(entry))), } } const goalViews: GoalView[] = goals .map((goal) => ({ ...buildView(makeInput(goal)), ...endingOf(goal), ...blessingsOf(goal), ...importsOf(goal), threadSources: [...(threadSourcesByGoal.get(goal.uri)?.values() ?? [])] .filter((record) => record.value.enabled) .sort(compareRecord) .map((record) => ({ service: record.value.service, roomId: record.value.roomId, record })), labels: dedupeLabels(labelsByGoal.get(goal.uri)?.value.labels), ...(goal.value.origin ? { origin: goal.value.origin } : {}), })) .sort((a, b) => compareCodePoints(a.target.uri, b.target.uri)) const projectViews: ProjectView[] = projects .map((project) => { const view = buildView(makeInput(project)) const currentUris = new Set(view.artifactChains.map((chain) => chain.current)) const base = project.value.autoReview && typeof project.value.autoReview === 'object' ? (project.value.autoReview as Record<string, boolean>) : {} const overlay: Record<string, boolean> = {} for (const [type, record] of autoReviewByProject.get(project.uri) ?? []) { overlay[type] = record.value.enabled } const fieldEdits = editsByProject.get(project.uri) const edited = <K extends (typeof EDITABLE)[number]>(field: K): string => fieldEdits?.get(field)?.value[field] ?? project.value[field] return { ...view, feedbackSources: [...(feedbackByProject.get(project.uri)?.values() ?? [])] .filter((record) => record.value.enabled) .sort(compareRecord) .map((record) => ({ source: record.value.source, record })), currentSystemArtifacts: view.artifacts.filter((artifact) => currentUris.has(artifact.uri)), autoReview: { ...base, ...overlay }, name: edited('name'), gitUrl: edited('gitUrl'), defaultBranch: edited('defaultBranch'), archived: archiveByProject.get(project.uri)?.value.archived === true, } }) .sort((a, b) => compareCodePoints(a.target.uri, b.target.uri))
const associatedUris = new Set([ trust.space.uri, ...agents.map((record) => record.uri), ...joins.map((record) => record.uri), ...images.map((record) => record.uri), ...artifactTypes.map((record) => record.uri), ...projects.map((record) => record.uri), ...goals.map((record) => record.uri), ...requests.map((record) => record.uri), ...artifacts.map((record) => record.uri), ...reviews.map((record) => record.uri), ...checkruns.map((record) => record.uri), ...merges.map((record) => record.uri), ...claims.map((record) => record.uri), // A clamped claim DID resolve into this space — it names a request here — so it must not also // collect the "does not resolve" diagnostic; it already has one of its own. ...outOfContract.map((entry) => entry.uri), ...messages.map((record) => record.uri), ...associatedRetractUris, ...associatedBlessUris, ...associatedRetractBlessUris, ...associatedImportUris, ...associatedRetractImportUris, ...associatedThreadSourceUris, ...associatedGuestCommentUris, // The device directory resolves into `index.devices` rather than into a goal or a project, so // its records are associated by collection. ...trust.records .filter( (record) => record.collection === COLLECTIONS.device || record.collection === COLLECTIONS.deviceAddress, ) .map((record) => record.uri), ...associatedEndingUris, ...associatedAutoReviewUris, ...associatedFeedbackUris, ...associatedEditUris, ...associatedProjectArchiveUris, ...associatedLabelUris, ...associatedSavedViewUris, ]) const membershipCollections: string[] = [COLLECTIONS.addMember, COLLECTIONS.removeMember] const unassociated = trust.records .filter( (record) => !associatedUris.has(record.uri) && !membershipCollections.includes(record.collection), ) .map((record) => ({ uri: record.uri, reason: 'record does not resolve into this space index' }))
const activeByDid = new Map(trust.members.map((member) => [member.did, member])) const liveByUri = new Map(current.map((record) => [record.uri, record])) const edits = store.edits().filter((edit) => { const record = liveByUri.get(edit.uri) if (!record) return false if (record.did === rootDid) return true return Boolean(activeByDid.get(record.did)?.active) })
const goalsByOrigin = new Map<string, GoalView[]>() for (const goal of goalViews) { if (!goal.origin) continue const matches = goalsByOrigin.get(goal.origin.uri) ?? [] matches.push(goal) goalsByOrigin.set(goal.origin.uri, matches) }
return { space: trust.space, private: isPrivate, members: trust.members, devices: trust.devices, agents, joins, artifactTypes, goals: goalViews, goalsByOrigin, projects: projectViews, images, guestCommentsEnabled: !isPrivate && guestCommentsSetting?.value.enabled === true, savedViews, ...(staleAt ? { staleAt } : {}), edits, ignored: [...trust.ignored, ...outOfContract, ...refusedInPrivate, ...unassociated].sort( (a, b) => compareCodePoints(a.uri, b.uri) || compareCodePoints(a.reason, b.reason), ), }}
// ── what is still live ──────────────────────────────────────────────────────────────────────────//// "Shelved" arrives from two directions and every agent-facing scan has to honour both, so the rule// lives here once rather than as the same two-line condition in the dispatcher, the auto-review// trigger, the check dispatcher and the merge poller — where the second half was, until projects// could be archived, not a condition at all.
/** Projects agents act on: everything the author has not archived. */export const activeProjects = (index: MaterializedIndex): ProjectView[] => index.projects.filter((view) => !view.archived)
/** * Goals agents act on: not ended and not hanging off an archived project. * * Archiving a project shelves the work under it too — a turn cloning a repository the space has put * away is the thing archiving exists to stop — so a goal whose project is archived is set aside even * though nothing was written about the goal itself. */export function activeGoals(index: MaterializedIndex): GoalView[] { const shelved = new Set( index.projects.filter((view) => view.archived).map((view) => view.target.uri), ) return index.goals.filter((view) => !view.ended && !shelved.has(view.target.value.project.uri))}