Files
alla-allaos-fullstack/plans/task-p.4.2-runtime-design.md
phaichayon 0c0450b152 taks-p.4.3
2026-06-29 11:18:08 +07:00

1229 lines
22 KiB
Markdown

# Task P.4.2 Runtime Design
## Scope
This document defines the revised architecture design for the next-generation CRM PDF runtime requested by [task-p.4.2.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-p.4.2.md).
This is architecture work only.
- no production code changes
- no template JSON changes
- no database changes
- no API changes
This design reuses verified findings from:
- [task-p.4-discovery-report.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-p.4-discovery-report.md)
- [task-p.4.1-verification-report.md](/C:/Users/mtpphtaps/Documents/gitea/alla-allaos-fullstack/plans/task-p.4.1-verification-report.md)
---
## 1. Current Architecture Summary
### 1.1 Current runtime shape
The current quotation PDF runtime is effectively:
```text
Quotation Document Data
-> Active Template Resolution
-> Mapping Resolution
-> Mutate Topic Page
-> PDF Generator
```
The same underlying pipeline is used by:
- document preview
- PDF preview
- PDF download
- approved PDF generation
### 1.2 Current strengths
The existing foundation already gives us:
- DB-backed active template versioning
- DB-backed placeholder mappings
- shared quotation document data builder
- shared PDF generation gateway
- working topic pagination
- working PDFMe table support
- safe rollback through template version activation
### 1.3 Current architectural problem
The main problem is not PDF generation itself.
The problem is that rendering behavior depends on page order.
Today the topic runtime assumes:
```ts
schemas[1]
```
is always the topic/signature page.
That means business logic currently depends on:
- physical page indexes
instead of:
- logical document roles
- logical sections
This makes page insertion fragile and blocks optional section composition.
---
## 2. Proposed Architecture
### 2.1 Architecture principles
The new runtime must be:
- page-role driven
- section-based
- version compatible
- renderer independent
- extensible
### 2.2 Architectural shift
The runtime should move from:
```text
Template
-> Mutate Page 2
-> Generate PDF
```
to:
```text
Document Data
-> Template Resolver
-> Mapping Resolver
-> Compatibility Adapter
-> Page Resolver
-> Render Policy Resolver
-> Section Composer
-> Template Assembler
-> PDF Generator
```
### 2.3 Main design idea
The core design change is:
- treat document content as logical sections
- treat pages as render targets discovered by role
- let render policy decide whether each section should render
- let the assembler compose the final template from section outputs
This means future changes are made by adding:
- a new section role
- a new section builder
- a new marker rule
- a new assembly rule
instead of rewriting the whole runtime.
---
## 3. Section-Based Rendering Model
### 3.1 Section concept
A `Document Section` represents logical document content, not fixed physical pages.
Example sections:
- `customer`
- `product_items`
- `topics`
- `conditions`
- `signature`
- `attachments`
- `appendix`
- `warranty`
- `cover`
### 3.2 Section output behavior
A section may generate:
- zero pages
- one page
- multiple pages
This is important because:
- product items may paginate to many pages
- topics may paginate to many pages
- optional sections may render nothing
### 3.3 Separation rule
Each section builder must own only its own content domain.
Examples:
- Product Item rendering must not know topic rendering
- Topic rendering must not know product rendering
- Signature placement must not know product table layout internals
---
## 4. Optional Sections and Render Policy
### 4.1 Motivation
The new requirement adds optional section support.
That means the runtime must support skipping logical sections without requiring:
- another template version
- another runtime pipeline
### 4.2 Section requirement model
Each section should be evaluated through an explicit render policy.
Proposed contract:
```ts
type PageRole =
| 'customer'
| 'product_items'
| 'topics'
| 'conditions'
| 'signature'
| 'attachments'
| 'appendix'
| 'warranty'
| 'cover'
| 'unknown';
interface RenderPolicy {
section: PageRole;
enabled: boolean;
required: boolean;
visibleWhenEmpty: boolean;
}
```
### 4.3 Policy meaning
- `enabled`
- whether the runtime should attempt to render the section
- `required`
- whether missing output should be treated as error
- `visibleWhenEmpty`
- whether an enabled section should still render a shell/placeholder when it has no content
### 4.4 Architectural examples
Example policy set:
```text
Customer: required
Product Items: optional
Topics: optional
Conditions: optional
Signature: required
```
### 4.5 Policy source
Policy source is intentionally undefined in this task.
Future tasks may supply it from:
- user selection
- customer preference
- template defaults
- organization defaults
The architecture only needs a clean slot for the policy, not a persistence/UI decision yet.
### 4.6 Important constraint
Template versions should be used for:
- structural differences
- layout differences
Template versions should not be used solely for:
- enabling or disabling logical sections
That responsibility belongs to render policy.
---
## 5. Runtime Pipeline
### 5.1 Proposed pipeline
```text
Document Data
-> Template Resolver
-> Mapping Resolver
-> Compatibility Adapter
-> Page Resolver
-> Render Policy Resolver
-> Section Composer
-> Customer Section Builder
-> Product Item Engine
-> Topic Engine
-> Condition Engine
-> Signature Resolver
-> Template Assembler
-> PDF Generator
```
### 5.2 Stage descriptions
#### Stage 1: Document Data
Builds normalized business data:
- quotation fields
- customer fields
- product items
- topics
- signatures
- approval metadata
Does not know:
- page structure
- template markers
- physical page ordering
#### Stage 2: Template Resolver
Finds:
- active template
- active version
- runtime `schemaJson`
- template metadata
Does not know:
- business sections
- page roles
- render policy
#### Stage 3: Mapping Resolver
Finds:
- placeholder mappings
- table column mappings
- defaults
- format masks
Does not know:
- page placement
- optionality rules
#### Stage 4: Compatibility Adapter
Normalizes legacy and future template models into one page-role-aware runtime model.
Owns:
- legacy page-role inference
- explicit marker interpretation
- marker validation
- compatibility warnings
#### Stage 5: Page Resolver
Builds a page inventory from the normalized template.
Owns:
- page lookup by role
- duplicate detection
- fallback ordering
- insertion anchors
#### Stage 6: Render Policy Resolver
Produces the effective policy for each section.
Owns:
- section enablement state
- required/optional state
- visible-when-empty behavior
Does not own:
- section rendering itself
#### Stage 7: Section Composer
Coordinates section builders in policy order.
Owns:
- builder orchestration
- passing section inputs
- collecting built sections
- skipping disabled sections
Does not own:
- detailed rendering logic of each section
#### Stage 8+: Section Builders
Each builder produces section output only for its own section.
Builders include:
- Customer Section Builder
- Product Item Engine
- Topic Engine
- Condition Engine
- Signature Resolver
#### Final Stage: Template Assembler
Combines:
- preserved template pages
- generated section pages
- section input patches
into one final assembled template.
#### Final Stage: PDF Generator
Renders the assembled result through the existing PDFMe gateway.
---
## 6. Runtime Components
### 6.1 Template Resolver
Responsibility:
- resolve active template/version
- return raw template payload
Input:
- organization id
- document type
- product type
- file type
Output:
- `ResolvedTemplate`
### 6.2 Mapping Resolver
Responsibility:
- resolve version mappings and table columns
Input:
- `ResolvedTemplate`
Output:
- `ResolvedTemplateMappings`
### 6.3 Compatibility Adapter
Responsibility:
- convert raw runtime template into normalized page-role-aware model
- handle legacy inference
- expose compatibility issues
Input:
- `ResolvedTemplate`
- `ResolvedTemplateMappings`
Output:
- `CompatibleTemplate`
### 6.4 Page Resolver
Responsibility:
- resolve page identities
- build role-based lookup
- determine insertion anchors
Input:
- `CompatibleTemplate`
Output:
- `ResolvedPages`
### 6.5 Render Policy Resolver
Responsibility:
- determine which sections should render
- determine required/optional behavior
- determine empty-state display rules
Input:
- document data
- template metadata
- default runtime policy
Output:
- `ResolvedRenderPolicies`
### 6.6 Section Composer
Responsibility:
- orchestrate section builders
- pass shared context
- collect built section outputs
Input:
- document data
- resolved pages
- resolved render policies
Output:
- `BuiltSections`
### 6.7 Customer Section Builder
Responsibility:
- build customer section only
- preserve customer-page behavior
Input:
- customer data
- target customer page
Output:
- `BuiltSection<'customer'>`
### 6.8 Product Item Engine
Responsibility:
- build product item pages only
- paginate table
- repeat headers
- handle dynamic row height
Input:
- items
- product-items base page
- product section render policy
Output:
- `BuiltSection<'product_items'>`
### 6.9 Topic Engine
Responsibility:
- build topic pages only
- paginate topics
Input:
- topics
- topic base page
- topic section render policy
Output:
- `BuiltSection<'topics'>`
### 6.10 Condition Engine
Responsibility:
- build condition/terms section only
Input:
- condition data or condition template markers
- condition policy
Output:
- `BuiltSection<'conditions'>`
### 6.11 Signature Resolver
Responsibility:
- build signature section only
- handle keep-together logic
- last-page placement policy
Input:
- signatures
- signature page
- prior assembled content
- signature policy
Output:
- `BuiltSection<'signature'>`
### 6.12 Template Assembler
Responsibility:
- combine preserved and generated pages
- apply assembly order
- merge input patches
Input:
- compatible template
- built sections
Output:
- `AssembledTemplate`
### 6.13 PDF Render Gateway
Responsibility:
- font loading
- plugin registration
- final PDFMe rendering
Input:
- `AssembledTemplate`
Output:
- rendered PDF bytes
---
## 7. Page Marker Strategy
### 7.1 Goal
The runtime must never depend on:
```ts
schemas[1]
```
The runtime must resolve pages by logical markers and fallbacks.
### 7.2 Explicit marker strategy
Future templates should include explicit role markers.
Example:
```ts
{
name: '__page_role__product_items',
type: 'text',
content: 'product_items'
}
```
or
```ts
{
name: '__page_role__topics',
type: 'text',
content: 'topics'
}
```
### 7.3 Legacy inference strategy
Existing templates do not have explicit markers.
Legacy inference rules should be centralized in the compatibility adapter.
Examples:
- page containing customer identity fields => `customer`
- page containing `topic` + `data_topic` => `topics`
- page containing `app1`/`app2`/`app3` and closing signature text => `signature`
- future page containing `items_table` => `product_items`
- future page containing dedicated condition markers => `conditions`
### 7.4 Fallback detection
If no explicit marker or legacy inference succeeds:
- mark page as `unknown`
- record a runtime issue
- optionally continue if page is non-required
### 7.5 Marker precedence
Priority order:
1. explicit marker
2. legacy inference
3. fallback guess
4. unknown
---
## 8. Runtime Contracts
### 8.1 Core contracts
```ts
type PageRole =
| 'customer'
| 'product_items'
| 'topics'
| 'conditions'
| 'signature'
| 'attachments'
| 'appendix'
| 'warranty'
| 'cover'
| 'unknown';
interface ResolvedTemplate {
templateId: string;
templateVersionId: string;
templateName: string;
version: string;
schemaJson: Template;
}
interface ResolvedTemplateMappings {
mappings: DocumentTemplateMappingWithColumns[];
}
interface ResolvedPage {
pageIndex: number;
role: PageRole;
strategy: 'explicit_marker' | 'legacy_inference' | 'fallback';
schema: Template['schemas'][number];
}
interface ResolvedPages {
all: ResolvedPage[];
byRole: Partial<Record<PageRole, ResolvedPage[]>>;
issues: RuntimeIssue[];
}
interface RenderPolicy {
section: PageRole;
enabled: boolean;
required: boolean;
visibleWhenEmpty: boolean;
}
interface BuiltSection<TRole extends PageRole = PageRole> {
role: TRole;
enabled: boolean;
rendered: boolean;
pages: Template['schemas'];
templateInputPatch: Record<string, unknown>;
issues: RuntimeIssue[];
}
interface RuntimeIssue {
code:
| 'MISSING_MARKER'
| 'DUPLICATE_MARKER'
| 'INVALID_TEMPLATE'
| 'MISSING_MAPPING'
| 'EMPTY_OPTIONAL_SECTION'
| 'EMPTY_REQUIRED_SECTION'
| 'LEGACY_COMPAT_MODE';
severity: 'warning' | 'error';
message: string;
details?: Record<string, unknown>;
}
interface AssembledTemplate {
template: Template;
templateInput: Record<string, unknown>;
issues: RuntimeIssue[];
}
```
### 8.2 Ownership boundaries
- `ResolvedTemplate` belongs to template resolution
- `ResolvedTemplateMappings` belongs to mapping resolution
- `ResolvedPages` belongs to page resolution
- `RenderPolicy` belongs to policy resolution
- `BuiltSection` belongs to section builders
- `AssembledTemplate` belongs to assembly only
- `RuntimeIssue` is shared, accumulative diagnostic output
---
## 9. Section Composer Design
### 9.1 Purpose
The section composer is a new first-class component introduced by the updated requirement.
Its job is to coordinate section rendering independently from page index assumptions.
### 9.2 Responsibilities
- evaluate effective render policy
- call only enabled section builders
- preserve required section order
- collect built outputs
- collect runtime issues
### 9.3 Example composition order
Suggested default composition order:
1. `customer`
2. `product_items`
3. `topics`
4. `conditions`
5. `signature`
6. `attachments`
7. `appendix`
This order is logical, not tied to page indexes.
### 9.4 Composer interface
```ts
interface SectionComposerInput {
documentData: QuotationDocumentData;
resolvedPages: ResolvedPages;
policies: RenderPolicy[];
}
interface SectionComposerOutput {
sections: BuiltSection[];
issues: RuntimeIssue[];
}
```
---
## 10. Compatibility Strategy
### 10.1 Objective
Support:
- legacy templates
- future marker-based templates
without duplicating runtime logic.
### 10.2 Design rule
Legacy support must be isolated in:
- `Compatibility Adapter`
Every component after that should consume normalized contracts only.
### 10.3 Why this matters
Without this layer, optional sections and page-role logic would leak branching across:
- topic engine
- product item engine
- assembler
- signature logic
That would make the runtime harder to extend and harder to verify.
### 10.4 Compatibility outcome
Legacy templates stay valid because:
- their pages can still be inferred by field patterns
- their mappings remain unchanged
- their active versions remain selectable
Future templates become simpler because:
- markers are explicit
- sections are additive
- optionality is policy-driven, not template-version-driven
---
## 11. Future Extensibility
### 11.1 Additive extension model
A new document section should require only:
1. a `PageRole`
2. a section builder
3. a marker rule
4. an assembly rule
### 11.2 Supported future examples
- drawing
- specification
- gallery
- warranty
- appendix
- inspection report
### 11.3 Builder isolation rule
Existing section builders must not need modification when a new section is added, unless a shared contract truly changes.
That keeps the runtime additive instead of fragile.
---
## 12. Error Handling Strategy
### 12.1 Missing marker
Behavior:
- try explicit marker
- try legacy inference
- try fallback guess
- otherwise emit `MISSING_MARKER`
### 12.2 Duplicate marker
Behavior:
- duplicate singleton section like `customer` or `signature` => error
- duplicate repeatable section like `attachments` or `appendix` => warning only if ambiguous
### 12.3 Missing mappings
Behavior:
- if required mapped section cannot render => error
- if optional section cannot render => warning and skip if policy allows
### 12.4 Empty optional section
Behavior:
- emit `EMPTY_OPTIONAL_SECTION`
- skip rendering when `visibleWhenEmpty = false`
- render placeholder shell when `visibleWhenEmpty = true`
### 12.5 Empty required section
Behavior:
- emit `EMPTY_REQUIRED_SECTION`
- fail gracefully before final PDF generation if policy requires output
### 12.6 Invalid template
Behavior:
- stop assembly
- return `INVALID_TEMPLATE`
### 12.7 Issue accumulation
All stages append to `RuntimeIssue[]`.
This supports:
- preview diagnostics
- audit tooling
- rollout safety
This remains consistent with ADR-0013 parity/audit discipline.
---
## 13. Migration Strategy
### 13.1 Migration goal
Move from the current runtime to a section-based runtime without breaking:
- preview
- download
- approved PDF
- existing template versions
### 13.2 Constraints
- no database migration
- no API changes
- no approved artifact rewrite
### 13.3 Safe migration path
1. introduce runtime contracts
2. introduce compatibility adapter
3. introduce page resolver
4. introduce render policy resolver
5. introduce section composer
6. refactor topic engine to consume resolved topic page
7. add product item engine
8. add assembler-driven final composition
9. publish new structural template versions only when needed
### 13.4 Approved PDF safety
Already stored approved PDFs remain safe because retrieval uses stored artifact binaries, not live re-rendering.
Only future generated documents will use the refactored runtime.
---
## 14. Sequence Diagrams
### 14.1 Current Runtime
```mermaid
sequenceDiagram
participant U as User/Route
participant DS as Document Service
participant TR as Template Resolver
participant MR as Mapping Resolver
participant TE as Topic Engine
participant PG as PDF Generator
U->>DS: Request preview/download
DS->>DS: buildQuotationDocumentData()
DS->>TR: resolveTemplateForDocument()
DS->>MR: resolveTemplateMappings()
DS->>DS: mapDocumentDataToTemplateInput()
DS->>TE: buildPdfTopicTemplate()
TE-->>DS: mutated template + topic inputs
DS-->>PG: final template + templateInput
PG-->>U: PDF bytes
```
### 14.2 Proposed Runtime
```mermaid
sequenceDiagram
participant U as User/Route
participant DD as Document Data
participant TR as Template Resolver
participant MR as Mapping Resolver
participant CA as Compatibility Adapter
participant PR as Page Resolver
participant RP as Render Policy Resolver
participant SC as Section Composer
participant CB as Customer Builder
participant PI as Product Item Engine
participant TO as Topic Engine
participant CO as Condition Engine
participant SG as Signature Resolver
participant TA as Template Assembler
participant PG as PDF Generator
U->>DD: Request preview/download
DD-->>TR: documentData
TR-->>MR: resolvedTemplate
MR-->>CA: mappings + template
CA-->>PR: compatibleTemplate
PR-->>RP: resolvedPages
RP-->>SC: render policies
SC->>CB: build customer section
SC->>PI: build product section
SC->>TO: build topic section
SC->>CO: build condition section
SC->>SG: build signature section
CB-->>SC: built customer section
PI-->>SC: built product section
TO-->>SC: built topic section
CO-->>SC: built condition section
SG-->>SC: built signature section
SC-->>TA: built sections
TA-->>PG: assembled template + input
PG-->>U: PDF bytes
```
---
## 15. Component Diagram
```mermaid
classDiagram
class TemplateResolver
class MappingResolver
class CompatibilityAdapter
class PageResolver
class RenderPolicyResolver
class SectionComposer
class CustomerSectionBuilder
class ProductItemEngine
class TopicEngine
class ConditionEngine
class SignatureResolver
class TemplateAssembler
class PdfRenderGateway
TemplateResolver --> MappingResolver
MappingResolver --> CompatibilityAdapter
CompatibilityAdapter --> PageResolver
PageResolver --> RenderPolicyResolver
RenderPolicyResolver --> SectionComposer
SectionComposer --> CustomerSectionBuilder
SectionComposer --> ProductItemEngine
SectionComposer --> TopicEngine
SectionComposer --> ConditionEngine
SectionComposer --> SignatureResolver
SectionComposer --> TemplateAssembler
TemplateAssembler --> PdfRenderGateway
```
---
## 16. Risk Assessment
### 16.1 Main risks
- legacy inference becomes too magical if not centralized
- optional section policy becomes entangled with template versioning
- section builders may start leaking logic into each other
- assembler may become implicit if order rules are not declared clearly
### 16.2 Mitigations
- keep all legacy branching inside compatibility adapter
- keep all enable/disable logic inside render policy resolver
- keep all orchestration inside section composer
- keep all page order rules inside assembler
- keep template versions for layout/structure, not section toggles
---
## 17. Implementation Roadmap
### Phase 1: Runtime contracts
Deliverables:
- `PageRole`
- `RenderPolicy`
- `BuiltSection`
- `RuntimeIssue`
- `AssembledTemplate`
### Phase 2: Compatibility adapter
Deliverables:
- explicit marker support
- legacy inference support
- normalized compatible template output
### Phase 3: Page resolver
Deliverables:
- role-based page inventory
- duplicate/missing marker diagnostics
### Phase 4: Render policy resolver
Deliverables:
- effective section policy resolution
- required/optional rules
- visible-when-empty rules
### Phase 5: Section composer
Deliverables:
- builder orchestration
- section collection
- policy-driven skipping
### Phase 6: Product item engine
Deliverables:
- item table rendering
- pagination
- repeated headers
### Phase 7: Template upgrade
Deliverables:
- structural template versions for new layouts only
### Phase 8: Regression verification
Deliverables:
- preview/download/approved regression checks
- old/new version parity checks
Each phase must be independently testable.
---
## 18. Final Recommendation
Task P.4.3 should implement a `section-based runtime` with `render policy` and `page-role resolution` as first-class architecture concepts.
The most important architectural order is:
1. contracts
2. compatibility adapter
3. page resolver
4. render policy resolver
5. section composer
6. section builders
7. assembler
This keeps the runtime:
- backward compatible
- optional-section capable
- future-extensible
- independent from page indexes
and leaves minimal architectural ambiguity for implementation.