← snapshot
13548 bytes
"use client";
import Link from "next/link";
import { useEffect, useMemo, useState } from "react";
import { api } from "@/lib/api";
import type {
ActorView,
FileEntry,
ProvenanceLabel,
RefDto,
SnapshotDetail,
} from "@/lib/types";
import { ProvenanceBadge } from "./provenance-badge";
import { Time } from "./time";
/**
* The whole bolt of cloth, in one ledger.
*
* Every other view answers a question about one kind of thing. This one shows
* what the substrate has actually produced — snapshots, the source inside
* them, decisions, the attestations that settled them — in the order it was
* made, with provenance as the axis rather than an annotation.
*
* The point it exists to make: provenance here is not metadata attached to an
* artifact, it is *part of the artifact's identity*. The same tree under
* different hands is a different object with a different address. Filtering
* this list by hand is therefore filtering by something the addresses
* themselves encode, which is why the counts are trustworthy.
*/
export type Artifact =
| {
kind: "snapshot";
id: string;
short: string;
at: number;
title: string;
provenance: ProvenanceLabel;
actor: ActorView;
refs: string[];
parents: string[];
tree: string;
}
| {
kind: "decision";
id: string;
short: string;
at: number;
title: string;
provenance: ProvenanceLabel;
actor: ActorView;
rationale: string;
families: string[];
scope: string[];
state: "unsettled" | "settled";
}
| {
kind: "attestation";
id: string;
short: string;
at: number;
title: string;
provenance: ProvenanceLabel;
actor: ActorView;
statement: string;
decision: string;
};
type KindFilter = "all" | Artifact["kind"];
type HandFilter = "all" | ProvenanceLabel;
const KIND_LABEL: Record<Artifact["kind"], string> = {
snapshot: "snapshot",
decision: "decision",
attestation: "attestation",
};
/** A glyph per kind — read at a glance, never the only signal. */
const GLYPH: Record<Artifact["kind"], string> = {
snapshot: "▤",
decision: "◈",
attestation: "✍",
};
export function WeaveArchive({
repo,
artifacts,
objectCount,
refs,
}: {
repo: string;
artifacts: Artifact[];
objectCount: number;
refs: RefDto[];
}) {
const [kind, setKind] = useState<KindFilter>("all");
const [hand, setHand] = useState<HandFilter>("all");
const [query, setQuery] = useState("");
const [open, setOpen] = useState<string | null>(artifacts[0]?.id ?? null);
const shown = useMemo(
() =>
artifacts.filter((a) => {
if (kind !== "all" && a.kind !== kind) return false;
if (hand !== "all" && a.provenance !== hand) return false;
if (query) {
const q = query.toLowerCase();
const hay = `${a.title} ${a.short} ${a.actor.display_name ?? ""}`.toLowerCase();
if (!hay.includes(q)) return false;
}
return true;
}),
[artifacts, kind, hand, query],
);
const counts = useMemo(() => {
const byKind = { snapshot: 0, decision: 0, attestation: 0 };
const byHand = { human: 0, agent: 0, mixed: 0 };
for (const a of artifacts) {
byKind[a.kind] += 1;
byHand[a.provenance] += 1;
}
return { byKind, byHand };
}, [artifacts]);
const selected = shown.find((a) => a.id === open) ?? shown[0] ?? null;
return (
<div className="jac-weavep">
<header className="jac-weavep-head">
<div>
<p className="jac-eyebrow">The weave</p>
<h2 className="jac-h2">Everything this repo has made</h2>
<p className="jac-small">
{artifacts.length} artifacts across {refs.length}{" "}
{refs.length === 1 ? "ref" : "refs"} · {objectCount} objects in the
store. Provenance is not a label here — it is hashed into each
address, so the same tree under different hands is a different
object.
</p>
</div>
</header>
<div className="jac-weavep-filters">
<div className="jac-seg">
{(["all", "snapshot", "decision", "attestation"] as KindFilter[]).map((k) => (
<button key={k} type="button" data-on={kind === k} onClick={() => setKind(k)}>
{k === "all" ? `all ${artifacts.length}` : `${k} ${counts.byKind[k]}`}
</button>
))}
</div>
<div className="jac-seg">
{(["all", "human", "agent", "mixed"] as HandFilter[]).map((h) => (
<button key={h} type="button" data-on={hand === h} onClick={() => setHand(h)}>
{h === "all" ? "any hand" : `${h} ${counts.byHand[h]}`}
</button>
))}
</div>
<input
className="jac-select jac-weavep-search"
placeholder="filter by message, id, or name"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
</div>
<div className="jac-weavep-body">
<ol className="jac-weavep-list">
{shown.length === 0 ? (
<li className="jac-small jac-weavep-empty">
Nothing matches. The weave is not empty — this filter is.
</li>
) : null}
{shown.map((a) => (
<li key={a.id}>
<button
type="button"
className="jac-weavep-row"
data-kind={a.kind}
data-hand={a.provenance}
data-on={selected?.id === a.id}
onClick={() => setOpen(a.id)}
>
<span className="jac-weavep-glyph" aria-hidden="true">
{GLYPH[a.kind]}
</span>
<span className="jac-weavep-main">
<span className="jac-weavep-title">{a.title}</span>
<span className="jac-weavep-sub">
<code className="jac-mono">{a.short}</code>
<span>{KIND_LABEL[a.kind]}</span>
<span>{a.actor.display_name ?? a.actor.id}</span>
{a.kind === "decision" && a.state === "unsettled" ? (
<span className="jac-weavep-warn">unsettled</span>
) : null}
{a.kind === "snapshot" && a.refs.length > 0
? a.refs.map((r) => (
<span key={r} className="jac-weavep-ref">
{r}
</span>
))
: null}
</span>
</span>
<span className="jac-weavep-when">
<Time at={a.at} />
</span>
</button>
</li>
))}
</ol>
<aside className="jac-weavep-detail">
{selected ? <Detail repo={repo} artifact={selected} /> : null}
</aside>
</div>
</div>
);
}
function Detail({ repo, artifact }: { repo: string; artifact: Artifact }) {
return (
<div className="jac-weavep-card">
<div className="jac-meta-row" style={{ marginTop: 0 }}>
<span className="jac-tag">{KIND_LABEL[artifact.kind]}</span>
<ProvenanceBadge provenance={artifact.provenance} />
<Time at={artifact.at} />
</div>
<p className="jac-weavep-address" title={artifact.id}>
<span className="jac-small">address</span>
<code className="jac-mono">{artifact.id}</code>
</p>
{artifact.kind === "snapshot" ? (
<SnapshotDetailView repo={repo} artifact={artifact} />
) : null}
{artifact.kind === "decision" ? (
<>
<h3 className="jac-h3">{artifact.title}</h3>
<div className="jac-panel-thesis jac-panel--machine">
<div className="jac-panel-label">
{artifact.actor.kind === "agent"
? "Machine-drafted rationale"
: "Rationale, as proposed"}
</div>
<p className="jac-rationale">{artifact.rationale}</p>
</div>
<div className="jac-meta-row">
<span className="jac-small">governs</span>
{artifact.scope.length === 0 ? (
<span className="jac-small">
nothing — an empty scope; the gate will never block on it
</span>
) : (
artifact.scope.map((p) => (
<span key={p} className="jac-pill">
{p}
</span>
))
)}
</div>
<p className="jac-small">
<Link href={`/repos/${repo}/decisions/${artifact.id}`}>
open the decision →
</Link>
</p>
</>
) : null}
{artifact.kind === "attestation" ? (
<>
<h3 className="jac-h3">{artifact.title}</h3>
<div className="jac-panel-thesis jac-panel--human">
<div className="jac-panel-label">Human attestation — verbatim</div>
<p className="jac-statement">{artifact.statement}</p>
<p className="jac-small">
— {artifact.actor.display_name ?? artifact.actor.id}. A separate
object with separate provenance; never merged into the rationale
it answers.
</p>
</div>
<p className="jac-small">
<Link href={`/repos/${repo}/decisions/${artifact.decision}`}>
the decision it settled →
</Link>
</p>
</>
) : null}
</div>
);
}
/** A snapshot's tree, with the source of any file in it one click away. */
function SnapshotDetailView({
repo,
artifact,
}: {
repo: string;
artifact: Extract<Artifact, { kind: "snapshot" }>;
}) {
const [detail, setDetail] = useState<SnapshotDetail | null>(null);
const [source, setSource] = useState<{ file: FileEntry; text: string } | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let alive = true;
setDetail(null);
setSource(null);
setError(null);
void api
.snapshot(repo, artifact.id)
.then((d) => {
if (alive) setDetail(d);
})
.catch(() => {
if (alive) setError("could not read this snapshot");
});
return () => {
alive = false;
};
}, [repo, artifact.id]);
const openFile = async (file: FileEntry) => {
setSource({ file, text: "reading…" });
try {
const blob = await api.blob(repo, file.blob);
setSource({ file, text: blob.content ?? "" });
} catch {
setSource({ file, text: "could not read this blob" });
}
};
return (
<>
<h3 className="jac-h3">{artifact.title}</h3>
<dl className="jac-weavep-facts">
<dt>tree</dt>
<dd>
<code className="jac-mono">{artifact.tree.slice(0, 16)}…</code>
</dd>
<dt>parents</dt>
<dd>
{artifact.parents.length === 0 ? (
<span className="jac-small">none — this is a root</span>
) : (
artifact.parents.map((p) => (
<Link
key={p}
href={`/repos/${repo}/snapshots/${p}`}
className="jac-mono jac-weavep-parent"
>
{p.slice(0, 12)}
</Link>
))
)}
</dd>
{artifact.refs.length ? (
<>
<dt>refs here</dt>
<dd>
{artifact.refs.map((r) => (
<span key={r} className="jac-weavep-ref">
{r}
</span>
))}
</dd>
</>
) : null}
</dl>
{error ? <p className="jac-small jac-error">{error}</p> : null}
{detail ? (
<>
<p className="jac-panel-label" style={{ marginTop: 14 }}>
Source — {detail.files.length}{" "}
{detail.files.length === 1 ? "file" : "files"}
{detail.changed_from_parent.length > 0
? `, ${detail.changed_from_parent.length} changed here`
: ""}
</p>
<ul className="jac-weavep-files">
{detail.files.map((f) => {
const changed = detail.changed_from_parent.includes(f.path);
return (
<li key={f.path}>
<button
type="button"
data-changed={changed}
data-on={source?.file.path === f.path}
onClick={() => void openFile(f)}
>
<span className="jac-weavep-file-path">{f.path}</span>
{changed ? (
<span className="jac-weavep-changed">changed</span>
) : null}
<code className="jac-mono jac-weavep-blob">
{f.blob.slice(0, 8)}
</code>
</button>
</li>
);
})}
</ul>
</>
) : (
<p className="jac-small">reading the tree…</p>
)}
{source ? (
<div className="jac-weavep-source">
<p className="jac-panel-label">
{source.file.path}
<span className="jac-weavep-blob-full"> · blob {source.file.blob}</span>
</p>
<pre className="jac-blob">{source.text}</pre>
</div>
) : null}
</>
);
}