1. Purpose

A Reference Rate is an admin-defined, manual benchmark unit rate for a category of work, used by Anomaly Review as the anchor layer for rate-based outlier detection. It is not used in cost build-up — purely a sanity-check mechanism to flag estimating outliers before Commercials.

Not versioned: Reference Rates store a single current rate per Reference Rate. When edited, the old value is overwritten. No rate history is tracked on Reference Rates themselves.

Reference Rates sit in Layer 2 of Anomaly Review’s three-layer model:

  • Layer 1: deterministic rule checks (Alpha)
  • Layer 2: Reference Rate checks — rules-first match (Alpha · BR-145), AI-assisted match (Release · BR-146). Reference Rates are manual, admin-defined benchmarks, not derived from historical data.
  • Layer 3: historical AI comparison (Release · distinct mechanism using archived Price Books and completed estimates, not a Reference Rate versioning feature)

Analogy (glossary cooking model): Reference Rates are expected ingredient costs — a benchmark against which to sanity-check the rates being used in the current estimate.


2. Attributes

AttributeTypeRequiredDefaultNotes
idUUIDgeneratedSystem-managed
namestring (text)e.g., “Reinforced concrete works”, “Bored piles”
unitUnit refOne of the managed Unit library (e.g., , lm, ). See Unit glossary
ratenumber (decimal)conditionalnullSingle point-value rate. Mutually exclusive with rate_min and rate_max; use this or the range pair, not both
rate_minnumber (decimal)conditionalnullMinimum of expected range. Must be paired with rate_max if either is used
rate_maxnumber (decimal)conditionalnullMaximum of expected range. Must be paired with rate_min if either is used
tolerancenumber (percent)nullOptional tolerance around point rate (e.g., 5 = ±5%). Only valid if rate is set, not with range
categorization_option_idCategorization Option refnullOptional scope — when set, this Reference Rate matches Items carrying this specific Categorization Option (in addition to Unit match)
description / noteslong textnullOptional context — e.g., “Reinforced concrete, 25–30 MPa, inland NZ”, “Includes cartage”
created_at / updated_attimestampsystemAudit
created_by / updated_byUser refsystemAudit

3. Rate specification (point vs range)

Reference Rates support two modes — exactly one must be supplied:

Point rate + tolerance

  • rate set (e.g., 500.00)
  • tolerance optional (e.g., 5 = ±5%, so 475–525 is accepted)
  • If no tolerance supplied, only exact matches (or within system precision epsilon) flag

Range (min / max)

  • rate_min and rate_max both set (e.g., 450.00 and 550.00)
  • tolerance must be null
  • Any Item rate outside [min, max] inclusive flags an anomaly

Validation invariant:

  • Exactly one of: (1) rate + optional tolerance, or (2) rate_min + rate_max
  • Cannot mix modes (e.g., rate + rate_min)

4. Matching logic (two-tier)

Alpha — Rules-first (BR-145)

Anomaly Review matches an Item against a Reference Rate when:

  1. Unit match: Item’s Unit = Reference Rate’s Unit (required)
  2. Categorization scope (if set): If the Reference Rate has a categorization_option_id, the Item must carry that Categorization Option (tagged)

If both conditions hold and the Item’s unit rate falls outside the Reference Rate’s range/tolerance, a firm anomaly flag is raised with a deterministic explanation: “Unit rate $620/m³ exceeds Reference Rate range $450–550/m³”.

Multiple Reference Rates can match one Item (different scopes, different categories); all are surfaced.

Release — AI-assisted (BR-146)

When an Item isn’t matched by BR-145 (typically untagged, missing Categorization), the AI layer:

  • Semantically matches the Item’s description against the Reference Rate library
  • High-confidence matches → raise a firm flag (as if BR-145 matched)
  • Low-confidence matches → surface as “possible anomalies” for estimator acceptance/dismissal
  • Feedback improves future matching
  • AI provides plain-English explanation cross-referencing similar items, rates, and project context

5. Relationships

Inbound (things referring to Reference Rate)

None — Reference Rates are a lookup; Anomaly Review references them, but no entity owns or links to a Reference Rate persistently. The match is computed at review time.

Outbound (things Reference Rate references)

ToCardinalityRequiredNotes
UnitReference Rate M:1 UnitEvery Reference Rate has a Unit (e.g., m³, lm, m²)
Categorization OptionReference Rate M:0..1 Categorization OptionOptional scope; when set, Reference Rate only matches Items tagged with that Option

6. Validation / Invariants

Rules that must hold at all times:

  1. Unit mandatory. unit cannot be null (BR-144).
  2. Rate specification exactly one. Either (rate + optional tolerance) or (rate_min + rate_max), not both, never neither.
  3. Range ordering. If using range, rate_min ≤ rate_max.
  4. Tolerance only with point rate. tolerance can only be set if rate is set; ignored or null if using range.
  5. Categorization scope optional. categorization_option_id is optional (null = matches all Items with matching Unit, regardless of tags).
  6. Scope must be valid. If categorization_option_id is set, it must reference an existing Categorization Option; the Option’s parent Categorization’s scope must include Item (since Reference Rates are used in Item matching).

7. Derived / Computed Attributes

AttributeDerivationNotes
effective_minrate_min if range, else rate - (rate × tolerance / 100) if tolerance, else rateUpper and lower bounds; used by Anomaly Review to determine in-range
effective_maxrate_max if range, else rate + (rate × tolerance / 100) if tolerance, else rate
is_scopedcategorization_option_id is not nullUI badge: “Scoped to [Category]” if true

8. Admin management & seeding

Who can manage Reference Rates: Admin and Lead Estimator (see roles-permissions.md).

Post-migration seeding (workflow idea): Benchmark’s archived Price Book contains historical rate ranges (min/max from historical transactions per category). A post-migration seeding workflow could:

  1. Extract category + min/max pairs from Benchmark’s Price Book
  2. Create Reference Rate records in bulk, tagged with the source category’s Categorization Option
  3. Flag for admin review before activation

9. Worked Examples

Example A — Reinforced concrete works (range, scoped)

Reference Rate:
  name: "Reinforced concrete works"
  unit: m³
  rate_min: 450.00
  rate_max: 550.00
  tolerance: null
  categorization_option_id: <Categorization Option "Concrete Works">
  description: "In-situ reinforced concrete, 25–30 MPa, placed and finished. Inland NZ"

Anomaly Review match (Alpha, BR-145):
  Item "Concrete pour — foundation slab":
    unit: m³
    quantity: 85
    Item unit rate: $620/m³
    Item Categorization tags: ["Concrete Works", "Foundation"]
    
  Match result:
    ✅ Unit matches (m³ = m³)
    ✅ Categorization matches (Item has "Concrete Works" tag)
    ⚠️ Rate out of range: $620/m³ exceeds max $550/m³
    
  Flag: "Unit rate $620/m³ exceeds Reference Rate range $450–$550/m³ for Reinforced concrete works"

Example B — Bored piles (range, no categorization scope)

Reference Rate:
  name: "Bored piles"
  unit: lm (linear metre)
  rate_min: 650.00
  rate_max: 850.00
  tolerance: null
  categorization_option_id: null
  description: "Bored piles, 600mm–1000mm diameter, 0–30m depth, inland NZ"

Anomaly Review match (Alpha, BR-145):
  Item "Piles — dock structure":
    unit: lm
    quantity: 230
    Item unit rate: $720/lm
    Item Categorization tags: ["Structural", "Piling"]
    
  Match result:
    ✅ Unit matches (lm = lm)
    ✅ No Categorization scope set → Unit match alone is sufficient (Item tags irrelevant)
    ✅ Rate in range: $720/lm within $650–$850/lm
    
  Flag: None. Item passes Reference Rate check.

Example C — AI-assisted semantic match (Release, BR-146)

This example shows the AI layer kicking in because the Item is missing the Categorization scope the Reference Rate requires — so the deterministic BR-145 path doesn’t fire.

Reference Rate:
  name: "Concrete deck pour — bridge works"
  unit: m²
  rate_min: 380.00
  rate_max: 450.00
  tolerance: null
  categorization_option_id: <CatOption "Bridge works" (Trade category)>

Item (untagged for Trade):
  description: "Pour concrete for bridge deck"
  unit: m²
  quantity: 300
  Item unit rate: $320/m²
  Item Categorization tags: []   (no "Bridge works" tag applied)

Anomaly Review match (Release, BR-146):
  ⚠️ Not matched by BR-145: Unit matches but the Reference Rate is scoped to
     Categorization "Bridge works", which this Item does not carry.
  → AI layer attempts semantic match:
      Item description "Pour concrete for bridge deck" →
      Candidate Reference Rate "Concrete deck pour — bridge works" (high confidence, 0.87)

    High-confidence match → firm flag:
    "AI matched to 'Concrete deck pour — bridge works' ($380–$450/m²).
     Item rate $320/m² is below range. Similar items cost $400–$420/m²."

  Estimator can:
    ✅ Accept the flag (retain it for review; may also tag the Item with
       "Bridge works" so future matches hit BR-145 deterministically)
    ❌ Dismiss it (feedback improves future matching)

Note: if the Reference Rate above had categorization_option_id: null (unscoped), Unit alone would match and BR-145 would flag the Item deterministically — BR-146 only handles the cases BR-145 cannot reach.