Skip to content

Commit a9278ce

Browse files
authored
Merge pull request #57 from thiagoluga/docs/v2-roadmap-dynamic-path
docs(plan): v2 roadmap + reopen-scope decisions (D21-D25)
2 parents 3cc71a6 + 662c5f6 commit a9278ce

2 files changed

Lines changed: 97 additions & 2 deletions

File tree

DECISIONS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,11 @@ Removed from v1 vs. Ch. 16: `IRetryPolicy`, `IExceptionClassifier`, `IAuthProvid
189189
| D18 | Jobs packaging | Base package `NeoReports.Jobs` (shared worker `ReportJobWorker` + `InMemoryJobStore` + `InMemoryJobScheduler` + `NoOpCheckpointStore` + DI) and `NeoReports.Jobs.Hangfire` extending it. Avoids a 3rd package just to share the worker; the plan's "InMemory" lives in the base package |
190190
| D19 | Worker and cancellation | `ReportJobWorker` is the single lifecycle core (running→completed/failed/cancelled), used by both schedulers. Idempotent restart (AC-16) comes from the pipeline (per-job temp + upload only at the end; temp cleanup is best-effort and never changes the status). InMemory: cancels via a per-job `CancellationTokenSource`. Hangfire: `CancellationToken` injected into the invoker; `CancelAsync` deletes the job; in-process id↔hangfire-id map (single-server; cross-restart is post-MVP, D2). Parameters travel as JSON (`JobParameters`, dates as ISO-8601 round-trip) |
191191
| D20 | Endpoints + artifact store | `MapNeoReports("/api")` (Minimal API). Download/sync need to retain the file beyond the pipeline's temp: `IReportArtifactStore` (+ `FileSystemArtifactStore`) in **Core** (an engine concern, not a plugin contract → outside `Abstractions`); the `ReportRunner` saves into it only if registered (opt-in via DI). Sync = single-output (multi → 400, AC-10); multi-output on download becomes a zip. Job status serialized as a **string** (`JsonStringEnumConverter` on the DTOs, without touching the enum in `Abstractions`). Auth inherits from the host (`RequireAuthorization` optional; no auth chain). Outputs with the same extension (e.g. two CSVs) get a **disambiguated** file name in the pipeline (`name.csv`, `name-2.csv`) so they don't collide on disk/artifact and can go together in the zip |
192+
| D21 | Dynamic path | Reopened in v2. Row type = positional `ReportRecord` (`object?[]` + `ReportSchema`), not a dictionary; reuses the whole v1 pipeline because the writer edge already speaks `object?[]` + schema. Config (JSON) + `IReportConfigParser` + JsonLogic filter return additively to `Abstractions` (SemVer-minor) |
193+
| D22 | Multi-sheet XLSX | Planned for v2 (Epic B). One workbook, several named sheets from different filters/sources. Monetization TBD. Design recorded before coding |
194+
| D23 | Multi-source | Planned for v2 (Epic B). Any report assembled from several sources (join/enrich). Likely paid. Design recorded before coding |
195+
| D24 | UI ordering | Blazor UI is the **last** v2 epic (after dynamic path + multi-source + a user-validation gate), per the maintainer. Always built from the Claude Design handoff, never invented |
196+
| D25 | v2 additivity | Every v2 addition is additive and SemVer-minor on `Abstractions`; v1's frozen surface is never broken, only extended. Removing anything stays SemVer-major |
192197
|| Design | Already done in Claude Design; export per the handoff; UI post-MVP |
193198

194199
---
@@ -225,3 +230,37 @@ Removed from v1 vs. Ch. 16: `IRetryPolicy`, `IExceptionClassifier`, `IAuthProvid
225230
- The `Schema` declared by the source is a minimal placeholder — the pipeline's projection uses the builder's columns (D1), not the source's schema.
226231

227232
**Why.** Keyset with an opaque `@cursor` and connection-per-page satisfies AC-2 (reads all pages in order, without skipping/repeating) and keeps constant memory, leaving checkpoint/multi-worker viable later without rework. Connection-by-name is configuration sugar that doesn't change the contract — cut from v1.
233+
234+
---
235+
236+
# v2 decisions (post-MVP — reopening scope)
237+
238+
> v1 (1.0.0) is published. The decisions below reopen scope **deliberately and additively**.
239+
> Locked order with the maintainer: **Epic A (dynamic path) → Epic B (multi-source / multi-sheet) → validation gate → Epic C (Blazor UI) last.** Nothing here breaks v1's frozen `Abstractions` (D25) — it only extends it.
240+
241+
## D21 — Dynamic path: positional `ReportRecord`, config + JsonLogic (v2 / Epic A)
242+
243+
**Decision.** The dynamic (config-driven) path returns in v2. It does **not** revisit rule 1 ("never a dictionary as the row type"): the dynamic row is a **positional `ReportRecord`** — an `object?[]` aligned to a declared `ReportSchema`, exactly the shape the writer edge already consumes (D1). The pipeline stays generic over `T`; the dynamic path simply runs with `T = ReportRecord`. Writers, destinations, jobs and resilience are **untouched**.
244+
245+
- `ReportRecord` and a `ReportRecord` `IBatchSource` return to `Abstractions` (additive, SemVer-minor — D25).
246+
- Config is **JSON**, parsed by `IReportConfigParser` into a runnable registration (the same internal model `AddReport<T>` produces). Columns/source/outputs/destinations/retry/onFailure mirror the fluent builder one-to-one.
247+
- The dynamic **filter** is a **JsonLogic** expression compiled to `Func<ReportRecord,bool>` (D10's deferred half). Typed delegates remain the code-first option; JsonLogic is the dynamic one. DynamicLinq stays out unless a concrete need appears.
248+
- No new execution path: a config report is just another registration. Same `ReportRunner`, same jobs, same endpoints.
249+
250+
**Why.** Reusing the positional edge means the dynamic path is *configuration on top of the existing engine*, not a parallel engine. Constant memory, resilience and the writer contract all carry over for free. This is the single biggest reason `Abstractions` was kept minimal-but-open in v1.
251+
252+
## D22 — Multi-sheet XLSX (v2 / Epic B — design before code)
253+
254+
**Decision (directional).** A single XLSX workbook with several **named sheets**, each fed by a different filter (and later a different source), without breaking the single-pass read (D14's ClosedXML in-memory model already holds the whole workbook, so multi-sheet fits naturally). Exact API and whether it is a **paid** capability are **TBD with the maintainer**; a concrete decision is recorded here before coding. Tracked in `memory/open-questions.md`.
255+
256+
## D23 — Multi-source reports (v2 / Epic B — design before code)
257+
258+
**Decision (directional).** Any report assembled from **several sources** (join/enrich) into one output — likely the headline **paid** feature. Monetization model (free core vs paid) and the join semantics are **TBD with the maintainer**; recorded before coding. Tracked in `memory/open-questions.md`.
259+
260+
## D24 — UI is the last v2 epic (Blazor + Claude Design handoff)
261+
262+
**Decision.** The Blazor Server + MudBlazor UI is built **last** in v2, after the dynamic path, multi-source, and a real user-validation gate — by the maintainer's explicit ordering. It is built **only** from the Claude Design handoff (`tokens.css`, `components.html`, per-screen `.html`, `handoff.md`); design is never invented or diverged from the Design System tokens.
263+
264+
## D25 — v2 additivity / SemVer discipline
265+
266+
**Decision.** Every v2 addition to `Abstractions` is **additive** (new types/members) and ships as **SemVer-minor**. v1's published surface is never changed in place; removing or changing a signature would be SemVer-major and is avoided. External plugins built against 1.x keep compiling against any 1.y.

PLAN.md

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,62 @@ Small, independent PRs, in order. Each one closes with green tests and closes on
6767

6868
---
6969

70-
## Post-MVP (do not start before validating with users)
70+
# NeoReports — Implementation Plan (v2 / post-MVP)
7171

72-
Likely order once there's traction: dynamic path (config + JsonLogic) → **Blazor UI from the Claude Design handoff** → variants/pipeline → multi-worker + mid-job resume → remaining sources/formats/destinations. Reminder: any UI work always starts from the Claude Design deliverables, never from invented design.
72+
v1 shipped (PR 0–8, published to NuGet as 1.0.0). v2 reopens scope deliberately:
73+
every epic below was kept *possible* by the frozen `Abstractions` (D1/D9 — "close no
74+
door"), so each addition is an **additive, SemVer-minor** change, never a rework.
75+
76+
**Order (locked with the maintainer):** Epic A (dynamic path) → Epic B (multi-source /
77+
multi-sheet) → validation gate → **Epic C (Blazor UI) last**. Same rules as v1: one
78+
small PR per item, green tests, and a recorded ADR entry before any out-of-v1-scope
79+
code. See **D21–D25** in `DECISIONS.md`.
80+
81+
## Epic A — Dynamic path (config-driven reports)
82+
83+
Run reports from JSON config with no compile-time POCO, reusing the *entire* existing
84+
pipeline. The row type is a positional `ReportRecord` (`object?[]` aligned to a declared
85+
`ReportSchema`); the writer edge already speaks `object?[]` + schema (D1), so writers,
86+
destinations and jobs are untouched. See **D21**.
87+
88+
- [ ] **A1 — `ReportRecord` + dynamic pipeline.** Reintroduce the positional row type
89+
(Abstractions, additive) and a `ReportRecord` `IBatchSource`. Prove the existing
90+
pipeline runs end-to-end with `T = ReportRecord` against an in-memory source.
91+
**Acceptance:** dynamic rows reach CSV/XLSX byte-identically to the typed path for the
92+
same data. **Depends on:** v1.
93+
- [ ] **A2 — Config model + parser.** `ReportConfig` DTOs (source · columns ·
94+
filter · outputs · destinations · retry · onFailure) + `IReportConfigParser` (JSON).
95+
Maps a parsed config to a runnable registration. **Acceptance:** golden config →
96+
registered, runnable report. **Depends on:** A1.
97+
- [ ] **A3 — SQL source from config.** Keyset SQL source driven by config (connection
98+
name/string · sql · key · pageSize), materializing columns to `ReportRecord` by
99+
name/ordinal. **Acceptance:** Testcontainers E2E config→SQL→CSV. **Depends on:** A2, A3 reuses v1 keyset.
100+
- [ ] **A4 — JsonLogic filter.** Compile a JsonLogic expression to
101+
`Func<ReportRecord,bool>` evaluated on the dynamic row. **Acceptance:** operator
102+
coverage + a filtered E2E. **Depends on:** A1.
103+
- [ ] **A5 — DI + dynamic trigger.** `AddReportsFromConfig(...)` (file/dir/json) and an
104+
optional dynamic-config trigger endpoint. **Acceptance:** register from JSON and run
105+
via the AspNetCore endpoints. **Depends on:** A2, A4, PR 7.
106+
107+
## Epic B — Multi-source & multi-sheet XLSX (possibly paid)
108+
109+
Tracked in `memory/open-questions.md`; monetization (free vs paid) is **TBD with the
110+
maintainer** before building. See **D22** (multi-sheet) and **D23** (multi-source).
111+
112+
- [ ] **B1 — Multi-sheet XLSX.** One workbook, several sheets, each fed by a different
113+
filter (and later a different source). Needs a writer that targets a named sheet within
114+
a shared workbook without breaking the single-pass read. **Acceptance:** TBD with the
115+
recorded decision.
116+
- [ ] **B2 — Multi-source reports.** Any report assembled from several sources
117+
(join/enrich) into one output. Likely the headline paid feature. **Acceptance:** TBD.
118+
119+
## Validation gate (do not skip before Epic C)
120+
121+
Validate Epic A/B with real users before investing in UI. This is a maintainer activity,
122+
not a coding task — it gates Epic C.
123+
124+
## Epic C — Blazor UI (LAST)
125+
126+
Blazor Server + MudBlazor, built **only** from the Claude Design handoff
127+
(`tokens.css`, `components.html`, per-screen `.html`, `handoff.md` — see the ADR). Never
128+
invent design. Deliberately last per the maintainer. See **D24**.

0 commit comments

Comments
 (0)