Administrative overhead is not billable
The engine holds a map of prohibited terms. If the narrative contains any of them, the rule fires once per matching term.
printphotocopyscanThe compliance engine is the first gate every billing line passes through. It accepts a typed payload, applies versioned rules that a person can read, recalculates the affected values, and returns a structured result. It reaches no conclusion it cannot attribute to a named rule — and it never approves anything.
The engine accepts exactly five fields. There is no client record, no matter history, no timekeeper file, and no prior invoice. This is deliberate: a rule that cannot reach outside its payload produces the same answer every time it is given the same input, which is what makes the result reproducible in an audit.
| Field | Type | What it carries and why the engine needs it |
|---|---|---|
line_item_id | string | The identity of the billing line. Carried through untouched so a finding can be traced back to the exact source row in the original file. |
timekeeper_role | string | The role that recorded the time. Present in the contract and available to rules; the two rules shipped today do not branch on it. |
hourly_rate_usd | number | The rate applied to the line. Used only as a multiplier — the engine never questions or alters the rate itself. |
hours_logged | number | The hours as submitted. This is the figure a deduction reduces, and the baseline every calculation compares against. |
raw_narrative_text | string | The free-text description of the work. Every rule in both families reads this field and nothing else. |
export interface RawBillingPayload { line_item_id: string; timekeeper_role: string; hourly_rate_usd: number; hours_logged: number; raw_narrative_text: string; } export interface RuleViolation { rule_id: string; description: string; action: 'STRIP_AND_RECALCULATE' | 'FLAG_FOR_AI_CONSENSUS_REWRITE'; }
The first family answers a question with an objectively correct answer: does this narrative describe work the guidelines say cannot be billed at all? Printing, photocopying and scanning are administrative overhead under a common Outside Counsel Guideline. No interpretation is required to recognise them, so no interpretation is invited.
The engine holds a map of prohibited terms. If the narrative contains any of them, the rule fires once per matching term.
printphotocopyscanEach match reduces billable hours by a fixed 0.5, floored at zero so a line can never go negative. The deduction is a policy constant, not an estimate of how long the printing actually took.
adjustedHours = max(0, hours − 0.5)A regular expression removes the clause containing the term, bounded by the nearest comma, period or semicolon, leaving the remaining narrative intact for the next stage.
[^,.;]*\bkeyword\b[^,.;]*[,.;]?The second family answers a different kind of question: is this narrative specific enough for a client to understand what they paid for? That question has no mechanical answer. “Research” may be perfectly adequate on one line and unacceptably thin on another, and only the surrounding context decides.
So the engine does not decide. It records that a judgment is required, sets a flag, and stops. No hours are deducted and no money moves when this family fires.
Terms that describe a category of work rather than the work itself are escalated for semantic review.
researchreviewfile prepneeds_ai_consensus becomes true. The orchestrator reads that flag and routes the line onward. The engine has expressed no opinion about whether the line is acceptable.
needs_ai_consensus: trueFour numbers, three multiplications, one subtraction. The engine performs no other financial operation. Below is the documented worked example, executed step by step exactly as the code executes it.
Submitted line. Senior Associate, 4.5 hours at $650/hour. Narrative mentions both research and printing.
Original value. hours_logged × hourly_rate_usd — the baseline everything is measured against.
OCG-04 fires on print. One match, one flat deduction of 0.5 hours.
Adjusted hours. max(0, 4.5 − 0.5)
Adjusted value. adjusted_hours × hourly_rate_usd. The rate is untouched.
OCG-11 fires on research. Flag set, no hours deducted, no effect on either value.
Calculated adjustment. original_value − adjusted_value
const originalValue = payload.hours_logged * payload.hourly_rate_usd; const adjustedValue = adjustedHours * payload.hourly_rate_usd; const leakagePrevented = originalValue - adjustedValue;
revenue_leakage_prevented_usd. It is a calculated figure: the arithmetic difference the rules produced. It is not realised savings, and it is not payable-amount guidance, until an authorised reviewer establishes a disposition on the line. The demonstration and the investor material both report proposed and authorised adjustments separately for exactly this reason.
Every field in the result exists because something downstream needs it. Original values are retained alongside adjusted ones so that no comparison requires re-reading the source file, and every finding keeps its rule identifier so an adjustment can always be attributed.
| Field | Type | Meaning | Consumed by |
|---|---|---|---|
original_hours | number | Hours as submitted, preserved unchanged. | Audit comparison |
adjusted_hours | number | Hours after all deterministic deductions, floored at zero. | Resulting invoice |
original_value_usd | number | Submitted hours × rate. | Audit comparison |
adjusted_value_usd | number | Adjusted hours × the same untouched rate. | Resulting invoice |
revenue_leakage_prevented_usd | number | The arithmetic difference between the two values above. Calculated, not realised. | Reporting · reviewer context |
violations_detected | RuleViolation[] | Every finding, each carrying its rule identifier, human-readable description and action. | Audit trail · reviewer display |
needs_ai_consensus | boolean | Whether any finding requires semantic review the engine will not perform. | Orchestrator routing |
sanitized_narrative_base | string | The narrative with prohibited clauses removed and punctuation tidied — the starting text for any downstream rewrite. | Consensus wrapper |
The two rule families run in sequence, not in parallel, and the sequence changes the result. Administrative rules run first and modify the working text. Vagueness rules then run against the already-stripped narrative, not against the original.
// Phase 1 — administrative overhead for (const [keyword, description] of forbiddenKeywords) { if (textBuffer.toLowerCase().includes(keyword)) { violations.push({ /* OCG-04 */ }); adjustedHours = Math.max(0, adjustedHours - 0.5); textBuffer = textBuffer.replace(regex, '').trim(); } } // Phase 2 — runs against the MODIFIED buffer for (const vagueWord of vagueKeywords) { if (textBuffer.toLowerCase().includes(vagueWord)) { violations.push({ /* OCG-11 */ }); needsAiConsensus = true; } }
photocopy and removes the entire clause up to the period. By the time phase 2 runs, research no longer exists in the buffer. The line takes its 0.5-hour deduction and clears with needs_ai_consensus: false — never flagged for semantic review.
This is the rule logic described above, ported faithfully to the browser — the same keyword maps, the same 0.5-hour constant, the same strip expression, the same order. Change any field and the result recalculates immediately. Nothing is transmitted; everything runs on this page.
The following behaviours are present in the implementation as supplied. They are published here because a reviewer will find them in ten minutes, and because a system that claims determinism has to be willing to state precisely what it currently does. Each is reproducible in the evaluator above.
Detection uses a substring test, but removal requires a whole-word match. Inflected forms are therefore detected and deducted, yet never stripped — so sanitized_narrative_base can still contain the clause the output implies was removed.
"printed and collated binders"
includes("print") → true → −0.5 h
/\bprint\b/ → no match → text unchanged
Because matching is not word-bounded, any word containing a prohibited term matches. A deposition of a witness named Scanlon and a reference to blueprint specifications each register as administrative overhead.
"deposition of Dr. Scanlon" → "scan" → −0.5 h "the blueprint specifications" → "print" → −0.5 h Result: 3.0 h → 2.0 h, $900 deducted
The loop fires once per prohibited term present, each time deducting a further 0.5 hours and each time appending a violation carrying the same rule identifier — so a reviewer sees OCG-04 listed more than once for a single line.
"Printed pleadings; scan of correspondence" → 2 × OCG-04 → −1.0 h → violations_detected.length === 2
The keyword maps and the 0.5-hour figure are declared inside the class. Per-client Outside Counsel Guidelines, rule versioning, effective dates and exception handling are not yet externalised — changing a rule currently means changing code.
private forbiddenKeywords = new Map([...]); private vagueKeywords = ['research', ...]; adjustedHours - 0.5
DRE addresses outside-counsel billing and Outside Counsel Guidelines review for corporate legal departments. The compliance engine occupies one clearly bounded position inside that scope.
What happens to a line after needs_ai_consensus becomes true — and why consensus produces evidence rather than approval.
Conditional routing between the engine, consensus and human review, resolving into one unified audit receipt.
See it operatingThe working demonstrationThis engine running across a full synthetic billing population, with human decisions, concealed QC and verified export.
ReferenceEngine flow diagramThe full-page responsive diagram of payload, rules, actions, calculations and output routing.