22 KiB
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.
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:
1. Current Architecture Summary
1.1 Current runtime shape
The current quotation PDF runtime is effectively:
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:
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:
Template
-> Mutate Page 2
-> Generate PDF
to:
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:
customerproduct_itemstopicsconditionssignatureattachmentsappendixwarrantycover
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:
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:
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
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:
ResolvedTemplateResolvedTemplateMappings
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:
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:
{
name: '__page_role__product_items',
type: 'text',
content: 'product_items'
}
or
{
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/app3and 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:
- explicit marker
- legacy inference
- fallback guess
- unknown
8. Runtime Contracts
8.1 Core contracts
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
ResolvedTemplatebelongs to template resolutionResolvedTemplateMappingsbelongs to mapping resolutionResolvedPagesbelongs to page resolutionRenderPolicybelongs to policy resolutionBuiltSectionbelongs to section buildersAssembledTemplatebelongs to assembly onlyRuntimeIssueis 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:
customerproduct_itemstopicsconditionssignatureattachmentsappendix
This order is logical, not tied to page indexes.
9.4 Composer interface
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:
- a
PageRole - a section builder
- a marker rule
- 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
customerorsignature=> error - duplicate repeatable section like
attachmentsorappendix=> 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
- introduce runtime contracts
- introduce compatibility adapter
- introduce page resolver
- introduce render policy resolver
- introduce section composer
- refactor topic engine to consume resolved topic page
- add product item engine
- add assembler-driven final composition
- 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
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
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
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:
PageRoleRenderPolicyBuiltSectionRuntimeIssueAssembledTemplate
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:
- contracts
- compatibility adapter
- page resolver
- render policy resolver
- section composer
- section builders
- assembler
This keeps the runtime:
- backward compatible
- optional-section capable
- future-extensible
- independent from page indexes
and leaves minimal architectural ambiguity for implementation.