lead: one entry per submission, and the finding above the fold

Two things on the lead page.

THE AUDIT TRAIL REPEATED ITSELF. "Qualify Lead" appeared three times in
a row and "Partner Agent Lead" twice, which reads as the AI having done
the same thing three times. It had not. One submission writes several
rows, told apart by execution_state:

  zk-act-qualify   TRIGGER_PERFORMED     the trigger's commit
  zk-act-qualify   TRIGGER_PERFORMED     again, on the settle path
  zk-act-qualify   zk-state-qualified    the one that moved the lead

Only the last means anything to somebody reading the file. They are now
collapsed into one entry carrying the earliest timestamp — when the
operator acted — and whichever state and payload is populated.

Matched on (same activity, within fifteen seconds) rather than on
execution_state, so a genuine repeat survives: Collect Documents really
is performed twice on a lead whose first upload was short, and those are
minutes apart. And matched by looking BACK through recent entries rather
than at the previous one, because the platform interleaves a DATA_UPDATE
row between the two halves of a submission — the rows to merge are near
each other in time but not adjacent in the list.

THE AI PARAGRAPHS HAD NO SUMMARY. attribution_reason and
eligibility_reason run to a paragraph each and opened folded, so the
finding could not be read without expanding. ClampText now lifts the
first sentence out as a headline and puts the rest behind "Show the
reasoning" — no new field, and nothing invented: the employees already
write a conclusion and then its evidence.

It falls back to plain folding when the split would be useless — no
sentence terminator, a first sentence that is the whole paragraph, or
one short enough to be a fragment rather than a finding.

The other half of that is in the charter (70_verdict_first.sql). The
headline is only as good as the sentence, and Intake was opening with
"Tool 29601 confirmed..." — an internal id — because it had been told to
"name the evidence, not the conclusion". Right about content, wrong about
order: it now leads with what it decided and gives the evidence second.
This commit is contained in:
Yashas 2026-09-07 17:42:36 +05:30
parent 4e5df66389
commit 2d9c072572
3 changed files with 119 additions and 6 deletions

View File

@ -57,3 +57,27 @@
.clamp.is-open .clamp__more svg {
transform: rotate(180deg);
}
/* The headline lifted out of an AI paragraph. Slightly heavier than the body
and never clamped it is one sentence by construction, and the point of it
is that the finding can be read without opening anything. */
.clamp__lead {
margin: 0;
font-size: 0.9rem;
line-height: 1.5;
color: var(--zk-ink);
font-weight: 500;
}
.clamp__lead + .clamp__text { margin-top: 8px; }
/* The finding, lifted out of an AI paragraph. Heavier than the body and never
clamped it is one sentence by construction, and the whole point is that it
can be read without opening anything. */
.clamp__lead {
margin: 0;
font-size: 0.9rem;
line-height: 1.5;
font-weight: 500;
color: var(--zk-ink);
}
.clamp__lead + .clamp__text { margin-top: 8px; }

View File

@ -1,15 +1,33 @@
import { useState } from 'react'
import './ClampText.css'
/**
* Splits a block of AI prose into a finding and its working.
*
* The employees write the conclusion first and the evidence after it
* "POSP-77341 is active and empanelled for motor." then four sentences of
* registry detail. So the first sentence already IS the summary, and lifting it
* out gives one without inventing text or asking the model for a second field
* it would have to be trusted to keep in step with the first.
*
* Returns null when the split would be useless: no sentence terminator, a first
* sentence so long it is the paragraph again, or one so short it is a fragment
* rather than a finding. Those fall back to plain folding.
*/
function splitLead(value) {
const m = value.match(/^(.{40,220}?[.!?])(\s+)(\S[\s\S]*)$/)
if (!m) return null
return { lead: m[1].trim(), rest: m[3].trim() }
}
/**
* Prose, folded.
*
* The AI employees write at length a recommendation rationale runs past 1,500
* characters and a lead file that prints all of it is a document, not a
* screen. Anything long opens as a few lines with the rest one click away, so
* the page can be scanned and still holds everything for whoever wants it.
* screen. Anything long shows its finding, with the reasoning one click away.
*
* Whether to fold is decided on length, not on measuring the rendered box: a
* Whether to fold is decided on length, not by measuring the rendered box: a
* ref-and-measure pass would reflow on every resize to answer a question the
* string itself already answers.
*/
@ -19,6 +37,10 @@ export default function ClampText({ text, lines = 3, threshold = 150 }) {
if (value.length <= threshold) return <p className="clamp__text clamp__text--short">{value}</p>
const split = splitLead(value)
// No sensible headline fold the whole thing, as before.
if (!split) {
return (
<div className={'clamp' + (open ? ' is-open' : '')}>
<p className="clamp__text" style={{ '--clamp-lines': lines }}>{value}</p>
@ -32,3 +54,18 @@ export default function ClampText({ text, lines = 3, threshold = 150 }) {
</div>
)
}
return (
<div className={'clamp' + (open ? ' is-open' : '')}>
<p className="clamp__lead">{split.lead}</p>
{open ? <p className="clamp__text clamp__text--short">{split.rest}</p> : null}
<button type="button" className="clamp__more" onClick={() => setOpen((o) => !o)}>
{open ? 'Hide the reasoning' : 'Show the reasoning'}
<svg viewBox="0 0 12 12" aria-hidden="true">
<path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" strokeWidth="1.5"
strokeLinecap="round" strokeLinejoin="round" />
</svg>
</button>
</div>
)
}

View File

@ -84,7 +84,59 @@ export default function Timeline({ instanceId }) {
// Oldest first: a timeline reads forwards.
const ordered = [...rows].sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)))
const items = ordered.map((r, i) => {
// ONE SUBMISSION WRITES SEVERAL ROWS. The platform records each stage of a
// submission separately, told apart by execution_state:
//
// zk-act-qualify TRIGGER_PERFORMED the trigger's commit
// zk-act-qualify TRIGGER_PERFORMED ...again, on the settle path
// zk-act-qualify zk-state-qualified the one that moved the lead
//
// Rendered literally that is "Qualify Lead" three times in a row, which
// reads as the AI having done the same thing three times. Only the row
// carrying a real state means anything to somebody reading the file.
//
// Collapsed on (same activity, within fifteen seconds) rather than on the
// execution_state alone, because a genuine repeat has to survive: Collect
// Documents really is performed twice on a lead whose first upload was
// incomplete, and those are minutes apart, not milliseconds.
//
// The kept row takes the EARLIEST timestamp when the operator acted and
// whichever state and payload is actually populated.
const isStage = (v) => STAGES.some((s) => s.uid === v)
const SAME_SUBMISSION_MS = 15000
const merged = []
for (const r of ordered) {
// Look BACK for a match rather than only at the previous entry: the
// platform interleaves a DATA_UPDATE row between the two halves of one
// submission, so the rows to merge are near each other in time but not
// adjacent in the list.
let at = -1
for (let i = merged.length - 1; i >= 0; i--) {
if (Date.parse(r.created_at) - Date.parse(merged[i].created_at) >= SAME_SUBMISSION_MS) break
if (merged[i].activity_id === r.activity_id) { at = i; break }
}
if (at >= 0) {
const prev = merged[at]
merged[at] = {
...prev,
// The settle row is the one that names the resulting stage.
execution_state: isStage(r.execution_state) ? r.execution_state : prev.execution_state,
// Whichever row actually carries the submission and the AI's working.
data: (r.data && Object.keys(r.data).length) ? r.data : prev.data,
ai_reasoning: r.ai_reasoning || prev.ai_reasoning,
ai_confidence: r.ai_confidence ?? prev.ai_confidence,
user_name: prev.user_name || r.user_name,
user_roles: (prev.user_roles && prev.user_roles.length) ? prev.user_roles : r.user_roles,
created_at: prev.created_at,
}
continue
}
merged.push(r)
}
const items = merged.map((r, i) => {
const roles = r.user_roles || []
const aiRole = roles.find((x) => AI_ROLES[x])
const isSystem = r.activity_id === 'DATA_UPDATE'