1. Purpose
A Heading is a structural grouping element inside an Estimate — used to organise Items into the client-schedule structure (e.g., “01. Preliminaries”, “02. Earthworks”, “03. Concrete Works”). Headings nest self-referentially up to 5 levels deep (BR-001), but they carry no cost of their own — cost rolls up from the Items beneath them.
Headings are about structure (how the schedule is presented); Items are about cost build-up. The two depth caps are independent: a Heading can be 3 deep and still contain Items nested another 5 deep underneath.
Analogy (glossary cooking model): Headings are the menu sections — “Starters”, “Mains”, “Desserts” — they organise the menu but don’t cost anything in themselves.
2. Attributes
| Attribute | Type | Required | Default | Notes |
|---|---|---|---|---|
id | UUID | ✅ | generated | System-managed |
title | string (short text) | ✅ | — | The Heading text shown in the tree and the published schedule (e.g., “03. Concrete Works”) |
parent_type | enum | ✅ | — | Estimate (root-level Heading) OR Heading (nested Heading) |
parent_id | UUID | ✅ | — | Points at Estimate or parent Heading depending on parent_type |
display_order | integer | ✅ | auto | Order within parent |
notes | long text | ❌ | null | Internal notes — free text, not shown to client |
created_at / updated_at | timestamp | ✅ | system | Audit |
created_by / updated_by | User ref | ✅ | system | Audit |
Derived attributes (computed, not stored) — see §8.
Note on formatting: Heading appearance is not stored on the Heading itself. In the working view, headings render with depth-based default styling (non-editable). In the Publisher output, heading appearance is driven by the Publisher branding config (admin-level). There is no per-Heading style override.
3. Hierarchy & nesting
A Heading sits in one of two positions:
- Root-level Heading —
parent_type = Estimate. Direct child of an Estimate. The top of a sub-tree. - Nested Heading —
parent_type = Heading. Child of another Heading. Used for sub-grouping within a section.
Depth cap (BR-001): the total Heading depth from root to leaf is capped at 5 levels. This is intentional — deep nesting becomes unreviewable in client schedules. Cap value (5) is 🟡 working pending Oxcon confirmation of realistic max in practice.
Children: a Heading can contain Headings (sub-grouping) and Items (cost lines). Both are unbounded in count; ordering within the Heading is governed by each child’s display_order.
Item nesting under a Heading: Items themselves nest up to 5 levels (BR-002), independent of the Heading depth. A Heading at depth 3 can contain an Item that nests another 5 levels of sub-Items.
4. Relationships
Inbound (things referring to Heading)
| From | Cardinality | Notes |
|---|---|---|
| Heading (parent) | Heading 1:M Heading | Self-referential nesting, depth ≤ 5 (BR-001) |
| Item | Item M:0..1 Heading | An Item’s parent is either a Heading OR another Item (XOR per BR-003/004). Schedule Items specifically must sit at the top of their branch (so their parent is a Heading); Normal Items can have either parent type |
Outbound (things Heading references)
| To | Cardinality | Required | Notes |
|---|---|---|---|
| Estimate | Heading M:0..1 Estimate | conditional | Required if parent_type = Estimate (root-level Heading) |
| Heading (parent) | Heading M:0..1 Heading | conditional | Required if parent_type = Heading (nested Heading) |
| Heading (children) | Heading 1:M Heading | ❌ | Optional sub-Headings |
| Item (children) | Heading 1:M Item | ❌ | Optional Items at this level |
Constraint: exactly one of (Estimate parent, Heading parent) must be set — never both, never neither (XOR enforced by parent_type).
5. Validation / Invariants
Rules that must hold at all times:
- Parent exactly one type.
parent_typemust beEstimateorHeading, never both, never null. - Depth cap. Heading depth (count of Heading ancestors + self) must be ≤ 5 (BR-001). Validated on create and on move.
- Tree integrity. No circular parent chains.
- Title required.
titlecannot be null or empty. - Cascade delete. When a Heading is deleted, all child Headings and Items beneath it are deleted (with confirmation in UI). Cascade traverses the full sub-tree.
- Move semantics. Moving a Heading (changing
parent_id/parent_type) re-validates depth cap on the new position; rejected if any descendant would exceed depth 5 in the new location.
6. Lifecycle States
Headings have no formal state machine. They exist as long as their parent Estimate exists. State semantics live on the Estimate (In Progress / Reviewed / Submitted / Archived) and cascade implicitly — a Heading inside a Submitted Estimate is read-only; inside an Archived Estimate it is read-only and hidden by default.
7. Derived / Computed Attributes
| Attribute | Derivation | Notes |
|---|---|---|
depth | Count of Heading ancestors + 1 | For depth-cap validation; root-level = 1 |
total_cost | Sum of total_cost across all child Items + nested Headings (recursive) | Recomputed when any descendant Item changes |
item_count | Count of all Items in the sub-tree (recursive) | Used for UI summary |
has_schedule_descendants | True if any descendant Item is a Schedule Item | Used by BR-003/004 enforcement and Indirect derivation |
8. Worked Examples
Example A — Simple two-level Heading structure
A standard schedule with top-level WBS sections and sub-sections:
Estimate: "Office Refurb — Base Case"
Heading "01. Preliminaries" (depth=1)
Heading "01.1 Site setup" (depth=2)
Item "Site office" (Schedule)
Item "Safety hoardings" (Schedule)
Heading "01.2 Mobilisation" (depth=2)
Item "Plant mobilisation" (Schedule)
Heading "02. Demolition" (depth=1)
Item "Strip out fitout" (Schedule)
Item "Remove ceiling tiles" (Schedule)
Heading "03. Concrete Works" (depth=1)
Heading "03.1 Foundations" (depth=2)
Heading "03.1.1 Pile caps" (depth=3)
Item "Concrete pour — pile caps" (Schedule)
Derived:
03. Concrete Works.depth = 103.1.1 Pile caps.depth = 3 (well under cap of 5)03. Concrete Works.total_cost = sum of all Items beneath it (recursive)03. Concrete Works.has_schedule_descendants = true
Example B — Heading depth cap rejection
Attempting to nest a 6th-level Heading:
Heading "A" (depth=1)
Heading "A.1" (depth=2)
Heading "A.1.1" (depth=3)
Heading "A.1.1.1" (depth=4)
Heading "A.1.1.1.1" (depth=5)
Heading "A.1.1.1.1.1" (depth=6) ← REJECTED
Validation error: "Heading depth cap exceeded (max 5 levels)."
Example C — Items nest deeper than Headings under them
A Heading at depth 3 can contain an Item that nests sub-Items independently:
Heading "03.1.1 Pile caps" (Heading depth=3)
Item "Concrete pile cap" (Item depth=1)
Sub-Item "Concrete supply" (Item depth=2)
Sub-Item "Reinforcement" (Item depth=2)
Sub-Item "Steel mesh" (Item depth=3)
Sub-Item "Stirrups" (Item depth=3)
Sub-Item "Stirrup labour" (Item depth=4)
Heading depth (3) and Item depth (4) are tracked independently; both are within their respective caps of 5.