Value origins
Every generated value says where it came from: real data, something the AI wrote, a gap, or a placeholder. A producer that can't ground a value can't call it real.
A generated dashboard looks equally confident whether its numbers came from the customer’s data or the model made them up. People can’t tell, and after one wrong number they stop trusting all of it.
So every value carries its origin, and the UI shows it.
const OriginZ = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("real"), // from the account's data
source: z.object({ type: z.string(), ref: z.string(), asOf: z.string().optional() }) }),
z.object({ kind: z.literal("authored") }), // the AI wrote it: prose, a choice
z.object({ kind: z.literal("gap"), // the account doesn't have this data
gap: z.object({ reason: z.string(), wouldNeed: z.string() }) }),
z.object({ kind: z.literal("mocked"), needs: z.string() }), // placeholder until the platform can
]);
type Field<T> = { value: T; origin: Origin };
"Open orders" 1,284 real ← query: orders where status = open · as of 9:14
"Summary" "Most…" authored
"Delivery time" — gap ← would need: a shipping integration
"Trend" ~~~ mocked ← needs: historical snapshots
Rules
- Only grounded producers emit
real. A producer with no data tools demotesrealtomocked. It can’t claim data it never read. - A gap is an answer. “You’d need X to see this” is more useful than a confident zero, and it points at the next step.
- Sources are clickable. A
realvalue’srefopens the query or report behind it.
Why it works
Trust becomes something the user can check value by value instead of all or nothing. And gaps stop being hidden failures: they’re a list of what the product would need to answer better.