---
title: SAIF.Framework.Common business rules
summary: Enumerated business rules with per-rule trigger, enforcement, and outcomes.
status: draft
owner: saif-docgen
last_reviewed: 2026-09-15
library: SAIF.Framework.Common
reference_type: package
---

# SAIF.Framework.Common business rules

This page captures business rules enforced by this library.

## BR-001 A date range is valid only when BeginDate is less than or equal to EndDate

| Field | Value |
| --- | --- |
| **Statement** | A date range is valid only when `BeginDate` is less than or equal to `EndDate`. |
| **Trigger** | Any range validity test |
| **Enforcement** | `DateRangeExtensions.IsValidRange(IDateRange)` — returns `false` for an inverted range. |
| **Outcome** | Callers treat an invalid range as non-overlapping / non-current. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-002 A range is "open ended" when BeginDate is DateTime.MinValue or EndDate is DateTime.MaxValue

| Field | Value |
| --- | --- |
| **Statement** | A range is "open ended" when `BeginDate` is `DateTime.MinValue` or `EndDate` is `DateTime.MaxValue`; these sentinels replace nulls throughout the library. |
| **Trigger** | Open-ended / sentinel test |
| **Enforcement** | `DateRangeExtensions.IsOpenEnded(IDateRange)`, `DateExtensions.IsMinOrMax(DateTime)` |
| **Outcome** | Returns `true`, marking the range as unbounded on that side. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs, Common/SAIF.Common/Extensions/DateExtensions.cs` |

---

## BR-003 Two ranges with identical begin and end dates overlap only when they are not a single-day

| Field | Value |
| --- | --- |
| **Statement** | Two ranges with identical begin and end dates overlap only when they are not a single-day (zero-length) range. |
| **Trigger** | Pairwise overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverlapping` |
| **Outcome** | A degenerate single-day range equal to another does not report as overlapping. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-004 Whether shared endpoints count as an overlap is caller-selectable

| Field | Value |
| --- | --- |
| **Statement** | Whether shared endpoints count as an overlap is caller-selectable: `inAllowSameDateOverlap = true` uses `Exclusive` comparison, `false` (default) uses `Inclusive`. |
| **Trigger** | Pairwise overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverlapping` |
| **Outcome** | Endpoint-touching ranges report overlap by default. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-005 A set of fewer than two date ranges is always contiguous

| Field | Value |
| --- | --- |
| **Statement** | A set of fewer than two date ranges is always contiguous. |
| **Trigger** | Contiguity test |
| **Enforcement** | `DateRangeExtensions.IsContiguous` |
| **Outcome** | Returns `true` without comparison. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-006 Contiguity is evaluated on ranges sorted ascending by BeginDate, and fails on the first violating pair

| Field | Value |
| --- | --- |
| **Statement** | Contiguity is evaluated on ranges sorted ascending by `BeginDate`, and fails on the first violating pair. |
| **Trigger** | Contiguity test |
| **Enforcement** | `DateRangeExtensions.IsContiguous` |
| **Outcome** | Returns `false` immediately. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-007 The default contiguity mode is StartIsNextDay

| Field | Value |
| --- | --- |
| **Statement** | The default contiguity mode is `StartIsNextDay`: each range must begin exactly one day after the previous range ends. `StartMatchesEnd` requires exact endpoint equality; `AllowGaps` only forbids going backwards. |
| **Trigger** | Contiguity test |
| **Enforcement** | `DateRangeExtensions.IsContiguous(IEnumerable<IDateRange>, ContiguousOptions)` |
| **Outcome** | Mode-specific pass/fail. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-008 A set of fewer than two date ranges never self-overlaps

| Field | Value |
| --- | --- |
| **Statement** | A set of fewer than two date ranges never self-overlaps. |
| **Trigger** | Collection overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverLapping` |
| **Outcome** | Returns `false` without comparison. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-009 A range whose begin and end are both sentinel values is treated as a null date and ignored during collection overlap evaluation

| Field | Value |
| --- | --- |
| **Statement** | A range whose begin **and** end are both sentinel values is treated as a null date and ignored during collection overlap evaluation. |
| **Trigger** | Collection overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverLapping` |
| **Outcome** | The range is skipped. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-010 An open-ended predecessor followed by any range with a real begin date is always an overlap

| Field | Value |
| --- | --- |
| **Statement** | An open-ended predecessor followed by any range with a real begin date is always an overlap. |
| **Trigger** | Collection overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverLapping` |
| **Outcome** | Returns `true` immediately. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-011 The default collection-overlap mode is DatesMayNotMatch

| Field | Value |
| --- | --- |
| **Statement** | The default collection-overlap mode is `DatesMayNotMatch`: a range beginning on or before the previous range's end date is an overlap. `DatesMayMatch` requires a strict `<`. |
| **Trigger** | Collection overlap test |
| **Enforcement** | `DateRangeExtensions.IsOverLapping(IEnumerable<IDateRange>, OverlappingOptions)` |
| **Outcome** | Mode-specific overlap verdict. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-012 The overlap of two nullable-end ranges is classified into exactly one of ten enumerated states

| Field | Value |
| --- | --- |
| **Statement** | The overlap of two nullable-end ranges is classified into exactly one of ten enumerated states, defaulting to `NoOverlap` when no condition matches. |
| **Trigger** | Categorical overlap classification |
| **Enforcement** | `NullableDateRange.GetOverlapState` |
| **Outcome** | A single `DateRangeOverlapResult` value. |
| **Source** | `Common/SAIF.Common/Classes/NullableDateRange.cs, Common/SAIF.Common/Enums/DateRangeOverlapResult.cs` |

---

## BR-014 Title casing of a null or empty string is a no-op

| Field | Value |
| --- | --- |
| **Statement** | Title casing of a null or empty string is a no-op. |
| **Trigger** | Title-case request |
| **Enforcement** | `TitleCase.ToTitleCase` |
| **Outcome** | The input is returned unchanged. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-015 A word that already contains both uppercase and lowercase characters is assumed intentionally cased

| Field | Value |
| --- | --- |
| **Statement** | A word that already contains both uppercase and lowercase characters is assumed intentionally cased and is never re-cased. |
| **Trigger** | Per-word title casing |
| **Enforcement** | `TitleCase.TitleCaseSingleWord` |
| **Outcome** | The word is returned unchanged. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-016 Words in the approved acronym list

| Field | Value |
| --- | --- |
| **Statement** | Words in the approved acronym list (`SAIF`, `KPD`, `BW`, `AIC`, `KBI`, `JBL`, `PRL`, `USI`, `ISU`, `AAAP`, `OEC`, `LLC`, `LLP`, `DDS`, `MD`, `DC`, `PPC`, `JV`, `JD`, `DMD`, `PC`, `LMT`, `LLLP`, `LTD`, `ND`, `OD`, `OT`, `PT`, `CMT`, `PCP`, `GP`, `VP`, `CEO`, `CFO`, `CST`, `AG`, `RC`, `MJ`, `PMS`, `II`, `III`, `IV`, `AC`, `NE`, `NW`, `SE`, `SW`, `PO`, `USA`, `CA`, `WA`, `BPOE`, `BHG`, `KOA`, `OHSU`, `AOI`, `TNT`, `MCO`, `GSA`, `AAA`, `IHC`, `ODS`, `OGSERP`, `CC`) are rendered fully uppercase. |
| **Trigger** | Per-word title casing |
| **Enforcement** | `TitleCase.UpperCaseWordList` + `TitleCaseSingleWord` |
| **Outcome** | The word is uppercased. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-017 Minor words (is, in, the, an, of, on, and, de) are rendered fully lowercase

| Field | Value |
| --- | --- |
| **Statement** | Minor words (`is, in, the, an, of, on, and, de`) are rendered fully lowercase. |
| **Trigger** | Per-word title casing |
| **Enforcement** | `TitleCase.LowerCaseWordList` + `TitleCaseSingleWord` |
| **Outcome** | The word is lowercased. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-018 Known mixed-case words (PhD, LaCombe) keep their exact registered casing

| Field | Value |
| --- | --- |
| **Statement** | Known mixed-case words (`PhD`, `LaCombe`) keep their exact registered casing; known mixed-case prefixes (`Mc`, `O'`) cause the remainder of the word to be proper-cased (`McMahon`, `O'Brien`). |
| **Trigger** | Per-word title casing |
| **Enforcement** | `TitleCase.MixedCaseWordList`, `TitleCase.MixedCasePrefixList` + `TitleCaseSingleWord` |
| **Outcome** | Canonical mixed casing is applied. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-019 A numeric word with an ordinal suffix (1st, 2nd, 3rd, 4th) keeps the suffix lowercase

| Field | Value |
| --- | --- |
| **Statement** | A numeric word with an ordinal suffix (`1st`, `2nd`, `3rd`, `4th`) keeps the suffix lowercase. |
| **Trigger** | Per-word title casing |
| **Enforcement** | `TitleCase.HasNumberSuffix` / `GetNumberSuffixParts` / `NumberSuffixes` |
| **Outcome** | The ordinal is left lowercase. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-020 A trailing possessive 's is normalized to lowercase after casing

| Field | Value |
| --- | --- |
| **Statement** | A trailing possessive `'s` is normalized to lowercase after casing. |
| **Trigger** | Final title-case pass |
| **Enforcement** | `TitleCase.ApplyExactSuffixList` / `MixedCaseSuffixList` |
| **Outcome** | The suffix is re-cased. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-021 The first character of any title-cased result is always uppercase, regardless of every other rule

| Field | Value |
| --- | --- |
| **Statement** | The first character of any title-cased result is always uppercase, regardless of every other rule. |
| **Trigger** | Final title-case pass |
| **Enforcement** | `TitleCase.ToTitleCase` (final statement) |
| **Outcome** | First letter uppercased. |
| **Source** | `Common/SAIF.Common/Utility/TitleCase.cs` |

---

## BR-022 A Julian date must contain at least 4 digits

| Field | Value |
| --- | --- |
| **Statement** | A Julian date must contain at least 4 digits. |
| **Trigger** | Julian validation |
| **Enforcement** | `Julian.IsValid(int, out string)` |
| **Outcome** | Returns `false` with message "The julian must contain at least 4 numbers"; `ToDate` escalates to `ApplicationException`. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-023 A Julian string that cannot be converted to Int32 is invalid

| Field | Value |
| --- | --- |
| **Statement** | A Julian string that cannot be converted to `Int32` is invalid. |
| **Trigger** | Julian validation |
| **Enforcement** | `Julian.IsValid(string, out string)` |
| **Outcome** | Returns `false` (exception swallowed); `ToDate` throws `ApplicationException` with an empty message. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-024 When no millennium is supplied (or it is below 1000), the millennium is derived from the current system year's first digit x 1000, and a 1- or 2-digit Julian year is offset by it

| Field | Value |
| --- | --- |
| **Statement** | When no millennium is supplied (or it is below 1000), the millennium is derived from the current system year's first digit × 1000, and a 1- or 2-digit Julian year is offset by it. |
| **Trigger** | Julian year resolution |
| **Enforcement** | `Julian.GetYear` |
| **Outcome** | A 4-digit calendar year. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-025 Leap-year status is determined using the Julian calendar (System.Globalization.JulianCalendar), not the Gregorian calendar

| Field | Value |
| --- | --- |
| **Statement** | Leap-year status is determined using the **Julian** calendar (`System.Globalization.JulianCalendar`), not the Gregorian calendar. |
| **Trigger** | Julian year resolution |
| **Enforcement** | `Julian.GetYear` |
| **Outcome** | `isLeapYear` flag; drives the 365/366 day cap. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-026 The day-of-year portion of a Julian date must not exceed 365, or 366 in a leap year

| Field | Value |
| --- | --- |
| **Statement** | The day-of-year portion of a Julian date must not exceed 365, or 366 in a leap year. |
| **Trigger** | Julian decode |
| **Enforcement** | `Julian.ToDate` |
| **Outcome** | Throws `ApplicationException("The julian day must be < {n}.")`. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-027 A Julian string is encoded as the year (2-digit by default, 4-digit on request) followed by the zero-padded 3-digit day-of-year

| Field | Value |
| --- | --- |
| **Statement** | A Julian string is encoded as the year (2-digit by default, 4-digit on request) followed by the zero-padded 3-digit day-of-year. |
| **Trigger** | Julian encode |
| **Enforcement** | `Julian.ToJulian` |
| **Outcome** | A Julian-format string. |
| **Source** | `Common/SAIF.Common/Classes/Julian.cs` |

---

## BR-028 In EDI date parsing, a 6-character date is a 2-digit year and is always treated as 21st century

| Field | Value |
| --- | --- |
| **Statement** | In EDI date parsing, a 6-character date is a 2-digit year and is always treated as 21st century (prefixed with `"20"`); no 20th-century handling exists. |
| **Trigger** | EDI date parse |
| **Enforcement** | `DateMaker.MakeDateTime` |
| **Outcome** | The year resolves to 20xx. |
| **Source** | `Common/SAIF.Common/Classes/DateMaker.cs` |

---

## BR-029 A short EDI time string is right-padded with zeros to hh24miss

| Field | Value |
| --- | --- |
| **Statement** | A short EDI time string is right-padded with zeros to `hh24miss`; the default time when none is supplied is midnight (`"000000"`). |
| **Trigger** | EDI date parse |
| **Enforcement** | `DateMaker.MakeDateTime` |
| **Outcome** | Missing time components default to zero. |
| **Source** | `Common/SAIF.Common/Classes/DateMaker.cs` |

---

## BR-030 EDI file date strings use the yyyyMMdd format and the - separator between a start and an end date

| Field | Value |
| --- | --- |
| **Statement** | EDI file date strings use the `yyyyMMdd` format and the `-` separator between a start and an end date. |
| **Trigger** | EDI date format / split |
| **Enforcement** | `DateMaker.DateFormat`, `DateMaker.DateSeperator`, `EdiFileDateString`, `ExtractDateRange` |
| **Outcome** | `yyyymmdd` or `yyyymmdd-yyyymmdd`. |
| **Source** | `Common/SAIF.Common/Classes/DateMaker.cs` |

---

## BR-031 Parsing a malformed date-range string never throws — it silently yields the unbounded default range

| Field | Value |
| --- | --- |
| **Statement** | Parsing a malformed date-range string never throws — it silently yields the unbounded default range. |
| **Trigger** | `DateRange.Parse` |
| **Enforcement** | `DateRange.Parse` (catch-all `catch (Exception)`) |
| **Outcome** | Returns `new DateRange()` = `MinValue`..`MaxValue`. |
| **Source** | `Common/SAIF.Common/Classes/DateRange.cs` |

---

## BR-032 A DateRange constructed with no end date is unbounded forward

| Field | Value |
| --- | --- |
| **Statement** | A `DateRange` constructed with no end date is unbounded forward: `EndDate` defaults to `DateTime.MaxValue`. A `DateRange` with no dates at all is `MinValue`..`MaxValue`. |
| **Trigger** | `DateRange` construction |
| **Enforcement** | `DateRange()` / `DateRange(DateTime)` / `DateRange.Empty` |
| **Outcome** | Sentinel endpoints assigned. |
| **Source** | `Common/SAIF.Common/Classes/DateRange.cs` |

---

## BR-033 An end date supplied as DateTime.MinValue is reinterpreted as no end date and coerced to DateTime.MaxValue

| Field | Value |
| --- | --- |
| **Statement** | An end date supplied as `DateTime.MinValue` is reinterpreted as "no end date" and coerced to `DateTime.MaxValue`. |
| **Trigger** | `DateRange(DateTime, DateTime)` |
| **Enforcement** | `DateRange(DateTime beginDate, DateTime endDate)` |
| **Outcome** | `EndDate` becomes `MaxValue`. |
| **Source** | `Common/SAIF.Common/Classes/DateRange.cs` |

---

## BR-034 Two date ranges are equal if and only if both BeginDate and EndDate match

| Field | Value |
| --- | --- |
| **Statement** | Two date ranges are equal if and only if both `BeginDate` and `EndDate` match; equality against a non-`IDateRange` object is always false, and `IsEqual` treats a null counterpart as unequal. |
| **Trigger** | Range comparison |
| **Enforcement** | `DateRange.operator ==` / `!=` / `Equals`, `DateRangeExtensions.IsEqual` |
| **Outcome** | Boolean equality verdict. |
| **Source** | `Common/SAIF.Common/Classes/DateRange.cs, Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-035 As of filtering of a range collection selects ranges containing the supplied date under the chosen inclusivity mode

| Field | Value |
| --- | --- |
| **Statement** | "As of" filtering of a range collection selects ranges containing the supplied date under the chosen inclusivity mode. |
| **Trigger** | `AsOf` / `IsInRange` |
| **Enforcement** | `DateRangeExtensions.AsOf<T>`, `DateRangeExtensions.IsInRange` |
| **Outcome** | A filtered deferred sequence. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-036 The default date-in-range inclusivity is FirstOnly — begin date inclusive, end date exclusive

| Field | Value |
| --- | --- |
| **Statement** | The default date-in-range inclusivity is `FirstOnly` — begin date inclusive, end date exclusive. |
| **Trigger** | `IsInRange` / `IsBetween` |
| **Enforcement** | `DateRangeExtensions.IsInRange`, `IComparableExtensions.IsBetween` |
| **Outcome** | Begin-inclusive, end-exclusive membership. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs, Common/SAIF.Common/Extensions/IComparableExtensions.cs` |

---

## BR-037 A null date is never in range

| Field | Value |
| --- | --- |
| **Statement** | A null date is never in range. |
| **Trigger** | `IsInRange(DateTime?, IncludeComparisons)` |
| **Enforcement** | `DateRangeExtensions.IsInRange(IDateRange, DateTime?, IncludeComparisons)` |
| **Outcome** | Returns `false`. |
| **Source** | `Common/SAIF.Common/Extensions/DateRangeExtensions.cs` |

---

## BR-038 A guard call must itself supply a non-blank parameter name

| Field | Value |
| --- | --- |
| **Statement** | A guard call must itself supply a non-blank parameter name. |
| **Trigger** | Guard invocation |
| **Enforcement** | `Enforce.ArgumentNotNull` |
| **Outcome** | Throws `ArgumentNullException("parameterName")`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs` |

---

## BR-039 A guarded argument must not be null

| Field | Value |
| --- | --- |
| **Statement** | A guarded argument must not be null. |
| **Trigger** | Guard invocation |
| **Enforcement** | `Enforce.ArgumentNotNull` (and every method that calls it) |
| **Outcome** | Throws `ArgumentNullException(parameterName)`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs` |

---

## BR-040 A guarded string argument must not be empty

| Field | Value |
| --- | --- |
| **Statement** | A guarded string argument must not be empty. |
| **Trigger** | Guard invocation |
| **Enforcement** | `Enforce.ArgumentNotNullOrEmpty` |
| **Outcome** | Throws `ArgumentException("Parameter ({name}) cannot be null or empty.")`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs` |

---

## BR-041 A guarded string argument must not be all white space

| Field | Value |
| --- | --- |
| **Statement** | A guarded string argument must not be all white space. |
| **Trigger** | Guard invocation |
| **Enforcement** | `Enforce.ArgumentNotNullOrWhiteSpace` |
| **Outcome** | Throws `ArgumentException("Parameter ({name}) cannot be null or white space.")`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs` |

---

## BR-042 A guarded state value (as opposed to an argument) must not be null

| Field | Value |
| --- | --- |
| **Statement** | A guarded state value (as opposed to an argument) must not be null; the violation is an `InvalidOperationException`, not an `ArgumentNullException`. |
| **Trigger** | State guard |
| **Enforcement** | `Enforce.NotNull<T>` → `GenericExtensions.IfNullThrow<T,TException>` |
| **Outcome** | Throws `InvalidOperationException("The type {FullName} cannot be null.")`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs, Common/SAIF.Common/Extensions/GenericExtensions.cs` |

---

## BR-043 A caller-supplied boolean or Func<bool> condition must evaluate true, or the caller-specified exception type/instance is thrown

| Field | Value |
| --- | --- |
| **Statement** | A caller-supplied boolean or `Func<bool>` condition must evaluate true, or the caller-specified exception type/instance is thrown. |
| **Trigger** | Condition guard |
| **Enforcement** | `Enforce.True` (4 overloads) |
| **Outcome** | Throws `T` or the supplied `Exception`. |
| **Source** | `Common/SAIF.Common/Classes/Enforce.cs` |

---

## BR-044 A SaifValidationException always exposes a non-null IValidationResult

| Field | Value |
| --- | --- |
| **Statement** | A `SaifValidationException` always exposes a non-null `IValidationResult`. |
| **Trigger** | Validation exception construction |
| **Enforcement** | `SaifValidationException` constructors |
| **Outcome** | `ValidationResult` is never null. |
| **Source** | `Common/SAIF.Common/Classes/SaifValidationException.cs` |

---

## BR-045 A validation exception built from a bare message carries a single failure whose PropertyName is the empty string, and the two-argument overload binds the failure to a named property

| Field | Value |
| --- | --- |
| **Statement** | A validation exception built from a bare message carries a single failure whose `PropertyName` is the empty string; the two-argument overload binds the failure to a named property. |
| **Trigger** | Validation exception construction |
| **Enforcement** | `SaifValidationException.CreateValidationResult` |
| **Outcome** | A one-failure `ValidationResult`. |
| **Source** | `Common/SAIF.Common/Classes/SaifValidationException.cs` |

---

## BR-046 A ValidationResult always exposes non-null Errors and Messages collections, even when constructed from a null failure sequence

| Field | Value |
| --- | --- |
| **Statement** | A `ValidationResult` always exposes non-null `Errors` and `Messages` collections, even when constructed from a null failure sequence. |
| **Trigger** | `ValidationResult` construction |
| **Enforcement** | `ValidationResult()` / `ValidationResult(IEnumerable<ValidationFailure>)` |
| **Outcome** | Empty lists rather than nulls. |
| **Source** | `Common/SAIF.Common/Classes/ValidationResult.cs` |

---

## BR-047 A validation result is valid if and only if it holds no errors

| Field | Value |
| --- | --- |
| **Statement** | A validation result is valid if and only if it holds no errors; informational `Messages` never affect validity. |
| **Trigger** | `IsValid` read |
| **Enforcement** | `ValidationResult.IsValid` (virtual) |
| **Outcome** | `!Errors.Any()`. |
| **Source** | `Common/SAIF.Common/Classes/ValidationResult.cs` |

---

## BR-048 Each context failure condition maps to a fixed default target view name

| Field | Value |
| --- | --- |
| **Statement** | Each context failure condition maps to a fixed default target view name: `NoContext`, `InvalidContext`, `InvalidContextForUser`, `DisabledView`; a caller may override the view, and `DisabledViewException` may additionally carry the model for redisplay. |
| **Trigger** | Context exception construction |
| **Enforcement** | `NoContextException`, `InvalidContextException`, `InvalidContextForUserException`, `DisabledViewException` constructors; `ContextException.TargetView` / `Model` |
| **Outcome** | `TargetView` (and optionally `Model`) is populated for the presentation layer to route on. |
| **Source** | `Common/SAIF.Common/Classes/ContextException.cs` |

---

## BR-049 Formatting a null exception yields an empty string rather than throwing

| Field | Value |
| --- | --- |
| **Statement** | Formatting a null exception yields an empty string rather than throwing. |
| **Trigger** | Exception formatting |
| **Enforcement** | `ExceptionExtensions.GetFullFormattedException`, `GetErrorLogFormattedException` |
| **Outcome** | Returns `string.Empty`. |
| **Source** | `Common/SAIF.Common/Extensions/ExceptionExtensions.cs` |

---

## BR-050 Exception chains are reported innermost (root cause) first by default

| Field | Value |
| --- | --- |
| **Statement** | Exception chains are reported innermost (root cause) first by default. |
| **Trigger** | Exception formatting |
| **Enforcement** | `ExceptionExtensions.GetFullFormattedException(innerToOuter = true)`, `GetErrorLogFormattedException` |
| **Outcome** | The exception list is reversed before rendering. |
| **Source** | `Common/SAIF.Common/Extensions/ExceptionExtensions.cs` |

---

## BR-051 The formatted exception report renders a summary block (type, message, source) for every exception

| Field | Value |
| --- | --- |
| **Statement** | The formatted exception report renders a summary block (type, message, source) for every exception in the chain, followed by a separate stack-trace block per exception; empty `Source` and empty `StackTrace` values are omitted. |
| **Trigger** | Exception formatting |
| **Enforcement** | `ExceptionExtensions.GetFullFormattedException` |
| **Outcome** | A two-part formatted report. |
| **Source** | `Common/SAIF.Common/Extensions/ExceptionExtensions.cs` |

---

## BR-052 An error-log entry begins with the innermost exception's message, followed by the sentinel @#ELOG#@, followed by the full report, so a log reader can split summary from detail

| Field | Value |
| --- | --- |
| **Statement** | An error-log entry begins with the innermost exception's message, followed by the sentinel `@#ELOG#@`, followed by the full report — so a log reader can split summary from detail. |
| **Trigger** | Error-log formatting |
| **Enforcement** | `ExceptionExtensions.GetErrorLogFormattedException`, `ExceptionExtensions.ErrorLogMessageString` |
| **Outcome** | Sentinel-delimited log string. |
| **Source** | `Common/SAIF.Common/Extensions/ExceptionExtensions.cs` |

---

## BR-053 A string search pattern over a binary payload is encoded as ASCII, not Unicode

| Field | Value |
| --- | --- |
| **Statement** | A string search pattern over a binary payload is encoded as **ASCII**, not Unicode. |
| **Trigger** | Byte search |
| **Enforcement** | `ByteExtensions.Search(byte[], string, int, int)` |
| **Outcome** | `Encoding.ASCII.GetBytes` needle. |
| **Source** | `Common/SAIF.Common/Extensions/ByteExtensions.cs` |

---

## BR-054 A byte search returns -1 when the haystack is empty, the needle is empty, the needle is longer than the haystack, or the start offset leaves too few bytes

| Field | Value |
| --- | --- |
| **Statement** | A byte search returns `-1` when the haystack is empty, the needle is empty, the needle is longer than the haystack, or the start offset leaves too few bytes. |
| **Trigger** | Byte search |
| **Enforcement** | `ByteExtensions.Search(byte[], byte[], int, int)` |
| **Outcome** | Returns `-1` (not found). |
| **Source** | `Common/SAIF.Common/Extensions/ByteExtensions.cs` |

---

## BR-055 A byte search may be capped

| Field | Value |
| --- | --- |
| **Statement** | A byte search may be capped: when `maxBytesToSearch > 0` the scan stops once the index exceeds that cap. Header stripping caps the scan at 100 bytes by default. |
| **Trigger** | Byte search / header strip |
| **Enforcement** | `ByteExtensions.Search`, `ByteExtensions.StripHeader` |
| **Outcome** | The scan terminates early; a signature beyond the cap is reported not-found. |
| **Source** | `Common/SAIF.Common/Extensions/ByteExtensions.cs` |

---

## BR-056 A header is stripped only when the signature is found at a position greater than zero

| Field | Value |
| --- | --- |
| **Statement** | A header is stripped only when the signature is found at a position **greater than zero**; a payload already starting at its signature (or with no signature) is returned unchanged with `found = false`. |
| **Trigger** | Header strip |
| **Enforcement** | `ByteExtensions.StripHeader` |
| **Outcome** | Original or forward-skipped array, plus the `found` flag. |
| **Source** | `Common/SAIF.Common/Extensions/ByteExtensions.cs` |

---

## BR-057 Disposable cryptographic and stream resources are created inside using blocks so they are released deterministically

| Field | Value |
| --- | --- |
| **Statement** | Disposable cryptographic and stream resources are created inside `using` blocks so they are released deterministically. |
| **Trigger** | Hashing / stream materialisation |
| **Enforcement** | `ByteExtensions.GetMD5Hash`, `FileExtensions.ToByteArray` |
| **Outcome** | Resources disposed on scope exit. |
| **Source** | `Common/SAIF.Common/Extensions/ByteExtensions.cs, Common/SAIF.Common/Extensions/FileExtensions.cs` |

---

## BR-058 JSON serialization uses JavaScriptSerializer (System.Web.Extensions) and XML serialization uses XmlSerializer; serializable targets must be reference types (where T : class) except for SaveToJson<T>, which is unconstrained

| Field | Value |
| --- | --- |
| **Statement** | JSON serialization uses `JavaScriptSerializer` (`System.Web.Extensions`) and XML serialization uses `XmlSerializer`; serializable targets must be reference types (`where T : class`) except for `SaveToJson<T>`, which is unconstrained. |
| **Trigger** | Serialization |
| **Enforcement** | `Serializer` (all members), `GenericExtensions.ToJsonString` / `ToXmlString`, `DotNet40Extensions` |
| **Outcome** | Serializer selection and type constraint. |
| **Source** | `Common/SAIF.Common/Classes/Serializer.cs, Common/SAIF.Common/Extensions/GenericExtensions.cs, Common/SAIF.Common/Extensions/DotNet40Extensions.cs` |

---

## BR-059 XML deserialization that produces an incompatible type yields null rather than throwing, because the result is cast with as T

| Field | Value |
| --- | --- |
| **Statement** | XML deserialization that produces an incompatible type yields `null` rather than throwing, because the result is cast with `as T`. |
| **Trigger** | XML load |
| **Enforcement** | `Serializer.LoadFromXml<T>` |
| **Outcome** | Returns `null`. |
| **Source** | `Common/SAIF.Common/Classes/Serializer.cs` |

---

## BR-060 Serialized output written to a file is explicitly flushed before the writer is disposed

| Field | Value |
| --- | --- |
| **Statement** | Serialized output written to a file is explicitly flushed before the writer is disposed. |
| **Trigger** | Save to file |
| **Enforcement** | `Serializer.SaveToJson<T>`, `Serializer.SaveToXml<T>` |
| **Outcome** | `writer.Flush()` is called. |
| **Source** | `Common/SAIF.Common/Classes/Serializer.cs` |

---

## BR-061 A property-selector lambda must select a member, not invoke a method

| Field | Value |
| --- | --- |
| **Statement** | A property-selector lambda must select a member, not invoke a method. |
| **Trigger** | Property expression resolution |
| **Enforcement** | `ExpressionExtensions.GetPropertyInfo` |
| **Outcome** | Throws `ArgumentException("Expression '{propertyLambda}' refers to a method, not a property.")`. |
| **Source** | `Common/SAIF.Common/Extensions/ExpressionExtensions.cs` |

---

## BR-062 A property-selector lambda must select a property, not a field

| Field | Value |
| --- | --- |
| **Statement** | A property-selector lambda must select a property, not a field. |
| **Trigger** | Property expression resolution |
| **Enforcement** | `ExpressionExtensions.GetPropertyInfo` |
| **Outcome** | Throws `ArgumentException("Expression '{propertyLambda}' refers to a field, not a property.")`. |
| **Source** | `Common/SAIF.Common/Extensions/ExpressionExtensions.cs` |

---

## BR-063 The selected property must be declared on the target type or one of its base types

| Field | Value |
| --- | --- |
| **Statement** | The selected property must be declared on the target type or one of its base types. |
| **Trigger** | Property expression resolution |
| **Enforcement** | `ExpressionExtensions.GetPropertyInfo` |
| **Outcome** | Throws `ArgumentException("Expresion '{propertyLambda}' refers to a property that is not from type {T}.")`. |
| **Source** | `Common/SAIF.Common/Extensions/ExpressionExtensions.cs` |

---

## BR-064 A property may be set only when it has a setter, but that setter may be non-public - reflective assignment deliberately bypasses accessibility

| Field | Value |
| --- | --- |
| **Statement** | A property may be set only when it has a setter, but that setter **may be non-public** — reflective assignment deliberately bypasses accessibility. |
| **Trigger** | Reflective property assignment |
| **Enforcement** | `ExpressionExtensions.SetPropertyValue` |
| **Outcome** | Throws `ArgumentException("Expresion '{propInfo.Name}' refers to a property that is not settable.")`, or assigns through the non-public setter. |
| **Source** | `Common/SAIF.Common/Extensions/ExpressionExtensions.cs` |

---

## BR-065 A sequence generator starts at the caller-supplied start value, defaulting to 0, and may be repositioned at any time via SetValue

| Field | Value |
| --- | --- |
| **Statement** | A sequence generator starts at the caller-supplied start value, defaulting to 0, and may be repositioned at any time via `SetValue`. |
| **Trigger** | Sequence construction / reposition |
| **Enforcement** | `SequenceGenerator<T>` ctor and `SetValue`, `Sequence` ctor, `LongSequence` ctor |
| **Outcome** | `CurrentValue` assigned. |
| **Source** | `Common/SAIF.Common/Classes/SequenceGenerator.cs, Common/SAIF.Common/Classes/Sequence.cs, Common/SAIF.Common/Classes/LongSequence.cs` |

---

## BR-066 NextValue is pre-increment

| Field | Value |
| --- | --- |
| **Statement** | `NextValue` is pre-increment: it advances the counter by 1 and returns the **new** value, so the first value issued from a zero start is 1. |
| **Trigger** | Sequence read |
| **Enforcement** | `Sequence.NextValue`, `LongSequence.NextValue` |
| **Outcome** | Incremented value returned. |
| **Source** | `Common/SAIF.Common/Classes/Sequence.cs, Common/SAIF.Common/Classes/LongSequence.cs` |

---

## BR-067 Reset() returns the counter to literal zero, not to the original start value

| Field | Value |
| --- | --- |
| **Statement** | `Reset()` returns the counter to literal zero, not to the original start value. |
| **Trigger** | Sequence reset |
| **Enforcement** | `Sequence.Reset`, `LongSequence.Reset` |
| **Outcome** | `CurrentValue = 0`. |
| **Source** | `Common/SAIF.Common/Classes/Sequence.cs, Common/SAIF.Common/Classes/LongSequence.cs` |

---

## BR-068 The application name is URL segment 1

| Field | Value |
| --- | --- |
| **Statement** | The application name is URL segment 1; a null URI or a URI with fewer than 2 segments yields an empty string rather than an error. |
| **Trigger** | URL parse |
| **Enforcement** | `UriExtensions.ApplicationName` |
| **Outcome** | Segment 1 without slashes, or `string.Empty`. |
| **Source** | `Common/SAIF.Common/Extensions/UriExtensions.cs` |

---

## BR-069 The page name is URL segment 2, defaulting to "Index" when absent

| Field | Value |
| --- | --- |
| **Statement** | The page name is URL segment 2, defaulting to `"Index"` when absent; unlike the other URI helpers, this member throws on a null URI. |
| **Trigger** | URL parse |
| **Enforcement** | `UriExtensions.PageName` |
| **Outcome** | Segment 2 or `"Index"`; `ArgumentNullException` on null. |
| **Source** | `Common/SAIF.Common/Extensions/UriExtensions.cs` |

---

## BR-070 The default controller/action route is Home/Index

| Field | Value |
| --- | --- |
| **Statement** | The default controller/action route is `Home/Index`; with 3 segments the action defaults to `Index`; with 4 or more, segments 2 and 3 are the controller and action. |
| **Trigger** | URL parse |
| **Enforcement** | `UriExtensions.ControllerAndActionName` |
| **Outcome** | A `controller/action` string. |
| **Source** | `Common/SAIF.Common/Extensions/UriExtensions.cs` |

---

## BR-071 A Windows identity name is interpreted as DOMAIN\user

| Field | Value |
| --- | --- |
| **Statement** | A Windows identity name is interpreted as `DOMAIN\user`: the account id is the last backslash-delimited segment and the domain is the first. |
| **Trigger** | Identity parse |
| **Enforcement** | `SecurityPrincipalExtensions.UserId`, `SecurityPrincipalExtensions.Domain` |
| **Outcome** | Account or domain substring. |
| **Source** | `Common/SAIF.Common/Extensions/SecurityPrincipalExtensions.cs` |

---

## BR-072 A string is truthy only when it uppercases to one of Y, YES, TRUE, 1, or ON

| Field | Value |
| --- | --- |
| **Statement** | A string is truthy only when it uppercases to one of `Y`, `YES`, `TRUE`, `1`, or `ON`; null and every other value are false. |
| **Trigger** | String-to-boolean conversion |
| **Enforcement** | `StringExtensions.ToBoolean` |
| **Outcome** | `true` / `false`. |
| **Source** | `Common/SAIF.Common/Extensions/StringExtensions.cs` |

---

## BR-073 Booleans render to flags as Y/N, YES/NO, or On/Off depending on the method chosen

| Field | Value |
| --- | --- |
| **Statement** | Booleans render to flags as `Y`/`N`, `YES`/`NO`, or `On`/`Off` depending on the method chosen. |
| **Trigger** | Boolean-to-string conversion |
| **Enforcement** | `BooleanExtensions.ToY_N_String`, `ToYesNoString`, `ToOnOffString` |
| **Outcome** | The corresponding flag string. |
| **Source** | `Common/SAIF.Common/Extensions/BooleanExtensions.cs` |

---

## BR-074 Non-ASCII typographic characters are folded to ASCII equivalents before text leaves the system

| Field | Value |
| --- | --- |
| **Statement** | Non-ASCII typographic characters are folded to ASCII equivalents before text leaves the system: curly single quotes → `'`, curly/angled double quotes → `"`, bullet glyphs → `*`, all dash/hyphen variants → `-`, and NUL → space. A null input yields an empty string. |
| **Trigger** | Text normalization |
| **Enforcement** | `StringExtensions.ReplaceUnicodeCharactersWithAscii` |
| **Outcome** | ASCII-safe string. |
| **Source** | `Common/SAIF.Common/Extensions/StringExtensions.cs` |

---

## BR-075 Character-class filters retain (or, for the Remove variants, exclude) exactly the characters matching the corresponding char.Is predicate, and treat a null input as an empty string

| Field | Value |
| --- | --- |
| **Statement** | Character-class filters retain (or, for the `Remove*` variants, exclude) exactly the characters matching the corresponding `char.Is*` predicate, and treat a null input as an empty string. |
| **Trigger** | Text filtering |
| **Enforcement** | `StringUtilities.Digits` / `Letters` / `LettersOrDigits` / `Punctuation` / `Symbols` / `ControlCharacters` / `RemoveControlCharacters` / `RemoveWhiteSpaces` |
| **Outcome** | A filtered string, never null. |
| **Source** | `Common/SAIF.Common/Utility/StringUtilities.cs` |

---

## BR-076 Truncate is length-safe (clamps and tolerates null)

| Field | Value |
| --- | --- |
| **Statement** | `Truncate` is length-safe (clamps and tolerates null); `Left` and `Right` are not and will throw if the requested length exceeds the string. |
| **Trigger** | String slicing |
| **Enforcement** | `StringExtensions.Truncate`, `Left`, `Right` |
| **Outcome** | Clamped substring, or `ArgumentOutOfRangeException`. |
| **Source** | `Common/SAIF.Common/Extensions/StringExtensions.cs` |

---

## BR-077 A string contains only numeric characters only when it is non-empty and contains no non-digit character at all

| Field | Value |
| --- | --- |
| **Statement** | A string "contains only numeric characters" only when it is non-empty and contains no non-digit character at all (regex `\D` must not match). |
| **Trigger** | Numeric-content test |
| **Enforcement** | `StringExtensions.ContainsOnlyNumericCharacters` |
| **Outcome** | `true` / `false`. |
| **Source** | `Common/SAIF.Common/Extensions/StringExtensions.cs` |

---

## BR-078 A null or whitespace decimal string converts to zero rather than raising an error

| Field | Value |
| --- | --- |
| **Statement** | A null or whitespace decimal string converts to zero rather than raising an error. |
| **Trigger** | Decimal conversion |
| **Enforcement** | `StringExtensions.ConvertToDecimalOrZero` |
| **Outcome** | `0` for blank input. |
| **Source** | `Common/SAIF.Common/Extensions/StringExtensions.cs` |

---

## BR-079 The IsNumeric test is 32-bit-integer-bounded

| Field | Value |
| --- | --- |
| **Statement** | The `IsNumeric` test is 32-bit-integer-bounded: decimals and values beyond `Int32.MaxValue` report as non-numeric. |
| **Trigger** | Numeric test |
| **Enforcement** | `NumericExtensions.IsNumeric` |
| **Outcome** | `int.TryParse` result. |
| **Source** | `Common/SAIF.Common/Extensions/NumericExtensions.cs` |

---

## BR-080 A decimal's precision is its declared scale, read from the scale byte extracted via BitConverter and decimal.GetBits, with trailing zeros counted

| Field | Value |
| --- | --- |
| **Statement** | A decimal's precision is its declared scale, read from the scale byte of `BitConverter.GetBytes(decimal.GetBits(decimalValue)[3])[2]`; trailing zeros count. |
| **Trigger** | Precision read |
| **Enforcement** | `NumericExtensions.Precision` |
| **Outcome** | Decimal-place count. |
| **Source** | `Common/SAIF.Common/Extensions/NumericExtensions.cs` |

---

## BR-081 Reducing a decimal's precision truncates

| Field | Value |
| --- | --- |
| **Statement** | Reducing a decimal's precision **truncates**; it never rounds. |
| **Trigger** | Precision reduction |
| **Enforcement** | `NumericExtensions.TruncatePrecision` |
| **Outcome** | Truncated decimal. |
| **Source** | `Common/SAIF.Common/Extensions/NumericExtensions.cs` |

---

## BR-082 Merging dictionaries overwrites existing keys by default

| Field | Value |
| --- | --- |
| **Statement** | Merging dictionaries overwrites existing keys by default; passing `replaceExisting = false` preserves the target's existing values. |
| **Trigger** | Dictionary merge |
| **Enforcement** | `DictionaryExtensions.Merge<TValue>` |
| **Outcome** | Target dictionary mutated accordingly. |
| **Source** | `Common/SAIF.Common/Extensions/DictionaryExtensions.cs` |

---

## BR-083 A dictionary read with a supplied default never throws on a missing key

| Field | Value |
| --- | --- |
| **Statement** | A dictionary read with a supplied default never throws on a missing key. |
| **Trigger** | Dictionary read |
| **Enforcement** | `DictionaryExtensions.TryGetValue<TKey,TValue>` |
| **Outcome** | Stored value or the default. |
| **Source** | `Common/SAIF.Common/Extensions/DictionaryExtensions.cs` |

---

## BR-084 An upsert overwrites an existing key or adds a new one and always returns the supplied value

| Field | Value |
| --- | --- |
| **Statement** | An upsert overwrites an existing key or adds a new one and always returns the supplied value. |
| **Trigger** | Dictionary upsert |
| **Enforcement** | `DictionaryExtensions.UpdateOrAdd<TKey,TValue>` |
| **Outcome** | Dictionary updated; value returned. |
| **Source** | `Common/SAIF.Common/Extensions/DictionaryExtensions.cs` |

---

## BR-085 Joining a sequence to a string requires a non-null separator

| Field | Value |
| --- | --- |
| **Statement** | Joining a sequence to a string requires a non-null separator (and, for the projection overload, a non-null projection function); elements are rendered with `ToString()` unless a projection is supplied. |
| **Trigger** | Sequence-to-string |
| **Enforcement** | `IEnumerableExtensions.ToStringAll` (2 overloads) |
| **Outcome** | Joined string, or `ArgumentNullException`. |
| **Source** | `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs` |

---

## BR-086 Taking N items from the end of a sequence preserves their original relative order

| Field | Value |
| --- | --- |
| **Statement** | Taking N items from the end of a sequence preserves their original relative order. |
| **Trigger** | Trailing take |
| **Enforcement** | `IEnumerableExtensions.TakeFromEnd<T>` |
| **Outcome** | The last N items in source order. |
| **Source** | `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs` |

---

## BR-087 Key-based distinct retains the first occurrence of each key

| Field | Value |
| --- | --- |
| **Statement** | Key-based distinct retains the **first** occurrence of each key. |
| **Trigger** | Keyed distinct |
| **Enforcement** | `IEnumerableExtensions.Distinct<TSource,TKey>` |
| **Outcome** | One element per distinct key. |
| **Source** | `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs` |

---

## BR-088 Outer joins compare keys with the caller's IEqualityComparer<TKey> when supplied, otherwise EqualityComparer<TKey>.Default, and substitute caller-supplied defaults for unmatched sides

| Field | Value |
| --- | --- |
| **Statement** | Outer joins compare keys with the caller's `IEqualityComparer<TKey>` when supplied, otherwise `EqualityComparer<TKey>.Default`, and substitute caller-supplied defaults for unmatched sides. |
| **Trigger** | Outer join |
| **Enforcement** | `IEnumerableExtensions.FullOuterGroupJoin`, `FullOuterJoin`, `LeftOuterJoin` |
| **Outcome** | Projected join results including unmatched rows. |
| **Source** | `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs` |

---

## BR-089 Converting a stream to a byte array copies from the stream's current position - the caller owns rewinding - and does not dispose the source stream

| Field | Value |
| --- | --- |
| **Statement** | Converting a stream to a byte array copies from the stream's **current position** — the caller owns rewinding — and does not dispose the source stream. |
| **Trigger** | Stream materialisation |
| **Enforcement** | `FileExtensions.ToByteArray` |
| **Outcome** | Byte array from the current position onward. |
| **Source** | `Common/SAIF.Common/Extensions/FileExtensions.cs` |

---

## BR-090 Property reflection returns only the type's public properties

| Field | Value |
| --- | --- |
| **Statement** | Property reflection returns only the type's public properties (default `Type.GetProperties()` binding flags). |
| **Trigger** | Property enumeration |
| **Enforcement** | `Reflector.GetPropertyInfos<TTarget>` |
| **Outcome** | `PropertyInfo[]` of public properties. |
| **Source** | `Common/SAIF.Common/Classes/Reflector.cs` |

---

## BR-091 A log file must be registered against a log id before it can be written to, and log writers are retrieved by that id

| Field | Value |
| --- | --- |
| **Statement** | A log file must be registered against a log id before it can be written to, and log writers are retrieved by that id. |
| **Trigger** | Log contract usage |
| **Enforcement** | `IConsoleLogService.RegisterLogFile` / `GetLog(string)` / `Logs` / `Flush` / `LogException`; `ILogWriter.SetTimeFormat` / `SetTimeDelimiter` / `SetWriteLinePrefix` |
| **Outcome** | Contract obligation on the implementing assembly* |
| **Source** | `Common/SAIF.Common/Interfaces/IConsoleLogService.cs, Common/SAIF.Common/Interfaces/ILogWriter.cs` |

---

## BR-092 A console application may address at most five conventional log files (File0-File4) plus a Custom category

| Field | Value |
| --- | --- |
| **Statement** | A console application may address at most five conventional log files (`File0`–`File4`) plus a `Custom` category. |
| **Trigger** | Log contract usage |
| **Enforcement** | `SAIF.Common.Enums.LogType`, `IConsoleLogService.GetLog(LogType)` |
| **Outcome** | Bounded set of well-known log slots. |
| **Source** | `Common/SAIF.Common/Enums/LogType.cs, Common/SAIF.Common/Interfaces/IConsoleLogService.cs` |

---

## BR-093 WriteToAppLog appends a line break by default when addressed by string log id, but not by default when addressed by LogType

| Field | Value |
| --- | --- |
| **Statement** | `WriteToAppLog` appends a line break by default when addressed by string log id, but **not** by default when addressed by `LogType`. |
| **Trigger** | Log contract usage |
| **Enforcement** | `IConsoleLogService.WriteToAppLog` (2 overloads) |
| **Outcome** | Differing default line-break behavior between overloads. |
| **Source** | `Common/SAIF.Common/Interfaces/IConsoleLogService.cs` |

---

## BR-094 Log services and log writers are disposable resources and must be released by the consuming application

| Field | Value |
| --- | --- |
| **Statement** | Log services and log writers are disposable resources and must be released by the consuming application. |
| **Trigger** | Log contract usage |
| **Enforcement** | `IConsoleLogService : IDisposable`, `ILogWriter : IDisposable` |
| **Outcome** | Contract obligation on the implementing assembly* |
| **Source** | `Common/SAIF.Common/Interfaces/IConsoleLogService.cs, Common/SAIF.Common/Interfaces/ILogWriter.cs` |

---

## Notes
- Mark inferred statements with `*` and unknown statements with `**`. Do not use bold inline confidence labels.

## Related documentation
- [Index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Operations and processes](operations-processes.md)
- [API reference](api/index.md)

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or contextual evidence where direct confirmation was not available in source artifacts.

## Sources
- `Common/SAIF.Common/Utility/TitleCase.cs`
- `Common/SAIF.Common/Classes/Julian.cs`
- `Common/SAIF.Common/Classes/DateMaker.cs`
- `Common/SAIF.Common/Classes/DateRange.cs`
- `Common/SAIF.Common/Extensions/DateRangeExtensions.cs`
- `Common/SAIF.Common/Extensions/IComparableExtensions.cs`
- `Common/SAIF.Common/Classes/Enforce.cs`
- `Common/SAIF.Common/Extensions/GenericExtensions.cs`
- `Common/SAIF.Common/Classes/SaifValidationException.cs`
- `Common/SAIF.Common/Classes/ValidationResult.cs`
- `Common/SAIF.Common/Classes/ContextException.cs`
- `Common/SAIF.Common/Extensions/ExceptionExtensions.cs`
- `Common/SAIF.Common/Extensions/ByteExtensions.cs`
- `Common/SAIF.Common/Extensions/FileExtensions.cs`
- `Common/SAIF.Common/Classes/Serializer.cs`
- `Common/SAIF.Common/Extensions/DotNet40Extensions.cs`
- `Common/SAIF.Common/Extensions/ExpressionExtensions.cs`
- `Common/SAIF.Common/Classes/SequenceGenerator.cs`
- `Common/SAIF.Common/Classes/Sequence.cs`
- `Common/SAIF.Common/Classes/LongSequence.cs`
- `Common/SAIF.Common/Extensions/UriExtensions.cs`
- `Common/SAIF.Common/Extensions/SecurityPrincipalExtensions.cs`
- `Common/SAIF.Common/Extensions/StringExtensions.cs`
- `Common/SAIF.Common/Extensions/BooleanExtensions.cs`
- `Common/SAIF.Common/Utility/StringUtilities.cs`
- `Common/SAIF.Common/Extensions/NumericExtensions.cs`
- `Common/SAIF.Common/Extensions/DictionaryExtensions.cs`
- `Common/SAIF.Common/Extensions/IEnumerableExtensions.cs`
- `Common/SAIF.Common/Classes/Reflector.cs`
- `Common/SAIF.Common/Interfaces/IConsoleLogService.cs`
- `Common/SAIF.Common/Interfaces/ILogWriter.cs`
- `Common/SAIF.Common/Enums/LogType.cs`
