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 { .clamp.is-open .clamp__more svg {
transform: rotate(180deg); 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 { useState } from 'react'
import './ClampText.css' 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. * Prose, folded.
* *
* The AI employees write at length — a recommendation rationale runs past 1,500 * 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 * 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 * screen. Anything long shows its finding, with the reasoning one click away.
* the page can be scanned and still holds everything for whoever wants it.
* *
* 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 * ref-and-measure pass would reflow on every resize to answer a question the
* string itself already answers. * string itself already answers.
*/ */
@ -19,11 +37,30 @@ export default function ClampText({ text, lines = 3, threshold = 150 }) {
if (value.length <= threshold) return <p className="clamp__text clamp__text--short">{value}</p> 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>
<button type="button" className="clamp__more" onClick={() => setOpen((o) => !o)}>
{open ? 'Show less' : 'Read more'}
<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>
)
}
return ( return (
<div className={'clamp' + (open ? ' is-open' : '')}> <div className={'clamp' + (open ? ' is-open' : '')}>
<p className="clamp__text" style={{ '--clamp-lines': lines }}>{value}</p> <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)}> <button type="button" className="clamp__more" onClick={() => setOpen((o) => !o)}>
{open ? 'Show less' : 'Read more'} {open ? 'Hide the reasoning' : 'Show the reasoning'}
<svg viewBox="0 0 12 12" aria-hidden="true"> <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" <path d="M2.5 4.5 6 8l3.5-3.5" fill="none" stroke="currentColor" strokeWidth="1.5"
strokeLinecap="round" strokeLinejoin="round" /> strokeLinecap="round" strokeLinejoin="round" />

View File

@ -84,7 +84,59 @@ export default function Timeline({ instanceId }) {
// Oldest first: a timeline reads forwards. // Oldest first: a timeline reads forwards.
const ordered = [...rows].sort((a, b) => String(a.created_at).localeCompare(String(b.created_at))) 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 roles = r.user_roles || []
const aiRole = roles.find((x) => AI_ROLES[x]) const aiRole = roles.find((x) => AI_ROLES[x])
const isSystem = r.activity_id === 'DATA_UPDATE' const isSystem = r.activity_id === 'DATA_UPDATE'