jacquardSnapshot

← 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}
    </>
  );
}