// The sourcing forge, driven from the screen — SRC-2 and SRC-3 given controls.
//
// WHAT WAS MISSING. `POST /sourcing/build` assembles a request for proposal or
// quotation from Legal's approved section library and attaches the draft
// contract terms the winner would be asked to sign. `POST /sourcing/intent`
// records whether an engagement is going to market at all. Both landed in
// `api.jsx`, both were tested, and neither was referenced by a single pane —
// so the differentiator existed and nobody could make one. PRODUCT.md §4 ranks
// this first of nine gaps.
//
// ── FOUR THINGS THIS SCREEN HAS TO GET RIGHT ──────────────────────────────
//
//   1. "NOT ASKED" IS A THIRD ANSWER. An engagement with no row in
//      `cw.current_sourcing_intent` has not been asked whether it goes to
//      market. That is not "decided against", and drawing it as one is S312's
//      defect and S330's, both of which shipped once already. The unasked get
//      their own figure, their own panel and their own words.
//
//   2. THE RECORD DECIDES, NOT THE FORM. Which skeleton sections assemble, and
//      which contract assembly the terms preview comes from, are the record's
//      answers rather than the caller's. This screen asks the two questions
//      Mike's 2026-08-04 carve-out actually gave the caller — what is being
//      bought, and the spans written for this deal alone — and asks no more.
//
//   3. A REFUSAL IS RENDERED, NEVER PARAPHRASED (S320). The doorway answers a
//      contradicted intent with 409 and a sentence naming both event types;
//      the Administrator's two preview settings answer with sentences of their
//      own. This screen shows those sentences. It does not pre-empt them with
//      a guess, because a guess that disagrees with the service teaches people
//      a rule the system does not have.
//
//   4. A MACHINE-WRITTEN SPAN NAMES ITS MODEL. `cw.sourcing_run_span` refuses
//      one that does not. That refusal is the entire value of the carve-out:
//      the AI may write the questions, and the record always says that it did.
//      The form asks in the same breath as the choice, so nobody meets the
//      constraint as a rejection at the end of a long form.

const { useState: useForgeState, useMemo: useForgeMemo } = React;

// ── Who may act here, and why this is written down at all ─────────────────
//
// THE PANE IS ROLE-BLIND ABOUT READING and cannot be about acting. Every read
// on these screens is scoped by the database and rendered from one branch, so
// nothing here decides what anybody may SEE. Two acts are different: a
// migration names exactly three roles, and an auditor is deliberately not one
// of them —
//
//     0096  builders_write on cw.sourcing_run
//     0097  deciders_write on cw.sourcing_intent
//     both: cw.app_role() in ('requester','legal_reviewer','legal_admin')
//
// An auditor reads everything and changes nothing (ADR-0008), so offering them
// a button whose only possible outcome is a refusal is an affordance that
// leads nowhere — the failure the stat tiles were repaired for, one control
// over. The refusal still renders if somebody arrives at the address anyway;
// what this stops is the screen INVITING it.
//
// ONE LIST, THREE SITES. A second copy of "who may build" is how the button,
// the row and the deep link come to disagree.
// FOUR SINCE 0124 (SRC-5). Procurement joined because it opens the competition
// and invites the suppliers, and was then told "not yours to build" by the only
// screen that produces the document it exists to send — while the requester who
// could build it cannot see a competition they did not open. This list mirrors
// cw.sourcing_run's builders_write policy; it does not decide anything.
const SOURCING_ACTORS = ['requester', 'legal_reviewer', 'legal_admin',
                         'procurement'];

function maySourceOn(me) {
  return SOURCING_ACTORS.includes(me && me.role);
}

// The three answers to "is this going to market". `none` is a REAL answer —
// a renewal or a direct award — and it carries a reason, in the same shape an
// override carries a justification.
const INTENT_CHOICES = [
  { key: 'rfp', label: 'a request for proposal',
    line: 'Suppliers propose how they would meet the need, and are compared on more than price.' },
  { key: 'rfq', label: 'a request for quotation',
    line: 'The need is already specified and suppliers are asked what they would charge for it.' },
  { key: 'none', label: 'no sourcing event',
    line: 'A renewal or a direct award. Nobody approves this and nothing waits on it — the reason is recorded so the choice is visible.' },
];

// Only these two parts may carry a span somebody wrote. Everything else in the
// document assembles from approved wording. Kept in step with
// `doorway/sourcing.py`'s WRITABLE_PARTS and with `cw.sourcing_run_span`'s own
// check constraint — three statements of one rule, and the database is the one
// that bites.
const FORGE_WRITABLE_PARTS = [
  { key: 'questions',
    label: 'Questions for suppliers',
    line: 'What you want every bidder to answer.' },
  { key: 'deliverables',
    label: 'Deliverables',
    line: 'What the winner will actually have to produce.' },
];

// A blank span, ready to be written into.
function emptyWrittenSpan(part) {
  return { part, body: '', origin: 'human_authored', model: '', model_version: '' };
}

// Whether a written span is complete enough to send. SHAPE ONLY — whether this
// person may write here at all is the database's question.
function spanIsReady(span) {
  if (!span.body.trim()) return false;
  if (span.origin === 'ai_drafted' && !span.model.trim()) return false;
  return true;
}

// A span somebody started and did not finish is neither sent nor silently
// dropped: `readySpans` sends the complete ones and `unfinishedSpans` counts
// the rest, so the screen can say so before the button is pressed.
function readySpans(spans) {
  return spans.filter(spanIsReady).map((s) => ({
    part: s.part,
    body: s.body.trim(),
    origin: s.origin,
    // A HUMAN-AUTHORED SPAN CARRYING A MODEL NAME IS REFUSED, and rightly: the
    // record holds one answer to who wrote a span, not two. So the model
    // fields go out only on the arm that claims a machine wrote it, however
    // the boxes happened to be filled in on the way there.
    ...(s.origin === 'ai_drafted'
      ? { model: s.model.trim(),
          ...(s.model_version.trim() ? { model_version: s.model_version.trim() } : {}) }
      : {}),
  }));
}

function unfinishedSpans(spans) {
  return spans.filter((s) => (s.body.trim() || s.model.trim()) && !spanIsReady(s));
}

// Which live deals carry no recorded intent.
//
// ASKED OF THE INTENTS READ, NEVER OF THE RUNS. A deal can have a document
// built against it with no intent recorded — the doorway allows exactly that,
// because a sourcing event often precedes the deal record — so counting built
// documents here would call an asked engagement unasked and the reverse.
function unaskedDeals(deals, intents) {
  const asked = new Set((intents || []).map((i) => String(i.agreement_id)));
  return (deals || []).filter((d) => isLive(d) && !asked.has(String(d.agreement_id)));
}

// ── A refusal, in the doorway's own sentence ──────────────────────────────
// Never a paraphrase. Those sentences name the rule and the role, and they are
// the only part of a refusal somebody can act on.
function ForgeRefusal({ reason }) {
  if (!reason) return null;
  return (
    <div className="panel p-3 mt-4" style={{ borderColor: 'var(--danger)' }}
         data-testid="forge-refusal" role="alert">
      <div className="tag" style={{ color: 'var(--danger)' }}>refused</div>
      <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
        {reason}
      </div>
    </div>
  );
}

// ── The engagements nobody has asked about ────────────────────────────────
//
// THE PANEL THAT MAKES THE THIRD ANSWER VISIBLE. Live deals with no row in
// `cw.current_sourcing_intent`. Not a queue and not a warning: nobody is late,
// and a deal that never goes to market is a perfectly good outcome. What the
// screen owes is that the question has not been put — which nothing said until
// this panel existed.
function UnaskedEngagements({ me, deals, intents, onDecide }) {
  const rows = useForgeMemo(() => unaskedDeals(deals, intents), [deals, intents]);
  const filter = useListFilter(rows, {
    view: 'sourcing:never-asked',
    fields: ['agreement_id', 'counterparty', 'requester', 'status'],
  });
  // AN AUDITOR READS THIS PANEL AND CANNOT ACT ON IT. Which engagements were
  // never asked is an audit fact and belongs on their screen; a row that
  // looked pressable and led to a refusal would not.
  const mayDecide = maySourceOn(me);

  return (
    <div>
      <PanelHead
        title="Not yet asked whether they go to market"
        sub="Live engagements with no sourcing decision on the record. Nobody has decided against competing them — nobody has been asked."
        right={<FilterCount filter={filter} />} />
      <ListFilter filter={filter} testid="sourcing-unasked"
                  placeholder="search by engagement, counterparty, or requester" />
      {rows.length === 0 ? (
        <Empty
          kicker="all asked"
          line="Every live engagement has a sourcing decision on the record."
          sub="Each one has been asked whether it goes to market, and the answer —
               including an answer of no — is recorded with a name on it." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="engagements" />
      ) : (
        <div className="panel">
          {/* IN A `.panel`, and that is not decoration. `registry.css` carries
              `.panel:has(table.ledger){overflow-x:auto}` — the repair for
              S324, where a ledger pushed the whole PAGE sideways at a narrow
              width and dragged the masthead and the rack with it. */}
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>engagement</th><th>counterparty</th><th>requester</th>
                <th>stage</th><th>decision</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((d) => (
                <tr key={d.agreement_id}
                    {...(mayDecide
                      ? openableRow(() => onDecide(d.agreement_id),
                                    `decide whether ${d.agreement_id} goes to market`)
                      : {})}>
                  <td className="font-mono">{d.agreement_id}</td>
                  <td>{d.counterparty}</td>
                  <td className="font-mono caption">{d.requester}</td>
                  <td className="caption">{d.status}</td>
                  <td className="caption">{mayDecide ? 'not asked →' : 'not asked'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

// ── The decision: does this go to market ──────────────────────────────────
function DecideIntent({ me, agreementId, deals, intents, onDone, onClose }) {
  const [choice, setChoice] = useForgeState('');
  const [reason, setReason] = useForgeState('');
  const [refused, setRefused] = useForgeState(null);

  const deal = (deals || []).find((d) => String(d.agreement_id) === String(agreementId));
  const already = (intents || []).find(
    (i) => String(i.agreement_id) === String(agreementId));

  // `none` CARRIES A REASON AND THE OTHER TWO DO NOT. The database's CHECK
  // refuses a blank reason on `none`; the button stays disabled rather than
  // spending that refusal on somebody's typing.
  const needsReason = choice === 'none';
  const ready = choice !== '' && (!needsReason || reason.trim().length > 0);

  return (
    <div>
      <PaneHead
        kicker="sourcing"
        title="Does this go to market?"
        sub="One decision per engagement, recorded with the name of whoever made it. Changing your mind is allowed, and is recorded as a second decision rather than replacing the first."
        right={<button type="button" className="btn" onClick={onClose}>back to sourcing</button>} />

      <div className="panel p-4 mt-6">
        <PanelHead title="The engagement"
                   sub="The decision is about one engagement, and the counterparty comes from it rather than being typed again." />
        <div className="mt-2 text-[13px]">
          <span className="font-mono">{agreementId}</span>
          {deal && <> · {deal.counterparty} · <span className="caption">{deal.status}</span></>}
        </div>
        {!deal && (
          <div className="caption mt-2">
            Nothing you can read answers to that reference. It may belong to
            somebody else's engagement, or the address may be stale. Nothing was
            hidden from this screen — the record simply did not answer with it.
          </div>
        )}
        {/* SOMEBODY ALREADY DECIDED THIS, and the screen says so before
            offering to decide it again. The record is append-only: a second
            decision does not erase the first, and a person about to make one
            should know they are changing an answer rather than giving one. */}
        {already && (
          <div className="panel-2 p-3 mt-3" data-testid="intent-already">
            <div className="tag">already decided</div>
            <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
              Recorded as <strong>{already.event_label || already.event_type}</strong> by{' '}
              <span className="font-mono">{already.decided_by}</span>, {since(already.decided_at)}.
              {already.reason && <> The reason given was: {already.reason}</>}
              {already.decisions_recorded > 1
                && <> This engagement has been decided {already.decisions_recorded} times.</>}
              {' '}Recording another decision adds to that history; it does not
              replace it.
            </div>
          </div>
        )}
      </div>

      {/* ARRIVED HERE AND CANNOT ACT. A deep link, a bookmark, or a colleague's
          message reaches this address for anybody. Saying so plainly beats
          both alternatives: an empty screen states nothing, and a form whose
          only outcome is a refusal wastes somebody's typing to tell them what
          the record could have said first. */}
      {!maySourceOn(me) ? (
        <div className="mt-6">
          <Empty
            kicker="not yours to decide"
            line="Whether an engagement goes to market is decided by the requester who owns it, or by Legal."
            sub="Your grant reads every sourcing record and writes none of them. What
                 was decided, by whom and when is above; the decision itself is
                 somebody else's act."
            action={<button type="button" className="btn" onClick={onClose}>back to sourcing</button>} />
        </div>
      ) : (
      <div className="panel p-4 mt-6">
        <PanelHead title="The answer"
                   sub="Three answers, and the third is as real as the other two." />
        {INTENT_CHOICES.map((c) => (
          <label key={c.key} className="panel-2 p-3 mt-2 flex items-start gap-3"
                 style={{ cursor: 'pointer' }}>
            <input type="radio" name="sourcing-intent" value={c.key}
                   style={{ marginTop: 3 }}
                   aria-label={c.label}
                   data-testid={`intent-${c.key}`}
                   checked={choice === c.key}
                   onChange={() => { setChoice(c.key); setRefused(null); }} />
            <span className="min-w-0">
              <span className="text-[13px]" style={{ color: 'var(--ink)' }}>{c.label}</span>
              <span className="block text-[12.5px] mt-0.5"
                    style={{ color: 'var(--mute)', lineHeight: 1.6 }}>{c.line}</span>
            </span>
          </label>
        ))}

        {needsReason && (
          <div className="mt-4" data-testid="intent-reason-block">
            <div className="section-label">Why this one is not going to market</div>
            <textarea className="mt-1.5 w-full" rows={3}
                      aria-label="Why this engagement is not going to market"
                      data-testid="intent-reason"
                      value={reason}
                      onChange={(e) => { setReason(e.target.value); setRefused(null); }} />
            <div className="caption mt-1.5">
              {/* WARN, DON'T GATE (Mike's standing rule). Nothing waits on this
                  and nobody approves it. What the record requires is that
                  somebody SAID something, never that anybody agreed. */}
              Nobody has to approve this. The reason is recorded so the choice
              is visible to Legal and to Audit, and it appears on the register
              of engagements handed over without competition.
            </div>
          </div>
        )}

        <ForgeRefusal reason={refused} />

        <div className="flex items-center gap-3 mt-4">
          <ActButton className="btn btn-primary" data-testid="record-intent"
                     disabled={!ready}
                     onClick={async () => {
                       setRefused(null);
                       const r = await API.recordSourcingIntent({
                         agreement_id: agreementId,
                         event_type: choice,
                         ...(needsReason ? { reason: reason.trim() } : {}),
                       });
                       if (!r.ok) { setRefused(r.reason); return; }
                       onDone(choice);
                     }}>
            record this decision
          </ActButton>
          <span className="caption">
            This records a decision with your name on it. It does not build
            anything.
          </span>
        </div>
      </div>
      )}
    </div>
  );
}

// ── The forge ─────────────────────────────────────────────────────────────
function SourcingForge({ me, deals, intents, startOn, built, onBuilt, onOpenBuilt,
                        onAnother, onClose }) {
  // RETAINED ACROSS A CHANGE OF TAB (S319). This is the most effortful form in
  // the application after the intake walk — a description of what is being
  // bought and a set of written questions — and a pane unmounts the moment
  // somebody clicks the rack. In memory and NEVER in storage: a half-written
  // sourcing document naming a counterparty must not outlive the session on
  // the disk of a shared desk.
  const [dealId, setDealId] = useRetainedState('forge:deal', '');
  // WHICH COMPETITION THIS DOCUMENT IS FOR (0124, SRC-5). Optional, because a
  // document built to be read belongs to no competition — the only kind that
  // existed before this arc. But without it the document can never be ISSUED:
  // cw.issue_sourcing_document refuses paper that was not built for the event
  // it is being issued for. Keyed 'forge:competition' and NOT 'forge:event',
  // which the event TYPE already holds three lines below.
  const [competitionId, setCompetitionId] = useRetainedState('forge:competition', '');
  const competitions = usePane(() => API.sourcingEvents());
  const [eventType, setEventType] = useRetainedState('forge:event', '');
  const [need, setNeed] = useRetainedState('forge:need', '');
  const [hint, setHint] = useRetainedState('forge:hint', '');
  const [wantsTerms, setWantsTerms] = useRetainedState('forge:terms', true);
  const [spans, setSpans] = useRetainedState('forge:spans', []);
  const [refused, setRefused] = useForgeState(null);

  const live = useForgeMemo(() => (deals || []).filter(isLive), [deals]);

  // WHICH ENGAGEMENT THIS FORM IS ACTUALLY ABOUT. `startOn` is where the
  // person came from — a decision they just recorded — and it fills the field
  // only while the field is empty. It never overwrites retained work: a draft
  // that names an engagement wins, and the note further down says so rather
  // than leaving somebody to notice on their own.
  const chosenDeal = dealId || startOn || '';
  const decided = (intents || []).find(
    (i) => String(i.agreement_id) === String(chosenDeal)) || null;

  // WHAT THE RECORD ALREADY SAYS, OFFERED RATHER THAN IMPOSED. When an
  // engagement has a decision on it, that is the event type this build should
  // carry, and the doorway refuses a build that contradicts it with a sentence
  // naming both. The screen offers the recorded answer as the selected one and
  // says where it came from; it does not lock the control, because the honest
  // path through a change of mind is to be refused and told to record the new
  // decision — which is exactly what happens.
  const effectiveType = eventType || (decided && decided.produces_document
    ? decided.event_type : '');

  const ready = effectiveType !== '' && need.trim().length > 0;
  const unfinished = unfinishedSpans(spans);
  const willSend = readySpans(spans);

  const head = (
    <PaneHead
      kicker="sourcing"
      title="Build a sourcing document"
      sub="Assembled from Legal's approved wording the same deterministic way a contract is, with your own questions declared separately and the draft contract terms riding along."
      right={<button type="button" className="btn" onClick={onClose}>back to sourcing</button>} />
  );

  // EVERY HOOK IS ABOVE THIS LINE. A hook after an early return blanks the
  // pane on the render AFTER the data lands, which is the one nobody watches
  // (S318) — and this file is in `hook-order.test.mjs`'s scope the same hour
  // it exists.
  if (built) {
    return (
      <div>
        {head}
        <BuiltReceipt built={built} onOpen={onOpenBuilt} onAnother={onAnother} />
      </div>
    );
  }

  // ARRIVED HERE AND CANNOT BUILD. `0096`'s builders_write policy names three
  // roles and an auditor is not one; the desk therefore never offers them this
  // form, and reaching it by address gets the reason rather than a long form
  // whose only outcome is a refusal.
  if (!maySourceOn(me)) {
    return (
      <div>
        {head}
        <div className="mt-6">
          <Empty
            kicker="not yours to build"
            line="A sourcing document is assembled by the requester whose engagement it is, or by Legal."
            sub="Your grant reads every document that was built and what each one was
                 made of, and writes none of them. The register is on the sourcing
                 desk; the building is somebody else's act."
            action={<button type="button" className="btn" onClick={onClose}>back to sourcing</button>} />
        </div>
      </div>
    );
  }

  return (
    <div>
      {head}

      {/* YOUR UNSENT WORK WON, AND THE SCREEN SAYS SO. Arriving here from a
          decision about one engagement while a half-written document names
          another is rare and completely silent otherwise: the retained draft
          is deliberately not overwritten (S319), so the only honest thing is
          to say which engagement this form is actually holding. */}
      {startOn && dealId && String(startOn) !== String(dealId) && (
        <div className="panel-2 p-3 mt-4" data-testid="forge-draft-kept">
          <div className="tag">a draft was already here</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            You came here from <span className="font-mono">{startOn}</span>, and
            this form is still holding work you have not sent. Nothing was
            thrown away. Change the engagement below if this document is for a
            different one.
          </div>
        </div>
      )}

      {/* ── 1 · Which engagement ───────────────────────────────────────── */}
      <div className="panel p-4 mt-6">
        <PanelHead
          title="Which engagement"
          sub="Optional. A sourcing event often comes before the deal record exists, and a document built without one is tied to nothing rather than tied to the wrong thing." />
        <select className="w-full font-mono" style={{ padding: '6px 8px' }}
                data-testid="forge-deal"
                aria-label="Which engagement this sourcing document is for"
                value={chosenDeal}
                onChange={(e) => { setDealId(e.target.value); setRefused(null); }}>
          <option value="">no engagement yet</option>
          {/* LIVE DEALS ONLY, asked of `isLive` — workspaces.jsx's own answer,
              which intake and negotiate already use, so this list cannot come
              to disagree with them about which deals are still moving. */}
          {live.map((d) => (
            <option key={d.agreement_id} value={d.agreement_id}>
              {d.agreement_id} — {d.counterparty}
            </option>
          ))}
        </select>
        {/* THREE ANSWERS, DRAWN AS THREE. Decided to compete, decided not to,
            and never asked — and the third is neither of the first two. */}
        {chosenDeal && (
          <div className="caption mt-2" data-testid="forge-intent-state">
            {!decided ? (
              <>Nobody has recorded whether this engagement goes to market. That
                is not a refusal — you may build now and record the decision
                afterwards — but the decision is the thing Legal and Audit read.</>
            ) : decided.produces_document ? (
              <>Recorded as <strong>{decided.event_label || decided.event_type}</strong> by{' '}
                <span className="font-mono">{decided.decided_by}</span>,{' '}
                {since(decided.decided_at)}.</>
            ) : (
              <>This engagement is recorded as <strong>not going to market</strong> by{' '}
                <span className="font-mono">{decided.decided_by}</span>. Building a
                document against it will be refused until a new decision is
                recorded, so that the change of mind has a name on it.</>
            )}
          </div>
        )}
      </div>

      {/* ── 2 · What kind of document ──────────────────────────────────── */}
      <div className="panel p-4 mt-6">
        <PanelHead
          title="What kind of document"
          sub="Which approved sections assemble into it is the library's answer, not yours — the same rule that makes a contract build reproducible." />
        {INTENT_CHOICES.filter((c) => c.key !== 'none').map((c) => (
          <label key={c.key} className="panel-2 p-3 mt-2 flex items-start gap-3"
                 style={{ cursor: 'pointer' }}>
            <input type="radio" name="forge-event" value={c.key}
                   style={{ marginTop: 3 }}
                   aria-label={c.label}
                   data-testid={`forge-event-${c.key}`}
                   checked={effectiveType === c.key}
                   onChange={() => { setEventType(c.key); setRefused(null); }} />
            <span className="min-w-0">
              <span className="text-[13px]" style={{ color: 'var(--ink)' }}>{c.label}</span>
              <span className="block text-[12.5px] mt-0.5"
                    style={{ color: 'var(--mute)', lineHeight: 1.6 }}>{c.line}</span>
            </span>
          </label>
        ))}
      </div>

      {/* ── 3 · What is being bought ───────────────────────────────────── */}
      <div className="panel p-4 mt-6">
        <PanelHead
          title="What is being bought"
          sub="In your own words. This is printed in the document, and it is what a supplier reads first." />
        <textarea className="mt-1.5 w-full" rows={3}
                  aria-label="What is being bought"
                  data-testid="forge-need"
                  value={need}
                  onChange={(e) => { setNeed(e.target.value); setRefused(null); }} />
        <div className="section-label mt-4">Who we expect to bid (optional)</div>
        <input type="text" className="mt-1.5 w-full"
               aria-label="Who we expect to bid"
               data-testid="forge-hint"
               value={hint}
               onChange={(e) => { setHint(e.target.value); setRefused(null); }} />
        <div className="caption mt-1.5">
          A note for whoever reads the record later. It does not restrict who
          may respond and it selects nobody.
        </div>
      </div>

      {/* ── 4 · The differentiator ─────────────────────────────────────── */}
      <div className="panel p-4 mt-6">
        <PanelHead
          title="The terms the winner will be asked to sign"
          sub="Suppliers see the draft contract terms before they bid, referenced from the assembly on this engagement rather than copied into the document." />
        <label className="panel-2 p-3 mt-2 flex items-start gap-3" style={{ cursor: 'pointer' }}>
          <input type="checkbox" style={{ marginTop: 3 }}
                 aria-label="Attach the draft contract terms to this document"
                 data-testid="forge-terms"
                 checked={!!wantsTerms}
                 onChange={(e) => { setWantsTerms(e.target.checked); setRefused(null); }} />
          <span className="min-w-0">
            <span className="text-[13px]" style={{ color: 'var(--ink)' }}>
              Attach the draft contract terms
            </span>
            <span className="block text-[12.5px] mt-0.5"
                  style={{ color: 'var(--mute)', lineHeight: 1.6 }}>
              {/* WHICH ASSEMBLY IS NOT THE CALLER'S TO PICK, and saying so here
                  is not pedantry: a person who believed they were choosing
                  would eventually believe they had chosen an old one. Where we
                  have no approved language for a term, the supplier is shown
                  the gap as a gap rather than shown nothing. */}
              The most recent contract assembly on this engagement is the one
              that rides along — you are choosing whether, not which. Terms we
              have no approved language for appear as gaps, because a supplier
              told the paper is finished when it is not is the failure this
              product exists to prevent.
            </span>
          </span>
        </label>
        <div className="caption mt-2">
          Your Administrator may have made this compulsory, and may or may not
          allow terms from an assembly that did not pass its gate. If a setting
          decides otherwise, the refusal will say so in its own words.
        </div>
      </div>

      {/* ── 5 · The half that is yours to write ────────────────────────── */}
      <div className="panel p-4 mt-6">
        <PanelHead
          title="Your own questions and deliverables"
          sub="The only two parts anybody writes. Everything else in the document assembles from approved wording and cannot be typed over."
          right={<span className="caption">{willSend.length} to send</span>} />

        {spans.length === 0 && (
          <Empty
            kicker="nothing written"
            line="This document will be assembled entirely from approved wording."
            sub="That is a complete and valid document. Add a question or a
                 deliverable when this purchase needs something the library's
                 standard sections do not ask for." />
        )}

        {spans.map((span, i) => (
          <WrittenSpan
            key={i}
            span={span}
            index={i}
            onChange={(next) => {
              setSpans(spans.map((s, j) => (j === i ? next : s)));
              setRefused(null);
            }}
            onDrop={() => { setSpans(spans.filter((_, j) => j !== i)); setRefused(null); }} />
        ))}

        <div className="flex items-center gap-2 mt-4 flex-wrap">
          {FORGE_WRITABLE_PARTS.map((p) => (
            <button key={p.key} type="button" className="btn btn-sm"
                    data-testid={`forge-add-${p.key}`}
                    onClick={() => setSpans([...spans, emptyWrittenSpan(p.key)])}>
              + {p.label.toLowerCase()}
            </button>
          ))}
        </div>

        {/* STARTED AND NOT FINISHED IS NOT SILENTLY DROPPED. A span with words
            in it that is missing its model name would otherwise vanish between
            the screen and the document, which is the quietest way there is to
            lose somebody's writing. */}
        {unfinished.length > 0 && (
          <div className="panel-2 p-3 mt-3" data-testid="forge-unfinished">
            <div className="tag">not finished</div>
            <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
              {unfinished.length} of what you have written is not complete and
              will not go into the document. A span needs words, and one that
              says a model wrote it needs the name of the model — the record
              holds no machine-written text nobody can account for.
            </div>
          </div>
        )}
      </div>

      {/* THE COMPETITION THIS IS FOR. Drawn only when there is at least one
          open competition to choose — an empty select teaching somebody the
          field does nothing is worse than no field at all. */}
      {competitions.status === 'loaded'
        && (competitions.rows || []).some((e) => e.state === 'open') && (
        <label className="block mt-4" data-testid="forge-competition">
          <span className="tag">which competition this is for, if any</span>
          <select className="mt-1 w-full" data-testid="forge-competition-pick"
                  value={competitionId}
                  onChange={(e) => setCompetitionId(e.target.value)}>
            <option value="">none — a document to be read, not sent</option>
            {(competitions.rows || []).filter((e) => e.state === 'open').map((e) => (
              <option key={e.event_id} value={e.event_id}>{e.title}</option>
            ))}
          </select>
          <span className="muted">
            A document can only be issued to suppliers if it names the
            competition it was built for.
          </span>
        </label>
      )}

      <ForgeRefusal reason={refused} />

      <div className="flex items-center gap-3 mt-6 flex-wrap">
        <ActButton className="btn btn-primary" data-testid="build-sourcing"
                   disabled={!ready}
                   onClick={async () => {
                     setRefused(null);
                     const r = await API.buildSourcing({
                       event_type: effectiveType,
                       need: need.trim(),
                       ...(chosenDeal ? { agreement_id: chosenDeal } : {}),
                       ...(hint.trim() ? { counterparty_hint: hint.trim() } : {}),
                       terms_preview: !!wantsTerms,
                       ...(competitionId ? { event_id: Number(competitionId) } : {}),
                       ...(willSend.length ? { spans: willSend } : {}),
                     });
                     if (!r.ok) { setRefused(r.reason); return; }
                     // THE DRAFT IS NOT A DRAFT ANY MORE. It is a stale copy of
                     // a document that now exists permanently, and leaving it
                     // would offer somebody the chance to build the same thing
                     // twice by walking away and coming back.
                     //
                     // CLEARED THROUGH THE SETTERS, not by discarding the keys
                     // behind them. `useRetainedState`'s setter writes the
                     // retained copy inside a state updater, so a discard
                     // followed by a set puts the value straight back — the
                     // tidier-looking order is the one that does not work.
                     setDealId(''); setEventType(''); setNeed(''); setHint('');
                     setWantsTerms(true); setSpans([]);
                     onBuilt(r.body);
                   }}>
          build the document
        </ActButton>
        <span className="caption">
          {ready
            ? <>This assembles a document and records it permanently. A sourcing
                run cannot be edited or deleted afterwards — the record and the
                bytes whose hash sits beside it can never come to disagree.</>
            : <>Choose what kind of document this is, and say what is being bought.</>}
        </span>
      </div>
    </div>
  );
}

// ── One span somebody wrote ───────────────────────────────────────────────
//
// WHO WROTE IT IS ASKED BESIDE THE WORDS, not at the end of the form. Mike's
// 2026-08-04 carve-out lets a model author these; `cw.sourcing_run_span`
// refuses a machine-written span that does not name its model, and the whole
// value of the carve-out is that the record always says which is which.
function WrittenSpan({ span, index, onChange, onDrop }) {
  const part = FORGE_WRITABLE_PARTS.find((p) => p.key === span.part)
    || FORGE_WRITABLE_PARTS[0];
  const byModel = span.origin === 'ai_drafted';
  return (
    <div className="panel-2 p-3 mt-3" data-testid="forge-span">
      <div className="flex items-center justify-between gap-3 flex-wrap">
        <div className="flex items-center gap-2 min-w-0">
          <select style={{ padding: '4px 8px' }}
                  aria-label={`Which part of the document written span ${index + 1} belongs to`}
                  value={span.part}
                  onChange={(e) => onChange({ ...span, part: e.target.value })}>
            {FORGE_WRITABLE_PARTS.map((p) => (
              <option key={p.key} value={p.key}>{p.label}</option>
            ))}
          </select>
          <span className="caption">{part.line}</span>
        </div>
        <button type="button" className="btn btn-sm"
                aria-label={`Remove written span ${index + 1}`}
                onClick={onDrop}>remove</button>
      </div>

      <textarea className="mt-2 w-full" rows={3}
                aria-label={`The text of written span ${index + 1}`}
                value={span.body}
                onChange={(e) => onChange({ ...span, body: e.target.value })} />

      <div className="flex items-center gap-3 mt-2 flex-wrap">
        <span className="caption">who wrote this</span>
        <select style={{ padding: '4px 8px' }}
                aria-label={`Who wrote written span ${index + 1}`}
                data-testid="forge-span-origin"
                value={span.origin}
                onChange={(e) => onChange({ ...span, origin: e.target.value })}>
          <option value="human_authored">a person</option>
          <option value="ai_drafted">a model</option>
        </select>
        {/* THE MODEL'S NAME, ASKED THE MOMENT SOMEBODY SAYS A MODEL WROTE IT.
            Not optional and not asked at the end: the database refuses the span
            without it, and meeting a constraint as a rejection after a long
            form is how people learn to write around the record. */}
        {byModel && (
          <>
            <input type="text" style={{ padding: '4px 8px', minWidth: 160 }}
                   placeholder="which model"
                   aria-label={`Which model wrote written span ${index + 1}`}
                   data-testid="forge-span-model"
                   value={span.model}
                   onChange={(e) => onChange({ ...span, model: e.target.value })} />
            <input type="text" style={{ padding: '4px 8px', minWidth: 120 }}
                   placeholder="version (optional)"
                   aria-label={`Which version of the model wrote written span ${index + 1}`}
                   value={span.model_version}
                   onChange={(e) => onChange({ ...span, model_version: e.target.value })} />
          </>
        )}
        <Status state={byModel ? 'pending' : 'effective'}>
          {byModel ? 'machine-written' : 'written by a person'}
        </Status>
      </div>
      {byModel && !span.model.trim() && (
        <div className="caption mt-1.5">
          A span claiming machine origin with no model on it is the row nobody
          can explain a year later. Name the model and this goes into the
          document; leave it blank and it does not.
        </div>
      )}
    </div>
  );
}

// ── What the build produced, before the record view opens ─────────────────
//
// THE THREE FIGURES COME BACK ON EVERY SUCCESSFUL BUILD, `unaccounted_chars`
// among them, and the record constrains it to zero. A person who never opens
// the dossier still learns what their document was made of.
function BuiltReceipt({ built, onOpen, onAnother }) {
  return (
    <div className="panel p-4 mt-6" data-testid="forge-receipt">
      <PanelHead
        title="Built"
        sub="Recorded permanently, with a hash over the bytes and a note of where every character came from." />
      <div className="tile-strip mt-3">
        <StatBox label="from approved wording" n={built.approved_chars} />
        <StatBox label="written for this engagement" n={built.engagement_chars} />
        <StatBox label="unattributed" n={built.unaccounted_chars} />
        <StatBox label="terms previewed" n={built.preview_clauses} />
      </div>
      {built.preview_unpapered > 0 && (
        <div className="caption mt-2">
          {built.preview_unpapered} of the previewed terms have no approved
          language, and are shown to suppliers as gaps.
        </div>
      )}
      {/* AN ABSENCE WITH ITS REASON (S299). The endpoint says why there is no
          preview whenever there is none, and a silent null would leave "the
          build did not ask for them" and "a setting refused" looking the
          same. */}
      {built.preview_note && (
        <div className="caption mt-2" data-testid="forge-preview-note">
          {built.preview_note}
        </div>
      )}
      <div className="flex items-center gap-3 mt-4 flex-wrap">
        <button type="button" className="btn btn-primary"
                data-testid="forge-open-built"
                onClick={() => onOpen(built.sourcing_run_id)}>
          open the document
        </button>
        <button type="button" className="btn" onClick={onAnother}>build another</button>
        <span className="caption font-mono">
          {String(built.document_sha256).slice(0, 12)} · {built.engine_version}
        </span>
      </div>
    </div>
  );
}


// ── What we said we would take to market ──────────────────────────────────
//
// THE FOURTH ANSWER, AND NOBODY WAS SHOWING IT. `0097` records whether an
// engagement goes to market; `0096` records what was built. Three states had
// screens — going to market, handed over without competition, and never asked
// — and the interesting one had none: **decided to compete, and then nothing
// built**. A decision that was never acted on is invisible in a list of
// decisions, because on that list it looks exactly like one that was.
//
// AND THE MIRROR IS REAL TOO. The doorway deliberately allows a document to be
// built against an engagement with no recorded decision — a sourcing event can
// precede the deal record and often does — so the record can disagree with
// itself in both directions. Only one of the two is a gap somebody should
// close; the other is usually a sequence, and the screen says which is which
// rather than colouring both red.
//
// THE FIGURE LEADS TO WHAT IT COUNTED (S333). `going to market` counts every
// engagement decided to compete, so this panel lists every one of them — not
// only the unfulfilled. The gap is a MARK on the rows and a focus you can
// press, never a shorter list wearing the figure's number.
function derivedMarketRows(intents, runs) {
  const builtFor = new Set(
    (runs || []).filter((r) => r.agreement_id).map((r) => String(r.agreement_id)));
  return (intents || [])
    .filter((i) => i.produces_document)
    .map((i) => ({
      ...i,
      built: builtFor.has(String(i.agreement_id)),
      // FACETED ON WHAT SOMEBODY WOULD SAY, never on a boolean. `true` in a
      // dropdown is the derivation's word rather than a buyer's.
      standing: builtFor.has(String(i.agreement_id))
        ? 'document built' : 'nothing built yet',
    }))
    // OLDEST DECISION FIRST — the desk idiom, and the only order that makes
    // the interesting rows surface: a decision made this morning and not yet
    // acted on is nothing; one made in April and not yet acted on is the row
    // this panel exists for.
    .sort((a, b) => String(a.decided_at || '').localeCompare(String(b.decided_at || '')));
}

// Engagements a document was built against with NO decision on the record.
// Derived from the runs rather than the intents, because that is the
// population it is about — and a run with no agreement at all is excluded, not
// counted as a gap: a sourcing event can precede the deal record entirely.
function builtWithoutADecision(intents, runs) {
  const decided = new Set((intents || []).map((i) => String(i.agreement_id)));
  return (runs || [])
    .filter((r) => r.agreement_id && !decided.has(String(r.agreement_id)));
}

function GoingToMarket({ me, intents, runs, deals, onOpen, onBuildFor }) {
  const rows = useForgeMemo(() => derivedMarketRows(intents, runs), [intents, runs]);
  const orphans = useForgeMemo(() => builtWithoutADecision(intents, runs), [intents, runs]);
  const filter = useListFilter(rows, {
    view: 'sourcing:go-to-market',
    fields: ['agreement_id', 'event_label', 'event_type', 'decided_by'],
    facet: 'standing',
  });
  const mayBuild = maySourceOn(me);
  const counterparty = (id) => {
    const deal = (deals || []).find((d) => String(d.agreement_id) === String(id));
    return deal ? deal.counterparty : null;
  };
  const unfulfilled = rows.filter((r) => !r.built).length;
  // Registered at render, not at the click, so a saved view can put the focus
  // back even when the button that sets it is not drawn this visit.
  const unbuiltFocus = filter.focusable('unbuilt', 'nothing built yet', (r) => !r.built);

  return (
    <div>
      <PanelHead
        title="What we said we would take to market"
        sub="Every engagement decided to compete, and whether the document exists yet. A decision nobody acted on looks exactly like one somebody did, until something separates them."
        right={<FilterCount filter={filter} />} />

      {rows.length > 0 && (
        <div className="caption mb-2" data-testid="market-summary">
          {/* THE SENTENCE AND THE ROWS ARE THE SAME SET, counted from the rows
              above rather than from a second derivation. */}
          {unfulfilled === 0
            ? <>All {rows.length} have a document. Nothing was decided and left.</>
            : <>{unfulfilled} of {rows.length} have no document yet.{' '}
                <button type="button" className="chip chip-focus"
                        data-testid="focus-unfulfilled"
                        aria-pressed={Boolean(filter.focus && filter.focus.key === 'unbuilt')}
                        onClick={() => filter.focusOn(unbuiltFocus)}>
                  show them
                </button></>}
        </div>
      )}

      <ListFilter filter={filter} testid="sourcing-market"
                  placeholder="search by engagement, event type, or who decided"
                  facetLabel="built or not" />

      {rows.length === 0 ? (
        <Empty
          kicker="nothing decided"
          line="No engagement has been decided to go to market."
          sub="A decision to compete is recorded on the engagement before the
               document is built. When one is, it appears here — and stays here
               with its date until the document exists, so a decision nobody
               acted on cannot hide among the ones somebody did." />
      ) : filter.shown.length === 0 ? (
        <NoMatch kicker="no match" noun="engagements" />
      ) : (
        <div className="panel">
          <table className="ledger w-full">
            <thead>
              <tr>
                <th>engagement</th><th>counterparty</th><th>decided as</th>
                <th>decided by</th><th>waiting</th><th>document</th>
              </tr>
            </thead>
            <tbody>
              {filter.shown.map((r) => {
                // A ROW OPENS THE THING IT IS ABOUT. One with no document opens
                // the forge on that engagement — the act it is waiting for; one
                // that HAS a document opens the document. A row that led to the
                // same place either way would be a list you cannot act from.
                const act = r.built
                  ? null
                  : (mayBuild ? () => onBuildFor(r.agreement_id) : null);
                return (
                  <tr key={r.agreement_id}
                      {...(act
                        ? openableRow(act,
                            `build the sourcing document for ${r.agreement_id}`)
                        : {})}>
                    <td className="font-mono">{r.agreement_id}</td>
                    <td>{counterparty(r.agreement_id) || <span className="caption">—</span>}</td>
                    <td>{r.event_label || r.event_type}</td>
                    <td className="font-mono caption">{r.decided_by}</td>
                    <td className="caption" title={r.decided_at}>
                      {/* HOW LONG THE DECISION HAS STOOD, not how long the
                          document has existed. A built one is not waiting for
                          anything, and drawing an age on it would invite
                          somebody to read it as a delay. */}
                      {r.built ? '—' : since(r.decided_at)}
                    </td>
                    <td>
                      {r.built
                        ? <Status state="effective">built</Status>
                        : <Status state="pending">
                            {mayBuild ? 'nothing built · build it →' : 'nothing built'}
                          </Status>}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}

      {/* ── THE MIRROR, AND IT IS NOT DRAWN AS A FAULT ──────────────────
          A document built against an engagement with no recorded decision. The
          doorway allows it deliberately — a sourcing event can precede the deal
          record — so this is a sequence far more often than an omission, and a
          screen that coloured it red would teach people a rule the system does
          not have. It is here because it is the other way the record can
          disagree with itself, and because somebody reading the figure above
          should know the two populations are not the same set. */}
      {orphans.length > 0 && (
        <div className="panel-2 p-3 mt-4" data-testid="market-orphans">
          <div className="tag">built without a recorded decision</div>
          <div className="text-[12.5px] mt-1.5"
               style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            {orphans.length} document{orphans.length === 1 ? '' : 's'} name
            {orphans.length === 1 ? 's' : ''} an engagement that has no
            go-to-market decision on the record:{' '}
            {orphans.map((r, i) => (
              <React.Fragment key={r.sourcing_run_id}>
                {i > 0 && ' · '}
                <button type="button" className="link-mono"
                        onClick={() => onOpen(r.sourcing_run_id)}
                        aria-label={`open the document for ${r.agreement_id}`}>
                  {r.agreement_id}
                </button>
              </React.Fragment>
            ))}
            . That is allowed and is usually an order of events rather than an
            omission — a sourcing event often comes before the deal record. It
            is shown because the figure above counts DECISIONS and this counts
            DOCUMENTS, and the two are not the same set.
          </div>
        </div>
      )}
    </div>
  );
}
