// The Clause Library Builder: the machine drafts, and a lawyer decides.
//
// WHAT THIS SCREEN IS FOR. Clausewerk has been able to say where its library
// has no approved wording since `0002`, and what negotiators keep conceding
// since `0003`. It could do nothing about either. ADR-0010 permitted a model to
// draft CANDIDATE wording — narrowly, with the whole provenance chain recorded
// — and `0008`/`0029` built the record for it. Nothing ever created one.
//
// ── THE ONE SENTENCE THIS PANE MUST NOT LET ANYBODY MISUNDERSTAND ─────────
//
// A draft is not a clause. It has no ID in the clause namespace, no selectable
// view reads it, and it reaches a contract only if a named lawyer verifies the
// ticket it opened — the same gate vendor language goes through. Every heading
// here says "candidate" or "proposal" for that reason, and the receipt after a
// draft says where it went rather than congratulating anybody.
//
// ── WHAT IS DERIVED, AND WHY IT MATTERS HERE ──────────────────────────────
//
// The drafting control is drawn from the COMPANY'S OWN SETTING, read from
// `GET /settings` — not from a guess, not from a role. When drafting is off,
// this pane says so in the words of what is off and who turns it on, and the
// control is not drawn at all. A screen offering a button that the schema
// refuses teaches people a rule the system does not have.
//
// The database refuses regardless: `cw.assert_library_drafting_enabled()` is
// the fence, and this is an affordance.
//
// ── WHAT THE MODEL WAS SHOWN IS DRAWN AS A COUNT, NOT AS A PROMISE ────────
//
// The endpoint answers `material`, which is how many rows of each kind went
// into the prompt. A draft made from empty lists is visible as one, and
// ADR-0010's "never from a blank prompt" becomes something a reader can check
// instead of something this comment claims.

const { useState: useBuilderState, useMemo: useBuilderMemo } = React;

// The switch `0102` writes. Named once here rather than at the three sites
// below, because a settings key spelled out by hand drifts.
const DRAFTING_SETTING = 'ai_library_drafting';

// `cw.setting_is_on()`'s truthiness rule, in the browser. THE SAME WORDS AS THE
// SCHEMA, deliberately: anything else reads as off, and a screen that treated
// "yes" as on where the database treats it as off would tell somebody drafting
// is available and then be refused by the database on their behalf.
function settingIsOn(rows, key) {
  const row = (rows || []).find((r) => r.key === key);
  if (!row) return false;
  return ['true', 'on', 'yes', '1'].includes(String(row.value).trim().toLowerCase());
}

// ── The disclosure, before anybody asks for anything ──────────────────────
// The intake screen's rule (NC-14), and this is the sharper case: there, a
// model READS what you wrote. Here it WRITES what a lawyer may approve into the
// company's own contract language.
function WhatDraftingIs() {
  return (
    <div className="panel-2 p-3 mt-3" data-testid="builder-disclosure">
      <div className="tag">what this does, and what it cannot do</div>
      <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
        An AI model drafts candidate wording from your own record — the risk
        category, the wording already approved in it, your fallback ladder, the
        conflict rules at this severity, and what your negotiators have actually
        conceded. It is shown how often you conceded and how far; it is never
        shown the counterparty’s own words, which stay quarantined. What comes
        back is a <strong>proposal</strong>: it is filed as a review ticket
        marked AI CANDIDATE and it changes nothing until a named lawyer approves
        it. Whether that lawyer changed a word is measured and recorded.
      </div>
    </div>
  );
}

// ── Drafting switched off, said as what it is ─────────────────────────────
// Not an error and not an empty state. The company has decided something, the
// sentence says what, and it names who can change it — because the person
// reading this is usually not that person.
function DraftingIsOff() {
  return (
    <div className="panel-2 p-3 mt-4" data-testid="builder-off">
      <div className="flex items-center gap-2">
        <div className="tag">drafting is switched off</div>
        <Status state="never">off for this company</Status>
      </div>
      <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
        This company has not switched on AI drafting of candidate library
        wording. Nothing below can be drafted until an Administrator turns on{' '}
        <span className="font-mono">{DRAFTING_SETTING}</span> in settings. The
        record of anything drafted before is still shown below: turning the
        switch off stops new drafts and changes nothing about wording already
        approved, which keeps its recorded origin permanently.
      </div>
    </div>
  );
}

// ── One draft asked for ───────────────────────────────────────────────────
//
// THE TWO WRITTEN FIELDS ARE NOT DECORATION. `0029` records them as owner
// rulings F2 and F3 — what this draft is for, and what is known to be
// unreliable about it AT THE TIME — and both are frozen the moment the draft
// exists. The column default reads "NOT RECORDED", which is what a draft made
// without them says for the rest of its life, so the button is disabled until
// somebody has written both.
function AskForADraft({ categories, start, onDrafted }) {
  const [kind, setKind] = useBuilderState('clause');
  const [category, setCategory] = useBuilderState(start.category_key || '');
  const [severity, setSeverity] = useBuilderState(start.severity || 'Standard');
  const [purpose, setPurpose] = useBuilderState('');
  const [limits, setLimits] = useBuilderState('');
  const [refused, setRefused] = useBuilderState(null);

  const ready = category && purpose.trim() && limits.trim();

  return (
    <div className="panel p-4 mt-4" data-testid="builder-form">
      <PanelHead
        title="Ask for candidate wording"
        sub="Grounded in your own record, and filed where a lawyer decides on it." />

      <div className="grid gap-3 mt-3" style={{ gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))' }}>
        <label className="block">
          <span className="tag">what kind of candidate</span>
          <select className="mt-1 w-full" style={{ padding: '6px 8px' }} data-testid="builder-kind"
                  value={kind} onChange={(e) => setKind(e.target.value)}>
            <option value="clause">a clause</option>
            <option value="rung">a rung on the fallback ladder</option>
            <option value="rule">a conflict rule</option>
          </select>
        </label>

        <label className="block">
          <span className="tag">risk category</span>
          <select className="mt-1 w-full" style={{ padding: '6px 8px' }} data-testid="builder-category"
                  value={category} onChange={(e) => setCategory(e.target.value)}>
            <option value="">choose a category…</option>
            {categories.map((c) => (
              <option key={c.key} value={c.key}>{c.label}</option>
            ))}
          </select>
        </label>

        <label className="block">
          <span className="tag">severity</span>
          <select className="mt-1 w-full" style={{ padding: '6px 8px' }} data-testid="builder-severity"
                  value={severity} onChange={(e) => setSeverity(e.target.value)}>
            <option value="Standard">Standard</option>
            <option value="High">High</option>
          </select>
        </label>
      </div>

      <label className="block mt-3">
        <span className="tag">what this draft is for</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={2} data-testid="builder-purpose"
                  value={purpose} onChange={(e) => setPurpose(e.target.value)}
                  placeholder="Why you are asking for this wording." />
      </label>

      <label className="block mt-3">
        <span className="tag">what is known to be unreliable about it today</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={2} data-testid="builder-limits"
                  value={limits} onChange={(e) => setLimits(e.target.value)}
                  placeholder="What a reader of this draft in two years should be warned about." />
      </label>
      <div className="caption mt-1">
        Both are written onto the draft and can never be changed afterwards —
        they are the record of what was known when it was made.
      </div>

      {refused && (
        <div className="panel-2 p-3 mt-3" data-testid="builder-refused">
          <div className="tag">refused</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            “{refused}”
          </div>
        </div>
      )}

      <div className="flex items-center gap-3 mt-4 flex-wrap">
        <ActButton className="btn btn-primary" data-testid="builder-draft"
                   disabled={!ready}
                   onClick={async () => {
                     setRefused(null);
                     const r = await API.draftLibraryCandidate({
                       kind,
                       category_key: category,
                       severity,
                       intended_purpose: purpose.trim(),
                       known_limitations: limits.trim(),
                     });
                     if (!r.ok) { setRefused(r.reason); return; }
                     // CLEARED ONLY ON A DRAFT. An absence leaves the words in
                     // the boxes: the person will press again when the budget
                     // resets or the key is installed, and making them retype
                     // what they wrote would be a punishment for the provider
                     // being down.
                     if (r.body && r.body.outcome === 'drafted') {
                       setPurpose(''); setLimits('');
                     }
                     onDrafted(r.body);
                   }}>
          ask for a draft
        </ActButton>
        <span className="caption">
          {ready
            ? <>This asks a model and records the call, whether or not anything
                comes back. Anything drafted is filed as a review ticket for
                Legal — it does not change the library.</>
            : <>Choose a category, and write what this is for and what is
                unreliable about it.</>}
        </span>
      </div>
    </div>
  );
}

// ── What came back ────────────────────────────────────────────────────────
//
// TWO OUTCOMES, DRAWN DIFFERENTLY AND NEITHER AS A FAILURE. 'drafted' shows the
// words and where they went; 'absent' shows the reason in the endpoint's own
// sentence. A spent budget, no key and a provider on fire all arrive here, and
// none of them is a bug the person reading can do anything about except later.
function DraftReceipt({ result, onAnother }) {
  if (!result) return null;
  const drafted = result.outcome === 'drafted';
  const material = result.material || {};
  const shown = Object.entries(material).filter(([, n]) => n > 0);

  return (
    <div className="panel p-4 mt-4" data-testid="builder-receipt">
      <PanelHead
        title={drafted ? 'A candidate, filed for review' : 'No wording was drafted'}
        sub={drafted
          ? 'A proposal with a ticket number. It is not library language and cannot be used until Legal approves it.'
          : 'The request was honoured and the record says what happened. Nothing was invented in place of an answer.'} />

      <div className="flex items-center gap-2 flex-wrap">
        <Status state={drafted ? 'pending' : 'never'}>
          {drafted ? 'awaiting a lawyer' : 'no draft'}
        </Status>
        <span className="chip chip-std">{result.category_label || result.category_key}</span>
        <span className="chip chip-std">{result.severity}</span>
        <span className="caption font-mono">
          {result.model}{result.model_version ? ` · ${result.model_version}` : ''}
        </span>
      </div>

      {!drafted && (
        <div className="panel-2 p-3 mt-3" data-testid="builder-absent">
          <div className="tag">why there is nothing</div>
          <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
            “{result.absent_reason}”
          </div>
          <div className="caption mt-2">
            {result.budget
              ? <>Today: {result.budget.calls_today} of {result.budget.calls_allowed} model
                  calls used.</>
              : null}
          </div>
        </div>
      )}

      {drafted && (
        <>
          <div className="tile-strip mt-3">
            <StatBox label="ticket" n={result.ticket_id} />
            <StatBox label="draft" n={result.draft_id} />
            <StatBox label="characters" n={(result.text || '').length} />
          </div>

          <div className="panel-2 p-3 mt-3" data-testid="builder-text">
            <div className="tag">the model’s words, as written</div>
            <div className="mt-2" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>
              {result.text}
            </div>
          </div>

          {result.basis && (
            <div className="caption mt-2" data-testid="builder-basis">
              What it says it followed: “{result.basis}”
            </div>
          )}

          <div className="caption mt-3">
            Filed as ticket <span className="font-mono">{result.ticket_id}</span>,
            marked AI CANDIDATE, on the review desk. It expires
            on <span className="font-mono">{String(result.expires_on || '').slice(0, 10)}</span> if
            nobody acts on it — an unactioned proposal goes stale rather than
            sitting there as a decision nobody made.
          </div>
        </>
      )}

      {/* WHAT IT WAS DRAFTED FROM, COUNTED. Drawn for both outcomes: a draft
          made from nothing and an absence caused by there being nothing to
          draft from are the same fact seen from two sides. */}
      <div className="caption mt-3" data-testid="builder-material">
        {shown.length
          ? <>Drafted from: {shown.map(([name, n]) => `${n} ${name.replace(/_/g, ' ')}`).join(' · ')}.</>
          : <>Nothing in your record was found to draft from for this category.</>}
      </div>

      <div className="flex items-center gap-3 mt-4">
        <button type="button" className="btn" data-testid="builder-another"
                onClick={onAnother}>ask for another</button>
      </div>
    </div>
  );
}

// ── Where the library is thin, and what negotiation is telling us ─────────
//
// THE TWO GROUNDS ADR-0010 NAMES, side by side, each one able to start a draft
// with the category and severity already chosen. Neither list is new — the
// coverage gap has had a screen since 2026-08-23 and the proposals since 0081 —
// and what is new is that a row can now lead somewhere.
function WhereToDraft({ gaps, proposals, canDraft, onStart }) {
  return (
    <div className="panel p-4 mt-4" data-testid="builder-grounds">
      <PanelHead
        title="What is worth drafting"
        sub="Where you have no approved wording, and what your own negotiations keep saying." />

      <div className="section-label mt-2">Where the library is thin</div>
      {gaps.length === 0 ? (
        <div className="caption mt-1">
          Every category has approved wording at both severities. Nothing is
          missing to draft for.
        </div>
      ) : (
        <div className="mt-2" data-testid="builder-gaps">
          {gaps.slice(0, 40).map((g) => (
            <div key={`${g.category_key}-${g.severity}`}
                 className="flex items-center justify-between gap-3 py-1.5 flex-wrap"
                 style={{ borderBottom: '1px solid var(--line-2)' }}>
              <div className="flex items-center gap-2 flex-wrap">
                <span>{g.label}</span>
                <span className="chip chip-std">{g.severity}</span>
                <Status state="never">no approved wording</Status>
              </div>
              {canDraft && (
                <button type="button" className="btn btn-sm"
                        data-testid={`builder-from-gap-${g.category_key}-${g.severity}`}
                        onClick={() => onStart({ category_key: g.category_key,
                                                 severity: g.severity })}>
                  draft a candidate
                </button>
              )}
            </div>
          ))}
          {gaps.length > 40 && (
            <div className="caption mt-2">
              {gaps.length} gaps in all; the first 40 are listed.
            </div>
          )}
        </div>
      )}

      <div className="section-label mt-4">What negotiation is telling us</div>
      {proposals.length === 0 ? (
        <div className="caption mt-1">
          No pattern in the concession record is strong enough to suggest a
          change of wording yet.
        </div>
      ) : (
        <div className="mt-2" data-testid="builder-proposals">
          {proposals.map((p) => (
            <div key={`${p.category_key}-${p.standard_clause_id}`}
                 className="flex items-center justify-between gap-3 py-1.5 flex-wrap"
                 style={{ borderBottom: '1px solid var(--line-2)' }}>
              <div style={{ minWidth: 0 }}>
                <div className="font-mono text-[12px]" style={{ color: 'var(--mute)' }}>
                  {p.standard_clause_id} · {p.category_key}
                </div>
                <div className="text-[13px]">{p.proposal}</div>
              </div>
              {canDraft && (
                <button type="button" className="btn btn-sm"
                        data-testid={`builder-from-proposal-${p.standard_clause_id}`}
                        onClick={() => onStart({ category_key: p.category_key,
                                                 severity: 'Standard' })}>
                  draft a candidate
                </button>
              )}
            </div>
          ))}
          <div className="caption mt-2">
            A suggestion counts every concession on the record, including ones
            nobody approved and ones that were withdrawn. Open a proposal on the
            ladders screen to see which.
          </div>
        </div>
      )}
    </div>
  );
}

// ── Every draft, and what became of it ────────────────────────────────────
//
// ADR-0010's guardrail, and the reason this list exists at all: "approving
// unedited is recorded distinctly from approving an edited draft … it must be
// visible to Legal leadership, not buried."
//
// A ROW WITH NO TICKET IS DRAWN, NOT DROPPED. That is a draft nobody filed, and
// hiding it would hide exactly the ones that were abandoned.
//
// NO RATE IS COMPUTED HERE. `cw.edit_quality` owns that figure; two screens
// computing one number over two row sets is how the figures in this system have
// been wrong before.
function DraftRegister({ drafts }) {
  const [open, setOpen] = useBuilderState(null);
  if (!drafts.length) {
    return (
      <div className="panel p-4 mt-4" data-testid="builder-register">
        <PanelHead title="Drafts" sub="Nothing has been drafted." />
        <div className="caption">
          No candidate wording has ever been drafted here. When something is,
          this list says what became of it — approved as written, edited first,
          rejected, or left until it expired.
        </div>
      </div>
    );
  }

  return (
    <div className="panel p-4 mt-4" data-testid="builder-register">
      <PanelHead
        title="Drafts, and what became of them"
        sub="What the machine proposed, and what a lawyer did about it." />
      {drafts.map((d) => {
        const isOpen = open === d.draft_id;
        const unchanged = d.state === 'verified' && d.edited_before_approval === false;
        return (
          <div key={d.draft_id} className="py-2"
               style={{ borderBottom: '1px solid var(--line-2)' }}
               data-testid={`builder-draft-${d.draft_id}`}>
            <div className="flex items-center justify-between gap-3 flex-wrap">
              <div className="flex items-center gap-2 flex-wrap" style={{ minWidth: 0 }}>
                <span className="font-mono text-[12px]" style={{ color: 'var(--mute)' }}>
                  #{d.draft_id}
                </span>
                <span className="chip chip-std">{d.kind}</span>
                {d.category_key && <span className="chip chip-std">{d.category_key}</span>}
                {/* THE FOUR ANSWERS, AND THEY ARE NOT THE SAME NEWS. */}
                {!d.ticket_id
                  ? <Status state="never">never filed</Status>
                  : d.state === 'verified'
                    ? <Status state="effective">approved</Status>
                    : d.state === 'rejected'
                      ? <Status state="refused">rejected</Status>
                      : d.expired
                        ? <Status state="superseded">expired unactioned</Status>
                        : <Status state="pending">awaiting a lawyer</Status>}
                {unchanged && (
                  <Status state="refused"
                          title="The lawyer approved the model's words without changing any of them.">
                    approved unchanged
                  </Status>
                )}
              </div>
              <button type="button" className="btn btn-sm"
                      data-testid={`builder-open-${d.draft_id}`}
                      onClick={() => setOpen(isOpen ? null : d.draft_id)}>
                {isOpen ? 'close' : 'what it was shown'}
              </button>
            </div>

            <div className="caption mt-1">
              {d.created_by} · {String(d.created_at || '').slice(0, 10)} ·{' '}
              {d.draft_chars} characters
              {d.decided_by && <> · decided by {d.decided_by}</>}
              {d.edit_similarity != null && (
                <> · the approved words are {Number(d.edit_similarity).toFixed(2)} of
                   the way to the model’s own, where 1.00 is unchanged</>
              )}
            </div>

            {isOpen && (
              <div className="panel-2 p-3 mt-2" data-testid={`builder-provenance-${d.draft_id}`}>
                <div className="tag">what the lawyer was shown</div>
                <div className="text-[12.5px] mt-1.5" style={{ color: 'var(--mute)', lineHeight: 1.7 }}>
                  <div><strong>Model:</strong> <span className="font-mono">{d.model}</span>
                       {' · '}<span className="font-mono">{d.model_version}</span></div>
                  <div className="mt-1"><strong>What it was for:</strong> {d.intended_purpose}</div>
                  <div className="mt-1"><strong>Known limitations at the time:</strong> {d.known_limitations}</div>
                  <div className="mt-1"><strong>Material it was given:</strong>{' '}
                    {(d.inputs || []).length
                      ? (d.inputs || []).map((i) => `${i.name} (${i.characters} chars)`).join(' · ')
                      : 'nothing was recorded'}
                  </div>
                </div>
                <div className="mt-2">
                  <div className="tag">the prompt, verbatim</div>
                  <div className="text-[12px] mt-1 font-mono"
                       style={{ whiteSpace: 'pre-wrap', color: 'var(--mute)' }}>
                    {d.prompt}
                  </div>
                </div>
                <div className="mt-2">
                  <div className="tag">the model’s words</div>
                  <div className="text-[13px] mt-1" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>
                    {d.draft_text}
                  </div>
                </div>
                {d.approved_text && d.approved_text !== d.draft_text && (
                  <div className="mt-2">
                    <div className="tag">what the lawyer approved instead</div>
                    <div className="text-[13px] mt-1" style={{ whiteSpace: 'pre-wrap', lineHeight: 1.7 }}>
                      {d.approved_text}
                    </div>
                  </div>
                )}
                {d.minted_clause_id && (
                  <div className="caption mt-2">
                    Approved as <span className="font-mono">{d.minted_clause_id}</span> version{' '}
                    <span className="font-mono">{d.minted_version}</span>. Its origin is recorded
                    as AI-drafted permanently, and every contract built from it can be found by it.
                  </div>
                )}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}


// ══════════════════════════════════════════════════════════════════════════
// A PROPOSAL THAT IS NOT A CLAUSE (0107)
// ══════════════════════════════════════════════════════════════════════════
//
// WHAT THIS PART OF THE SCREEN IS FOR. The Builder can ask for three things:
// wording, a fallback POSITION on a ladder, and a validation RULE. Only the
// first is decided by reading words, and until `0107` the review desk offered
// exactly one act — verify, which mints a clause version — for all three. A
// proposed rule is a machine-readable predicate, and verifying it published
// that predicate as selectable contract language while creating no rule at
// all. A proposed rung was minted and the ladder never moved.
//
// So these two do not belong on the clause desk, and this is where they go.
// What a lawyer decides here is not the words:
//
//   · FOR A RUNG — WHERE it sits. The ladder is drawn as it stands, the
//     proposal is drawn at the position chosen, and the floor is named,
//     because the floor is the last position the company will accept and
//     everything below it escalates.
//   · FOR A RULE — WHETHER THE PREDICATE IS EVEN LEGAL, said in sentences
//     before anything is committed. The grammar has three primitives and no
//     logic; a model asked for one can return prose. The verdict comes down
//     the view computed rather than stored, so it can never be stale.
//
// ── WHAT IS PUBLISHED IS WHAT THE LAWYER CONFIRMS ─────────────────────────
//
// The predicate box below is EDITABLE and starts at the model's answer. That
// is the point rather than a convenience: the model's own bytes stay frozen on
// the draft as the baseline the edit-quality figure is measured against, so a
// lawyer who corrects the JSON shows up in the record as having corrected it,
// which is true and is what the Builder is measured by.
//
// ── EVERY REFUSAL IS SHOWN IN THE DATABASE'S OWN WORDS ────────────────────
//
// Nothing here re-implements a rule. A reviewer pressing place is refused by
// the function with a sentence saying it is an admin's act; an illegal
// predicate is refused naming every problem. Both are drawn as they arrive.

const STRUCTURAL_KINDS = {
  rung: {
    label: 'a fallback position',
    what: 'wording AND a place on the ladder',
  },
  rule: {
    label: 'a validation rule',
    what: 'a predicate in the three-primitive grammar, not language',
  },
};

// A ladder drawn as it stands, with the proposal slotted in where the lawyer
// has put it. THE FLOOR IS NAMED at whichever row holds it after the
// insertion, because that is the number that changes underneath somebody.
function LadderPreview({ proposal, rung, isFloor }) {
  const have = proposal.target_rungs || 0;
  const floorNow = proposal.target_floor_rung;
  const at = Math.max(0, Math.min(Number(rung) || 0, have));
  const rows = [];
  for (let i = 0; i < have + 1; i += 1) {
    if (i === at) {
      rows.push({ n: at, mine: true });
    }
    if (i < have) {
      // Where each existing rung ends up: everything at or below the
      // insertion point moves down one.
      rows.push({ n: i < at ? i : i + 1, existing: i, mine: false });
    }
  }
  const floorAfter = isFloor ? at
    : (floorNow === null || floorNow === undefined) ? null
    : (floorNow >= at ? floorNow + 1 : floorNow);

  return (
    <div className="panel-2 p-3 mt-3" data-testid="structural-ladder">
      <div className="tag">
        {have === 0
          ? 'this category has no live ladder — this would be its first rung'
          : `the ladder as it would stand · ${have + 1} rungs`}
      </div>
      <div className="mt-2" style={{ display: 'grid', gap: 4 }}>
        {rows.map((r) => (
          <div key={`${r.mine ? 'new' : r.existing}`}
               className="text-[12.5px]"
               style={{
                 display: 'flex', gap: 10, alignItems: 'baseline',
                 padding: '4px 8px', borderRadius: 4,
                 background: r.mine ? 'var(--panel-2)' : 'transparent',
                 border: r.mine ? '1px solid var(--line)' : '1px solid transparent',
               }}>
            <span className="tag" style={{ minWidth: 52 }}>rung {r.n}</span>
            <span style={{ color: r.mine ? 'var(--ink)' : 'var(--mute)', flex: 1 }}>
              {r.mine ? 'THIS PROPOSAL' : 'the wording already at this position'}
            </span>
            {floorAfter === r.n && <span className="tag">floor</span>}
          </div>
        ))}
      </div>
      <div className="caption mt-2">
        {floorAfter === null
          ? <>This ladder has no floor, so the rung being placed has to be it.
              The floor is the last position the company will accept; below it,
              escalation is mandatory.</>
          : <>Placing a rung republishes the ladder — the live one retires and
              stays readable, because past concessions record the rung number
              they went to and renumbering underneath them would rewrite
              history.</>}
      </div>
    </div>
  );
}

// The grammar's verdict, in the database's own sentences. NOT a summary of
// them: each problem is one thing that is wrong, and a reader fixing them one
// at a time needs all of them rather than the first.
function GrammarVerdict({ proposal }) {
  const problems = proposal.grammar_problems || [];
  const ok = proposal.grammar_ok === true;
  return (
    <div className="panel-2 p-3 mt-3" data-testid="structural-grammar">
      <div className="tag">
        {ok ? 'the model’s predicate is legal in the grammar'
            : `the model’s predicate is not usable as it stands · ${problems.length} ${problems.length === 1 ? 'problem' : 'problems'}`}
      </div>
      {!ok && (
        <ul className="mt-2" style={{ display: 'grid', gap: 6 }}>
          {problems.map((p, i) => (
            <li key={i} className="text-[12.5px]"
                style={{ color: 'var(--mute)', lineHeight: 1.7 }}>· {p}</li>
          ))}
        </ul>
      )}
      <div className="caption mt-2">
        The grammar has exactly three primitives — <code>all_present</code>,
        {' '}<code>none_present</code> and <code>conflicting_values</code> — and
        no logic of its own. Anything counsel cannot say with those needs a new
        primitive, added deliberately. What you publish is what is in the box
        below, not what the model wrote.
      </div>
    </div>
  );
}

// ── Placing a rung ────────────────────────────────────────────────────────
function PlaceRung({ proposal, onDone }) {
  const [body, setBody] = useBuilderState(proposal.proposed_text || '');
  const [clauseId, setClauseId] = useBuilderState('');
  const [title, setTitle] = useBuilderState('');
  const [rationale, setRationale] = useBuilderState('');
  const [rung, setRung] = useBuilderState(String(proposal.target_rungs || 0));
  const [isFloor, setIsFloor] = useBuilderState((proposal.target_rungs || 0) === 0);
  const [reason, setReason] = useBuilderState('');
  const [refused, setRefused] = useBuilderState(null);

  const ready = clauseId.trim() && title.trim() && rationale.trim()
    && body.trim() && reason.trim().length >= 5;

  return (
    <div className="mt-3">
      <label className="block mt-2">
        <span className="tag">the wording being approved — edit it if you must</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={4}
                  data-testid="structural-rung-body"
                  value={body} onChange={(e) => setBody(e.target.value)} />
      </label>
      <div className="caption mt-1">
        The model’s own words stay on the draft whatever you do here. What you
        change is measured against them, which is how the Builder is judged.
      </div>

      <div className="flex gap-3 mt-3 flex-wrap">
        <label className="block" style={{ flex: '1 1 180px' }}>
          <span className="tag">the clause id it becomes</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }}
                 data-testid="structural-rung-clause"
                 value={clauseId} onChange={(e) => setClauseId(e.target.value)} />
        </label>
        <label className="block" style={{ flex: '1 1 180px' }}>
          <span className="tag">its title</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }}
                 data-testid="structural-rung-title"
                 value={title} onChange={(e) => setTitle(e.target.value)} />
        </label>
        <label className="block" style={{ flex: '0 0 110px' }}>
          <span className="tag">at rung</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }} type="number"
                 min={0} max={proposal.target_rungs || 0}
                 data-testid="structural-rung-position"
                 value={rung} onChange={(e) => setRung(e.target.value)} />
        </label>
      </div>

      <label className="block mt-3">
        <span className="tag">why this is our position</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={2}
                  data-testid="structural-rung-rationale"
                  value={rationale} onChange={(e) => setRationale(e.target.value)} />
      </label>

      <label className="block mt-3">
        <span className="tag">why the ladder is being republished</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={2}
                  data-testid="structural-rung-reason"
                  value={reason} onChange={(e) => setReason(e.target.value)}
                  placeholder="Recorded on the audit chain against both ladders." />
      </label>

      <label className="flex items-center gap-2 mt-3 text-[12.5px]"
             style={{ color: 'var(--mute)' }}>
        <input type="checkbox" checked={isFloor} data-testid="structural-rung-floor"
               onChange={(e) => setIsFloor(e.target.checked)} />
        this position is the floor — the last one we will accept
      </label>

      <LadderPreview proposal={proposal} rung={rung} isFloor={isFloor} />

      {refused && (
        <div className="panel-2 p-3 mt-3" data-testid="structural-refused">
          <div className="tag">refused</div>
          <div className="text-[12.5px] mt-1.5"
               style={{ color: 'var(--mute)', lineHeight: 1.7 }}>“{refused}”</div>
        </div>
      )}

      <div className="flex items-center gap-3 mt-4 flex-wrap">
        <ActButton className="btn btn-primary" data-testid="structural-place"
                   disabled={!ready}
                   onClick={async () => {
                     setRefused(null);
                     const r = await API.placeDraftedRung({
                       ticket_id: proposal.ticket_id,
                       approved_text: body,
                       new_clause_id: clauseId.trim(),
                       title: title.trim(),
                       rationale: rationale.trim(),
                       rung: Number(rung),
                       // A STRING, not a boolean. Every field on this
                       // surface crosses as a plain value —
                       // refuse_structured() refuses a JSON boolean
                       // in one copy for the whole doorway — and the
                       // write casts it. Sending `true` here is
                       // refused at the binding layer with a sentence.
                       is_floor: isFloor ? 'true' : 'false',
                       reason: reason.trim(),
                     });
                     if (!r.ok) { setRefused(r.reason); return; }
                     onDone();
                   }}>
          approve and place on the ladder
        </ActButton>
        <span className="caption">
          {ready
            ? <>One act: the wording is minted with your name on it and the
                ladder is republished with it at rung {Number(rung)}. Either
                both happen or neither does.</>
            : <>Name the clause, its title, why it is our position, and why the
                ladder is being republished.</>}
        </span>
      </div>
    </div>
  );
}

// ── Publishing a rule ─────────────────────────────────────────────────────
function PublishRule({ proposal, onDone }) {
  const asText = proposal.predicate
    ? JSON.stringify(proposal.predicate, null, 2)
    : (proposal.proposed_text || '');
  const [predicate, setPredicate] = useBuilderState(asText);
  const [ruleId, setRuleId] = useBuilderState('');
  const [name, setName] = useBuilderState('');
  const [title, setTitle] = useBuilderState('');
  const [detail, setDetail] = useBuilderState('');
  const [refused, setRefused] = useBuilderState(null);

  // Whether what is IN THE BOX is even JSON. This is an affordance and nothing
  // more — the database checks the grammar itself and refuses in sentences,
  // and this only stops somebody pressing a button that cannot work.
  let parsed = null;
  let parseError = null;
  try { parsed = JSON.parse(predicate); }
  catch (e) { parseError = e.message; }

  const ready = ruleId.trim() && name.trim() && title.trim() && detail.trim()
    && parsed && typeof parsed === 'object' && !Array.isArray(parsed);

  return (
    <div className="mt-3">
      <GrammarVerdict proposal={proposal} />

      <label className="block mt-3">
        <span className="tag">the predicate being published — yours, not the model’s</span>
        <textarea className="mt-1 w-full"
                  style={{ padding: '6px 8px', fontFamily: 'var(--mono, monospace)' }}
                  rows={7} data-testid="structural-rule-predicate"
                  value={predicate} onChange={(e) => setPredicate(e.target.value)} />
      </label>
      {parseError && (
        <div className="caption mt-1" data-testid="structural-rule-parse">
          This is not JSON yet: {parseError}
        </div>
      )}

      <div className="flex gap-3 mt-3 flex-wrap">
        <label className="block" style={{ flex: '0 0 140px' }}>
          <span className="tag">rule id</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }}
                 data-testid="structural-rule-id" placeholder="AB-123"
                 value={ruleId} onChange={(e) => setRuleId(e.target.value)} />
        </label>
        <label className="block" style={{ flex: '1 1 180px' }}>
          <span className="tag">its short name</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }}
                 data-testid="structural-rule-name"
                 value={name} onChange={(e) => setName(e.target.value)} />
        </label>
        <label className="block" style={{ flex: '1 1 220px' }}>
          <span className="tag">what a person sees when it fires</span>
          <input className="mt-1 w-full" style={{ padding: '6px 8px' }}
                 data-testid="structural-rule-title"
                 value={title} onChange={(e) => setTitle(e.target.value)} />
        </label>
      </div>

      <label className="block mt-3">
        <span className="tag">what the finding should say</span>
        <textarea className="mt-1 w-full" style={{ padding: '6px 8px' }} rows={2}
                  data-testid="structural-rule-detail"
                  value={detail} onChange={(e) => setDetail(e.target.value)} />
      </label>

      {refused && (
        <div className="panel-2 p-3 mt-3" data-testid="structural-refused">
          <div className="tag">refused</div>
          <div className="text-[12.5px] mt-1.5"
               style={{ color: 'var(--mute)', lineHeight: 1.7 }}>“{refused}”</div>
        </div>
      )}

      <div className="flex items-center gap-3 mt-4 flex-wrap">
        <ActButton className="btn btn-primary" data-testid="structural-publish"
                   disabled={!ready}
                   onClick={async () => {
                     setRefused(null);
                     const r = await API.publishDraftedRule({
                       ticket_id: proposal.ticket_id,
                       rule_id: ruleId.trim(),
                       name: name.trim(),
                       title: title.trim(),
                       detail: detail.trim(),
                       predicate: parsed,
                     });
                     if (!r.ok) { setRefused(r.reason); return; }
                     onDone();
                   }}>
          approve and publish the rule
        </ActButton>
        <span className="caption">
          {ready
            ? <>This publishes a rule VERSION and mints no contract language.
                A rule change alters which contracts are blocked, so every
                finding it raises will cite this version.</>
            : <>Name the rule, what it says when it fires, and leave a predicate
                that is at least JSON.</>}
        </span>
      </div>
    </div>
  );
}

// ── One waiting proposal ──────────────────────────────────────────────────
function StructuralProposal({ proposal, mayDecide, onDone }) {
  const [open, setOpen] = useBuilderState(false);
  const meta = STRUCTURAL_KINDS[proposal.kind] || { label: proposal.kind, what: '' };
  const decided = proposal.state !== 'pending';

  return (
    <div className="panel p-4 mt-3" data-testid={`structural-${proposal.ticket_id}`}>
      <div className="flex items-baseline gap-3 flex-wrap">
        <span className="tag">AI CANDIDATE · {meta.label}</span>
        <span className="tag">{proposal.category_key} · {proposal.severity}</span>
        {proposal.kind === 'rule' && proposal.grammar_ok === false && (
          <span className="tag" data-testid="structural-badge-illegal">
            not legal in the grammar as drafted
          </span>
        )}
        {decided && <span className="tag">{proposal.state}</span>}
      </div>
      <div className="caption mt-1">
        What is being decided here is {meta.what}. Drafted by {proposal.model}
        {proposal.model_version ? ` (${proposal.model_version})` : ''}, expires{' '}
        {proposal.draft_expires_on}.
      </div>

      <div className="text-[12.5px] mt-2"
           style={{ color: 'var(--mute)', lineHeight: 1.7,
                    whiteSpace: 'pre-wrap', fontFamily: proposal.kind === 'rule'
                      ? 'var(--mono, monospace)' : 'inherit' }}>
        {proposal.proposed_text}
      </div>

      <div className="caption mt-2">
        <strong>What it was for:</strong> {proposal.intended_purpose}
        {' · '}
        <strong>Known to be unreliable:</strong> {proposal.known_limitations}
      </div>

      {!decided && mayDecide && !open && (
        <div className="mt-3">
          <ActButton className="btn" data-testid="structural-open"
                     onClick={() => setOpen(true)}>
            {proposal.kind === 'rung' ? 'decide where this goes' : 'decide this rule'}
          </ActButton>
        </div>
      )}
      {!decided && !mayDecide && (
        <div className="caption mt-3" data-testid="structural-read-only">
          Placing a rung and publishing a rule are a legal admin’s acts — both
          change what every future contract is built and checked against. This
          desk can read the proposal and reject it on the review queue.
        </div>
      )}

      {open && proposal.kind === 'rung' && (
        <PlaceRung proposal={proposal} onDone={() => { setOpen(false); onDone(); }} />
      )}
      {open && proposal.kind === 'rule' && (
        <PublishRule proposal={proposal} onDone={() => { setOpen(false); onDone(); }} />
      )}
    </div>
  );
}

// ── The section ───────────────────────────────────────────────────────────
//
// AN EMPTY QUEUE IS DRAWN, NOT HIDDEN. It says what would appear here, because
// "nothing waiting" and "this screen does not exist" look identical when a
// section disappears, and the second is what people conclude.
function StructuralProposals({ proposals, mayDecide, onDone }) {
  const waiting = (proposals || []).filter((p) => p.state === 'pending');
  const settled = (proposals || []).filter((p) => p.state !== 'pending');

  return (
    <div className="mt-6" data-testid="structural-section">
      <div className="flex items-baseline gap-3 flex-wrap">
        <div className="tag">proposals that are not wording</div>
        {/* NO NUMBER WHERE THERE IS NO SET BEHIND IT — the pane's rule. Both
            counts here reach the list immediately below them. */}
        <span className="tag" data-testid="structural-waiting">
          {waiting.length} waiting
        </span>
      </div>
      <div className="caption mt-1">
        A proposed fallback position, or a proposed validation rule. Neither is
        contract language, and neither is decided by reading it — a rung needs a
        place on the ladder and a rule needs a predicate that is legal. Until
        2026-08-23 approving either filed it as approved wording.
      </div>

      {waiting.length === 0 && (
        <div className="caption mt-3" data-testid="structural-empty">
          Nothing is waiting. Ask the Builder for a fallback position or a
          validation rule above and it will arrive here for a decision.
        </div>
      )}

      {waiting.map((p) => (
        <StructuralProposal key={p.ticket_id} proposal={p}
                            mayDecide={mayDecide} onDone={onDone} />
      ))}

      {settled.length > 0 && (
        <div className="mt-4">
          <div className="tag">already decided · {settled.length}</div>
          {settled.map((p) => (
            <StructuralProposal key={p.ticket_id} proposal={p}
                                mayDecide={false} onDone={onDone} />
          ))}
        </div>
      )}
    </div>
  );
}
// ── The pane ──────────────────────────────────────────────────────────────
//
// THREE ROLES SEE IT, and it is the grant that decides which:
// `cw.library_draft_register` is granted to legal_reviewer, legal_admin and the
// auditor. The AUDITOR reads and drafts nothing — they hold no insert on
// `cw.clause_draft`, so the drafting form is not drawn for them. That is an
// affordance; the database refuses regardless.
function LibraryBuilderPane({ me }) {
  const settings = usePane(() => API.settings());
  const gaps = usePane(() => API.libraryCoverageGaps());
  const proposals = usePane(() => API.libraryProposals());
  const categories = usePane(() => API.categories());
  const drafts = usePane(() => API.libraryDrafts());
  // 0107. The proposals that are NOT wording. Read by the same three roles
  // the register is, because cw.structural_proposal is scoped in
  // cw.review_ticket's own read policy words.
  const structural = usePane(() => API.structuralProposals());

  const [start, setStart] = useBuilderState({});
  const [result, setResult] = useBuilderState(null);

  if (settings.status === 'loading' || drafts.status === 'loading') return <Loading />;
  if (drafts.status === 'failed') return <LoadFailed reason={drafts.reason} />;

  // DERIVED FROM THE COMPANY'S OWN ROW, never assumed. `GET /settings` is
  // readable by every signed-in role (cw.governance_setting read_all), so this
  // is the same answer the schema will give when the act is attempted.
  const draftingOn = settingIsOn(settings.rows, DRAFTING_SETTING);
  // WHO HOLDS THE INSERT (0008): requester, legal_reviewer, legal_admin. The
  // auditor does not, and neither does the administrator or a viewer — neither
  // of whom can reach this pane at all.
  const mayDraft = me.role === 'legal_reviewer' || me.role === 'legal_admin';
  const canDraft = draftingOn && mayDraft;

  return (
    <div>
      <PaneHead
        kicker="the library builder"
        title="Candidate wording"
        sub="A machine drafts; a named lawyer decides. Nothing here changes the library." />

      <WhatDraftingIs />
      {!draftingOn && <DraftingIsOff />}
      {draftingOn && !mayDraft && (
        <div className="caption mt-3" data-testid="builder-read-only">
          Drafting is switched on for this company. Asking for a draft is
          Legal’s act — this desk reads the record of what was drafted and what
          became of it.
        </div>
      )}

      <WhereToDraft
        gaps={gaps.status === 'loaded' ? gaps.rows : []}
        proposals={proposals.status === 'loaded' ? proposals.rows : []}
        canDraft={canDraft}
        onStart={(where) => { setStart(where); setResult(null); }} />

      {canDraft && (
        <AskForADraft
          categories={categories.status === 'loaded' ? categories.rows : []}
          start={start}
          onDrafted={(body) => { setResult(body); drafts.reload(); }} />
      )}

      <DraftReceipt result={result} onAnother={() => setResult(null)} />

      <StructuralProposals
        proposals={structural.status === 'loaded' ? structural.rows : []}
        mayDecide={me.role === 'legal_admin'}
        onDone={() => { structural.reload(); drafts.reload(); }} />

      <DraftRegister drafts={drafts.rows} />
    </div>
  );
}
