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

AttributeTypeRequiredDefaultNotes
idUUIDgeneratedSystem-managed
titlestring (short text)The Heading text shown in the tree and the published schedule (e.g., “03. Concrete Works”)
parent_typeenumEstimate (root-level Heading) OR Heading (nested Heading)
parent_idUUIDPoints at Estimate or parent Heading depending on parent_type
display_orderintegerautoOrder within parent
noteslong textnullInternal notes — free text, not shown to client
created_at / updated_attimestampsystemAudit
created_by / updated_byUser refsystemAudit

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 Headingparent_type = Estimate. Direct child of an Estimate. The top of a sub-tree.
  • Nested Headingparent_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)

FromCardinalityNotes
Heading (parent)Heading 1:M HeadingSelf-referential nesting, depth ≤ 5 (BR-001)
ItemItem M:0..1 HeadingAn 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)

ToCardinalityRequiredNotes
EstimateHeading M:0..1 EstimateconditionalRequired if parent_type = Estimate (root-level Heading)
Heading (parent)Heading M:0..1 HeadingconditionalRequired if parent_type = Heading (nested Heading)
Heading (children)Heading 1:M HeadingOptional sub-Headings
Item (children)Heading 1:M ItemOptional 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:

  1. Parent exactly one type. parent_type must be Estimate or Heading, never both, never null.
  2. Depth cap. Heading depth (count of Heading ancestors + self) must be ≤ 5 (BR-001). Validated on create and on move.
  3. Tree integrity. No circular parent chains.
  4. Title required. title cannot be null or empty.
  5. 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.
  6. 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

AttributeDerivationNotes
depthCount of Heading ancestors + 1For depth-cap validation; root-level = 1
total_costSum of total_cost across all child Items + nested Headings (recursive)Recomputed when any descendant Item changes
item_countCount of all Items in the sub-tree (recursive)Used for UI summary
has_schedule_descendantsTrue if any descendant Item is a Schedule ItemUsed 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 = 1
  • 03.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.