Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ Removed from v1 vs. Ch. 16: `IRetryPolicy`, `IExceptionClassifier`, `IAuthProvid
| 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) |
| 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 |
| 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) |
| D22 | Multi-sheet XLSX | Planned for v2 (Epic B). One workbook, several named sheets from different filters/sources. Monetization TBD. Design recorded before coding |
| D22 | Multi-sheet XLSX | **First v2 paid feature (maintainer-locked).** One workbook, several named sheets, each from a different filter over the same source (different sources per sheet = B2). Single pass preserved via a generic "multi-section output" hook in OSS Core; the XLSX workbook writer + fluent API ship in the commercial `NeoReports.Xlsx.Pro` package (D27). Blueprint: `docs/epic-b1-multisheet-pro.md`. Some sub-decisions still open there |
| D27 | Pro package model | Advanced features are **paid**, in a **separate package** with a **commercial license**; the OSS core stays MIT and never depends on Pro. Pro plugs in through existing extensibility (`IWriterFactory` + the new multi-section hook). v1 of Pro has **no runtime license enforcement** (contractual/honor-system; a license-key gate can come later). The legal license text is the maintainer's to provide; builds carry a placeholder commercial `LICENSE` until then. Pro packages are excluded from the OSS NuGet release flow. Open: exact package name, source-available vs closed |
| D23 | Multi-source | Planned for v2 (Epic B). Any report assembled from several sources (join/enrich). Likely paid. Design recorded before coding |
| 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 |
| 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 |
Expand Down
35 changes: 24 additions & 11 deletions PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,17 +119,30 @@ destinations and jobs are untouched. See **D21**.
safety choice). **Acceptance:** register from JSON/file/dir and run by name. ✅ 51 green
Core tests (+3); samples 04 and 05 now use the DI sugar. **Depends on:** A2, A4, PR 7.

## Epic B — Multi-source & multi-sheet XLSX (possibly paid)

Tracked in `memory/open-questions.md`; monetization (free vs paid) is **TBD with the
maintainer** before building. See **D22** (multi-sheet) and **D23** (multi-source).

- [ ] **B1 — Multi-sheet XLSX.** One workbook, several sheets, each fed by a different
filter (and later a different source). Needs a writer that targets a named sheet within
a shared workbook without breaking the single-pass read. **Acceptance:** TBD with the
recorded decision.
- [ ] **B2 — Multi-source reports.** Any report assembled from several sources
(join/enrich) into one output. Likely the headline paid feature. **Acceptance:** TBD.
## Epic B — Multi-sheet XLSX (first paid "Pro" feature), then multi-source

Maintainer decisions (locked): **start with multi-sheet XLSX (B1)**, shipped as a **paid, separate
"Pro" package** (commercial license), leaving the OSS core MIT. See the design blueprint in
[`docs/epic-b1-multisheet-pro.md`](docs/epic-b1-multisheet-pro.md), **D22** (multi-sheet) and **D27**
(Pro package model). Some sub-decisions (package name, license type/enforcement, exact OSS/Pro
boundary) are still open in that doc and must be settled before B1.2.

### B1 — Multi-sheet XLSX (Pro)

- [ ] **B1.1 — OSS multi-section hook (MIT).** An output may carry sections `(name, filter, columns)`;
the pipeline drives per-section projection + a section-aware write, single-pass preserved. Tests
with a fake section writer.
- [ ] **B1.2 — `NeoReports.Xlsx.Pro` package (commercial).** Fluent `XlsxWorkbook(...)` API + ClosedXML
workbook writer (one named sheet per section). Placeholder commercial LICENSE/metadata. Golden-file
test.
- [ ] **B1.3 — Packaging & CI.** Pro package builds/packs but is excluded from the OSS NuGet release.
- [ ] **B1.4 — Sample** `06-multi-sheet-xlsx` (typed: Approved/Rejected sheets).
- [ ] **B1.5 — Dynamic config** support for `xlsx-workbook` (optional, after the typed API settles).

### B2 — Multi-source reports (later)

- [ ] Any report assembled from several sources (join/enrich) into one output — likely also Pro.
Design (join semantics, memory/perf) recorded before coding. See **D23**.

## Validation gate (do not skip before Epic C)

Expand Down
93 changes: 93 additions & 0 deletions docs/epic-b1-multisheet-pro.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Epic B1 — Multi-sheet XLSX (first "Pro" feature)

> **Status: design, not yet built.** This is the blueprint to approve before any code or
> commercial-license file is created. Decisions land in `DECISIONS.md` (D22 updated, D27 new).

## Goal

One `.xlsx` workbook with **several named sheets**, each fed by a different **filter** over the same
report source (e.g. an "Approved" sheet and a "Rejected" sheet from one dataset). Different *sources*
per sheet is **out of B1** (that is B2 / multi-source).

This ships as the first **paid "Pro" feature**, in a **separate package** with a commercial license,
leaving the OSS core MIT and untouched in spirit.

## Why it does not fit the v1 model as-is

Today: a report has **one** schema and **one** filter set; the pipeline projects each row to
`object?[]` **once** and fans it out to N single-sheet writers (each output = one file = one sheet).

Multi-sheet inverts part of that: **one** output (a workbook) with **N sheets**, where each sheet has
its **own** filter and columns. A single projection can't produce per-sheet columns, and writers are
non-generic (`object?[]` + schema), so they can't apply a per-sheet filter on the typed row. So we
need per-sheet `(filter, columns)` captured where the typed `T` is still available (the builder), and
a writer that can target a **named sheet** within a shared workbook.

## Recommended architecture (single pass preserved)

A **multi-section output**: instead of one `(filter, columns)`, an output may carry a list of
**sections**, each `(name, optional filter, columns)`. The source is still read **once**; for each
batch, every row is offered to each section (the section filter decides inclusion, the section columns
project to that section's `object?[]` + schema), and rows are written to the matching sheet.

**OSS / Pro split (minimal OSS surface):**

- **OSS Core (MIT, additive — D25):** a small, generic hook so an output can declare multiple sections
and the pipeline can drive per-section projection + a section-aware write. Generic, format-agnostic,
reusable. This is the only change to the open engine.
- **`NeoReports.Xlsx.Pro` (commercial — D27):** the **XLSX workbook writer** (ClosedXML, one workbook,
one named sheet per section, native types/auto-filter per sheet) **and** the ergonomic fluent API.
This is the polished, supported implementation — the value customers pay for.

The open engine never references the Pro package; Pro plugs in through the existing extensibility
(`IWriterFactory` + the new multi-section hook). Anyone *could* hand-roll multi-section against the OSS
hook; the Pro package is the batteries-included, licensed, supported XLSX writer.

## API sketch (from the Pro package)

```csharp
using static NeoReports.Xlsx.Pro.Format; // Pro entry point

builder
.From(Source.Sql(...).Keyset<Sale, long>(v => v.Id))
.To(XlsxWorkbook(wb => wb
.Sheet("Approved", s => s
.Filter(v => v.Amount > 0)
.Column(v => v.Id, "Sale ID")
.Column(v => v.Amount, "Amount", format: "C2", culture: "pt-BR"))
.Sheet("Rejected", s => s
.Filter(v => v.Amount <= 0)
.Column(v => v.Id, "Sale ID"))));
```

- Dynamic (config) path: `"outputs": [ { "format": "xlsx-workbook", "properties": { "sheets": [ ... ] } } ]`
— a later step once the typed API is settled.

## Open sub-decisions (need the maintainer)

1. **Package name** — `NeoReports.Xlsx.Pro` (proposed) vs `NeoReports.Pro.Xlsx` vs an umbrella `NeoReports.Pro`.
2. **License type** — source-available (visible, paid to use) vs closed-source. The legal text is the
maintainer's to provide; the build will carry a placeholder `LICENSE` marked commercial until then.
3. **License enforcement** — none at first (honor-system / contractual), or a license-key gate later.
Recommend **none for v1 of Pro** (ship the capability; enforce later if needed).
4. **OSS/Pro boundary** — confirm the generic multi-section hook is acceptable in the MIT core (it makes
a free hand-rolled multi-section possible). Alternative: keep more in Pro at the cost of a Pro-side
pipeline shim.
5. **Same-extension/CSV behavior** — what a CSV output does with sections (one file per section? zip?
reject?). Recommend: multi-section is an XLSX-workbook concept; other formats reject sections with a
clear error in v1.

## Implementation PR breakdown (after this design is approved)

- **B1.1 — OSS multi-section hook (Core/Abstractions, MIT):** output carries optional sections; pipeline
drives per-section projection + section-aware write; single-pass preserved. Tests with a fake
section writer.
- **B1.2 — `NeoReports.Xlsx.Pro` package skeleton (commercial):** project, placeholder commercial
LICENSE + metadata (not MIT, not auto-published with the OSS packages), fluent `XlsxWorkbook(...)`
API, ClosedXML workbook writer. Golden-file test (one workbook, N sheets).
- **B1.3 — Packaging & CI:** the Pro package builds and packs but is **excluded** from the OSS NuGet
release flow (separate/none until the maintainer decides distribution). Docs.
- **B1.4 — Sample:** `06-multi-sheet-xlsx` (typed) demonstrating Approved/Rejected sheets.
- **B1.5 — Dynamic config support** for `xlsx-workbook` (optional, after the typed API settles).

Each PR small, green tests, one at a time — same workflow as Epic A.