Governed Reports and Resource Composition
Decide what an App must publish for reusable reporting, and how Core keeps Reports, Dashboards, and Agent queries under one authorization path.
Governed Reports and Resource Composition
An App does not build a report engine to make its Resource data reusable. It publishes a well-described, actor-authorized Resource. Core then combines only the Resources and relationships it can prove, saves an analysis as a Report, and can place a chosen Report revision on a Dashboard. This keeps an App's business rules and data policy in the App while keeping cross-App planning, revision history, and presentation in Core.
Start with Resource contracts. A Resource must be active, operational for the tenant, safe for the builder, and backed by its normal read permission and row predicate. Its field schema identifies which business fields may be selected, grouped, aggregated, or time-bucketed. Core does not infer any of that from database columns or a list screen.
Choose the smallest App contribution
Most Apps need no reporting-specific code. Keep the Resource descriptor,
localized labels, resourceListFields(), and standard authorization accurate;
the catalog can use those declarations directly.
Generated Resource modules start with a UUID identity, a reviewed name, and a
timestamp baseline. Keep only fields that exist and are safe for reporting;
add status enums, references, amounts, or a custom provider only after their
labels, capabilities, and row authority have been deliberately reviewed.
Use CompositionQueryContribution only when the App's normal Resource query
needs a custom scope, redaction rule, or safe projection that Core cannot
express from the standard catalog. Its authorized provider receives the
restored actor, request, Resource key, and exact organization target. It returns
an already-scoped query plus App-approved aliases and capabilities. It never
accepts SQL, a browser column name, or a cross-App model.
ReportingViewDescriptor remains optional App metadata. Publish it through
AppDescriptorContribution only when a cataloged surface needs to identify an
App-owned reporting view. It is not a saved Report, a permission grant, or a
second query API. Do not add it merely because a user could make a Report from
your Resource.
| Need | App action |
|---|---|
| A normal Resource should be selectable | Publish safe field capabilities and keep its existing read policy authoritative. |
| A domain-specific scoped or redacted read is needed | Implement CompositionQueryContribution for that Resource. |
| An App-owned catalog view needs stable metadata | Optionally publish ReportingViewDescriptor. |
| A reusable analysis, chart, export, schedule, or Dashboard placement is needed | Publish no new App report contract; Core owns it. |
Current composition contract
CompositionSpec has one current schema: schema_version: 1. It names a
bounded graph with source aliases and server-issued relationship keys.
population is required: an aggregate chooses an anchor, union, or
intersection population; a row result also declares its row_source. Every
relationship names its source, target, and explicit match behavior. The shape
supports typed row and aggregate expressions, nested where predicates,
conditional measures, having, comparisons, sort, and bounded limits.
The payload remains semantic intent. A report may request an orders source,
an approved ordered_at dimension, and a count measure. It cannot request an
orders table, write a join expression, or name a raw SQL aggregate. Core
checks the current catalog, proven relationship topology, actor, organization
target, field policy, result grain, and query budgets on every preview and
execution. Decimal outputs use canonical numeric strings where their declared
type proves exact decimal precision.
Amounts and other exact decimal results stay canonical numeric strings from the
Composition query through saved Report results, tables, Dashboard metrics, and
exports. This includes sum, min, max, comparisons, and derived values.
Do not coerce such a value through JavaScript Number; format the string for
display and keep the wire value for export. Charts omit a series that cannot be
drawn without rounding and identify that limitation instead of changing the
reported amount. Ordinary integers remain JSON numbers for compatibility, but
an integer beyond JavaScript's safe range is returned as the same canonical
numeric string, marked by meta.integer_encoding: canonical_numeric_string_when_unsafe.
App providers should publish won amounts as numeric fields and
let Core perform the aggregation; they must not pre-round or publish a parallel
string-only amount field.
The PHP DTO and matching frontend validator are the syntax authority. A
persisted definition uses this one shape; an App publishes no alternate report
wire. The catalog includes each authorized Resource's app_key and localized
app_label; filtering Apps never widens organization scope.
Report, Dashboard, and Agent have different jobs
A Report is a reusable saved definition with immutable revisions, sharing,
and a current revision. Editing or restoring it creates a new revision. A
Dashboard owns layout and pins a specific reportRef; a later Report edit
does not silently change that placement. Older savedDataWidgetId placements
remain copied snapshots for compatibility.
Dashboard common filters are board-local runtime values, never changes to a saved Report or its revision. A board saves at most ten definitions and an explicit mapping for each pinned report: source alias, Resource key, field, and one compatible operator, or an exclusion with its reason. A chart or pivot selection can apply only through that same exact mapping; matching labels does not connect reports. Clearing that selection restores the user's ordinary filter value. Date-only values stay dates; datetime input is local browser time and Core sends its ISO instant consistently to every mapped report.
Sharing a Report lets another actor find and use its definition. It never gives that actor access to a source Resource. Every Report preview, pinned revision query, export, and scheduled run applies the current viewer's report access, organization target, App lifecycle, source permission, and row policy. An unavailable source produces an unavailable or repair state instead of a redacted data leak.
When Core exposes a Report query to an Agent, the Agent uses the same pinned revision and planner as the human surfaces. It has no separate report registry, permission vocabulary, data grant, or App-defined reporting tool. App code continues to publish Resources and their authorization; Core restores the actor and evaluates the same authority for every caller.
A practical decision
Suppose an App exposes an operations.order Resource. Its status, ordered date,
and approved amount are safe composition fields, while an internal fraud score
is not. The App publishes the safe field capabilities and retains its existing
row policy. A user can then build a Report that groups order counts by month,
filters to an approved status, adds an allowed conditional measure, and pins a
revision to a Dashboard. The App does not create a report table, a Dashboard
endpoint, or an Agent tool.
If that App later needs a regional projection that its ordinary Resource query cannot safely express, it adds one authorized composition provider. The provider still applies the App's policy before Core combines the output with other proven sources. That is the extension point; copying Core's planner or loosening a policy to make a chart work is not.
Related
- Resource contracts — publishes the Resource and optional authorized provider.
- Contribution contracts — publishes descriptor metadata.
- Add a Dashboard Widget — chooses a bespoke App widget when the shared builder is not the right surface.