jacquardSnapshot

← snapshot

7166 bytes
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { api, ApiError } from "@/lib/api";
import type { ImportReport } from "@/lib/types";
import { ProvenanceBadge } from "./provenance-badge";

/**
 * Pulling a pull request in, and being honest about what came with it.
 *
 * An import is the one moment where it would be easiest to lie. GitHub hands
 * over a commit author and a description, and it would take nothing to render
 * those as provenance and rationale — which is exactly what every other tool
 * does, and exactly what this project exists to refuse.
 *
 * So the report leads with the gaps. What arrived is a *synthetic* history:
 * real source, real diff, an inferred hand, and a decision nobody has signed.
 * The only way to close it is for a person to answer, which is what Jackie's
 * interview is for.
 */
export function GithubImport() {
  const router = useRouter();
  const [owner, setOwner] = useState("BurntSushi");
  const [repo, setRepo] = useState("ripgrep");
  const [pr, setPr] = useState("3496");
  const [token, setToken] = useState("");
  const [busy, setBusy] = useState(false);
  const [report, setReport] = useState<ImportReport | null>(null);
  const [error, setError] = useState<string | null>(null);

  const run = async () => {
    setBusy(true);
    setError(null);
    setReport(null);
    try {
      const out = await api.importGithub({
        owner: owner.trim(),
        repo: repo.trim(),
        pr: Number(pr),
        token: token.trim() || undefined,
      });
      setReport(out);
    } catch (e) {
      setError(e instanceof ApiError ? e.message : "the import failed");
    } finally {
      setBusy(false);
    }
  };

  const valid = owner.trim() && repo.trim() && Number(pr) > 0 && !busy;

  return (
    <div className="jac-page">
      <p className="jac-eyebrow">Import</p>
      <h1 className="jac-h1">Bring a pull request in</h1>
      <p className="jac-lede">
        The source and the diff come across intact. Provenance does not — GitHub
        records who pushed, which is a different question from whose hands made
        the work. What arrives is a synthetic history that says so, and a list
        of what a person still has to answer.
      </p>

      <div className="jac-import-form">
        <label className="jac-label" htmlFor="imp-owner">
          owner
        </label>
        <input
          id="imp-owner"
          className="jac-select"
          value={owner}
          onChange={(e) => setOwner(e.target.value)}
        />
        <label className="jac-label" htmlFor="imp-repo">
          repo
        </label>
        <input
          id="imp-repo"
          className="jac-select"
          value={repo}
          onChange={(e) => setRepo(e.target.value)}
        />
        <label className="jac-label" htmlFor="imp-pr">
          pull request
        </label>
        <input
          id="imp-pr"
          className="jac-select"
          inputMode="numeric"
          value={pr}
          onChange={(e) => setPr(e.target.value)}
        />
        <label className="jac-label" htmlFor="imp-token">
          token — optional
        </label>
        <input
          id="imp-token"
          className="jac-select"
          type="password"
          placeholder="for private repos or a higher rate limit"
          value={token}
          onChange={(e) => setToken(e.target.value)}
        />
        <p className="jac-small">
          The token is used for this request and never stored. It goes to
          jac-serve, which talks to GitHub; the browser never holds it after
          the call.
        </p>
        <button
          type="button"
          className="jac-btn jac-btn--primary"
          disabled={!valid}
          onClick={() => void run()}
        >
          {busy ? "Reading GitHub…" : "Import it"}
        </button>
        {error ? <p className="jac-small jac-error">{error}</p> : null}
      </div>

      {report ? <Report report={report} onOpen={(s) => router.push(`/repos/${s}`)} /> : null}
    </div>
  );
}

function Report({
  report,
  onOpen,
}: {
  report: ImportReport;
  onOpen: (slug: string) => void;
}) {
  const { source, imported, inference, gaps } = report;
  return (
    <section className="jac-import-report">
      <h2 className="jac-h2">
        {source.owner}/{source.repo} #{source.pull_request}
      </h2>
      <p className="jac-small">
        opened by <strong>{source.author}</strong> ·{" "}
        <code className="jac-mono">{source.base.slice(0, 7)}</code> →{" "}
        <code className="jac-mono">{source.head.slice(0, 7)}</code>
      </p>

      <div className="jac-import-cols">
        <div className="jac-import-col">
          <p className="jac-panel-label">What came across</p>
          <ul className="jac-import-facts">
            <li>
              <b>{imported.files}</b> files, at both their base and head states
              — so the diff is real, not a patch blob
            </li>
            <li>
              <b>{imported.changed_on_github}</b> changed on GitHub
              {imported.truncated ? " — more than fit, so this is a slice" : ""}
            </li>
            {imported.skipped.length ? (
              <li>
                <b>{imported.skipped.length}</b> skipped as binary or oversized
              </li>
            ) : null}
            <li>one decision, proposed and unsettled</li>
          </ul>
        </div>

        <div className="jac-import-col">
          <p className="jac-panel-label">What was inferred</p>
          <div className="jac-import-inference" data-assumed={inference.assumed}>
            <div className="jac-meta-row" style={{ marginTop: 0 }}>
              <ProvenanceBadge provenance={inference.provenance} />
              {inference.assumed ? (
                <span className="jac-tag jac-tag--warn">assumed</span>
              ) : (
                <span className="jac-tag">from evidence</span>
              )}
            </div>
            <p className="jac-small">{inference.because}</p>
          </div>
        </div>
      </div>

      <div className="jac-import-gaps">
        <p className="jac-panel-label">
          What only a person can answer — {gaps.length}
        </p>
        <ol>
          {gaps.map((g) => (
            <li key={g.kind}>
              <span className="jac-import-ask">{g.ask}</span>
              <span className="jac-small">{g.why}</span>
            </li>
          ))}
        </ol>
      </div>

      <p className="jac-import-honesty">{report.honesty}</p>

      <div className="jac-remark-actions">
        <button
          type="button"
          className="jac-btn jac-btn--primary"
          onClick={() => onOpen(report.slug)}
        >
          Walk it with Jackie →
        </button>
        <Link href={`/repos/${report.slug}/weave`} className="jac-btn">
          See everything it made
        </Link>
        <Link href={`/repos/${report.slug}/decisions/${report.decision}`} className="jac-btn">
          The unsigned decision
        </Link>
      </div>
    </section>
  );
}