// The visual surface — a self-contained, interactive HTML explorer over the // JSON the other commands emit. No server, no build step, no external resources: // one file a teammate opens in a browser. // // Two ways to navigate, both feeding one detail pane: // - Tree view — from pieces down (pattern · input · owned cells/streams), // children named by their role. // - Graph view — re-rootable from ANY node; click to select / re-root. // The detail pane shows EVERY salient field for the selected entity: value // (links clickable), schema, CFC (information-flow) labels, version history, // resolved lineage (clickable), outgoing links, and module source. Entity ids // are click-to-copy, and each piece/entity has a deep link into the live shell. import { DID_PREFIX } from "@commonfabric/identity/did"; import type { SpaceDb } from "./db.ts"; import { type SpaceSummary, summarizeSpace } from "./queries.ts"; import { buildSpaceGraph, type SpaceGraph } from "./graph.ts"; import { spaceTimeline, type SpaceTimelineEntry } from "./timetravel.ts"; import { buildAllDetails, type EntityDetail } from "./detail.ts"; import type { ScanExtent } from "./model.ts"; import { visibleRevisionRows } from "./reconstruct.ts"; import { listScopes, type Participant, type Scope, type ScopeOverlay, scopeOverlay, spaceParticipants, } from "./scopes.ts"; import { contendedEntities, type ContendedEntity, entityConflicts, } from "./conflicts.ts"; export interface InspectorBundle { space: string; generatedAt: string; /** Base origin of the live shell, for deep links (`//`). */ liveBase: string; summary: SpaceSummary; details: EntityDetail[]; /** How far the entity scan behind `details` and `graph` reached. */ extent: ScanExtent; graph: SpaceGraph; timeline: SpaceTimelineEntry[]; /** Per-identity scopes present (space / user: / session::*). */ scopes: Scope[]; /** Per-entity scope overlays — only for cells with non-space/multi-scope state. */ overlays: ScopeOverlay[]; /** Identities that touched this space (committers + per-user/session owners). */ participants: Participant[]; /** Contested entities (≥2 writer sessions); multi-user ones carry stale reads. */ conflicts: ContendedEntity[]; } /** Assemble everything the explorer needs from one space DB. */ export function buildInspectorBundle( space: SpaceDb, opts: { branch?: string; scope?: string; generatedAt?: string; liveBase?: string; limit?: number; } = {}, ): InspectorBundle { const branch = opts.branch ?? ""; const scope = opts.scope ?? "space"; const limit = opts.limit; const did = (space.path.split("/").pop() ?? "").replace(/\.sqlite$/, ""); // Entities that carry per-user/session state (in a non-space scope, or in // more than one scope) get a full scope overlay so the explorer can show the // per-identity divergence the space-scope view hides. Discovered over the // same branch-visible domain the tree and the scope list use — a child branch // inherits its parent's per-user state, and a page that named a user scope // while finding no cells in it would report the divergence as absent. Records // rather than readable entities, so an entity deleted in one scope and live // in another still reaches the overlay that exists to show that. const scoped = new Map>(); for (const row of visibleRevisionRows(space, { branch })) { const scopes = scoped.get(row.id) ?? new Set(); scopes.add(row.scope); scoped.set(row.id, scopes); } const overlayIds = [...scoped.entries()] .filter(([, scopes]) => scopes.size > 1 || [...scopes].some((scope) => scope !== "space") ) .map(([id]) => id); const overlays = overlayIds.map((id) => scopeOverlay(space, id, { branch })); const listing = buildAllDetails(space, { branch, scope, limit }); return { space: did, generatedAt: opts.generatedAt ?? "", liveBase: opts.liveBase ?? "", summary: summarizeSpace(space), details: listing.details, extent: listing.extent, graph: buildSpaceGraph(space, { branch, scope, limit }), timeline: spaceTimeline(space, { branch, scope }), scopes: listScopes(space, { branch }), overlays, participants: spaceParticipants(space, { branch }), conflicts: buildConflicts(space, branch, scope), }; } /** Contested entities; attach stale-read analysis to the multi-user ones. */ function buildConflicts( space: SpaceDb, branch: string, scope: string, ): ContendedEntity[] { const STALE_READ_ANALYSIS_CAP = 60; const contended = contendedEntities(space, { branch, scope, limit: 300 }); let analyzed = 0; for (const c of contended) { if (c.multiUser && analyzed < STALE_READ_ANALYSIS_CAP) { analyzed++; c.staleReads = entityConflicts(space, c.id, { branch, scope }).staleReads; c.staleReadsAnalyzed = true; } // Beyond the cap, leave staleReadsAnalyzed falsy so the UI shows "not // analyzed" rather than rendering an unanalyzed multi-user cell as clean. } return contended; } /** Embed JSON safely inside an HTML ` breakout) and the U+2028/U+2029 line // terminators, which are legal in JSON strings but break a ` `; }