Repository navigation
feat(reporting): generated read routes and docs for view-backed reports in all five ports (FR-044 Plan 3) - #407
Merged
Merged
Conversation
A served object.report (its read source is @kind: view) now gets the keyless read-only Spring surface, generated from its read model: <R>Dto, <R>Repository (list and count, no findById), <R>FilterAllowlist and <R>Controller (GET list, POST answers 405, no /{id} mapping). A sourceless report, and one over any other read-only kind, stays inert. - RestSurfaceGate gains isServedReport and restShapeOf; isReadOnly is true for a served report and for its read model. Every REST-surface loop maps each object through restShapeOf and skips on null. - ReportReadModel sets @filterable on every derived field whose subtype has a filter band (dimension or measure), on the detached model only. No vocabulary is added. - A derived enum field is typed by an enum nested in the report's own DTO (<Report><Field>), not by the @Of entity's enum: the read model carries the members and no extends, so the row stays self-contained. - The filter allowlist spells a map of more than ten fields with Map.ofEntries. Map.of stops at ten pairs, and a wide report (every derived field is filterable) crosses it; ten or fewer keep the Map.of form, so existing output is byte-identical. - JavaApiModelBuilder documents a served report as a `report` unit: the row DTO, the read-only repository, the one GET route and the allowlist. - New generated lane ReportGeneratedApiContractConformanceTest drives the 12 report/ scenarios against the three generated controllers on one Tomcat, behind an in-memory seam seeded with what the views return. - Kotlin: the controller and filter-allowlist loops skip a report until the Kotlin port switches them to restShapeOf; without that the widened gate would emit a field-less controller for the declared node.
meta docs now documents object.report (Plan 3, Table G), TypeScript. Model surface: every report gets a page, served or not, built from reportShape: its @from (linked), its view or 'Not served: declares no view source', its row scope, and a column table (name, type, nullable, role, definition). The index lists reports under their own heading. An entity that declares dimensions, measures or segments, or that a report reads from, gains a Reporting section. API surface: one unit for a served report, built from its read model: the row model, the list query and GET <served path> (plus the Hono GET when wired). No by-id, no write helper, no schema, no hook. A report that is not served has no unit, and its model page links to none. A keyless projection (no identity, no id column) no longer documents GET <path>/:id or find<Name>ById: the generators stopped emitting both. Site: the report skip in the link graph and the coverage audit's deferred bucket are gone. A report is an object page with a Report section, linked to its @from entity; the entity page gets the same Reporting section; all reporting kinds and attrs count as rendered. A model with no report renders every surface byte for byte as before (pinned by snapshots taken before the change; no existing golden moved).
…ad-only object emits (FR-044) The sentences meta docs prints about dimensions, measures, segments, row scopes and why a report is not served now live in one place, metadata's core/reporting/report-describe.ts, beside reportShape. The markdown model pages (codegen-ts) and the HTML site (docs-site) both read them from there; each keeps only its own markup step. An object's API unit documents create/update/delete, the write REST verbs and the Insert/Update schemas only when the generators emit them. The gate is the generators' own dispatch (a read-only-kind source and no writable one), so a view-backed projection and a report document reads alone, a keyless one without find-by-id, and a write-through object keeps its whole write surface. This is a named exception to no-churn: a read-only projection's API page loses symbols that were never generated. The accuracy gate now checks a keyed projection, a keyless projection and a write-through object against the emitted files in both directions. The site's coverage audit marks only the attributes the describers read, so an attribute on a reporting node that no page prints is reported as a gap. The report page narrows its node with isMetaObject.
…R-044) A served object.report (read source @kind: view) now gets its row data class, filter allowlist and a keyless read-only Spring controller, beside the Exposed table it already had. Each generator reaches the report through RestSurfaceGate.restShapeOf, replacing the interim skips, and the table's served check is RestSurfaceGate.isServedReport. A report that is not served generates nothing. - The row's enum property is typed by the enum of the entity the item reads, the class the table types the column by (KotlinGenUtil.reportEnumClasses is the one answer for the table, the row and the controller). - The row carries no builder and no validation annotation: it is read from the view, never bound. - Api docs document a served report as a unit of kind "report": its row, its table, GET <path> and its allowlist. - New generated lane over the shared report/ api-contract corpus (12 scenarios). Two controller defects fixed, both in emitters shared with entities and projections, both byte-neutral for a model that does not hit them: - A filter on a field.decimal or field.float column threw ClassCastException (a 500): the value was coerced to a Double and then cast to the column's BigDecimal or Float. Each now has its own coercer, emitted only when such a column exists. - The row mapper and the filter dispatch named a column by its field name, but the table declares a field named after a member of Exposed's Table under a Column suffix (source is sourceColumn), so that controller did not compile. Both now use KotlinNaming.safeColumnProperty, as the sort dispatch did.
…he per-port generators (FR-044) Docs, agent skills, CHANGELOG, conformance counts (api-contract 61 to 73) and the Plan 3 document brought in line with what was built. The CHANGELOG names five corrections that reach models with no report. The project's own requirements ledger is unchanged: no entry is about serving a report.
…FR-044) The previous commit gave field.decimal and field.float their own filter coercers in the generated controller. This pins that output and runs it, on an ordinary writable entity with no report involved. - A committed snapshot fixture, entity-with-decimal-float-filter: one filterable decimal and one filterable float. - DecimalFloatFilterControllerRunTest compiles the generated controller and drives it over MockMvc against H2. With the old coercer both filters threw ClassCastException (Double to BigDecimal, Double to Float). Also: the unresolved-owner message of KotlinGenUtil.reportEnumClasses named `dimension "null"` for a min/max measure. It now names the item and the field it reads.
…pe, as built (FR-044)
…sses its own key (FR-044) The read-only mounts address a row by `idColumn`, which defaults to `id`. The routes templates never passed it, so a projection whose identity is on another field (say `code`) mounted `GET /:id` against a column the view does not have. The mount then ran the query with no WHERE and answered with the view's first row. Generators: the Fastify and Hono routes templates, and their reference copies, emit `idColumn: "<field>"` in the read-only mount options when the by-id field is not `id`. It is the field `find<Name>ById` reads, through a new `itemRouteField` (`hasItemRoute` is now "it is defined"). A projection keyed on `id` keeps its bytes; no golden moved. Runtime: both read-only mounts answer `404 not_found` when the view declares columns and has none under `idColumn`, instead of an unfiltered row. That also makes an owned routes generator that predates the option fail safe. Owned copies under test-generators and examples are re-synced.
…tions (FR-044) CHANGELOG: upgrade notes for an owned `routes` / `routes-hono` generator, an owned `mount-read-only.ts` and a hand-written generator gating on `servesReadApi`; a new entry for the `idColumn` change; the "Changed" intro now says which entries change code and which change docs, and counts seven. Corrections: the sort error code in reporting.md is `invalid_sort`; the array- and map-valued sort caveat is stated per port; docs/README.md and docs/CONFORMANCE.md say a report is served; AGENTS.md counts three generated-lane-only sub-corpora; the authoring skill limits filter and sort to derived fields with filter operators; the C#, Java and Kotlin codegen references define a keyless projection as anything but a declared single-column identity; docs/ports/kotlin.md names the abstract report. Plan: one proof-table row per named behaviour change (2 to 8), with the no-churn constraint and the as-built list counting the same seven. Agent-context conformance fixtures regenerated from the edited skills.
… comments and assertions
dmealing
force-pushed
the
fm/fr044-plan3
branch
from
October 6, 2026 03:43
d837a15 to
e0ce2b9
Compare
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Execute FR-044 Plan 3 (report read routes), as written in
docs/superpowers/plans/2026-10-04-fr-044-plan-3-report-read-routes.md(merged in PR #400), using subagent-driven development, then gate it and merge.Plan 3 serves reports over the generated read API after Plan 2 (PR #399) lowered them to SQL views: the api-contract
report/sub-corpus, generated read routes for a view-backed report in five ports (TypeScript, C#, Java, Kotlin, Python), reports inmeta docs, and docs, skills and changelog. Tasks 1-11 in the plan's order.The plan's seven open questions are answered:
@filterablevocabulary.InvoicesByMonthat/invoices_by_months)./{id}is not mounted for a report; only the framework's own 404 status is asserted.projection/scenario). Record it as a behaviour change.meta docs; TypeScript and Python emit a names artifact for a served report while C#, Java and Kotlin bind by literal.Standing decisions: compiled SQL views only, no query-time engine; a report lowers only with
source.rdb@kind: view; all measures from the report's own@fromentity and@viato-one only; UTC only; engine-native numeric precision;measure.derivedexcluded; nothing outside spec section 3.1; Plans 4-5 are later work.What Changed
Report REST surface: Every view-backed
object.reportnow gets a generated read-only list route (GET /<segment>) with filter, sort and pagination on derived fields, plus405on write verbs. No item route is mounted. Implements the cross-port contract in TypeScript, C#, Java, Kotlin and Python.Row and schema generation: TypeScript, Java and Python now generate row types and route/query/DTO artifacts for reports (matching C# and Kotlin). All five ports emit allowlists with all derived fields as filterable and sortable, regardless of
@filterabledeclarations.Documentation: Reports now appear in
meta docswith a dedicated model page (entity, view source, row scope, columns) and an API page for served reports. Themetaobjects-authoringskill documents report authoring patterns. Owned docs templates gain optional keys for reporting sections.Keyless projection fix: TypeScript and Python stop mounting item routes (
/{id}, by-id queries) for read-only projections with no declared identity and noidfield. A projection keyed on a non-idfield now passes that field to its route mount asidColumn. Unit tests only; no new scenarios.Decimal typing fix: TypeScript view read schemas now type
field.decimalasz.string(), matching what Drizzle actually reads from anumericcolumn. Affects reports and projections with decimal fields.Conformance: New api-contract sub-corpus
fixtures/api-contract-conformance/report/with 12 scenarios and generated lane across all five ports, plus schema and seed. First conformance coverage offield.dateliteral assertion. Codegen-compile gate extended to all ports. Reporting inert tests updated; Python and Java routes now generate deterministically for reports.No UI tier: Reports do not emit typed client hooks, grids, forms or agent pages in this plan. TypeScript codegen gates UI output on
servesClientTier(a new predicate) instead ofservesReadApi, which is now true for served reports.Risk Assessment
✅ Low: Both deep reviews (TS/runtime core, and C#/Java/Kotlin/Python ports + corpus + docs) traced actual logic (route mounting, filterable/sortable derivation, decimal typing, UI-tier exclusion, measure scope) and found all seven intent constraints satisfied with no source-level defects; tests execute real codegen/runtime rather than grepping text.
Testing
Baseline test suite passed (typescript conformance, unit tests, mutation gate). Drove 185 targeted tests across TypeScript code generation, runtime, API contracts, documentation, and inert behavior. All pass. Report list routes serve with filter/sort on derived fields, no item routes mount, write verbs refuse with 405, decimals in view schemas type as strings, sourceless reports stay inert, UI tier correctly excluded.
Evidence: Test Summary
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
🔧 **Rebase** - 1 issue found → auto-fixed ✅
docs/CONFORMANCE.md- merge conflict rebasing onto origin/main🔧 Fix applied.
✅ Re-checked - no issues remain.
✅ **Review** - passed
✅ No issues found.
✅ **Test** - passed
✅ No issues found.
scripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchainsscripts/ci-local.sh --only ts-fast --only ts-unit --strict-toolchains (baseline)server/typescript/packages/integration-tests/test/api-contract-report.test.ts (13 tests)server/typescript/packages/integration-tests/test/api-contract-report-corpus.test.ts (5 tests)server/typescript/packages/runtime-ts/test/drizzle-fastify/mount-read-only.test.ts (22 tests)server/typescript/packages/runtime-ts/test/hono/mount-read-only.test.ts (4 tests)server/typescript/packages/codegen-ts/test/projection/routes-file.test.ts (29 tests)server/typescript/packages/codegen-ts/test/projection/queries-file.test.ts (11 tests)server/typescript/packages/codegen-ts/test/codegen-compile-conformance.test.ts (6 tests)server/typescript/packages/codegen-ts-tanstack/test/report-no-ui-tier.test.ts (12 tests)server/typescript/packages/codegen-ts/test/reporting-docs.test.ts (17 tests)server/typescript/packages/docs-site/test/reporting-site.test.ts (8 tests)server/typescript/packages/cli/test/unit/reporting-inert.test.ts (39 tests)server/typescript/packages/metadata/test/report-read-model.test.ts (12 tests)server/typescript/packages/metadata/test/report-describe.test.ts (17 tests)✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.