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 demotes real to mocked. 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 real value’s ref opens 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.