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

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:

  • 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:

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:

  • 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:

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/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

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

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

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:

  • 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.