Commit Graph

10 Commits

Author SHA1 Message Date
c0059194e4 feat: chain-of-custody trace — every step, attributed and expandable
The AI trail tab listed who did what but nothing about WHAT each step did, and
the agent's contribution was the one step with no visible action behind it.

New InstanceTrace: one chronological list of every completed step on a file,
read from the workflow's own audit trail.

  * Each row names the actor and role, with a bot glyph for the agent and a
    person for a human, plus a header count — "1 by an agent, 3 by people".
  * ELAPSED TIME between steps, and this is the point: humans act seconds
    apart because someone is clicking, and the agent's row shows a minute and
    a half of unattended work. That number is the demo.
  * Expand a row to see exactly what was submitted — labelled, typed, from
    view-service, so a human's remarks and the agent's analysis are inspected
    the same way.
  * Expanding the AGENT's row also shows the three checks it ran before
    writing, each as asked/returned, with the breached one in amber.

Removed the standalone "tools the assessor calls" card: the same information
now sits attached to the step where it happened, which is where a reader looks
for it, and showing it twice was clutter.

Still not shown, still not invented: individual model calls, the SQL each tool
ran, tokens and cost. That is in aiemployee.tbl_ai_trace_events and needs
either an org-scoped token or an rdbms bridge. Tool RESULTS are shown and
attributed; raw payloads are not fabricated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:25:52 +05:30
8a3ec901b6 fix: draft the policy-supported amount, not an invented lending rule
The Approve File draft computed
    min(requested, max_eligible, 25% of the lowest turnover source)
which produced 7,00,000 on a 15,00,000 request.

That 25% rule is nowhere in HDFC-CP-2026.08. It was invented here. Inventing a
lending rule inside a draft credit note is worse than leaving the field empty:
it reads as policy to whoever signs it, and nobody could say where the number
came from. It also contradicted every demo script, which uses 12,00,000 — a
figure with no derivation either.

Clause 7.1 is the only rule that applies: a file may not be offered above the
computed maximum eligible without a documented deviation. So the draft is now
min(requested, computed ceiling) and nothing cleverer.

Whether to cap below that is a JUDGEMENT, which is what the credit manager is
for. The draft remarks now state the fact that bears on it — the lowest of the
three declared turnover figures — and invite him to size against it, without
choosing the number for him.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:11:23 +05:30
e745cc5649 feat: action forms open with a defensible draft, not a blank page
Approve File asked a credit manager to type two paragraphs and his own name,
on a screen that already displayed every fact he would type. That is not
diligence, it is transcription — and in front of an audience it is two
paragraphs of typing.

Each action form now opens pre-filled from the file, EDITABLE. New `prefill`
prop on ActivityForm, distinct from `seed`: seed locks a field (identifiers you
should not be able to get wrong), prefill only drafts it. The remarks are still
his remarks and the amount is still his amount; what is removed is the blank
page.

src/prefill.ts computes it, and follows two rules.

  1. Draft the WORDING, never the DECISION. The suggested amount is the largest
     figure the policy can defend, not a recommendation: where three turnover
     figures disagree it sizes the facility against the LOWEST of them, so the
     unanswered question stops mattering — which is also the sentence worth
     saying out loud about this file. Declines open EMPTY, because a
     pre-written justification for refusing someone credit is not a
     convenience, and the reason is the whole record.

  2. Attribution comes from the SIGNED-IN USER, never typed. "Prepared By" and
     "Sanctioned By" are whoever is holding the mouse. Asking a maker to type
     his own name into a four-eyes control is an invitation to type someone
     else's.

Also: a file carrying a deviation is priced 0.75% above the standard rate, so
the offer draft reflects that the file needed judgement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:08:36 +05:30
11da9abed5 feat: AI trail tab — who did what, which tools, what it reasoned
The console showed the agent's OUTPUT but never showed that an agent produced
it. On a screen full of numbers a viewer cannot tell which came from a model,
which from a rule, and which from a person. This makes it visible.

Second tab on the file: "AI trail", beside "Evidence & decision".

  * WHO DID WHAT — the real audit trail from /view/audit, one row per
    completed step, each attributed. "Credit Assessor AI" appears there
    because the agent submitted the activity under its OWN JWT and the
    platform recorded it, not because this component says so. Agent rows get
    a bot glyph and a violet chip; human rows get a person and no chip, plus
    a count: "1 of 6 by an agent".

  * TOOLS IT CALLS — the three deterministic SQL tools, named, each with the
    figures it returns for this file shown against the threshold it was
    tested on. Spread beyond tolerance goes amber.

  * WHAT IT REASONED — the narrative, citations and deviation flag verbatim,
    violet as everywhere else, closing with the explicit statement that it
    computed no ratio and chose no outcome.

Deliberately NOT shown: the per-step trace (individual model calls, raw tool
payloads, durations, tokens, cost). It exists in
aiemployee.tbl_ai_trace_events and is served by
/ai-employee/monitor/instances/:id/trace, but that endpoint requires an
ORG-SCOPED token and the app JWT carries no org claim — it answers 403 "no
organization scope on this account" for every user of this console. A footnote
says so and points at Studio. Rendering a plausible waterfall from guesses
would be the one dishonest thing on a screen whose whole purpose is showing
what actually happened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 19:51:28 +05:30
8e846d6df3 fix: send datetime fields as RFC3339, not the picker's local string
Release Disbursal failed with:

    validation failed for activity hdfc-act-disburse: disbursed_at(type)

An HTML datetime-local input yields "2026-08-17T18:31" — minute precision,
no seconds, no timezone. The workflow validates the field as a timestamp and
rejects that shape. The API smoke test passed because it sent RFC3339
("2026-08-17T11:20:00Z"), so the gap only ever showed through the UI.

Converts local wall-clock to RFC3339 UTC on the way out and back again on the
way in. Storing the ISO string and slicing it for display would have shown the
UTC time in the picker — 13:01 for an 18:31 selection — which reads as the
control losing the input.

An unparseable value is handed back untouched rather than blanked, so a value
the server sent stays visible even if we misread it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:33:35 +05:30
046c4951db fix: resolve roles from the server, and fail open when they are unknown
Every action button vanished on every file. The header read "No role" for a
user who has one.

CAUSE: the JWT carries no roles claim — only user_id / name / email / sub /
exp / iat. Roles live in the login RESPONSE BODY and at GET /usr/me, nowhere
else. The provider restored a session by decoding the token, so any page
refresh produced a user with an empty role list, and actionsFor() filtered
every action away.

Two fixes, and the second matters more than the first:

  * ZinoProvider now fetches /usr/me whenever it has a token but no roles, so
    a restored session recovers the real permission set. A fresh login already
    has them from the login response, so this costs no extra round trip there.

  * actionsFor() FAILS OPEN. With no roles known it returns every action the
    stage allows and lets the server refuse what it must. There is a real
    window where the console cannot know permissions — a restored session
    before /usr/me answers, or that call failing — and filtering on an empty
    list in that window hid every button and made a working app look broken.
    The server's refusal is safe and legible ("user 29533 does not have
    permission for activity ..."), and the form shows it verbatim. Hiding a
    button the user needs is the worse failure, because nothing on screen
    explains it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 18:09:47 +05:30
0b4c5576e4 feat: OCR-driven capture and document upload 2026-08-17 17:27:54 +05:30
602b570064 fix(brand): the logo, the typeface and the palette are HDFC's actual ones
The console was wearing an approximation of the bank. Everything here was
read off HDFC Bank's own production assets instead.

The mark was inside out. It had been drawn as a blue square with a red
square inset; HDFC's is the reverse — a red ground, a white channel cross
splitting it into four corner blocks, and a small blue square at the
centre. Every coordinate now comes verbatim from the SVG the bank serves
in its own site header, including the "HDFC BANK" wordmark outlines, so
the lockup is the bank's rather than a redrawing of it. The favicon in
index.html carried the same inversion and is fixed with the same numbers.

Open Sans replaces the Segoe UI stack. hdfcbank.com preloads OpenSans
Regular / SemiBold / Bold / ExtraBold and sets its whole site in it. It is
self-hosted through @fontsource-variable/open-sans rather than pulled from
a CDN, for the reason the API URL is read at runtime: this build gets
promoted between environments and dropped behind bank proxies, and a font
that sometimes fails to arrive is a product that sometimes looks like
someone else's.

The header stops being a navy band. HDFC puts the full lockup on a light
ground on every one of its own digital surfaces, and a navy header made
the real logo unusable — the lockup's own blue bar would have dissolved
into it. The blue now does its work as the rule under the header, the
active queue and the primary button, which is how the bank uses it. The
lockup's 3px white keyline is the bank's own, so it sits correctly on the
panel in dark mode too and no reversed variant is needed.

Palette, grounded rather than invented: the page canvas is #F0F6FB, the
tint hdfcbank.com uses behind content; the good/ tokens sit on the hue of
the bank's #00B947 instead of a generic emerald, darkened to #00713D
because #00B947 is 2.6:1 on white and unreadable as a label. The two brand
hexes were already right and are unchanged.

Also closes three AA failures the palette claimed it did not have —
--color-faint in light, and --color-faint / --color-brand in dark, each
below 4.5:1 on --color-panel3. White on --color-signal (4.31:1) is left
alone knowingly: darkening HDFC red for the Decline button would have made
it a different red from the logo beside it.
2026-08-17 16:55:50 +05:30
8949a1b0d7 chore: trigger first frontgen build 2026-08-17 15:59:41 +05:30
2e8cc580ac feat: HDFC Loan Desk operator console
Custom frontend for the HDFC loan-origination demo (dev, org 83 / app 524,
workflow hdfc_wf_loan). MSME and personal loan files: agents assemble the
evidence, rules compute capacity, credit decides.

Stack and platform contract follow the Flight-Disruption-Management console,
which runs against this same cluster:

  * API client carries its predecessor's hard-won notes — instance_id goes
    over the wire as a NUMBER (a quoted id fails the int64 decode), org_id as
    a STRING, record views answer under `data` OR `records`, and audit rows
    need the TRIGGER_ prefix filtered or every activity appears three times.
  * VITE_ZINO_API_URL is read at RUNTIME from the config.js the server writes
    at placement, never compiled in, so one artifact is promoted between
    environments unchanged. Missing config fails loudly.
  * base: './' plus a router basename from <base href>, so one build serves
    any mount path.
  * Forms are read from the LIVE activity schema — no field definitions in
    this repo. Add a field in Studio, redeploy, it appears.

Written for this app:

  * EvidencePanel, the centrepiece. Three independent income sources side by
    side with the widest pair marked; every computed ratio shown against the
    threshold it was tested on; and a visible line between what a rule
    computed and what a model wrote (rules are never violet).
  * Queues are the one record view filtered server-side on
    current_state_name — the sidebar is the pipeline. Income variance is
    surfaced in the list, not only on the file.
  * Application 360 with the evidence panel above the offer and the sanction,
    so the screen reads in the order the decision was made.
  * HDFC palette where red is never decoration: the logo block and declines
    only. The referred queue's amber is the only amber in the pipeline.

Known gaps, documented in README rather than hidden: OCR uploads render as a
visible pending row instead of a control that pretends to work, and Credit
Assessment is still performed by a human pending the agent wiring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 15:42:55 +05:30