Back office: order management, procurement, workforce, IT and cyber operations, commercial and S&OP, finance¶
This section is the implementation contract for the enterprise functions that sit on top of the physical twin. It covers the front office, the buy side, the people layer, the twin's own IT and security operations, the demand-side brain, and the finance department that prices all of it.
This section conforms to docs/design/DOCTRINE.md. Where a doctrine ruling is applied the ruling id
is cited at the point of application. The rulings that reach this section are D-01, D-03, D-04,
D-05, D-07, D-09, D-10, D-11, D-12, D-13, and D-14.
1. Scope¶
This section covers the following numbered requirements in full.
| Requirement | Title in the source | Owned here |
|---|---|---|
| 6a12 | Order management and customer service | Entire component |
| 6a13 | Procurement and purchasing | Entire component |
| 6a14 | HR and workforce management | Entire component except the roster solver itself (E23), whose input and output contracts are defined here |
| 6a15 | IT and cybersecurity operations | Entire component. The adversarial scenario catalog is E18 and consumes the hooks defined here |
| 6a16 | Marketing, sales operations, and the full S&OP cycle | Entire component. Causal estimation of marketing-mix ROI is E30 and consumes the ground-truth generator defined here |
| 6a17 | Finance and accounting operations | Entire component |
| E14 | Tariff and trade-policy scenario engine | Entire item |
| E22 | Financial twin overlay | Entire item |
| E38 | Insurance and risk transfer | Entire item |
Requirements owned elsewhere that this section consumes or extends, listed so the boundary is explicit and no implementer builds the same thing twice:
| Requirement | Boundary |
|---|---|
| 5 (LSS engine) | Every KPI defined here is trended by the LSS engine. This section names the chart type per metric and emits the sample stream; it never implements a control chart |
| 6a2 (supplier network) | Supplier reliability profiles, ASNs, and OTIF observations are produced there. twinflow-procurement consumes them and adds the buyer-side view |
| 6a3, 6a6 (outbound, e-commerce) | Shipment and pack events are produced there. twinflow-orders consumes them for order state and perfect-order scoring |
| 6a4 (returns) | Return and disposition events are produced there. Consumed here for complaint generation and for the returns P&L postings |
| 6a9 (upstream production) | Production completion, material issue, scrap, and yield events are produced there. Consumed here for standard-cost variances |
| 6a10 (ergonomics) | Strain index and TRIR are produced there. Consumed here for attrition hazard and for the workers' compensation experience modifier |
| 6a11 (QMS) | NCR and CAPA workflow is there. Complaints raised here become external-failure COPQ there |
| 6a7 (transport) | Freight rates, premium freight quotes, and in-transit shock/temperature telemetry are produced there. Consumed here for expediting economics and cargo claims |
| 6a1, 6a8 (planning, MEIO) | Forecast, reorder signals, and echelon policies are produced there. Consumed here for requisitions and for the S&OP supply review |
| E5, E21 (autonomy tiers, decision register) | The append-only decision register schema is a dependency of 6a16 and is pulled forward (see section 8). Multi-agent negotiation stays in E21 |
| E16 (ATP/CTP promising) | Promise computation is E16. twinflow-orders calls it through a port and owns quoted-versus-actual promise reliability |
| E19 (n-tier mapping) | The supplier DAG stays in Phase 6. In-band, SupplierDagPort returns unavailable, tier-2 concentration is reported as unenforced, and no in-band test asserts a tier-2 number. When E19 lands the same port returns the DAG and the constraint becomes enforceable with no code change |
| E23 (rostering) | The constraint solver is there. This section publishes the labor requirement and absence forecast it consumes, and consumes the roster it produces |
| E26 (accuracy stack) | Layers a, b, d, and f are resequenced ahead of this band (section 8.2) because this section's tools and tests exercise them. This section contributes metric definitions and tool schemas; E26 owns layers c, e, and g |
| E27 (agent evaluation harness) | The harness runner and the eval-suite file format are resequenced ahead of this band. This section contributes eval questions and their ground-truth answers, not the scoring machinery |
| E5 (autonomy tiers) | The tier enum and the approval gate for mutating tools are resequenced ahead of this band. E5 later adds L3 auto-apply and the guardrail evaluator |
| E43 (AI security evals) | The four indirect prompt-injection fixtures defined here ship as this section's own tool tests now. E43 later adopts them into the red-team suite and adds scoring across the whole tool surface |
| E30 (causal inference) | Promotion lift, cannibalisation, and forward buy are generated here with known ground-truth parameters. E30's pipeline lands before the generator and its scoring lands after it (section 8.3) |
| E28 (learned surrogate) | The surrogate model is there. The S&OP supply review declares a surrogate mode that calls it through CapacityPort and defaults to the analytic mode, so the cycle runs with or without E28 |
| Chaos scenario framework | This section defines and publishes chaos.scenario_started and chaos.scenario_ended for its own restore drills and business-interruption triggers. The Phase 4 store-and-forward catalog extends the same schema |
| E37 (PLM) | BOM and recipe versioning with effectivity dates is there. Standard-cost roll-up here consumes a single effective revision and gains effectivity handling when E37 lands |
2. Packages¶
Eight independently installable distributions. Distribution names use hyphens, import names use
underscores, no namespace package. Every package depends on twinflow-kernel (the Clock, Rng,
Network, EventBus, Storage, Inference, and WhatIfPort protocols plus the seeded RNG tree)
and on twinflow-schemas (the generated event models from /schemas). No package in this section
imports another's internals. Where one needs data from another, it declares a typing.Protocol port
and ships a null implementation so pip install of that package alone produces a runnable example
(A1).
Two packaging rules from the doctrine bind every list below.
Per D-09, every public symbol has exactly one owning package in this section. A name that would
otherwise appear in two packages is disambiguated at its source rather than by import alias, which
is why the workforce package exports JobRole and HiringRequisition while the IT operations
package exports AccessRole and the procurement package exports PurchaseRequisition. A protocol
that two packages here both need (WhatIfPort) is declared once in twinflow-kernel and imported,
never redeclared. CI walks the import graph, fails on a cycle, and asserts that every name in a
package's __all__ is defined in that package.
Per D-10, each core install stays minimal and every heavy dependency ships as an extra. A port whose
signature would drag a columnar or modeling library in is typed against a narrow structural
protocol, with the concrete type imported only under TYPE_CHECKING. The A1 job in section 7.6
installs each package alone in a clean environment and runs its example, so the claim is tested.
2.1 twinflow-orders¶
Purpose: order lifecycle, allocation policy, promise tracking, and the customer service operation.
Public API:
from twinflow_orders import OrderBook, OrderLifecycle, PerfectOrderScorer
from twinflow_orders.allocation import AllocationEngine, FairShare, Priority, Hybrid, AllocationResult
from twinflow_orders.allocation import JainFairnessIndex
from twinflow_orders.promising import PromiseEngine, PromisePort
from twinflow_orders.changes import ChangeRequestHandler, ChangeCostModel
from twinflow_orders.service import ServiceCenter, ContactGenerator, ContactRouter, Visibility
from twinflow_orders.customer import CustomerModel, SatisfactionState, ChurnHazard, ClvCalculator
from twinflow_orders.arrival import ArrivalProcess, ArrivalIntensity, ChannelDraw
from twinflow_orders.ports import InventoryAvailabilityPort, ShipmentPort, AgentRosterPort
from twinflow_orders.ports import ReturnsPort, CostToServePort
from twinflow_kernel.ports import DemandShapingPort, ChannelMixPort
Depends on: twinflow-kernel, twinflow-schemas. Optional extra [analysis] adds duckdb for
the standalone KPI queries. Ports resolved at runtime; twinflow_orders.ports.stubs ships
StaticInventory, InstantShipment, FixedAgentRoster, NoReturns, BaselineCostToServe,
NoDemandShaping, and StaticChannelMix so the package runs alone. BaselineCostToServe returns
the customer's annual_baseline_margin divided by the periods per year, which is what makes the
customer-lifetime value calculation runnable before activity-based costing lands (section 5.2).
ArrivalProcess is the one place order arrivals are generated, and it is the consumer side of the
two demand-shaping seams. It multiplies its baseline intensity by whatever DemandShapingPort
returns for the period and samples the order's channel from whatever ChannelMixPort returns.
Both protocols are declared once in twinflow-kernel and imported by this package and by
twinflow-commercial (D-09), so the coupling 6a16 needs is a typed interface rather than a shared
module.
AgentRosterPort is named for the service agents it staffs, and is distinct from
twinflow_workforce.ports.RosterPort, which carries the whole-building roster. The two are separate
symbols with separate owners under D-09 rather than one name declared twice.
Standalone example: examples/allocation_policy_bakeoff.py runs 500 orders against a fixed supply
under all three allocation policies and prints fill rate by segment plus the Jain fairness index over
every demand in the run. A quality or planning reader can take this brick for the allocation policy
library alone.
2.2 twinflow-procurement¶
Purpose: procure-to-pay, strategic sourcing, contract management, spend analytics, and the buy-side KPI stream.
Public API:
from twinflow_procurement import RequisitionQueue, PurchaseOrderBook, ApprovalRouter
from twinflow_procurement.matching import ThreeWayMatcher, MatchTolerances, MatchException
from twinflow_procurement.payments import PaymentScheduler, TermsCalendar, DiscountEvaluator
from twinflow_procurement.sourcing import RfxEvent, BidCurve, WeightedScorecard, AwardEngine
from twinflow_procurement.sourcing import GreedyAwardStrategy, ExactAwardStrategy, AwardGapReport
from twinflow_procurement.contracts import Contract, TierSchedule, RebateAccrual, RenegotiationTrigger
from twinflow_procurement.spend import SpendClassifier, MaverickDetector, SavingsLedger
from twinflow_procurement.tactics import ForwardBuyEvaluator, ExpediteEvaluator, EoqValidator
from twinflow_procurement.tactics import SpotBuyEvaluator, SpotBuyDecision
from twinflow_procurement.ports import SupplierPort, InventoryPolicyPort, LandedCostPort, FreightQuotePort
from twinflow_procurement.ports import SpotPricePort, SupplierDagPort
PurchaseRequisition is the requisition type this package owns; the hiring requisition of the same
concept name lives in twinflow-workforce as HiringRequisition (D-09).
Depends on: twinflow-kernel, twinflow-schemas. LandedCostPort is satisfied by
twinflow-trade when installed; the null implementation returns duty zero. SpotPricePort is
satisfied by the transport and supplier packages when installed; the null implementation returns the
contract price plus the configured spot_premium_default_pct, which makes the spot-buy evaluator
runnable alone and biased against the spot buy rather than for it. SupplierDagPort is the seam to
E19's n-tier map; its null implementation returns SupplierDag.unavailable(), and the award engine
reports max_tier2_concentration as unenforced rather than as satisfied, so a missing map is never
read as an absence of hidden concentration.
Standalone example: examples/rfx_award.py runs a synthetic three-supplier RFQ with price-volume
curves and prints the weighted scorecard, the single-award and dual-award outcomes, the cost of
the resilience constraint, and the measured gap between the greedy award and the exact award.
2.3 twinflow-trade (E14)¶
Purpose: classification, tariff schedules, policy scenario overlays, landed cost, FTZ and drawback.
Public API:
from twinflow_trade import Classification, TariffSchedule, DutyRate, LandedCostCalculator
from twinflow_trade.scenarios import ScenarioOverlay, RateShock, Retaliation, DeMinimisChange
from twinflow_trade.scenarios import apply_overlay, merge_overlays
from twinflow_trade.programs import ForeignTradeZone, DrawbackClaim, DutyDeferral
from twinflow_trade.ports import FreightCostPort, FxRatePort, MeioPort
Depends on: twinflow-kernel, twinflow-schemas. Classification, duty evaluation, overlay
composition, and landed cost are pure functions of their inputs and draw no randomness, which makes
this the smallest brick in this section and the easiest to adopt alone. One quantity in the package
is stochastic and it is declared as such: drawback refund timing draws from the
root/trade/drawback stream (section 6.9), so the package takes an Rng like every other package
rather than claiming to need none. Duty arithmetic stays reproducible without an Rng, and
examples/landed_cost_scenarios.py runs with the drawback program disabled to show that.
MeioPort is the seam through which a scenario re-run reaches multi-echelon inventory optimization
(6a8). Its null implementation returns MeioDelta.unavailable(), and every consumer renders that as
"not available" rather than as zero, so a missing planning package can never be read as no impact.
Standalone example: examples/landed_cost_scenarios.py prices one SKU from three origins under the
base schedule and three named scenario overlays and prints the re-ranked sourcing table.
2.4 twinflow-workforce¶
Purpose: hiring pipeline, onboarding learning curves, skills and certification gating, cross-training, attrition, absenteeism prediction, and labor cost accounting.
Public API:
from twinflow_workforce import Worker, WorkerRegistry, JobRole, WageTable
from twinflow_workforce.hiring import HiringRequisition, CandidatePipeline, TimeToFillModel
from twinflow_workforce.skills import Skill, Certification, SkillsMatrix, StationEligibility
from twinflow_workforce.learning import LearningCurve, WrightCurve, ProductivityRamp, ErrorRamp
from twinflow_workforce.attrition import AttritionHazard, TerminationClassifier, ReplacementCost
from twinflow_workforce.absence import AbsenceGenerator, AbsencePredictor, AbsenceForecast
from twinflow_workforce.engagement import ScheduleStabilityIndex, WeekendShiftRate, StrainTrend
from twinflow_workforce.costing import LaborLedger, HourType, BurdenModel
from twinflow_workforce.ports import StrainPort, RosterPort, DemandPlanPort
Depends on: twinflow-kernel, twinflow-schemas. Optional extra [ml] adds scikit-learn for the
absence predictor challenger; the baseline predictor has no ML dependency. Per D-04 the challenger
is constructed with random_state derived from the run seed through the RNG tree, with the
thread count pinned to one and the fitted artifact hashed; the hash is recorded in the provenance
sidecar and asserted by test_absence_challenger_artifact_hash_is_stable. The challenger reaches
the simulation only through the Inference port, so simulation mode can bind a recorded-response
adapter and the tape never depends on library-version numerics.
The engagement module is the behavioral measurement 6a14 asks for. It computes schedule stability, weekend shift rate, and strain trend from state the twin already records, and publishes them as metric samples for the LSS engine to chart. No survey instrument is modeled anywhere.
Standalone example: examples/crosstrain_payback.py runs a 12-week horizon with and without a
cross-training investment under an absenteeism shock and prints coverage, overtime hours, and the
payback period.
2.5 twinflow-itops¶
Purpose: ITSM, SRE observability and error budgets, DORA metrics, vulnerability and patch economics, IEC 62443 zones and conduits, the detection-rule engine, RBAC, and backup drills.
Public API:
from twinflow_itops import ConfigurationItem, CmdbGraph, ServiceCatalog
from twinflow_itops.itsm import Incident, Problem, Change, ChangeAdvisoryBoard, ChangeWindowPolicy
from twinflow_itops.sre import Sli, Slo, ErrorBudget, BurnRateAlert, GoldenSignals, DoraMetrics
from twinflow_itops.sre import BurnRateTable, derive_burn_rates
from twinflow_itops.telemetry import Span, TraceSample, LogRecord, TraceIndex, SpanDerivedMetric
from twinflow_itops.vuln import SyntheticCveFeed, Vulnerability, CvssV31, PatchWindowEvaluator
from twinflow_itops.vuln import ExposureModel, CvarObjective
from twinflow_itops.zones import SecurityZone, Conduit, ConduitMonitor, ZoneCrossing
from twinflow_itops.detect import DetectionRule, RuleRegistry, RuleEngine, RuleFixture
from twinflow_itops.correlate import CorrelationRule, CorrelationEngine, CorrelationWindow, Alert
from twinflow_itops.access import AccessRole, Permission, Grant, AccessDecision, RbacEngine
from twinflow_itops.backup import BackupSchedule, BackupCatalog, RestoreDrill, RpoRtoMeasurement
from twinflow_itops.ports import TelemetryPort, NetworkTapPort
from twinflow_kernel.ports import WhatIfPort, GrossProfitRatePort
Depends on: twinflow-kernel, twinflow-schemas. NetworkTapPort is the seam that makes conduit
monitoring identical in both DST modes: in simulation mode it is bound to the in-memory Network
middleware, in production mode to the broker bridge exporter. WhatIfPort is imported from the
kernel rather than redeclared, because twinflow-finance needs the same protocol and D-09 allows
one owner per public symbol.
The telemetry module carries the logs and traces half of 6a15's observability requirement. Without it, deployment lead time and change-caused recovery time have no request-level evidence, which is the gap section 5.6 closes.
The correlation module is the lightweight SIEM analog 6a15 names. Detection rules judge one event stream; correlation rules join findings across zones inside a time window and raise a single correlated alert, which is what makes IT-004's suggested next tool a component rather than a label.
Standalone example: examples/patch_window_economics.py takes one synthetic CVE, three candidate
windows, and a stubbed production cost curve, and prints the expected-cost ranking with the
risk-aversion weight swept from risk neutral to strongly risk averse.
2.6 twinflow-commercial¶
Purpose: promotions and demand shaping, sales pipeline and forecast bias, quota effects, NPI cold-start, and the five-step S&OP cycle.
Public API:
from twinflow_commercial.promotions import Promotion, LiftCurve, PullForwardKernel, CannibalisationMatrix
from twinflow_commercial.promotions import PromoCalendar, PromoFeatureFrame
from twinflow_commercial.channels import ChannelMixModel, ChannelMixState, DirichletDrift
from twinflow_commercial.pipeline import Opportunity, StageModel, RepBiasModel, QuotaCalendar
from twinflow_commercial.npi import NewProduct, BassDiffusion, AnalogProfile, ColdStartBlender
from twinflow_commercial.fva import FvaLadder, FvaStep, FvaReport
from twinflow_commercial.sop import SopCycle, ProductReview, DemandReview, SupplyReview
from twinflow_commercial.sop import RoughCutCapacityCheck, SupplyReviewMode
from twinflow_commercial.sop import SopReconciliation, ExecutiveMeeting, DecisionPacket, OneNumberPlan
from twinflow_commercial.maturity import PlanAdherence, DecisionLatency, OneNumberVariance
from twinflow_commercial.ports import ForecastPort, CapacityPort, PricingPort, DecisionRegisterPort
from twinflow_commercial.ports import PromoFeaturePort
from twinflow_kernel.ports import DemandShapingPort, ChannelMixPort
SopReconciliation is the S&OP step; the month-end close step of the same English name is
CloseReconciliation in twinflow-finance (D-09).
Three ports are outbound, which is what makes this package a demand-shaping engine rather than a
reporting layer. PromoFeaturePort hands the promotion calendar to the forecaster as a feature
frame, which is 6a16's requirement that the forecaster ingest the promo calendar as a feature.
DemandShapingPort gives the order arrival process a per-SKU per-period intensity multiplier.
ChannelMixPort gives it the drifting wholesale, e-commerce, and marketplace shares to sample the
channel from. The last two are declared in twinflow-kernel and implemented in twinflow-orders
(D-09); this package is the caller. Null implementations return a multiplier of 1.0 and the
configured static shares, so the package runs alone and a reader can see what each coupling carries.
Depends on: twinflow-kernel, twinflow-schemas.
Standalone example: examples/promo_decomposition.py generates 26 weeks of baseline demand, applies
one promotion, and prints the exact decomposition into incremental, pulled-forward, cannibalised,
and halo units with the conservation check.
2.7 twinflow-finance¶
Purpose: general ledger, subledgers, statements, standard costing and variance attribution, inventory valuation, activity-based costing, FP&A, capex governance, month-end close, and controls. Also carries E22 (AP/AR terms, cash-to-cash, working capital per echelon) because the working capital calculation is a query over the same ledger and splitting it would duplicate the schema.
Public API:
from twinflow_finance.money import Money, Currency, minor_units
from twinflow_finance.gl import ChartOfAccounts, Account, JournalEntry, JournalLine, Ledger, Period
from twinflow_finance.posting import PostingRule, PostingRuleSet, PostingEngine
from twinflow_finance.statements import ProfitAndLoss, BalanceSheet, CashFlow, TrialBalance
from twinflow_finance.costing import StandardCost, CostRollUp, VarianceEngine, VarianceAttribution
from twinflow_finance.inventory import ValuationMethod, WeightedAverage, Fifo, EandOReserve, CycleCountProgram
from twinflow_finance.activity_costing import ActivityPool, CostDriver, AbcModel, CostToServe
from twinflow_finance.fpa import DriverBasedBudget, RollingReforecast, VarianceBridge
from twinflow_finance.capex import CapexRequest, Appraisal, npv, irr, payback, PostInvestmentAudit
from twinflow_finance.close import CloseChecklist, CloseTask, Accrual, CloseReconciliation
from twinflow_finance.close import CloseMetrics, ClosingEntryGenerator, IncomeSummary
from twinflow_finance.controls import AuthorityMatrix, SodRule, Control, ControlLibrary, ControlTest
from twinflow_finance.working_capital import Dso, Dpo, Dio, CashToCash, EchelonWorkingCapital
from twinflow_finance.ports import ValuationSourcePort, ActivityRecordPort, GrantsPort
from twinflow_kernel.ports import WhatIfPort
The activity-costing module is named activity_costing rather than abc so it cannot be confused
with the standard library module of that name in a traceback or an import-graph report.
Depends on: twinflow-kernel, twinflow-schemas. Optional extra [report] adds the HTML statement
renderer, which writes through Storage and never opens a path itself. numpy-financial is a
test-only dependency used to cross-check NPV and IRR at float64 precision, never a runtime
dependency; the exactness gate for NPV is evaluated in decimal, because a float64 library cannot
witness a claim stated in more digits than float64 carries (section 7.3).
Standalone example: examples/variance_drilldown.py replays a fixture event log through the posting
rules and prints the gross margin bridge with each variance classified common cause or assignable
and each assignable variance expanded to its source events.
2.8 twinflow-insurance (E38)¶
Purpose: policies, cargo and business-interruption claims, experience-rated premiums, and total cost of risk.
Public API:
from twinflow_insurance import Policy, CoverageType, Limit, Deductible, WaitingPeriod, Sublimit
from twinflow_insurance import SublimitResolver
from twinflow_insurance.claims import Claim, ClaimLifecycle, CargoClaimTrigger, BiClaimTrigger
from twinflow_insurance.rating import RatingTable, ExposureBase, ExperienceModifier, PremiumQuote
from twinflow_insurance.tcor import TotalCostOfRisk, RiskTransferOption, RiskTransferComparison
from twinflow_insurance.ports import LossHistoryPort, SafetyMetricsPort
from twinflow_kernel.ports import GrossProfitRatePort
SublimitResolver maps a loss to at most one sublimit key by an explicit, ordered rule set, so the
sublimits shipped in config change the payout rather than sitting inert (section 5.9).
GrossProfitRatePort is declared once, in twinflow-kernel, and imported by both this package and
twinflow-itops (D-09). Both are satisfied by the same fin.gross_profit_rate event, so business
interruption loss and breach exposure are valued from one number rather than two.
Depends on: twinflow-kernel, twinflow-schemas.
Standalone example: examples/deductible_vs_control.py compares a higher deductible against a
risk-control capex over 1000 seeded loss draws and prints the TCOR distribution for each with the
significance test result.
2.9 Cross-package artifacts every package in this section contributes¶
| Artifact | Path | Purpose |
|---|---|---|
| Event schemas | /schemas/<domain>/<event>/v<major>.json |
C3 registry, additive-only within a major version |
| Metric definitions | packages/<pkg>/metrics/<pkg>.metrics.yaml |
E26(b) governed semantic layer, merged at load |
| Stream manifest | packages/<pkg>/seeds.toml |
The RNG stream names this package draws from, checked against the twinflow-rng registry at import |
| Finding catalog | packages/<pkg>/findings/catalog.yaml |
Finding codes, severity floors, dedupe keys, shelve policy |
| Agent tool manifest | packages/<pkg>/tools/manifest.yaml |
Tool name, Pydantic input and output model, required RBAC permission, autonomy tier |
| Config schema | /schemas/config/<pkg>.v1.json |
C5 validation with line-numbered errors |
| Posting rule fixtures | packages/<pkg>/tests/fixtures/postings/ |
One fixture per event type this package emits that has a GL consequence |
| Eval questions | packages/<pkg>/evals/<pkg>.evals.yaml |
At least three questions per agent tool with ground truth computed from the simulation |
The stream manifest is a declaration, not a second registry. docs/design/variability-and-faults.md
section A.4 makes twinflow-rng the only place allowed to construct a generator, and every stream
name in a seeds.toml must resolve there. A name present in a manifest and absent from the registry
fails at import with the offending name quoted, which is how a new draw gets a reviewed stream name
instead of a silent reuse of an existing one.
Process mining over the case notions this section supplies (order, PO, incident, service contact,
S&OP cycle, claim, close period) runs on twinflow-procmine, the Apache-2.0 miner this repository
implements per D-14. No package here imports an AGPL process mining library, because doing so would
place the whole work under AGPL and break the dual license.
3. Domain model¶
Conventions used throughout.
Time. All timestamps are sim-time from the Clock port (C2). No model here reads a wall clock.
Per D-02 the four places a wall clock may legally be read (the provenance sidecar writer, the paced
clock, the observability exporter, and operator log lines) are all outside this section's packages.
Money. All money is Money(amount_minor: int, currency: str) with integer minor units. Floating
point money is banned and a CI lint rejects float annotations on any field whose name matches
(amount|price|cost|value|premium|duty|payout|salary|wage).
Rounding. Every money-producing formula in this section multiplies an integer minor-unit amount
by a rate, so the rounding contract is stated once here and implemented once in Money.
Money.__mul__(rate)evaluates indecimal.Decimalat a context precision of 34 significant digits and rounds the result to whole minor units withROUND_HALF_EVEN. The mode is stated rather than inherited, becausedecimal's default is alreadyROUND_HALF_EVENand a reader must not have to know that to predict a cent.- Rounding happens exactly once per posted amount, at the point the amount becomes a
JournalLine, a payout, a duty, or a price. Intermediate products stay inDecimal. A chain of three rates rounds once, not three times. - When a rounded total is split across dimension lines (cost center, station, SKU, customer, supplier, channel, lot, scenario), the split uses the largest-remainder method with the dimension key as the tie-break, so the lines sum to the total exactly and the allocation is deterministic. The residual never lands on an arbitrary line.
- Property test
rounding_allocation_closesasserts, for any total and any weight vector, that the allocated lines sum to the total and that each line differs from its unrounded share by less than one minor unit. Property testmoney_multiply_is_half_evenasserts the mode at the boundary, including the two cases that separate half-even from half-up.
Without this contract journal_balances, tcor_reconciles_to_gl, and variance_attribution_closure
are not implementable as stated, because two paths to the same cent could disagree.
Currency. Version 1 is single-currency. Config validation rejects any document, rate table, or
authority row whose currency differs from finance.functional_currency, quoting the offending file,
line, and value. The currency field stays on every money-carrying model and the FxRatePort seam
stays declared, because removing them would make multi-currency a rewrite rather than an addition.
Multi-currency posting, translation, and revaluation are sequenced as a named ROADMAP milestone with
its dependency on E14 recorded; they are not dropped and they are not assumed away. Open question 1
records what that milestone must still decide.
Identifiers. All identifiers are deterministic strings derived from the run seed and a per-entity
counter, assigned from a declared id policy, never from a random UUID. This is what makes the
per-entity RNG streams of docs/design/variability-and-faults.md section A.2 addressable, and it is
what keeps the event log stable.
Determinism, scoped per D-05. Two tiers, and this section claims only what each tier supports. Byte-identical: same seed, same resolved config, same platform, same pinned dependency set, checked by hash equality over the event log, the GL table, and the generated statements. Value-equivalent: same seed and config across platforms, where business events are identical and continuous fields agree within a tolerance derived from measured divergence. The cross-platform job reports the observed maximum divergence rather than asserting a number chosen in advance. Nowhere does this section claim byte identity across platforms.
Iteration order, per D-03. No collection whose iteration order can reach an event payload, a
hash, or a control decision is a set. SkillsMatrix, single_point_of_failure_stations,
CannibalisationMatrix, the promoted SKU list, the conduit list, and the grant set are all sorted
sequences with a declared sort key. Where a set is the right semantic type, the iteration site
writes sorted(s) with the key named in a comment. Float summations over such a collection
(cannibalisation conservation, ABC pool totals, variance attribution) sum in sorted key order using
math.fsum, so the result does not depend on dictionary insertion history.
Storage. No model in this section carries a filesystem path. Artifacts are addressed by storage
key through the Storage port, which is what lets the same code run against a local directory, an
object store, or an in-memory fixture. Section 7.6's C1 lint bans open(, pathlib.Path,
os.path, and shutil in these packages alongside time, random, and socket. Config file
paths in section 6 are resolved once by the config loader, before any package is constructed, and
the loader hands each package parsed objects and storage keys. A directory key such as
detections_dir names a Storage prefix, and the rule loader enumerates it in lexicographic key
order so the rule set is the same set in the same order on every platform.
Query determinism. Several models carry SQL: Sli.definition_sql, DetectionRule.query,
CorrelationRule inputs, Control.evidence_query, and every metric expression in section 6.10.
Four rules make a query result part of a reproducible tape.
- Every query that returns rows carries a total
ORDER BY. The canonical order is(sim_ts, producer_id, seq)from D-07, and a query whose declared ordering is not total fails config validation with the query quoted. A query returning a single scalar is exempt and is markedscalar: trueso the exemption is declared rather than inferred. - Floating-point aggregation over event rows uses an order-stable sum. The metric layer compiles
SUMover a float column into a sum in the query's declared order, never into an engine-chosen parallel reduction, and integer minor units are summed as integers. - Any query feeding a control decision or an event payload runs against a snapshot of the historian
pinned to a stated
sim_timewatermark, which is carried on the emitting event. A query that reads rows later than its watermark is a defect, andtest_query_respects_watermarkasserts it. - Engine version and the compiled SQL text are hashed into the metric definition's
query_hash, which is recorded in the provenance sidecar. A columnar-engine upgrade that changes a number shows up as a hash change with a diff, rather than as a golden-file failure with no explanation.
3.1 twinflow-orders¶
Customer¶
| Field | Type | Notes |
|---|---|---|
customer_id |
str | Deterministic |
segment |
enum contract | spot | marketplace |
Drives promise rule and allocation tier |
allocation_tier |
int 1..5 | 1 is highest. Invariant: contract customers have tier <= 3 |
sla |
ServiceLevelAgreement |
Promise lead time, on-time definition, fill-rate commitment, penalty schedule |
credit_limit |
Money | Credit check stub compares open AR plus order value |
payment_terms_id |
str | Resolves in twinflow-finance terms calendar |
region |
str | Feeds transport lane selection |
channel |
enum wholesale | ecommerce | marketplace |
Home channel. An order's channel is drawn per order from the drifting mix |
satisfaction |
float 0..1 | State, updated by service outcomes |
contract_commitment_units |
int | None | Volume commitment honored first under hybrid allocation |
annual_baseline_margin |
Money | Fallback margin used by BaselineCostToServe only, never by the ABC path |
Invariants: 0.0 <= satisfaction <= 1.0; a customer with segment == contract has a non-null
sla; allocation_tier is stable for the life of a run unless a customer.tier_changed.v1 event
is emitted.
annual_baseline_margin exists so the customer-lifetime-value calculation is runnable before
activity-based costing lands. It is read only through CostToServePort's null implementation, and
customer.churned records which implementation produced the margin it used, so a reader can always
tell a modeled contribution from a fallback (section 5.2).
SalesOrder¶
| Field | Type | Notes |
|---|---|---|
order_id |
str | Process-mining case id for order-to-cash |
customer_id |
str | |
channel |
enum | Sampled by ArrivalProcess from ChannelMixPort shares at capture |
captured_at |
SimTime | |
requested_ship_date |
SimDate | |
promise_date |
SimDate | None | Set by the promise engine, null until promised |
promise_source |
enum, five values, see below | The supply tier the promise was taken from |
promise_breached_at |
SimTime | None | Set when the promise date passes with no delivery confirmation |
status |
enum, see state machine | |
lines |
list[OrderLine] | Non-empty |
holds |
list[Hold] | |
priority_score |
float | Computed, not stored input |
value |
Money | Sum of line extended price |
promise_source takes one of five values, one per supply tier the promise engine tries:
on_hand, in_transit, scheduled_receipt, ctp, default_lead_time. The first three are the
available-to-promise tiers and the fourth is capable-to-promise. They are separate values, not one
atp value, because promise reliability decomposed by source is the analysis section 5.1 claims and
a collapsed enum cannot answer whether the capable-to-promise path is trustworthy relative to a
scheduled receipt. The enum is the same list as orders.promise.sources in config, and a config
validation rule asserts the two are identical so they cannot drift.
Status set: CAPTURED, VALIDATED, ON_HOLD, PROMISED, ALLOCATED, PARTIALLY_ALLOCATED,
RELEASED, PICKING, PACKED, STAGED, SHIPPED, INVOICED, CLOSED, CANCELED,
REJECTED. Fifteen statuses.
Legal transitions (the only ones the state machine accepts):
CAPTURED -> VALIDATED | REJECTED | CANCELED
VALIDATED -> ON_HOLD | PROMISED | CANCELED
ON_HOLD -> VALIDATED | CANCELED
PROMISED -> ALLOCATED | PARTIALLY_ALLOCATED | ON_HOLD | CANCELED
PARTIALLY_ALLOCATED -> ALLOCATED | RELEASED | CANCELED
ALLOCATED -> RELEASED | CANCELED
RELEASED -> PICKING | CANCELED
PICKING -> PACKED | CANCELED
PACKED -> STAGED
STAGED -> SHIPPED
SHIPPED -> INVOICED
INVOICED -> CLOSED
Twenty-five legal transitions. CAPTURED -> CANCELED and VALIDATED -> CANCELED are present
because cancel is a legal OrderChangeRequest.type at any stage_at_request, and a request type
the state machine cannot execute is a contradiction rather than a policy.
Invariant order_state_machine_legality: no transition outside this table is ever recorded, and
CANCELED is unreachable from PACKED onward (a cancel after pack becomes a return, routed to
6a4, which is exactly why the change-cost model rises at that boundary).
OrderLine¶
line_id, order_id, sku, qty_ordered, qty_allocated, qty_backordered, qty_picked,
qty_shipped, qty_cancelled, qty_substituted, unit_price: Money, substitution_of: str|None,
line_promise_date.
Invariant order_quantity_conservation, checked after every mutation:
qty_ordered == qty_allocated + qty_backordered + qty_cancelled and
qty_shipped <= qty_picked <= qty_allocated.
Allocation¶
allocation_id, line_id, source: enum on_hand|in_transit|scheduled_receipt|ctp_build,
qty, lot_ids: list[str], allocated_at, released_at, policy_run_id.
Invariant allocation_no_oversell: for every lot, the sum of open allocations never exceeds the
lot's on-hand quantity as reported by InventoryAvailabilityPort at the allocation instant.
Backorder¶
backorder_id, line_id, qty, created_at, aged_days (derived), fill_priority (recomputed
each allocation run), promised_recovery_date.
Hold¶
hold_id, order_id, type: enum credit|fraud|quality|trade_compliance|price, placed_at,
placed_by, released_at, released_by, reason.
OrderChangeRequest¶
change_id, order_id, type: enum qty_increase|qty_decrease|date_pull_in|date_push_out|
address_change|line_add|line_remove|cancel, requested_at, stage_at_request (the order status
when the request landed), decision: enum accepted|rejected|accepted_with_cost,
cost: Money, rework_minutes: float, rejection_reason.
SubstitutionRule¶
from_sku, to_sku, ratio: float, allowed_segments: list[str],
requires_customer_approval: bool, price_treatment: enum hold_original|use_substitute.
ServiceContact¶
contact_id (process-mining case id for the service process), customer_id, order_id | None,
reason_code: enum wismo|late_delivery|short_ship|damage|wrong_item|invoice_dispute|
return_request|order_change|other, channel: enum phone|email|chat, created_at,
caused_by_event_id (mandatory, never null except for the other reason code),
queued_at, answered_at, agent_id, handle_seconds, resolved_first_contact: bool,
escalation_level: int 0..3, abandoned: bool, resolution_at.
Invariant contact_generation_causality: for every contact with a non-null caused_by_event_id,
that event exists in the log and its sim-time is strictly earlier than created_at.
Every reason code has a producing event, including wismo. A promise date that passes without a
delivery confirmation emits orders.promise_breached, and that event is the causation for every
WISMO contact. The absence of an event cannot be a cause, so the event that records the absence is
emitted instead.
ServiceAgent¶
agent_id, worker_id (links to twinflow-workforce), skills: list[str],
tool_visibility: enum none|order_header|order_detail|twin_grounded,
aht_distribution (per reason code), shift_id.
PerfectOrderRecord¶
order_id, complete: bool, on_time: bool, damage_free: bool, correctly_invoiced: bool,
perfect: bool (the AND), failed_components: list[str], evaluated_at.
Invariant perfect_order_bound: over any population, perfect_order_rate <= min(component rates).
PromiseBreach¶
order_id, promise_date, breached_at, elapsed_days (recomputed each day until delivery or
cancellation), open_at_breach: bool. The record is what gives ORD-004's u-chart a countable
exposure: the denominator is order-days open past promise, and the numerator is WISMO contacts. A
rate with no denominator is not a u-chart, so the denominator is a modeled quantity rather than an
assumed constant.
3.2 twinflow-procurement¶
PurchaseRequisition: req_id, source: enum reorder_point|manual|project|expedite|spot_buy,
sku, qty, need_by, requester_role, cost_center, category_id, status: enum open|sourced|
converted|canceled, triggering_event_id (the reorder signal from the inventory optimizer, or the
supplier slip event that opened the spot-buy question).
PurchaseOrder: po_id (process-mining case id for procure-to-pay), supplier_id, lines,
contract_id | None, incoterm: enum EXW|FCA|FOB|CIF|DAP|DDP, currency, payment_terms_id,
status, approval_chain: list[ApprovalStep], created_at, sent_at, acknowledged_at,
confirmed_dates: dict[line_id, SimDate], buy_mode: enum contract|spot.
PO status set and legal transitions:
DRAFT -> PENDING_APPROVAL -> APPROVED -> SENT -> ACKNOWLEDGED
ACKNOWLEDGED -> PARTIALLY_RECEIVED | RECEIVED
PARTIALLY_RECEIVED -> PARTIALLY_RECEIVED | RECEIVED
RECEIVED -> CLOSED
PENDING_APPROVAL -> REJECTED
any state before the first receipt -> CANCELED
ACKNOWLEDGED -> RECEIVED is direct, because a single full receipt is the common case and a chain
that forces every order through PARTIALLY_RECEIVED would put a state in the log that never
happened.
The invoice is not a PO state. INVOICED, MATCHED, and PAID are gone from this list and live on
SupplierInvoice instead, because an invoice can arrive with no PO at all, can arrive before any
receipt, and can cover lines from several POs. Modeling invoicing as a PO state made three of the
declared match exception classes unreachable. The two aggregates are linked by reference, not by a
shared status field, and a PO can reach CLOSED with an unmatched invoice still open against it,
which is exactly the condition the accrual for goods received not invoiced exists to cover.
Which PO states admit an invoice: SENT, ACKNOWLEDGED, PARTIALLY_RECEIVED, RECEIVED, and
CLOSED. An invoice referencing a PO in DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, or
CANCELED is classified invoice_against_invalid_po rather than rejected silently.
POLine: po_line_id, sku, qty_ordered, qty_received, qty_invoiced, unit_price: Money,
contract_price: Money | None, hs_code, country_of_origin, requested_date, confirmed_date.
Invariant receipt_never_exceeds_order_beyond_tolerance:
qty_received <= qty_ordered * (1 + over_receipt_tolerance_pct).
ApprovalStep: step_index, required_role, threshold: Money, approver_id, decided_at,
decision, delegated_from. Invariant approval_authority: no step is marked approved by a
subject whose authority limit for that category is below the PO value, and the approver is never
the requester (sod_requester_not_approver).
SupplierInvoice: an aggregate in its own right with its own lifecycle. invoice_id,
supplier_id, po_id | None, lines, subtotal, tax, freight, duty, total,
invoice_date, received_at, duplicate_of | None,
status: enum RECEIVED|MATCHED|EXCEPTION|APPROVED|PAID|REJECTED.
Invoice legal transitions:
RECEIVED -> MATCHED | EXCEPTION | REJECTED
EXCEPTION -> MATCHED | REJECTED
MATCHED -> APPROVED -> PAID
Because the invoice is independent of the PO, every declared exception class is reachable. An
invoice with no po_id reaches missing_po. An invoice whose PO exists but has no receipt against
the invoiced lines reaches missing_receipt, which happens whenever an invoice arrives while the PO
sits in SENT or ACKNOWLEDGED. VG-PRC-01's promise that every exception class is produced is
satisfiable under this model, and it was not while invoicing was a PO state.
ThreeWayMatchResult: match_id, po_id | None, receipt_ids, invoice_id,
result: enum matched|price_mismatch|qty_mismatch|missing_receipt|missing_po|duplicate_invoice|
uom_mismatch|freight_mismatch|tax_mismatch|invoice_against_invalid_po, variance_amount: Money,
tolerance_applied, opened_at, resolved_at, resolution: enum auto_accept|credit_memo|
price_correction|receipt_correction|reject, handler_id.
Payment: payment_id, supplier_id, invoice_ids, scheduled_for, executed_at,
amount, discount_taken: Money, discount_available: Money, terms_id.
RfxEvent: rfx_id, category_id, volume_forecast: dict[period, qty],
invited_suppliers, issued_at, bid_deadline, criteria: list[ScoringCriterion],
award_constraints: AwardConstraints.
ScoringCriterion: name, weight: float, direction: enum lower_better|higher_better,
normalization: enum min_max|z_score|ratio_to_best, source: enum bid|scorecard|risk_map.
Invariant: weights sum to 1.0 within 1e-9.
Bid: bid_id, rfx_id, supplier_id, price_curve: list[(break_qty, unit_price)],
lead_time_days, capacity_units_per_period, quality_ppm_history, payment_terms_offered,
submitted_at. Invariant price_curve_monotone: unit price is non-increasing in break quantity.
AwardConstraints: min_suppliers, max_share_per_supplier, must_include, must_exclude,
total_volume, max_concentration_by_tier2_node (reads the E19 DAG when available).
Contract: contract_id, supplier_id, category_id, start, end,
tier_schedule: list[(cumulative_volume, unit_price)], volume_commitment,
rebate: RebateTerm | None, price_index_escalator | None, renegotiation_lead_days,
auto_renew: bool.
RebateTerm: basis: enum retrospective|incremental, tiers: list[(threshold, rate)],
settlement_period. Invariant rebate_never_exceeds_spend.
SpendRecord: spend_id, source_document_id, supplier_id, category_path: list[str]
(three-level synthetic taxonomy), amount, contract_id | None, is_maverick: bool,
leakage: Money (zero when not maverick).
SavingsEntry: entry_id, kind: enum savings|avoidance, category_id, supplier_id,
baseline_method: enum prior_price|market_index|budget|announced_increase,
baseline_unit_price, achieved_unit_price, volume, amount, evidence_event_ids,
gl_posting_id | None.
Invariant savings_baseline_is_gl_auditable: an entry with kind == savings has
baseline_method == prior_price and a non-null gl_posting_id that resolves to a material_price
variance posting (PPV). An entry with any other baseline method has kind == avoidance, a null
gl_posting_id, and at least one entry in evidence_event_ids.
The restriction is not a preference, it is what the chart of accounts can witness. Section 5.8's posting table records a price difference against standard cost, which is the PPV account. No account records a difference against a market index, a budget line, or an announced increase, so an entry on those baselines has nothing in the ledger to point at. Calling it a saving and demanding a posting id would make the invariant unsatisfiable; calling it an avoidance and demanding event evidence makes it checkable. The README limitations section states plainly that only prior-price savings reach the P&L, which is the honest version of the claim procurement organizations make.
Invariant savings_avoidance_disjoint is unchanged: no event id appears as evidence in both an
entry with kind == savings and an entry with kind == avoidance.
SpotBuyDecision: the fourth tactical decision 6a13 names, alongside forward buy, economic order
quantity, and expediting. decision_id, triggering_event_id (the supplier slip or the shortage
that opened the question), sku, qty_short, need_by, contract_supplier_id,
contract_unit_price, spot_unit_price (from SpotPricePort), spot_lead_time_days,
contract_recovery_date, stockout_cost_avoided: Money, price_premium: Money,
quality_risk_ppm_delta, contract_penalty: Money (any minimum-volume or exclusivity charge the
contract levies on an off-contract buy), net_benefit: Money,
decision: enum spot|wait_for_contract|expedite_contract, decided_by, decided_at.
net_benefit is stockout_cost_avoided - price_premium - contract_penalty - quality_cost, where
quality_cost is quality_risk_ppm_delta times the units times the standard cost of an external
failure from 6a11. Invariant spot_buy_never_beats_available_contract: when the contract supplier
can still meet need_by, decision is never spot, because a spot buy that is not driven by a
slip is maverick spend and is classified as such rather than as a tactic. The decision is recorded
whichever way it goes, so the ledger of declined spot buys is as auditable as the ledger of taken
ones.
3.3 twinflow-trade (E14)¶
Classification: sku_id, hs_code (6 digit heading plus optional national 4 digit suffix,
fully synthetic values drawn from the public HS nomenclature structure), country_of_origin,
origin_rule: enum wholly_obtained|substantial_transformation|tariff_shift,
preferential_program_id | None, classified_at, classified_by.
DutyRate: hs_code, origin_country, destination_country, effective_from,
effective_to | None, kind: enum ad_valorem|specific|compound, ad_valorem_pct: float,
specific_per_unit: Money, uom. Invariant no_overlapping_effectivity for the same key tuple.
TariffSchedule: an immutable, versioned collection of DutyRate rows with a schedule_id and
content hash. Loading the same YAML twice yields the same hash (C1).
ScenarioOverlay: scenario_id, name, deltas: list[RateDelta],
de_minimis_threshold: Money | None, retaliation_of: str | None, effective_from,
narrative. RateDelta targets by (hs_chapter or hs_code, origin_country, destination_country)
and carries additive_pct and multiplicative_factor.
Two operations, two signatures. apply_overlay(schedule, overlay) -> schedule produces a new
schedule. merge_overlays(overlay, overlay) -> overlay produces a single overlay from two. The
single compose name previously stood for both, which made its associativity invariant unstatable:
the outer call took a schedule and the inner call took two overlays, so the two sides of the equation
did not have the same type.
The merge rule is stated because additive_pct and multiplicative_factor do not commute and the
shipped scenarios overlap. A merged overlay carries, per target, additive_total: Decimal and
factors: list[Decimal] in trade.active_scenarios order. merge_overlays concatenates the factor
lists and sums the additive terms in Decimal, so the merged form is canonical and comparable
without a float product. Targets are matched most specific first: an hs_code delta wins over an
hs_chapter delta covering the same code, and two deltas at the same specificity for the same
target merge by the rule above.
Every delta is expressed against the base schedule rate, never against the running overlaid
rate. The effective rate for a target is
(base_rate + additive_total) * prod(factors) evaluated left to right in Decimal. Defining the
deltas against the base is what makes overlay order irrelevant to the additive part and makes the
multiplicative part depend only on the declared list order, rather than on how many overlays
happened to be applied before.
Invariants, each a property test over generated overlays with deliberately overlapping targets:
merge_overlays is associative on the canonical form for any three overlays;
apply_overlay(apply_overlay(base, A), B) and apply_overlay(base, merge_overlays(A, B)) produce
the same schedule content hash for any A and B; deactivating every overlay restores the base
schedule to the same content hash. The previous invariant held only for non-overlapping targets,
which proved nothing about the configuration the repository actually ships, since
retaliation_mirror can overlap rate_shock_plastics.
LandedCost: sku_id, supplier_id, qty, unit_price, freight, insurance,
duty, broker_fee, handling, total, unit_landed_cost, incoterm, schedule_id,
scenario_id | None, computed_at. Invariant landed_cost_monotone_in_duty.
ForeignTradeZone: zone_id, admission_events, withdrawal_events,
inverted_tariff_allowed: bool. Invariant ftz_duty_neutrality: for goods admitted to an FTZ and
later withdrawn for domestic consumption at an unchanged rate, total duty paid equals the duty that
would have been paid on direct import; only the payment date differs.
DrawbackClaim: claim_id, export_event_ids, import_duty_paid, refund_rate,
claim_filed_at, refund_received_at, status. Invariant drawback_bounded:
refund <= import_duty_paid * refund_rate and refund_rate <= 1.0.
3.4 twinflow-workforce¶
Worker: worker_id, role_id, hire_date, termination_date | None, wage_rate: Money,
shift_pattern_id, home_station_id, certifications: list[CertificationHolding],
tenure_days (derived), cumulative_units_produced (drives the learning curve),
productivity_multiplier (derived), error_rate_multiplier (derived),
strain_index (read from StrainPort, owned by 6a10),
overtime_hours_rolling_4w, weekend_shifts_rolling_8w, schedule_stability_index,
attrition_hazard (derived, recomputed weekly).
Invariant no_work_after_termination: no labor record exists for a worker with sim-time beyond
termination_date.
JobRole: role_id, title, base_wage, burden_pct, required_certifications,
time_to_fill_distribution, recruiting_cost, target_headcount_driver (which twin metric drives
required headcount). Named JobRole because AccessRole in twinflow-itops is a different concept
and D-09 allows one owner per public symbol.
HiringRequisition: hr_req_id, role_id, opened_at, target_start, status,
pipeline: CandidatePipeline, filled_at | None, time_to_fill_days (derived).
CandidatePipeline: ordered stages sourced -> screened -> interviewed -> offered -> accepted ->
started, each with a conversion probability and a time-in-stage distribution. Invariant
pipeline_monotone: a candidate never moves backwards; a drop is terminal.
Certification: cert_id, name, stations_unlocked, training_hours, trainer_role,
validity_days, renewal_hours. CertificationHolding: worker_id, cert_id, granted_at,
expires_at, lapsed: bool.
Invariant certification_gating: no task assignment exists where the assigned worker lacks a valid,
unexpired holding of every certification the station requires at the assignment's sim-time. This is
enforced at assignment time and asserted as a property over the whole event log.
SkillsMatrix: sparse matrix of (worker_id, station_id) -> proficiency 0..1 derived from
certifications and cumulative station hours. Backed by a dict keyed on the pair and iterated as
sorted(matrix, key=lambda k: (k.station_id, k.worker_id)) wherever the iteration can reach an
event, a hash, or a decision (D-03). StationEligibility.eligible(worker, station, t) returns a
boolean plus the blocking reason.
Derived metric single_point_of_failure_stations: stations with exactly one eligible worker on the
current roster, returned as a list sorted by station_id, never as a set. Raised as finding HR-005,
and the finding's evidence_event_ids inherit that order, so the log is stable across processes
with different hash seeds. CI runs the determinism scenario twice under different PYTHONHASHSEED
values and compares hashes.
EngagementSnapshot: worker_id, period, schedule_stability_index,
weekend_shift_rate_8w, strain_trend_slope, overtime_pct_4w, sample_count. Published weekly
per worker and aggregated per shift and per station.
This is the behavioral engagement measurement 6a14 requires, and it is behavioral precisely because every input is a quantity the twin already records from the roster, the labor ledger, and the ergonomics layer. No survey exists in the model. The three leading indicators go on control charts (section 7.4), which is what the requirement asks for: engagement trended, not surveyed.
schedule_stability_index is 1 - (shift-start changes inside the notice window / shifts worked)
over the trailing eight weeks, bounded in [0, 1]. weekend_shift_rate_8w is weekend shifts over
shifts worked in the same window. strain_trend_slope is the ordinary-least-squares slope of the
worker's strain index over the trailing eight weeks, in strain units per week, read from
StrainPort and owned by 6a10.
LearningCurve: Wright log-linear cumulative-average model.
cumulative_average_hours(x) = a * x**b where b = ln(learning_rate) / ln(2), a is the
first-unit time, and x is cumulative units produced. Productivity multiplier at unit x is
a / marginal_hours(x) normalized so a fully ramped worker equals 1.0, with a configured
plateau_units beyond which the multiplier is clamped. Error rate uses the same functional form
with its own error_learning_rate and floor.
AttritionHazard: discrete-time hazard evaluated weekly.
logit(p_quit_week) = b0
+ b_ot * overtime_hours_rolling_4w
+ b_strain * strain_index
+ b_under * understaffing_ratio_rolling_4w
+ b_sched * (1 - schedule_stability_index)
+ b_wknd * weekend_shifts_rolling_8w
+ f_tenure(tenure_days)
f_tenure is a piecewise-linear spline with knots in config (new hires and long-tenure workers have
different baseline hazards). All coefficients live in config and are documented as illustrative
synthetic values, not fitted to any real workforce data.
Termination: worker_id, at, reason: enum voluntary|involuntary|end_of_assignment,
performance_percentile: float 0..1, regretted: bool, replacement_cost: ReplacementCost.
regretted is true when the reason is voluntary and performance_percentile exceeds
attrition.regret_threshold_percentile. The percentile is not drawn at termination time. It is a
provisioned worker attribute, drawn once from the provision.workforce.W-<id>.performance_percentile
stream before the sim clock starts, following the provisioning rule in
docs/design/variability-and-faults.md section A.2. Drawing it at termination would make the
regretted flag depend on how many workers quit before this one, which is the draw-order hazard that
section A.3 names.
ReplacementCost: recruiting: Money, onboarding_labor: Money,
ramp_loss: Money (the integral of the productivity gap over the ramp period, valued at the
station's contribution per hour), backfill_overtime: Money, total.
AbsenceRecord and AbsenceForecast: per worker per day, probability, predicted: bool,
actual: bool, driver_contributions: dict[str, float]. The forecast is published for E23.
LaborLedger entries: worker_id, date, station_id, activity_id,
hour_type: enum regular|overtime|double_time|training|idle|absent_paid, hours,
rate, burden, cost, charged_to: enum direct|indirect. Invariant hours_conservation:
per worker per day, ledger hours equal roster hours minus absence hours plus recorded overtime.
3.5 twinflow-itops¶
ConfigurationItem: ci_id, name, type: enum broker|historian|mes_analog|edge_gateway|
dashboard|agent_service|twin_service|database|ca, version: semver, zone_id,
purdue_level: int 0..5, depends_on: list[ci_id], owner_role, slo_id | None,
patch_requires_production_window: bool, criticality: int 1..4.
Invariant cmdb_acyclic: depends_on forms a DAG.
Sli: sli_id, ci_id, signal: enum latency|traffic|errors|saturation,
definition_sql, good_events_expr, valid_events_expr, unit.
Slo: slo_id, sli_id, target: float, window_days: int, objective_kind: enum
availability|latency_percentile, threshold.
ErrorBudget: derived, budget_seconds_or_events, consumed, remaining, burn_rate_1h,
burn_rate_6h, exhausted_at | None.
BurnRateTable: derived at config load from window_days and the declared budget fractions, not
stored as literals. derive_burn_rates(window_days, fraction, long_window) returns
fraction * window_days * 24 / long_window_hours, the formula published in the Google SRE Workbook
chapter 5. Section 6.5 states the shipped values and section 7.3 states the gate.
Span: span_id, trace_id, parent_span_id | None, ci_id, operation, started_at,
ended_at, duration_ms, status: enum ok|error, attributes: dict[str, str],
sampled_by: enum head|tail|always. Spans carry sim-time from the Clock port like every other
timestamp in this section.
TraceSample: trace_id, root_span_id, span_count, critical_path_span_ids: list[str],
total_duration_ms, error_span_count, case_id (the business case the request belongs to, when
one exists). The critical path is what makes deployment lead time and change-caused recovery time
measurable at request level rather than inferred from ticket timestamps.
LogRecord: log_id, ci_id, severity: enum debug|info|warn|error|fatal, at, template_id
(the structured message template, so log volume is countable per template rather than per rendered
string), fields: dict[str, str], trace_id | None, span_id | None.
Three derived metrics come from spans and logs and appear in the semantic layer:
request_error_rate, request_latency_p95_ms, and log_error_rate_per_1k_requests. Golden signals
in section 5.6 read the same span stream, so the observability claim rests on modeled evidence
rather than on a stated intention.
Invariants: trace_spans_form_a_tree (every non-root span's parent exists in the same trace and the
parent's interval contains the child's); span_time_is_sim_time (no span timestamp comes from a
wall clock).
Incident: inc_id (process-mining case id for the ITSM process), ci_id, severity: P1..P4,
detected_at, detection_source: enum monitor|user_report|detection_rule|drill,
acknowledged_at, mitigated_at, resolved_at, closed_at, caused_by_change_id | None,
problem_id | None, impact: ImpactAssessment.
Derived: mtta = acknowledged_at - detected_at, mttr = resolved_at - detected_at.
Problem: problem_id, linked_incidents, root_cause_method (shared RCA toolkit with 6a11),
known_error_published_at, workaround, permanent_fix_change_id | None.
Change: chg_id, ci_id, type: enum standard|normal|emergency, requested_window,
implemented_at, duration_minutes, risk_score, has_rollback_plan: bool,
approvals: list[ApprovalStep], outcome: enum successful|failed|rolled_back,
caused_incident_id | None.
ChangeWindowPolicy: freeze_windows: list[(dow, start_hour, end_hour)],
risk_multipliers: dict, emergency_override_role, error_budget_gate: bool.
Vulnerability: vuln_id (format TWF-CVE-<year>-<seq>, deliberately not a real CVE
identifier), affected_component, affected_version_range, cvss_v31_vector, base_score,
exploit_probability (a synthetic EPSS analog in 0..1), known_exploited: bool,
published_at, patch_available_at, remediation: PatchAction.
PatchAction: vuln_id, ci_id, requires_window: bool, window_minutes,
decision: enum patch|defer|compensating_control, deferred_until | None,
accepted_risk_amount: Money, decided_by, decision_event_id.
SecurityZone: zone_id, name, purdue_level, criticality, member_ci_ids,
security_level_target: SL-T 1..4 (IEC 62443 vocabulary).
Conduit: conduit_id, src_zone, dst_zone, allowed_protocols, allowed_ports,
direction: enum unidirectional|bidirectional, baseline_profile: TrafficBaseline.
ZoneCrossing: every message the NetworkTapPort observes crossing a zone boundary, with
src_ci, dst_ci, protocol, bytes, at, conduit_id | None (null means no conduit permits
this flow, which is a violation by construction).
DetectionRule: rule_id, version: semver, title, query (over the historian event tables),
severity, attack_ics_technique_ids: list[str], suppression_window,
fixtures: list[RuleFixture].
RuleFixture: kind: enum positive|negative, event_log_key, expected_fire_count. The fixture
is addressed by storage key and read through the Storage port, never by filesystem path.
The technique identifiers come from the MITRE ATT&CK for ICS matrix, published at
https://attack.mitre.org/matrices/ics/ (retrieved 2026-08-09, matrix version v19). Config
validation rejects a technique id that does not match the published identifier grammar, and the
shipped rules record the matrix version they were written against so a matrix revision is a visible
config change rather than a silent drift.
CorrelationRule: corr_rule_id, version: semver, title,
inputs: list[finding_code | detection_rule_id], join_keys: list[str] (for example src_ci,
zone_id, subject_id), window_seconds, min_distinct_zones: int, min_inputs: int,
severity, suppression_window, fixtures: list[RuleFixture].
CorrelationEngine consumes the finding stream and the detection-rule stream, groups by the
declared join keys inside a tumbling window of window_seconds, and raises
sec.correlation_alert_raised when at least min_inputs distinct inputs appear across at least
min_distinct_zones zones. This is the lightweight SIEM analog 6a15 names, and it is what IT-004's
suggested next tool points at. Correlation rules are versioned code in the same directory as
detection rules, ship the same positive and negative fixtures, and fail CI under the same three
rules: a missing fixture, a logic change without a version bump, and a rule that stops firing on its
own positive fixture.
Invariant correlation_is_deterministic_under_reordering: for a fixed event set, the alerts raised
do not depend on the arrival order of events inside a window, because the engine sorts each window
by the canonical total order (sim_ts, producer_id, seq) before evaluating (D-07).
Grant: subject_id, subject_kind: enum human|agent|service, role_id, granted_at,
expires_at | None, granted_by. Permission: resource, action. AccessRole: role_id,
permissions: list[Permission] held as a sorted sequence, never a set (D-03).
AccessDecision: granted or denied, matching the two events sec.access_granted and
sec.access_denied so one word covers the concept everywhere.
Invariant rbac_deny_by_default: RbacEngine.decide(subject, resource, action) returns granted
if and only if some unexpired grant maps the subject to a role holding a permission matching that
resource and action. Wildcards are explicit permission entries, never implicit.
BackupSchedule: ci_id, cadence, retention, target_rpo_minutes, target_rto_minutes.
BackupRecord: backup_id, ci_id, taken_at, size_bytes, integrity_hash, verified: bool.
RestoreDrill: drill_id, ci_id, failure_injected_at, detected_at, restore_started_at,
service_healthy_at, backup_used_id, measured_rpo_minutes, measured_rto_minutes,
target_met: bool, data_loss_event_count.
ChaosScenario: the schema this section defines because two of its own capabilities need it and
no earlier requirement owns it. scenario_run_id, scenario_id, kind: enum ci_failure|
zone_partition|broker_outage|store_and_forward|restore_drill, target_ci_ids: list[str] sorted,
injected_at, planned_duration_minutes, actual_ended_at, injected_by,
downtime_window: (start, end), affected_zone_ids: list[str] sorted.
Two consumers in this section: a restore drill is a chaos scenario of kind restore_drill
(section 5.6), and a business-interruption claim reads downtime_window to compute indemnified
hours (section 5.9). The Phase 4 store-and-forward catalog extends the same schema with the
store_and_forward kind and adds no field, which is why the schema is settled here rather than
invented twice. Injection is deterministic: the schedule of scenario runs is part of the resolved
config and its hash is in the hashed core (D-01), so a chaos run replays exactly.
3.6 twinflow-commercial¶
Promotion: promo_id, skus, mechanic: enum pct_off|bogo|display|feature|feature_and_display,
depth_pct, start, end, planned_lift_multiplier, pull_forward_fraction: float 0..1,
pull_forward_decay_periods: int, halo_skus: dict[sku, float], funding: Money,
channel_scope.
LiftCurve: a saturating response lift(depth) = 1 + L_max * (1 - exp(-k * depth)) with
L_max and k per SKU class in config. Deterministic given the parameters; the stochastic layer
adds a multiplicative lognormal noise term drawn from the commercial.promo_noise seed namespace.
PullForwardKernel: the incremental units in the promo window are split into true incremental
and pulled-forward. The pulled-forward share is removed from the following
pull_forward_decay_periods at geometrically decaying weights that sum to exactly 1.0.
CannibalisationMatrix: C[i][j] is the fraction of SKU j's baseline demand transferred to
SKU i while i is promoted. Rows and columns are held in SKU-id sorted order, and every summation
over promoted SKUs uses math.fsum over that order (D-03), so the conservation check does not
depend on which SKUs happened to be inserted first. Invariants: C[i][i] == 0; for every j, the
sum over promoted i of C[i][j] is at most 1.0; every transferred unit is removed from j and
added to i in the same period, so units are conserved.
The conservation tolerance is 1e-9 relative, which is above the accumulated rounding of an
fsum over the shipped catalog and is stated as a tolerance rather than as exactness because the
shares are floats. Transferred units are integers and their conservation is exact; the 1e-9
tolerance applies to the share matrix only.
ChannelMixState: period, shares: list[(channel, float)] sorted by channel name,
concentration_hhi, drift_step, source: enum config_static|dirichlet_drift. Channel shares
drift over the run rather than staying at their configured start values, because 6a16 requires the
wholesale versus e-commerce balance to be dynamic rather than a constant the building argues about
once. The shares are published through ChannelMixPort, and ArrivalProcess in twinflow-orders
samples each order's channel from them.
The drift is a Dirichlet random walk on the share simplex, the same family
docs/design/variability-and-faults.md section B.4 uses for slotting mix drift, drawn from the
root/commercial/channel_mix stream. Invariants: shares are non-negative and sum to 1.0 within
1e-12; with drift_step set to zero the shares equal the configured start values exactly, which is
the switch a determinism test uses to separate drift from noise. concentration_hhi is the
Herfindahl-Hirschman index of the share vector, which is what makes a channel-concentration finding
countable rather than a matter of opinion.
PromoFeatureFrame: the feature the forecaster ingests, which is 6a16's stated requirement rather
than a convenience. One row per (sku, period) over the forecast horizon, with columns
promo_active: bool, depth_pct, mechanic_one_hot, days_into_promo, days_since_promo_end,
planned_lift_multiplier, is_pull_forward_decay_period: bool, and halo_source_sku | None.
Rows are emitted in (sku, period) sorted order.
The frame is built from the promo calendar alone, never from realized demand, so a forecaster
consuming it cannot leak the outcome it is predicting. Invariant promo_feature_frame_is_causal:
every column for period t is computable from the calendar as it stood at the forecast cut-off, and
a fixture that back-dates a calendar entry past the cut-off fails the check. Without this frame the
statistical forecaster sees the promotion only as unexplained demand, which is the naive behavior
section 5.7 uses as the FVA baseline rather than the shipped behavior.
DemandMultiplier: sku, period, multiplier: float > 0, sources: list[(source, factor)]
sorted by source name, where source is one of promotion, quota_pressure, channel_mix, or
npi_launch. The multiplier is the product of its factors, computed in sorted source order with
math.fsum over the logs so the product does not depend on insertion history (D-03). This is the
value DemandShapingPort returns, and it is the whole of the coupling between the commercial brain
and the arrival process.
Invariant demand_multiplier_decomposes: the product of the listed factors equals multiplier to
1e-12 relative, so a demand spike can always be attributed to the levers that caused it rather than
being an unexplained jump in the arrival rate.
Opportunity: opp_id, customer_id, rep_id, stage, amount, expected_close,
created_at, stage_history, outcome: enum won|lost|open.
StageModel: per-stage conversion probability and dwell distribution.
RepBiasModel: per rep, bias_multiplier, mode: enum optimistic|sandbagging|calibrated,
variance. The submitted rep forecast equals the probability-weighted pipeline times the bias
multiplier plus noise.
QuotaCalendar: period_boundaries, pressure_exponent k, pressure_amplitude q,
discount_escalation_pct. Order arrival intensity within a quota period is multiplied by
1 + q * ((t - t_start) / (t_end - t_start)) ** k, which produces the quarter-end hockey stick.
NewProduct: sku, launch_date, bass_p, bass_q, market_potential_m,
analog_sku | None, cold_start_blend_periods.
FvaLadder: ordered steps, by default naive -> statistical -> sales_override -> consensus,
each with its own forecast series. FvaReport is the stairstep table: per step, the accuracy
metric, the delta versus the previous step, and the delta versus naive.
SopCycle: cycle_id (process-mining case id for the S&OP process), period,
steps: list[SopStep], plan: OneNumberPlan | None, decisions: list[DecisionPacket],
scored_prior_cycle_id | None.
SopStep: step: enum product_review|demand_review|supply_review|reconciliation|executive,
started_at, completed_at, inputs_event_ids, outputs_event_ids, owner_role.
RoughCutCapacityCheck: the supply review's computation, specified here rather than left open.
mode: enum analytic|surrogate|full_simulation (config key sop.supply_review_mode, default
analytic), resource_set: list[resource_id], capacity_by_period: list[(period, resource_id,
available_hours, required_hours)], gaps: list[(period, resource_id, shortfall_hours,
shortfall_units)], constraint_resources: list[resource_id] sorted by shortfall descending then by
resource_id, evaluated_at, inputs_event_ids.
The analytic mode is a rough-cut capacity plan. For each period in the horizon and each resource in
resource_set, required hours are the consensus volume per family multiplied by the family's
bill-of-resource coefficient, summed over families in sorted family order. Available hours come from
the three capacity ports: the factory finite schedule (6a9), the DC labor requirement
(hr.labor_requirement_published), and transport capacity (6a7). A resource whose required hours
exceed available hours in any period is a gap, and the constraining resource for a period is the one
with the largest shortfall. Bill-of-resource coefficients live in
config/commercial/bill_of_resource.yaml and carry provenance: synthetic.
The surrogate mode calls E28's learned surrogate through CapacityPort and is available only when
E28 is installed; the full simulation mode runs the twin over the horizon. Both are configuration,
and neither is required for the S&OP cycle to run, which is what makes an 18-period monthly cycle
affordable inside the sim-time budget. sop.supply_review_completed records which mode produced its
numbers, so a reader can never mistake a rough-cut figure for a simulated one. Open question 9 is
narrowed accordingly: what stays open is which mode the shipped demo profile selects, not whether
the computation exists.
OneNumberPlan: plan_id, cycle_id, horizon_periods,
volume_by_period_by_family: dict, revenue_by_period, supply_commitment_by_period,
published_at. Invariant one_number_identity: the volume the finance reforecast uses, the volume
the supply plan commits, and the consensus demand volume are the same numbers for every period in
the horizon.
DecisionPacket: decision_id, cycle_id, question, options: list[Option],
recommendation, assumptions: list[Assumption], confidence, authority_tier,
decided_by, decided_at, expected_outcome: dict[metric, value]. Each Assumption carries a
metric, an assumed_value, and a measurement_query so next cycle can score it.
3.7 twinflow-finance¶
Account: account_id (four digit synthetic numbering), name,
type: enum asset|liability|equity|revenue|expense, normal_balance: enum debit|credit,
parent_id | None, is_postable: bool, is_control_account: bool, subledger | None.
JournalEntry: je_id, posting_date, period_id, source_event_id, source_event_type,
rule_id, lines: list[JournalLine], description, reversal_of | None, is_accrual: bool,
posted_by. JournalLine: account_id, debit: Money, credit: Money, dimensions: dict
(cost center, station, sku, customer, supplier, channel, lot, scenario).
Invariants: journal_balances (sum of debits equals sum of credits, exactly, in integer minor
units, per entry); no_negative_amounts (a line carries a positive debit or a positive credit,
never both, never negative); period_closed_is_immutable (no posting to a closed period without an
explicit reopening event).
Ledger: append-only. The statements are pure functions of the ledger:
statements = f(journal_entries, period, chart_of_accounts). No statement value comes from any
other source. This is the property that makes the drill-down honest.
PostingRule: rule_id, source_event_type, version, condition (a predicate over the event
payload), lines: list[LineTemplate] where each template names the account (directly or by a
lookup key such as valuation class), the amount expression, and the dimensions to copy from the
event. Rules live in posting_rules.yaml and every rule has at least one fixture.
StandardCost: sku, revision_id, effective_from,
material_standards: list[(component_sku, std_qty, std_price)],
labor_standards: list[(operation_id, std_hours, std_rate)],
variable_oh_rate, fixed_oh_rate, oh_base: enum labor_hours|machine_hours,
total_standard_cost. Roll-up is bottom-up over the BOM and asserts
total == sum(material) + sum(labor) + variable_oh + fixed_oh.
Variance: variance_id, period_id, kind, amount: Money,
favorable: bool, attribution: list[VarianceAttribution],
causal_class: enum common_cause|assignable|insufficient_data,
control_chart_evidence: FindingRef | None.
Variance kinds implemented, each with its published algebraic definition:
| Kind | Formula |
|---|---|
material_price (PPV) |
(actual_price - standard_price) * actual_qty_purchased |
material_quantity |
(actual_qty_used - standard_qty_allowed) * standard_price |
material_mix |
sum_i (actual_mix_i - standard_mix_i) * total_actual_qty * standard_price_i |
material_yield |
(total_actual_input - standard_input_for_output) * weighted_standard_price |
labor_rate |
(actual_rate - standard_rate) * actual_hours |
labor_efficiency |
(actual_hours - standard_hours_allowed) * standard_rate |
variable_oh_spending |
actual_variable_oh - (actual_hours * standard_variable_rate) |
variable_oh_efficiency |
(actual_hours - standard_hours_allowed) * standard_variable_rate |
fixed_oh_budget |
actual_fixed_oh - budgeted_fixed_oh |
fixed_oh_volume |
(budgeted_hours - standard_hours_allowed) * standard_fixed_rate |
freight |
actual_freight - standard_freight_per_unit * units |
premium_freight |
expedite spend isolated from the freight variance |
scrap |
scrapped_units * standard_cost_at_scrap_point |
rework |
rework_hours * standard_rate + rework_material_at_standard |
duty |
actual_duty - standard_duty (the E14 hook) |
VarianceAttribution: variance_id, source_event_id, source_event_type, amount: Money,
explanation_key. Invariant variance_attribution_closure: the attributed amounts sum exactly to
the variance amount.
ValuationMethod: weighted_average or fifo, config selected, applied per valuation class.
Invariant inventory_flow_identity: beginning + receipts - issues - adjustments = ending, in both
quantity and value, per SKU per period, exactly.
EandOReserve: sku, period, on_hand_qty, forward_coverage_periods (from the demand
model), excess_qty, aging_bucket, reserve_rate, reserve_amount, driver_event_ids.
CycleCountProgram: abc_class -> count_frequency_days, count_method: enum cycle|wall_to_wall,
counter_role, tolerance. CountResult: location, sku, book_qty, physical_qty,
variance_qty, variance_value, absolute_value_accuracy, rfid_confirmed: bool.
Inventory record accuracy is measured two ways and both are reported: location-level exact-match
accuracy, and absolute-value accuracy 1 - sum(|book - physical| * unit_cost) / sum(book * unit_cost).
AbcModel: activity_pools: list[ActivityPool], each with pool_id, cost_accounts,
driver: CostDriver, rate (recomputed each period as pool cost divided by total driver quantity).
CostToServe: subject: enum order|customer|channel|sku, subject_id, period,
activity_costs: dict[pool_id, Money], freight, returns_cost, service_cost, total,
revenue, gross_margin, contribution_after_cost_to_serve.
CapexRequest: capex_id, origin_whatif_id, title, investment: Money,
cash_flows: list[(period, Money)], discount_rate, npv, irr, simple_payback_periods,
discounted_payback_periods, authority_tier, approvals, decision,
post_audit: PostInvestmentAudit | None.
PostInvestmentAudit: capex_id, audit_at, projected_benefit, realised_benefit
(measured by the twin over the same metric definition), variance, hit: bool,
explanation_event_ids.
CloseTask: task_id, period_id, name, owner_role, depends_on: list[task_id],
planned_duration, started_at, completed_at, blocked_minutes, rework_count.
CloseMetrics: period_id, close_cycle_days, critical_path, tasks_late,
accrual_count, manual_journal_count, reconciliation_breaks.
ClosingEntryGenerator and IncomeSummary: the roll-up mechanism the period-boundary
invariants need. Without it, revenue and expense balances never move to equity, retained earnings
never changes, and both trial_balance_closure and statement_articulation fail by construction
rather than by defect.
At every period close the generator emits exactly one closing journal entry, in this order:
- Every account of type
revenuewith a non-zero balance is debited to zero, credit toIncome summary. - Every account of type
expensewith a non-zero balance is credited to zero, debit toIncome summary. - The
Income summarybalance is closed toRetained earnings, debit or credit as its sign requires.
Accounts are visited in account_id order so the entry's line order is stable across processes
(D-03). The entry carries rule_id = "close.period_roll_up" and a source_event_id pointing at
gl.period_closed, so it drills down like any other posting. gl.period_closed carries
closing_entry_id alongside trial_balance_hash.
For a four_four_five calendar the same mechanism runs on the period boundaries the calendar file
declares. Interim periods inside a fiscal year close to Retained earnings directly; there is no
separate year-end entry, and the fiscal-year opening balance test asserts that.
Invariant closing_entry_zeroes_nominal_accounts: after the closing entry posts, every revenue and
expense account has a zero balance and the change in Retained earnings for the period equals net
income for the period, exactly, in integer minor units.
AuthorityMatrix: rows of
(role_id, process, category, basis, max_amount, currency, max_authority_tier).
basis: enum amount|tier says which dimension the row authorizes on, because the three processes
that read this matrix do not all carry a money amount. A PO and a capex request carry a value, so
their rows use basis: amount and resolve against max_amount. A change carries no value, so its
rows use basis: tier and resolve against max_authority_tier.
Change authority tier is derived, not invented at the point of approval. The mapping is a published table in the same config artifact:
risk_score band |
CI criticality 1 to 2 |
CI criticality 3 to 4 |
|---|---|---|
| 0.00 to 0.29 | tier 1 | tier 2 |
| 0.30 to 0.69 | tier 2 | tier 3 |
| 0.70 to 1.00 | tier 3 | tier 4 |
Resolution rule, one per process, stated so three packages cannot each invent their own:
procurement resolves (role_id, "purchase_order", category) on amount; finance resolves
(role_id, "journal", category) and (role_id, "capex", category) on amount; IT operations
resolves (role_id, "change", category) on tier using the table above. A row whose basis does not
match the resolution rule for its process fails config validation with the row quoted.
Shared config artifact at /schemas/config/authority_matrix.v1.json, read by procurement (PO
approval), IT operations (change approval), and finance (journal and capex approval). No package
imports another to use it.
Because three packages read one file, agreement on semantics is proved rather than assumed. The
cross-package conformance test authority_matrix_interpretation_agrees loads one fixture matrix,
asks each package's resolver for the minimal approving role over a shared grid of
(process, category, amount, risk_score, criticality) cases, and fails when any two packages
disagree on the same case or when a package accepts a case its basis does not cover. Open
question 2 covers ownership of the artifact, which is a separate question from this one.
SodRule: rule_id, conflicting_permissions: tuple[Permission, Permission], severity,
mitigating_control_id | None. Control: control_id, objective, process,
frequency, type: enum preventive|detective, automation: enum manual|automated,
owner_role, test_procedure_query, evidence_query.
Terms (E22): terms_id, net_days, discount_pct, discount_days,
applies_to: enum ap|ar. EchelonWorkingCapital: echelon: enum supplier_stock|in_transit_inbound|
dc_stock|wip|finished_goods|in_transit_outbound|accounts_receivable, period, value: Money,
days, carrying_cost.
Cash-to-cash is DIO + DSO - DPO, each computed from the ledger with the definitions published in
the metrics layer so the agent can never compute them a different way.
3.8 twinflow-insurance (E38)¶
Policy: policy_id, coverage: enum cargo|property|business_interruption|general_liability|
workers_compensation, insurer_name (synthetic), period_start, period_end,
limit: Money, deductible: Money, sublimits: dict[str, Money],
waiting_period_hours (BI only), indemnity_period_days (BI only), coinsurance_pct,
premium: Money, exposure_base_value.
Claim: claim_id (process-mining case id for the claims process), policy_id,
trigger_event_ids, loss_type, reported_at, documented_at, adjusted_at, decided_at,
paid_at, gross_loss: Money, deductible_applied, payout: Money,
status: enum open|documented|under_review|settled|denied, denial_reason | None,
recovery_ratio (payout over gross loss).
Claim also carries sublimit_key | None and applicable_limit: Money, so the limit that actually
bound a payout is on the record rather than inferred later.
SublimitResolver: maps a loss to at most one sublimit key. The rule set is ordered and declared
in insurance.sublimit_rules, each rule a predicate over (loss_type, trigger_event_type,
peril_tag). The first matching rule wins, and a loss matching no rule takes the policy limit. The
shipped temperature_excursion sublimit matches losses whose trigger is
transport.temperature_excursion, which is the loss class the cold-chain scenario produces. Without
the resolver the shipped sublimit value would sit in config and change nothing, which is worse than
not shipping it.
Invariant claim_payout_bounds, restated so the sublimit participates:
applicable_limit = policy.sublimits[key] if key is not None else policy.limit
payout = min(applicable_limit, max(0, gross_loss - deductible) * coinsurance_pct)
with 0 <= payout <= applicable_limit <= policy.limit, and payout monotone non-decreasing in
gross_loss. Config validation rejects a sublimit greater than the policy limit, quoting both.
RetainedLoss: loss_id, claim_id | None, period, loss_type, gross_loss: Money,
recovered: Money, retained: Money, gl_posting_id. Every loss the twin generates is recognized
in the ledger when it occurs, whether or not it is ever claimed, and the recovery is posted
separately when a claim settles. Invariant retained_equals_gross_minus_recovered, exactly, in
integer minor units.
This record is what lets tcor_reconciles_to_gl hold. retained_losses is the largest component of
total cost of risk in most periods, and until the gross loss itself had a posting there was nothing
in the ledger for that component to tie to.
ExperienceModifier: policy_id, rating_period, loss_history_ratio, trir (from 6a10),
modifier: float, inputs_event_ids. Invariant experience_modifier_monotone: the modifier is
non-decreasing in both the loss ratio and TRIR, holding exposure fixed.
TotalCostOfRisk: period, premiums, retained_losses, risk_control_spend,
admin_cost, total. Invariant tcor_reconciles_to_gl: every TCOR component equals the sum of
its mapped GL accounts for the period, exactly.
4. Events¶
4.1 Envelope¶
Every event in this section uses the common envelope defined by the schema registry (C3). Fields this section relies on:
| Field | Type | Notes |
|---|---|---|
event_id |
str | uuid5(namespace=run_namespace, name=f"{producer_id}:{seq}"), see below |
schema |
str | twinflow.<domain>.<event_name> |
schema_version |
semver | Additive-only within a major version |
sim_time |
ISO-8601 sim-clock instant | From the Clock port, never wall clock |
run_id |
str | |
stream_name |
str | The RNG stream that produced any draw behind this event, for example orders.service.contacts, or null when the event is deterministic |
producer_id |
str | The distribution name alone, for example twinflow-orders. No version (D-07) |
producer_version |
semver | The producing package's version, carried outside the identity so a release does not renumber history |
seq |
int | Dense per (run_id, producer_id), starting at 1 (D-07) |
case_id |
str or null | Process-mining case key. This section supplies additional case notions: order_id, po_id, inc_id, contact_id, cycle_id, claim_id, and period_id for the close |
correlation_id |
str | Business transaction thread |
causation_id |
str or null | The event_id that caused this one. Mandatory for every derived event in this section |
zone_id |
str | Producer's IEC 62443 zone, used by conduit monitoring |
actor |
object with kind (human, agent, service), id, role_id |
The audit trail identity, required on every state-changing event |
Three envelope rules this section depends on, all from D-07 and D-01.
The sequence number is per producer, not global. A single dense counter across every producer
has no allocator once the garage tier runs several containers plus the Rust agent, so seq is dense
per (run_id, producer_id) and each package allocates its own. The canonical total order over the
log is (sim_ts, producer_id, seq), and both the replay reader and the pagination cursor use it.
event_id does not embed a version. producer_id is the distribution name with no version
suffix, and run_namespace is uuid5(TWINFLOW_ROOT_NAMESPACE, run_id), because uuid5 requires a
UUID namespace and run_id is a string. Embedding package@version in the name would change every
event id on every release, which would break the byte-identical log check, invalidate every golden
file, and make C6's compatibility table unanswerable. The version travels in producer_version,
where a release changes a field rather than an identity.
The hashed log covers no wall clock and no machine identity. Per D-01 the run manifest is split:
run_started carries the hashed core only (seed, config hash, schema snapshot hash, scenario id,
mode, tick rate, horizon, warmup, fault schedule hash), and started_wall_utc, git provenance,
platform, package versions, and host go to the provenance sidecar at manifest.json. Every hash
comparison in section 7.6 is over the log, so the log holds nothing that two runs seconds apart must
differ on.
Schema files live at /schemas/<domain>/<event_name>/v<major>.json with a companion Pydantic model
generated into twinflow-schemas. CI producer and consumer contract tests fail on drift: every
event named in a published table has a schema file and at least one declared consumer or an explicit
consumers: [] marker, and every event named in a consumed list has a producer in some section.
4.2 twinflow-orders (domains orders, service, customer)¶
Published:
| Event | Version | Key payload |
|---|---|---|
orders.order_captured |
1.0.0 | order_id, customer_id, channel, lines[], requested_ship_date, value |
orders.order_validated |
1.0.0 | order_id, validations[], passed |
orders.order_rejected |
1.0.0 | order_id, reason_code |
orders.credit_check_completed |
1.0.0 | order_id, open_ar, credit_limit, decision |
orders.hold_placed |
1.0.0 | order_id, hold_id, type, reason |
orders.hold_released |
1.0.0 | hold_id, released_by, held_hours |
orders.promise_quoted |
1.0.0 | order_id, promise_date, source (one of the five tiers), confidence, supply_basis[] |
orders.promise_breached |
1.0.0 | order_id, promise_date, breached_at, elapsed_days, open_lines, promise_source |
orders.allocation_run_completed |
1.0.0 | policy_run_id, policy, demand_units, supply_units, filled_units, fairness_index, by_segment |
orders.line_allocated |
1.0.0 | line_id, qty, source, lot_ids[], policy_run_id |
orders.line_backordered |
1.0.0 | line_id, qty, reason, promised_recovery_date |
orders.backorder_filled |
1.0.0 | backorder_id, qty, aged_days, fill_priority |
orders.substitution_applied |
1.0.0 | line_id, from_sku, to_sku, ratio, customer_approved |
orders.change_requested |
1.0.0 | change_id, order_id, type, stage_at_request |
orders.change_accepted |
1.0.0 | change_id, cost, rework_minutes |
orders.change_rejected |
1.0.0 | change_id, reason |
orders.order_cancelled |
1.0.0 | order_id, stage_at_cancel, cost, initiator |
orders.order_released |
1.0.0 | order_id, wave_id |
orders.order_invoiced |
1.0.0 | order_id, invoice_id, amount, terms_id |
orders.order_closed |
1.0.0 | order_id, cycle_time_hours |
orders.perfect_order_evaluated |
1.0.0 | order_id, four component booleans, perfect, failed_components[] |
service.contact_created |
1.0.0 | contact_id, customer_id, order_id, reason_code, channel, caused_by_event_id |
service.contact_queued |
1.0.0 | contact_id, queue_id, queue_depth |
service.contact_answered |
1.0.0 | contact_id, agent_id, wait_seconds |
service.contact_abandoned |
1.0.0 | contact_id, wait_seconds, patience_seconds |
service.contact_resolved |
1.0.0 | contact_id, handle_seconds, first_contact_resolution, visibility_level, blocking_data |
service.contact_escalated |
1.0.0 | contact_id, to_level, reason |
service.external_failure_classified |
1.0.0 | contact_id, order_id, copq_class, copq_amount, reason_code, evidence_event_ids[] |
customer.satisfaction_updated |
1.0.0 | customer_id, before, after, driver_event_id |
customer.churned |
1.0.0 | customer_id, hazard_at_churn, remaining_clv, clv_margin_source, failure_event_ids[] |
customer.tier_changed |
1.0.0 | customer_id, from_tier, to_tier, reason |
service.external_failure_classified is the outbound half of the loop 6a12 names. Every resolved
complaint whose reason code maps to an external quality failure is classified into the
cost-of-poor-quality taxonomy (copq_class is one of external_failure or appraisal) with the
cost the complaint consumed attached, and 6a11 consumes it as external-failure COPQ. Without this
event the section consumed qms.ncr_raised and published nothing back, so the loop the source calls
out ran one way only.
customer.churned carries clv_margin_source, one of abc_cost_to_serve or baseline_fallback,
so a churn number computed before activity-based costing lands can never be read as a modeled
contribution (section 5.2).
Consumed: inventory.availability_snapshot, outbound.shipment_confirmed, outbound.pick_short,
outbound.pack_completed, returns.return_received, returns.disposition_completed,
transport.delivery_confirmed, transport.delivery_exception, planning.atp_response,
planning.ctp_response, ar.dispute_opened and ar.invoice_corrected (together, the
invoice-accuracy leg of the perfect order), hr.shift_assigned (service agent staffing),
qms.ncr_raised, mkt.demand_multiplier_published (the arrival intensity multiplier),
mkt.channel_mix_published (the channel shares each order's channel is drawn from).
The invoice-accuracy leg reads customer-invoice events, not
procurement.three_way_match_evaluated. The three-way match compares a supplier's invoice against a
PO the customer never sees, so it cannot witness whether the customer was correctly invoiced.
Section 5.1 defines the component as no accounts-receivable dispute and no pricing correction, and
those are the two events now consumed.
4.3 twinflow-procurement (domains procurement, sourcing, contract, spend)¶
Published:
| Event | Version | Key payload |
|---|---|---|
procurement.requisition_created |
1.0.0 | req_id, source, sku, qty, need_by, triggering_event_id |
procurement.po_created |
1.0.0 | po_id, supplier_id, lines[], contract_id, incoterm, value |
procurement.po_approval_requested |
1.0.0 | po_id, step_index, required_role, threshold |
procurement.po_approved |
1.0.0 | po_id, approver_id, authority_limit, elapsed_hours |
procurement.po_rejected |
1.0.0 | po_id, approver_id, reason |
procurement.po_sent |
1.0.0 | po_id, channel |
procurement.po_acknowledged |
1.0.0 | po_id, confirmed_dates, date_slip_days |
procurement.po_line_changed |
1.0.0 | po_line_id, field, from, to, initiator |
procurement.expedite_requested |
1.0.0 | po_id, premium_freight_quote, stockout_cost_avoided, decision |
procurement.expedite_completed |
1.0.0 | po_id, days_pulled_in, actual_premium |
procurement.goods_receipt_matched |
1.0.0 | po_id, receipt_id, qty, over_receipt_pct |
procurement.invoice_received |
1.0.0 | invoice_id, po_id, total, duplicate_of |
procurement.three_way_match_evaluated |
1.0.0 | match_id, result, variance_amount, tolerance_applied |
procurement.match_exception_opened |
1.0.0 | match_id, exception_type, assigned_to |
procurement.match_exception_resolved |
1.0.0 | match_id, resolution, open_hours |
procurement.payment_scheduled |
1.0.0 | payment_id, invoice_ids[], scheduled_for, discount_available |
procurement.payment_executed |
1.0.0 | payment_id, amount, discount_taken, days_from_invoice |
sourcing.rfx_issued |
1.0.0 | rfx_id, category_id, invited_suppliers[], criteria[] |
sourcing.bid_submitted |
1.0.0 | bid_id, rfx_id, supplier_id, price_curve[], lead_time_days |
sourcing.bids_scored |
1.0.0 | rfx_id, scores, normalization, ranking[] |
sourcing.award_decided |
1.0.0 | rfx_id, awards[], constraint_costs, decision_id |
contract.created |
1.0.0 | contract_id, supplier_id, tier_schedule[], commitment, end |
contract.tier_achieved |
1.0.0 | contract_id, cumulative_volume, new_unit_price |
contract.rebate_accrued |
1.0.0 | contract_id, period, basis, amount |
contract.expiring |
1.0.0 | contract_id, days_to_expiry, annual_spend_at_risk |
contract.renegotiated |
1.0.0 | contract_id, from_terms, to_terms, savings_entry_id |
spend.transaction_classified |
1.0.0 | spend_id, category_path[], contract_id, confidence |
spend.maverick_detected |
1.0.0 | spend_id, covering_contract_id, leakage, price_delta_pct |
procurement.savings_recorded |
1.0.0 | entry_id, kind, baseline_method, amount, evidence_event_ids[] |
procurement.forward_buy_decided |
1.0.0 | sku, qty, carrying_cost, duty_avoided, scenario_id, decision, breakeven_days |
procurement.spot_buy_evaluated |
1.0.0 | decision_id, triggering_event_id, sku, qty_short, contract_unit_price, spot_unit_price, stockout_cost_avoided, price_premium, contract_penalty, net_benefit, decision |
procurement.eoq_validated |
1.0.0 | sku, analytic_eoq, simulated_argmin_qty, grid_points[], total_cost_at_analytic, total_cost_at_argmin, agreement |
Consumed: planning.reorder_point_signal, supplier.asn_issued, supplier.otif_observed,
supplier.quality_event, supplier.commit_date_slipped (the trigger that opens a spot-buy
question), receiving.goods_receipt, trade.landed_cost_computed, trade.scenario_activated,
transport.premium_freight_quote, orders.line_backordered (stockout cost in the expedite and
spot-buy decisions), gl.journal_posted (savings reconciliation).
4.4 twinflow-trade (domain trade, E14)¶
| Event | Version | Key payload |
|---|---|---|
trade.classification_assigned |
1.0.0 | sku_id, hs_code, country_of_origin, origin_rule |
trade.tariff_schedule_loaded |
1.0.0 | schedule_id, content_hash, row_count, effective_from |
trade.scenario_activated |
1.0.0 | scenario_id, name, deltas[], de_minimis_threshold |
trade.scenario_deactivated |
1.0.0 | scenario_id, restored_schedule_hash |
trade.landed_cost_computed |
1.0.0 | sku_id, supplier_id, qty, component breakdown, unit_landed_cost, scenario_id |
trade.duty_accrued |
1.0.0 | receipt_id, hs_code, origin, basis_value, rate_kind, duty |
trade.duty_paid |
1.0.0 | payment_id, duty, entry_reference |
trade.ftz_admission |
1.0.0 | zone_id, lot_ids[], deferred_duty |
trade.ftz_withdrawal |
1.0.0 | zone_id, lot_ids[], destination (domestic or export), duty_due |
trade.drawback_claimed |
1.0.0 | claim_id, export_event_ids[], import_duty_paid, refund_rate, refund_requested |
trade.drawback_received |
1.0.0 | claim_id, refund_received, days_to_refund |
trade.de_minimis_rule_changed |
1.0.0 | from_threshold, to_threshold, affected_channels[] |
Consumed: receiving.goods_receipt, outbound.shipment_confirmed (export leg for drawback),
procurement.po_created (classification requirement), transport.leg_costed.
4.5 twinflow-workforce (domain hr)¶
| Event | Version | Key payload |
|---|---|---|
hr.requisition_opened |
1.0.0 | hr_req_id, role_id, target_start, driver_metric, driver_value |
hr.candidate_stage_changed |
1.0.0 | hr_req_id, candidate_id, from_stage, to_stage, days_in_stage |
hr.offer_accepted |
1.0.0 | hr_req_id, candidate_id, start_date, time_to_fill_days |
hr.hire_started |
1.0.0 | worker_id, role_id, wage_rate, cohort_id |
hr.onboarding_progressed |
1.0.0 | worker_id, cumulative_units, productivity_multiplier, error_rate_multiplier |
hr.time_to_productivity_reached |
1.0.0 | worker_id, days, threshold_multiplier |
hr.certification_granted |
1.0.0 | worker_id, cert_id, training_hours, cost, expires_at |
hr.certification_expired |
1.0.0 | worker_id, cert_id, stations_lost[] |
hr.cross_training_completed |
1.0.0 | worker_id, station_id, investment, coverage_before, coverage_after |
hr.station_eligibility_changed |
1.0.0 | worker_id, station_id, eligible, blocking_reason |
hr.shift_assigned |
1.0.0 | worker_id, shift_id, station_id, roster_id, source (roster_solver or fallback) |
hr.absence_forecast_published |
1.0.0 | date, by_worker, model_id, expected_absent_fte |
hr.absence_recorded |
1.0.0 | worker_id, date, type (sick, no_show, planned), predicted_probability |
hr.overtime_recorded |
1.0.0 | worker_id, hours, hour_type, rolling_4w_total |
hr.attrition_risk_scored |
1.0.0 | worker_id, hazard, driver_contributions |
hr.termination |
1.0.0 | worker_id, reason, regretted, tenure_days, replacement_cost |
hr.labor_cost_posted |
1.0.0 | period, by_station, by_hour_type, direct, indirect, total |
hr.labor_requirement_published |
1.0.0 | date, half_hourly[], by_station, source_forecast_id |
hr.engagement_snapshot |
1.0.0 | worker_id, period, schedule_stability_index, weekend_shift_rate_8w, strain_trend_slope, overtime_pct_4w, sample_count, shift_id, station_id |
Consumed: planning.forecast_published, ergonomics.strain_updated, ergonomics.trir_updated,
twin.station_activity_recorded, roster.roster_published (E23), qms.error_attributed.
4.6 twinflow-itops (domains itops, sec, chaos)¶
| Event | Version | Key payload |
|---|---|---|
itops.ci_registered |
1.0.0 | ci_id, type, version, zone_id, purdue_level, depends_on[] |
itops.ci_version_changed |
1.0.0 | ci_id, from_version, to_version, chg_id |
itops.health_sample |
1.0.0 | ci_id, signal, value, window |
itops.slo_budget_updated |
1.0.0 | slo_id, consumed_pct, remaining, burn_rate_1h, burn_rate_6h |
itops.slo_budget_exhausted |
1.0.0 | slo_id, at, gating_policy_applied |
itops.incident_opened |
1.0.0 | inc_id, ci_id, severity, detection_source, impact |
itops.incident_acknowledged |
1.0.0 | inc_id, mtta_minutes, responder_role |
itops.incident_mitigated |
1.0.0 | inc_id, mitigation, minutes_from_detect |
itops.incident_resolved |
1.0.0 | inc_id, mttr_minutes, caused_by_change_id |
itops.problem_opened |
1.0.0 | problem_id, linked_incidents[] |
itops.known_error_published |
1.0.0 | problem_id, root_cause, workaround, runbook_ref |
itops.change_requested |
1.0.0 | chg_id, ci_id, type, requested_window, risk_score, has_rollback_plan |
itops.change_approved |
1.0.0 | chg_id, approver_id, authority_tier, conditions[] |
itops.change_rejected |
1.0.0 | chg_id, reason |
itops.change_implemented |
1.0.0 | chg_id, duration_minutes, window_actual |
itops.change_failed |
1.0.0 | chg_id, failure_mode, caused_incident_id |
itops.change_rolled_back |
1.0.0 | chg_id, rollback_minutes, restored_version |
itops.freeze_window_violation |
1.0.0 | chg_id, window, override_role |
itops.dora_snapshot |
1.0.0 | period, deployment_frequency, lead_time_hours, change_failure_rate, failed_deployment_recovery_hours |
itops.trace_sampled |
1.0.0 | trace_id, root_span_id, span_count, critical_path_span_ids[], total_duration_ms, error_span_count, case_id, sampled_by, chg_id |
itops.log_volume_snapshot |
1.0.0 | ci_id, window, by_template[] (template id, severity, count), error_rate_per_1k_requests |
chaos.scenario_started |
1.0.0 | scenario_run_id, scenario_id, kind, target_ci_ids[], injected_at, planned_duration_minutes, affected_zone_ids[] |
chaos.scenario_ended |
1.0.0 | scenario_run_id, actual_ended_at, downtime_window, recovered: bool |
sec.vulnerability_published |
1.0.0 | vuln_id, affected_component, version_range, cvss_v31_vector, base_score, exploit_probability, known_exploited |
sec.vulnerability_assigned |
1.0.0 | vuln_id, ci_id[], zone_id[], exposure_days |
sec.patch_window_evaluated |
1.0.0 | vuln_id, candidate_windows[], modelled_breach_exposure, recommended_window |
sec.patch_applied |
1.0.0 | vuln_id, ci_id, chg_id, patch_latency_hours |
sec.patch_deferred |
1.0.0 | vuln_id, until, accepted_risk_amount, compensating_control_id, decided_by |
sec.zone_crossing_observed |
1.0.0 | src_zone, dst_zone, src_ci, dst_ci, protocol, bytes, conduit_id |
sec.conduit_violation |
1.0.0 | src_zone, dst_zone, protocol, reason (no_conduit, protocol_not_allowed, baseline_deviation), evidence_event_ids[] |
sec.detection_rule_fired |
1.0.0 | rule_id, rule_version, severity, attack_ics_technique_ids[], evidence_event_ids[] |
sec.correlation_alert_raised |
1.0.0 | corr_rule_id, corr_rule_version, severity, window_start, window_end, join_key_values, distinct_zone_ids[], contributing_finding_ids[], contributing_rule_ids[] |
sec.access_granted |
1.0.0 | subject_id, resource, action, role_id, tool_name |
sec.access_denied |
1.0.0 | subject_id, resource, action, reason |
sec.backup_completed |
1.0.0 | backup_id, ci_id, size_bytes, integrity_hash, verified |
sec.restore_drill_completed |
1.0.0 | drill_id, ci_id, measured_rpo_minutes, measured_rto_minutes, target_met, data_loss_event_count |
Consumed: every subsystem's health sample stream, network.message_observed (the tap),
twin.whatif_result (production cost of a patch window), fin.gross_profit_rate (breach exposure
valuation), hr.shift_assigned (on-call roster).
One domain prefix, one event. Breach exposure and business-interruption loss are valued from the
same fin.gross_profit_rate, published by twinflow-finance in section 4.8 and reached through the
kernel-owned GrossProfitRatePort (D-09). Two spellings of one event name would have broken the
producer and consumer contract test on the first CI run.
4.7 twinflow-commercial (domains mkt, sales, npi, sop)¶
| Event | Version | Key payload |
|---|---|---|
mkt.promotion_planned |
1.0.0 | promo_id, skus[], mechanic, depth_pct, window, planned_lift, funding |
mkt.promotion_started |
1.0.0 | promo_id, baseline_units_forecast |
mkt.promotion_ended |
1.0.0 | promo_id, realised_units |
mkt.lift_realised |
1.0.0 | promo_id, period, baseline, incremental_true, pulled_forward, cannibalised, halo, ground_truth_params |
mkt.campaign_spend_posted |
1.0.0 | promo_id, amount, channel |
mkt.promo_features_published |
1.0.0 | frame_id, cut_off, horizon_periods, rows[] (sku, period, promo_active, depth_pct, mechanic, days_into_promo, days_since_promo_end, planned_lift_multiplier, is_pull_forward_decay_period, halo_source_sku) |
mkt.demand_multiplier_published |
1.0.0 | period, by_sku[] (sku, multiplier, sources[]), horizon_periods |
mkt.channel_mix_published |
1.0.0 | period, shares[], concentration_hhi, drift_step, source |
sales.opportunity_created |
1.0.0 | opp_id, customer_id, rep_id, amount, expected_close |
sales.opportunity_stage_changed |
1.0.0 | opp_id, from_stage, to_stage, days_in_stage |
sales.opportunity_closed |
1.0.0 | opp_id, outcome, amount, days_open |
sales.rep_forecast_submitted |
1.0.0 | rep_id, period, submitted_amount, pipeline_weighted_amount, bias_multiplier_truth |
sales.quota_period_closed |
1.0.0 | period, by_rep, last_week_share, discount_escalation_realised |
npi.product_launched |
1.0.0 | sku, bass_p, bass_q, market_potential, analog_sku |
npi.adoption_observed |
1.0.0 | sku, period, cumulative_adopters, analytic_expectation |
sop.product_review_completed |
1.0.0 | cycle_id, npi_decisions[], rationalisation_decisions[] |
sop.demand_review_completed |
1.0.0 | cycle_id, consensus_by_period, fva_report, inputs[] |
sop.supply_review_completed |
1.0.0 | cycle_id, mode, capacity_by_period, gaps[], constraint_resources[], inputs_event_ids[] |
sop.reconciliation_completed |
1.0.0 | cycle_id, priced_gaps[] |
sop.executive_decision_logged |
1.0.0 | cycle_id, decision_id, question, options[], recommendation, assumptions[], confidence, authority_tier |
sop.plan_published |
1.0.0 | plan_id, cycle_id, horizon, volume_by_period_by_family, revenue_by_period |
sop.decision_scored |
1.0.0 | decision_id, scored_at, assumption_errors[], outcome_vs_expected, verdict |
sop.maturity_snapshot |
1.0.0 | cycle_id, plan_adherence, decision_latency_days, one_number_variance, fva_by_source |
Consumed: planning.forecast_published, planning.backtest_scored,
factory.finite_schedule_published, transport.capacity_published,
hr.labor_requirement_published, fpa.reforecast_published, orders.order_captured
(actual demand realization), governance.decision_logged (the register).
sop.executive_decision_logged is mirrored into governance.decision_logged.v1 using the
register's own schema so E21 can audit it later without a schema translation.
The three outbound events are what make 6a16 a demand-shaping component rather than a reporting
layer. mkt.promo_features_published is consumed by the planning package (6a1), whose forecaster
ingests the frame as a feature. mkt.demand_multiplier_published and mkt.channel_mix_published
are consumed by twinflow-orders, whose ArrivalProcess multiplies its baseline intensity by the
first and samples each order's channel from the second. Demand stops being purely exogenous at
exactly these three seams, and each one has a schema, a consumer, and a contract test.
4.8 twinflow-finance (domains gl, ap, ar, inv, cost, fpa, capex, close, ctrl, fin)¶
| Event | Version | Key payload |
|---|---|---|
gl.journal_posted |
1.0.0 | je_id, period_id, source_event_id, rule_id, lines[] |
gl.journal_reversed |
1.0.0 | je_id, reversal_of, reason |
gl.period_opened |
1.0.0 | period_id, opened_at |
gl.period_closed |
1.0.0 | period_id, closed_at, trial_balance_hash, closing_entry_id, net_income, retained_earnings_delta |
ap.invoice_recorded |
1.0.0 | invoice_id, supplier_id, amount, terms_id, due_date |
ap.payment_made |
1.0.0 | payment_id, amount, discount_taken, dpo_contribution_days |
ar.invoice_issued |
1.0.0 | invoice_id, customer_id, amount, terms_id, due_date |
ar.cash_received |
1.0.0 | invoice_id, amount, days_outstanding, discount_taken |
ar.dispute_opened |
1.0.0 | invoice_id, reason, amount_in_dispute, caused_by_event_id |
ar.invoice_corrected |
1.0.0 | invoice_id, order_id, correction_kind (price, quantity, tax, terms), from_amount, to_amount, credit_memo_id, caused_by_event_id |
inv.valuation_updated |
1.0.0 | sku, method, qty, unit_value, total_value |
inv.reserve_updated |
1.0.0 | sku, aging_bucket, excess_qty, reserve_rate, reserve_amount |
inv.count_completed |
1.0.0 | location, sku, book_qty, physical_qty, variance_value, rfid_confirmed |
inv.adjustment_posted |
1.0.0 | sku, qty_delta, value_delta, reason, je_id |
inv.accuracy_snapshot |
1.0.0 | period, location_accuracy, absolute_value_accuracy, shrink_value, rfid_coverage_pct |
cost.standard_published |
1.0.0 | sku, revision_id, material, labor, variable_oh, fixed_oh, total |
cost.variance_computed |
1.0.0 | variance_id, kind, amount, favorable, attribution[], causal_class, control_chart_evidence |
cost.abc_rate_updated |
1.0.0 | pool_id, driver, pool_cost, driver_qty, rate |
cost.cost_to_serve_computed |
1.0.0 | subject, subject_id, period, activity_costs, total, contribution |
fpa.budget_published |
1.0.0 | budget_id, period_range, drivers, by_account, source_plan_id |
fpa.reforecast_published |
1.0.0 | reforecast_id, horizon, by_account, source_plan_id |
fpa.bridge_computed |
1.0.0 | from_label, to_label, components[], residual |
capex.request_created |
1.0.0 | capex_id, origin_whatif_id, investment, cash_flows[] |
capex.appraised |
1.0.0 | capex_id, npv, irr, simple_payback, discounted_payback, discount_rate |
capex.approved |
1.0.0 | capex_id, approver_id, authority_tier |
capex.rejected |
1.0.0 | capex_id, reason |
capex.post_audit_completed |
1.0.0 | capex_id, projected_benefit, realised_benefit, variance, hit |
close.task_started |
1.0.0 | period_id, task_id, owner_role |
close.task_completed |
1.0.0 | period_id, task_id, duration_minutes, rework_count |
close.reconciliation_break |
1.0.0 | period_id, control_account, subledger, difference |
close.period_certified |
1.0.0 | period_id, close_cycle_days, critical_path[], manual_journal_count |
ctrl.sod_conflict_detected |
1.0.0 | subject_id, rule_id, conflicting_permissions[], mitigating_control_id |
ctrl.control_test_executed |
1.0.0 | control_id, period, result, sample_size, exceptions, evidence_query_hash |
fin.working_capital_snapshot |
1.0.0 | period, by_echelon, dio, dso, dpo, cash_to_cash_days |
fin.cash_forecast_published |
1.0.0 | as_of, horizon_weeks, inflows[], outflows[], closing_balance[] |
fin.statements_generated |
1.0.0 | period_id, pnl_hash, balance_sheet_hash, cash_flow_hash, artifact_keys[] |
fin.gross_profit_rate |
1.0.0 | period, basis (rolling_12m or period), revenue, cost_of_goods_sold, gross_profit, rate_per_hour, rate_pct, source_je_ids[] |
fin.gross_profit_rate is the one published gross profit rate in the repository, and it exists
because two other packages were valuing losses from a number nobody produced. twinflow-insurance
reads it to value a business-interruption loss and twinflow-itops reads it to value breach
exposure, both through the kernel-owned GrossProfitRatePort (D-09). rate_per_hour is gross
profit divided by operating hours in the period, which is the unit a downtime claim needs;
rate_pct is gross profit over revenue. Both are computed from the ledger alone, so a claim payout
and the P&L can never disagree about what an hour of downtime was worth.
fin.statements_generated carries storage keys, not filesystem paths. The rendered statement
artifacts are written through Storage like every other artifact in this section.
Consumed: every operational event with a financial consequence. The authoritative list is the key
set of posting_rules.yaml, which includes receiving.goods_receipt, production.completion,
production.material_issue, production.scrap_recorded, outbound.shipment_confirmed,
returns.disposition_completed, transport.freight_invoice, hr.labor_cost_posted,
trade.duty_accrued, risk.loss_recognised, risk.claim_settled, risk.premium_rated,
mkt.campaign_spend_posted, procurement.three_way_match_evaluated, and
procurement.payment_executed.
4.9 twinflow-insurance (domain risk, E38)¶
| Event | Version | Key payload |
|---|---|---|
risk.policy_bound |
1.0.0 | policy_id, coverage, limit, deductible, premium, period |
risk.policy_renewed |
1.0.0 | policy_id, prior_premium, new_premium, experience_modifier |
risk.loss_recognised |
1.0.0 | loss_id, period, loss_type, gross_loss, trigger_event_ids[], claim_id (null when never claimed), peril_tag |
risk.recovery_posted |
1.0.0 | loss_id, claim_id, recovered, retained, gl_posting_id |
risk.claim_filed |
1.0.0 | claim_id, policy_id, trigger_event_ids[], loss_type, gross_loss, sublimit_key, applicable_limit |
risk.claim_documented |
1.0.0 | claim_id, documents[], days_from_fnol |
risk.claim_adjusted |
1.0.0 | claim_id, adjuster_finding, recommended_payout |
risk.claim_settled |
1.0.0 | claim_id, payout, deductible_applied, sublimit_key, applicable_limit, binding_constraint, recovery_ratio, cycle_days |
risk.claim_denied |
1.0.0 | claim_id, denial_reason |
risk.bi_claim_triggered |
1.0.0 | claim_id, disruption_event_id, downtime_hours, waiting_period_hours, indemnified_hours, lost_gross_profit |
risk.premium_rated |
1.0.0 | policy_id, exposure_base_value, base_rate, experience_modifier, loss_ratio, trir, premium |
risk.tcor_snapshot |
1.0.0 | period, premiums, retained_losses, risk_control_spend, admin_cost, total |
Consumed: transport.shock_excursion, transport.temperature_excursion,
returns.disposition_completed (damage confirmation), ergonomics.trir_updated,
itops.incident_resolved (business interruption trigger), fin.gross_profit_rate,
chaos.scenario_started and chaos.scenario_ended (downtime windows, schema in section 3.5).
risk.loss_recognised fires when the twin generates a loss, not when a claim is filed, and it fires
whether or not the loss is ever claimed. Section 5.8's posting table debits the loss account and
credits the asset or accrual on that event, and risk.recovery_posted records the insurance
recovery separately when a claim settles. Retained loss is the difference, and it is the largest
component of total cost of risk in most periods. Until the gross loss itself had a posting, that
component had nothing in the ledger to tie to and tcor_reconciles_to_gl could not hold.
4.10 Findings contributed¶
Each code is emitted as the standard finding.raised.v1 with code, severity,
evidence_event_ids, suggested_next_tool, dedupe_key, and shelve_policy, so the alarm
rationalization layer applies uniformly and the findings stream cannot flood.
| Code | Trigger | Severity floor | Suggested next tool |
|---|---|---|---|
| ORD-001 | Promise reliability p-chart signal | medium | hypothesis test on before and after |
| ORD-002 | Backorder aged beyond backorder_max_age_days |
medium | allocation policy what-if |
| ORD-003 | priority_dominance violated in an allocation run |
high | allocation audit |
| ORD-004 | WISMO u-chart signal | low | promise reliability drill-down |
| ORD-005 | First contact resolution below target with a named blocking data element | medium | visibility what-if |
| ORD-006 | CLV at risk above churn_alert_value |
high | customer contact ranking |
| PRC-001 | Maverick leakage above maverick_alert_pct |
medium | spend Pareto |
| PRC-002 | Match exception backlog aging beyond policy | medium | process mining on procure-to-pay |
| PRC-003 | Contract expiry inside the renegotiation lead time | medium | sourcing what-if |
| PRC-004 | Material price variance I-MR assignable signal | medium | variance drill-down |
| PRC-005 | Tier-1 award concentration above max_share_per_supplier |
high | reverse stress test |
| PRC-006 | Early payment discount missed | low | payment scheduling review |
| PRC-007 | Spot buy taken while the contract supplier could still meet the need-by date | medium | spend Pareto |
| HR-001 | Attrition hazard concentration by station or shift | high | labor what-if |
| HR-002 | Overtime above overtime_alert_hours for n consecutive weeks |
high | rostering what-if |
| HR-003 | Certification lapse removes the last eligible worker for a station | high | cross-training what-if |
| HR-004 | Time-to-productivity I-MR signal | medium | onboarding review |
| HR-005 | Single point of failure station | medium | cross-training what-if |
| HR-006 | Schedule stability index I-MR signal, or weekend shift rate p-chart signal | medium | rostering what-if |
| IT-001 | Error budget burn rate above the fast or slow threshold | high | change gating |
| IT-002 | Change failure rate p-chart signal | medium | change window policy what-if |
| IT-003 | Critical vulnerability unpatched past the zone SLA | high | patch window economics |
| IT-004 | Conduit violation or cross-zone baseline deviation | high | SIEM correlation |
| IT-005 | Detection rule stopped firing on its positive fixture | high | rule regression review |
| IT-006 | Backup age exceeds the target RPO | high | restore drill |
| IT-007 | Restore drill overdue | medium | schedule drill |
| IT-008 | Live grant set contains an unmitigated SOD conflict | high | access review |
| IT-009 | Correlated cross-zone alert raised by CorrelationEngine |
high | zone containment review |
| CMR-001 | Planned promotion lift exceeds modeled servable capacity | high | S&OP reconciliation |
| CMR-002 | A forecast ladder step has negative forecast value added | medium | FVA review |
| CMR-003 | Rep forecast bias sustained beyond tolerance | low | bias adjustment |
| CMR-004 | S&OP decision latency I-MR signal | low | cycle retrospective |
| CMR-005 | One-number-plan variance above tolerance | high | reconciliation rerun |
| FIN-001 | Assignable-cause variance with attribution | medium | variance drill-down |
| FIN-002 | Inventory record accuracy below target | high | cycle count what-if |
| FIN-003 | Excess and obsolete reserve step change | medium | demand aging review |
| FIN-004 | Close cycle time I-MR signal or a close task past due | low | close kaizen |
| FIN-005 | Control test failure | high | CAPA in 6a11 |
| FIN-006 | Cash-to-cash degradation signal | medium | working capital drill-down |
| FIN-007 | Capex post-audit shortfall beyond tolerance | medium | assumption review |
| TRD-001 | Landed cost shift beyond tolerance under an active scenario | medium | sourcing re-rank |
| TRD-002 | Drawback claim window expiring | low | claim filing |
| INS-001 | Claim cycle time I-MR signal | low | claims process mining |
| INS-002 | Loss class outside all policy coverage | high | coverage gap review |
| INS-003 | Experience modifier deterioration | medium | risk transfer what-if |
| INS-004 | A payout bound by a sublimit rather than by the policy limit | medium | coverage gap review |
Two of these codes are scoped so they cannot become permanent alarms under the shipped configuration, which is the failure mode this catalog exists to prevent.
ORD-003 fires on a violation of the priority_dominance property (section 7.2), not on a raw fill
comparison between two lines. Under fair_share and under the shipped hybrid default, a
lower-tier line receiving remainder units while a tier-1 contract remainder is short is the policy
working as specified, not a defect. Under priority the same observation is a real violation. The
finding evaluates the property the active policy declares, so a policy change changes
what counts as a violation rather than flooding the stream.
PRC-005 asserts only the tier-1 concentration the award engine can measure in-band. Tier-2
concentration needs the E19 supplier DAG, which stays in Phase 6, so SupplierDagPort returns
unavailable, max_tier2_concentration is reported as unenforced, and no in-band finding or
scenario asserts a tier-2 number. When E19 lands, the constraint becomes enforceable and PRC-005
gains a second trigger clause.
5. Behavior¶
5.0 How this section obeys the dual-mode rule¶
Nothing in this section reads a wall clock, calls random, opens a socket, or touches a file
directly. Every package receives Clock, Rng, Network, and Storage from twinflow-kernel at
construction. Three consequences that matter for the implementer:
- Every stochastic draw declares a seed namespace.
seeds.tomlper package lists them, and the RNG tree derives a child stream per namespace, so adding a new draw in the service center cannot perturb the promotion noise stream and break C1's hash check. - The service center and the AP exception queue are SimPy resources in both modes. In production mode the same code drives the same queue model; only the arrival source differs (simulated failure events versus real events off the broker). The queue mathematics is never re-implemented.
- Conduit monitoring uses one code path.
NetworkTapPortis bound to the in-memoryNetworkmiddleware in simulation mode and to the broker bridge exporter in production mode. The zone lookup, baseline comparison, and violation logic are identical, which is what makes the IEC 62443 claim testable at all rather than a diagram. - Every paired comparison runs on common random numbers. Per-namespace streams stop one subsystem's draws from perturbing another's. They do nothing for the A/B studies this section rests on, and without the rule below those studies measure path divergence rather than treatment.
Common random numbers for paired studies¶
Six comparisons in this section run the same seeded scenario twice and hypothesis-test a delta: the visibility study in section 5.2, the Friday freeze study in section 5.6, the RFID confirmation study in section 5.8, the allocation policy bake-off in section 5.1, the cross-training what-if in section 5.5, and the risk transfer comparison in section 5.9.
Changing the treatment changes control flow. A more visible agent resolves at first contact, so no
escalation is generated, so the number of draws taken from root/orders/service/handle differs from
that point on and the two arms desynchronise. Every draw after the first divergence is then a
different draw for a reason that has nothing to do with the treatment, and the measured delta
contains that noise. The rule that removes it:
- Every draw is addressed by entity, not by call order. A stream is subscripted by the id of
the entity the draw belongs to, following the substream rule in
docs/design/variability-and-faults.mdsection A.2:root/orders/service/handle/<contact_id>,root/itops/change_outcome/<chg_id>,root/workforce/absence/<worker_id>/<date>,root/finance/count_error/<location>/<sku>,root/insurance/loss_severity/<loss_id>. Contact 4104's handle time is the same draw in both arms because it comes from contact 4104's substream, whatever happened to contacts 1 through 4103. - Entity ids are assigned before the treatment can affect them. Ids come from the deterministic id policy in section 3, and the entities a paired study compares are provisioned before the sim clock starts wherever the treatment could otherwise change how many exist.
- A draw an arm does not need is not taken. Skipping a substream costs nothing, because a substream is addressed rather than consumed in order.
- The pairing is asserted, not assumed.
test_paired_arms_share_drawsruns both arms of each of the six studies and asserts that every entity present in both arms drew identical values from every shared substream. A study whose arms diverge on a shared substream fails as a defect, which is the only way to know the hypothesis test is measuring the treatment.
The variance reduction this buys is the reason the section can claim a significant difference from 20 replications rather than needing hundreds. The paired tests in sections 5.2, 5.5, 5.6, 5.8, and 5.9 are all paired-difference tests over matched entities, and each names its replication count where it is stated.
5.1 Order lifecycle (6a12)¶
Capture and validation. An order arrives from one of three sources: the wholesale demand
process, the e-commerce stream (6a6), or the marketplace feed. Validation checks SKU existence,
minimum order quantity, ship-to serviceability, and trade classification presence (a missing HS code
raises a trade_compliance hold, which is how E14 makes itself felt on the sell side). The credit
check is a stub by design: it compares open AR from the finance package plus the order value against
credit_limit and places a credit hold on breach. The source calls it a stub, so it stays one, and
the README limitations section says so.
Promising. PromiseEngine calls PromisePort, which E16 implements. The order of attempts is
on-hand, then in-transit, then scheduled receipts (ATP), then a capable-to-promise build slot from
the finite scheduler (CTP), then a configured default lead time as the last resort. The chosen
source is recorded on the event so promise reliability can later be decomposed by promise source,
which is the analysis that tells an operator whether the CTP path is trustworthy. Quoted date versus
actual ship or delivery date becomes the promise reliability series.
Allocation. An allocation run collects open demand within allocation_horizon_days and available
supply, then applies the configured policy.
Priority: sort demand by(allocation_tier, promise_date, -order_value, order_id)and fill greedily. The finalorder_idterm makes the sort total, and so deterministic.FairShare: each demand receivesfloor(supply * demand_i / total_demand)units, then the remainder is distributed by largest fractional part withorder_idas the tie-break (the largest remainder method). Deterministic and provably proportional.Hybrid: contract customers are filled up tocontract_commitment_unitsunder priority, then all remaining demand including the contract remainder is fair-shared.
Every run publishes fairness_index, so the policy what-if has a fairness number next to the
service number rather than an argument. The index is Jain's fairness index over the fill ratio of
every demand in the run, including demands filled zero:
x_i = allocated_i / requested_i for every demand i in the run
fairness_index = (sum_i x_i) ** 2 / (n * sum_i x_i ** 2)
Three properties make it usable as a gate. It lies in [1/n, 1] and reaches 1 only when every
demand has the same fill ratio. It counts a starved customer, because a demand with x_i = 0 stays
in the population and drags the index down, so a policy cannot improve its score by starving the
worst-off customer completely. It is defined when total supply is zero: every x_i is 0, the
numerator and denominator are both 0, and the implementation returns 1.0 by the stated convention
that a run in which nobody is filled is perfectly equal rather than undefined. That convention is
asserted by test_fairness_index_at_zero_supply, so the edge case is a decision rather than an
exception.
The earlier definition, the ratio of the minimum to the maximum fill rate across served customers,
is not used anywhere, because excluding zero-fill customers made it rise when a customer was starved
and left it undefined at zero supply. fairness_index is declared in orders.metrics.yaml with
this exact expression (section 6.10), which is what lets run_allocation_whatif return it and
scenario s_backoffice_02 assert it.
Sums over demands run in order_id order with math.fsum, so the index does not depend on
insertion history (D-03).
Backorders. Unfilled demand becomes a backorder with a recovery date taken from the next
scheduled receipt covering the SKU. When supply arrives, backorders are filled by
backorder_fill_policy, either fifo_by_created_at or priority_then_age. Aging is trended, and
ORD-002 fires past the policy age.
Changes and cancellations. ChangeCostModel returns a cost and a rework time from a table keyed
by (change_type, stage_at_request). The cost rises across stages because the physical work already
done must be undone: a quantity decrease before release costs an allocation rewrite, after pick it
costs a putaway, after pack it costs a repack, and after ship it is not a change at all but a return
routed to 6a4. Invariant change_cost_monotone_in_stage holds for every change type. Accepted
changes republish the promise if the change affects the date.
Substitution. When a line cannot be filled and a SubstitutionRule exists for the SKU and the
customer's segment, the engine offers the substitute. If requires_customer_approval is true, a
service contact is generated with reason order_change, and the approval decision and delay are
drawn from the root/orders/substitution_response stream, subscripted by line_id. Substitutions
never change the invoice price unless price_treatment says so.
Perfect order. Evaluated at close, from four independently sourced booleans:
| Component | Source of truth |
|---|---|
| Complete | All lines shipped in full on the first shipment, from outbound events |
| On time | Ship date or delivery date versus promise date, per perfect_order.on_time_basis |
| Damage free | No damage-coded return and no damage claim linked to the order |
| Correctly invoiced | No AR dispute and no pricing correction on the invoice |
perfect_order_rate is defined once in the metrics layer. The agent cannot compute it another way
because the tool returns the metric, not a table to aggregate.
5.2 Customer service as an operation (6a12)¶
Contact generation. ContactGenerator subscribes to a configured set of operational failure
events. For each, it draws whether a contact occurs and when:
p_contact = base_rate[reason_code]
* segment_sensitivity[customer.segment]
* (1 + escalation_gain * prior_failures_90d)
delay ~ LogNormal(mu[reason_code], sigma[reason_code]) # seed root/orders/service/contacts
The generated contact carries caused_by_event_id, which is why the causality invariant is
enforceable and why a demo can trace a phone call back to a dock door that stuck.
WISMO takes a second path to the same invariant. An order whose promise date passes with no delivery
confirmation emits orders.promise_breached at that instant, and thereafter accrues a WISMO hazard
per elapsed day drawn from root/orders/service/contacts. Every WISMO contact names that
orders.promise_breached event as its caused_by_event_id. The absence of a delivery is not an
event and cannot be a cause, so the event that records the absence is emitted and used instead. The
WISMO rate still rises on its own when promise reliability falls, which was the point of the hazard
formulation, and it now satisfies contact_generation_causality rather than needing an exemption
from it.
The same event supplies ORD-004's exposure denominator. A u-chart needs a countable exposure, and
PromiseBreach.elapsed_days summed over open breached orders gives order-days open past promise.
Numerator is WISMO contacts, denominator is order-days, and neither is an assumed constant.
The queue. ServiceCenter is a SimPy resource with capacity taken from the roster
(RosterPort). Contacts queue, wait, and abandon if wait exceeds a drawn patience. Handle time is
drawn per reason code and scaled by the agent's proficiency from the workforce learning curve.
Reported KPIs: average speed of answer, service level (fraction answered within
service_level_seconds), abandonment rate, occupancy, and contacts per order.
First contact resolution. Each reason code declares required_visibility. An agent resolves at
first contact if agent.tool_visibility >= reason.required_visibility and a skill draw succeeds;
otherwise the contact escalates or produces a callback, and the event records blocking_data, the
name of the data element the agent could not see. This is what makes the visibility argument
measurable: run the same seeded shift at order_header and at twin_grounded, and the LSS engine
hypothesis-tests the FCR delta. The system demonstrates the value of its own API rather than
asserting it.
Satisfaction, churn, CLV. Satisfaction decrements by impact[reason_code] on each unresolved or
repeated failure and recovers toward baseline at recovery_rate per period. Churn is a weekly
hazard p = logistic(a + b * (1 - satisfaction) + c * failures_90d). On churn, remaining CLV is
computed as
where margin_t comes from CostToServePort. That port has two implementations and the calculation
ships in two stages, because the activity-based costing model it wants lands eighteen slices later
than the churn model that needs it.
| Stage | Implementation | margin_t |
clv_margin_source |
|---|---|---|---|
| Slice 4 | BaselineCostToServe |
annual_baseline_margin divided by the periods per year, per customer |
baseline_fallback |
| Slice 22 | AbcCostToServe |
Contribution after activity-based cost to serve, per customer per period | abc_cost_to_serve |
Wiring the second implementation is a config change, not a code change, and no churn or CLV code
moves. customer.churned carries clv_margin_source, so every CLV number in the log says which
implementation produced the margin behind it, and a reader can never mistake a fallback for a
modeled contribution. Customer.annual_baseline_margin exists only to feed the fallback and is
read nowhere else.
The agent question "what does it cost us if the top two churn" is answered by summing two
remaining_clv values, and get_impacted_customers returns clv_margin_source alongside them so
the answer states which stage it was computed at. From slice 22 those values come from the ABC
model. Before slice 22 they come from the baseline, and saying so is the difference between a
staged delivery and a false claim.
5.3 Procure-to-pay (6a13)¶
Requisitions arrive from the inventory optimizer's reorder signals, carrying the triggering event id so the whole chain from a stockout risk to a payment is one traceable thread. PO creation selects the contract that covers the category and supplier; if none exists, the buy is flagged for maverick evaluation at spend classification time.
Approval. ApprovalRouter walks the authority matrix for the PO's category and value, producing
the minimal chain of approvers whose limits cover it. Two rules are enforced and tested: the
requester is never an approver, and no approver's limit is below the value they approve. Approval
delay is drawn per role, which is what makes PO cycle time a real distribution instead of a constant.
Acknowledgment. The supplier model returns confirmed dates drawn from its reliability profile. The slip between requested and confirmed is recorded and feeds the buyer-side OTIF view, which is deliberately different from the supplier scorecard in 6a2: the buyer measures against the requested date, the supplier measures against the confirmed date, and the gap between the two views is itself a finding worth showing.
Three-way match. On invoice receipt the matcher pulls the PO and every receipt against it and
evaluates in a fixed order: duplicate detection first (same supplier, same amount, invoice date
within duplicate_window_days), then PO existence, then receipt existence, then unit of measure,
then quantity within tolerance, then price within tolerance, then freight and tax. The first failing
check determines the exception type, so the exception taxonomy is deterministic rather than
order-dependent. Exceptions enter a queue staffed by AP handlers modeled the same way as service
agents, which gives match exception cycle time a real queueing explanation and gives the process
miner a second office process to discover.
Payment. PaymentScheduler evaluates each matched invoice against its terms. If a discount is
available, it compares the discount against the annualised cost of capital
(discount_pct / (1 - discount_pct) * 365 / (net_days - discount_days)) and takes it when it beats
the hurdle rate. Missed economic discounts raise PRC-006. Payment timing drives DPO and the cash
forecast.
Strategic sourcing. An RFX event issues to invited suppliers, who submit price-volume curves,
lead times, and capacity. Scoring normalizes each criterion per normalization, applies weights, and
ranks.
The award engine ships two strategies against one interface. GreedyAwardStrategy fills by
descending score subject to capacity, max-share, and min-supplier constraints, breaking ties on
(-score, supplier_id) so the fill order is total and the result is deterministic. That tie-break
is stated because the allocation engine's is, and an award engine with no tie-break would make two
equal-scoring suppliers order-dependent. ExactAwardStrategy enumerates feasible allocations on a
declared quantity lattice and takes the argmax under the same (-score, supplier_id) ordering, so
the exact answer is unique too.
Greedy is not optimal in general and the section does not claim it is. Price-volume break curves
make the objective non-convex in the allocation: a supplier whose unit price drops at a break
quantity can be worth more volume than its marginal score justifies at the previous break, and
min-supplier and max-share caps make the feasible set non-matroidal. The shipped counterexample
tests/fixtures/sourcing/greedy_suboptimal.yaml has three suppliers, one break curve, and
min_suppliers: 2, on which greedy lands below the exact optimum. The fixture is committed together
with the gap the suite measures on it, so the number is recorded rather than asserted from memory,
and VG-PRC-03 fails if the measured gap moves. It is a fixture rather than a footnote, so the
limitation cannot quietly disappear.
AwardGapReport is what the engine returns alongside the award: the greedy objective, the exact
objective when the instance is small enough to enumerate, and the relative gap. The enumeration
budget is a config key (sourcing.exact_award_max_suppliers, default 6) and the report says
exact_unavailable above it rather than reporting a gap it did not measure. VG-PRC-03 asserts the
measured gap on the fixture suite; it does not assert that the gap is zero.
A mixed-integer formulation that closes the gap in general is a named ROADMAP milestone with its dependency on the OR-Tools deterministic budget contract (D-04) recorded. It is sequenced, not dropped, and until it lands the report states the gap rather than hiding it.
Tier-1 concentration is computed from the award itself. Tier-2 concentration needs the E19 supplier
DAG, which stays in Phase 6, so in-band SupplierDagPort returns unavailable and the consolidation
what-if reports tier-2 concentration as unenforced rather than as zero.
Spot buy versus contract. The fourth tactical decision 6a13 names, and the one that only exists
when a supplier slips. SpotBuyEvaluator subscribes to supplier.commit_date_slipped and to
orders.line_backordered, and opens a decision whenever a slip leaves a shortage that the contract
supplier cannot recover before need_by.
It prices three options against each other: buy spot at SpotPricePort's quote, wait for the
contract recovery date and carry the stockout, or expedite the contract order through
ExpediteEvaluator. The spot option carries the price premium, any minimum-volume or exclusivity
charge the contract levies, and a quality cost from the spot supplier's higher defect rate. The wait
option carries the stockout cost from the at-risk order lines and the SLA penalty schedule, the same
input the expedite decision uses, so the three options are priced on one basis.
The decision is recorded whichever way it goes, and a requisition raised from it carries
source: spot_buy and buy_mode: spot on the PO, which is what keeps a justified spot buy out of
the maverick spend numbers. A spot buy taken while the contract supplier could still meet the
need-by date is not a tactic, it is maverick spend; spot_buy_never_beats_available_contract
forbids the engine from recommending it and PRC-007 raises it when a human does it anyway.
Contracts. Cumulative volume advances through the tier schedule, republishing the unit price on
tier achievement. Rebates accrue per period on either a retrospective basis (the whole volume
reprices at the achieved tier) or an incremental basis (only volume above the threshold reprices).
Both are implemented because the accounting differs and the difference shows up in the GL. Expiry
inside renegotiation_lead_days raises PRC-003 with annual spend at risk attached.
Spend analytics and maverick detection. Every settled spend transaction is classified into the
three-level taxonomy. A transaction is maverick when a live contract covers its category and supplier
class but the transaction did not reference it. Leakage is
(paid_unit_price - contract_unit_price) * qty, floored at zero, and the Pareto by category and by
supplier is the standard first move.
Savings versus avoidance. Two ledgers, never merged, and the split is drawn where the chart of
accounts can witness it. A savings entry has baseline_method: prior_price and a GL posting that
resolves to the material price variance account, which is the one account in section 5.8's posting
table that records a price difference. Every other baseline (market index, budget, announced
increase) is an avoidance entry: it records an increase that did not happen, has no GL posting by
construction, and carries the announced increase or the index quote in evidence_event_ids. The
report presents them side by side with a note that only one of them reaches the P&L, which is the
honest version of the claim procurement teams make.
Tactical decisions. ForwardBuyEvaluator compares duty and price increase avoided against
carrying cost, obsolescence risk from the E&O model, and the cash cost of buying early, and returns a
breakeven horizon. ExpediteEvaluator compares the premium freight quote from the transport package
against the stockout cost derived from the order book's at-risk lines and the customer's SLA penalty
schedule. EoqValidator computes the closed-form EOQ and then runs the twin at a grid of order
quantities around it to confirm the analytic answer is actually the simulated minimum, which is the
same analytic-versus-simulated discipline the MEIO layer uses.
5.4 Tariffs and trade policy (E14)¶
LandedCostCalculator computes, per incoterm, the components each party bears, and returns a
breakdown that always sums to the total. Duty is evaluated from the active schedule composed with
any active scenario overlays:
duty = ad_valorem_pct * dutiable_value (kind = ad_valorem)
= specific_per_unit * qty (kind = specific)
= ad_valorem_pct * dutiable_value + specific_per_unit * qty (kind = compound)
dutiable_value is config-selectable between transaction value and transaction value plus freight
and insurance, because the choice changes the answer and a reader from trade compliance will look
for it.
Scenario overlays are pure functions on the schedule, and the two operations have separate names
because they have separate types (section 3.3). merge_overlays(overlay, overlay) -> overlay
combines two overlays into a canonical one, and it is tested for associativity over generated
overlays with deliberately overlapping targets, not only disjoint ones. apply_overlay(schedule,
overlay) -> schedule produces a new schedule, and the test that matters is that applying A then B
yields the same content hash as applying merge_overlays(A, B). The shipped retaliation_mirror
can overlap rate_shock_plastics, so an invariant that held only on disjoint targets proved nothing
about the configuration the repository actually ships. De minimis changes affect the parcel channel
only, which is where the e-commerce flow (6a6) feels trade policy directly. Retaliation overlays are
declared as mirrors of another overlay so a scenario author writes one line rather than a table.
FTZ admission defers duty until withdrawal. If the withdrawal is for export, no duty is due and the lot becomes eligible for drawback instead. If it is for domestic consumption, duty is due at the rate in force at withdrawal, which is what makes the FTZ interesting under a rate shock scenario. The neutrality invariant pins the implementation: with an unchanged rate, FTZ changes only the payment date, so any test that shows a different total duty has found a bug.
The tariff engine is the input to three things outside itself, and each one is reached through a declared seam rather than an assumption.
| Consumer | Seam | Behavior when the other side is absent |
|---|---|---|
| Procurement forward-buy and sourcing | trade.landed_cost_computed, trade.scenario_activated |
Procurement's LandedCostPort null implementation returns duty zero |
| Multi-echelon inventory re-run (6a8) | MeioPort.rerun(scenario_id, landed_costs) -> MeioDelta |
Returns MeioDelta.unavailable(), rendered as "not available" and never as zero |
| Finance duty variance | trade.duty_accrued through the posting rules |
The duty variance kind reports zero rows rather than a wrong number |
MeioPort is the interface run_landed_cost_scenario's MEIO delta comes from. Without it the tool
declared an output field nothing could produce. The port is declared in twinflow_trade.ports and
implemented by the planning package (6a8) when installed; its null implementation returns
MeioDelta.unavailable(), and every consumer renders that as "not available". A missing planning
package can never be read as no inventory impact, which is the failure mode a zero default creates.
The landed cost event carries scenario_id so every downstream number can be attributed to the
policy assumption that produced it.
5.5 Workforce (6a14)¶
Hiring lag. A requisition opens when a headcount driver crosses its threshold. The pipeline runs stage by stage with drawn conversions and dwell times, so time-to-fill is an emergent distribution, not a parameter. The consequence the source cares about is that headcount decisions lag demand: a peak that needs people in week 40 needs a requisition in week 33, and the twin makes that lag visible instead of assuming labor is instantly available.
Ramp. A new hire starts at initial_productivity_ratio and climbs the Wright curve as cumulative
units accumulate. Error rate follows its own curve downward. Both feed the physical twin: a station
staffed by ramping workers is slower and produces more quality findings, which is the loop the source
asks for between onboarding and the quality stream.
Skills gating. Station assignment consults StationEligibility. An expired certification removes
stations immediately, and if that leaves a station with no eligible worker on the roster, HR-003
fires at high severity because the line is about to stop. single_point_of_failure_stations is
computed each roster publication and raises HR-005.
Cross-training as an investment. The what-if takes a list of (worker, station) pairs, prices the
training (trainee hours, trainer hours, lost production during training), applies the new eligibility,
and reruns a seeded absenteeism scenario. The payoff is measured in coverage, overtime hours avoided,
and service level held, and the LSS engine tests whether the difference is significant across
replications rather than across one lucky seed.
Attrition. The hazard is evaluated weekly per worker. Because its drivers are quantities the twin already measures, burning the workforce out shows up as regretted turnover with a bill attached: recruiting cost, onboarding labor, the ramp-loss integral, and backfill overtime. That is what turns workforce care into a P&L argument instead of a slogan.
Absenteeism. The generator draws absence from a model whose true coefficients are known. The
predictor is trained on the generated history and scored with the Brier score and a calibration
curve against a base-rate benchmark. The forecast publishes to E23, and the roster it produces
returns as hr.shift_assigned, closing the loop. When E23 has not landed, a fallback roster
allocates workers to stations by eligibility and seniority so the workforce package is never blocked.
Engagement, measured behaviorally. 6a14 asks for engagement measured behaviorally rather than
by survey, trended on control charts as a leading indicator. No survey instrument exists anywhere in
the model. EngagementSnapshot is published weekly per worker from state the twin already records,
and aggregated per shift and per station.
| Indicator | Computed from | Chart |
|---|---|---|
schedule_stability_index |
Shift-start changes inside the notice window over shifts worked, 8 weeks | I-MR |
weekend_shift_rate_8w |
Weekend shifts over shifts worked, 8 weeks | p-chart |
strain_trend_slope |
OLS slope of the strain index over 8 weeks, strain units per week | I-MR |
overtime_pct_4w |
Overtime hours over total hours, 4 weeks | I-MR on log transform |
The chart assignments are in section 7.4 and the metric definitions are in section 6.10, because a
leading indicator that is not in the semantic layer cannot be charted and cannot be asked about.
HR-006 fires on a signal from the first two. The loop closes back on attrition: the same
schedule_stability_index and weekend_shifts_rolling_8w are drivers in the attrition hazard, so a
schedule-stability signal is an early warning of the turnover the hazard prices later.
Labor cost accounting. Every hour lands in the labor ledger with its hour type and its activity, which is what lets ABC allocate labor to orders later and what lets the labor efficiency variance drill down to a specific shift.
The peak season what-if. Three strategies are priced over the same seeded demand: temps (fast, lower productivity, higher error rate, no attrition cost), overtime (no ramp, rising strain and attrition hazard, premium rates), and a hiring pulse with partial post-peak layoff (ramp cost, severance, morale effect on the remaining workforce through schedule stability). Output is a total cost comparison with the error-rate and attrition consequences priced in, and the LSS engine's verdict on whether the differences are real.
5.6 IT and cybersecurity operations (6a15)¶
The twin's own infrastructure is modeled. Each CI has a service model with a capacity, a queue,
and a failure process. Golden signals come from that model in simulation mode and from OpenTelemetry
in production mode, but the SLI computation is one implementation over itops.health_sample.
Logs and traces. 6a15 asks for metrics, logs, and traces on every service, and metrics alone
cannot carry two of the four DORA measures. Every request through a modeled CI produces a Span,
spans join into a TraceSample with a critical path, and every log line is a LogRecord carrying a
template_id and, where one exists, the trace_id it belongs to.
Three things become measurable that were previously inferred from ticket timestamps. Lead time for
changes reads the trace that carries the change's chg_id from first commit-analog event to the
span in which the new version first serves a request. Failed deployment recovery time reads the
first error-free trace after the incident's mitigation span. Request error rate and latency p95 come
from the span stream rather than from a health sample that already aggregated them away.
Sampling is head-based at a configured rate plus tail-based retention of every trace containing an
error span, and sampled_by records which rule kept a trace so a rate estimate can correct for it.
The sampling decision is drawn from root/itops/trace_sampling subscripted by trace_id, which
keeps it deterministic and keeps it out of the way of every other draw. Log volume is published as
itops.log_volume_snapshot per template rather than per rendered string, so log volume is countable
and a log storm is a chart rather than an anecdote.
Error budgets. Budget consumed is (1 - achieved_sli) / (1 - target) over the window. Burn-rate
alerting uses the multi-window multi-burn-rate pattern published in the Google SRE Workbook,
chapter 5: a fast burn alert on a short window and a long window together, and a slow burn alert on
longer windows, with both windows required to be in breach so a brief blip does not page. When the
budget is exhausted, the change policy gate flips: only emergency changes are approved until the
budget recovers. That gate is a testable behavior, not a policy document.
The burn-rate thresholds are derived at config load, never written as literals:
The Workbook's table gives budget fractions and windows, not portable constants. Its recommended 2 percent in one hour, 5 percent in six hours, and 10 percent in three days evaluate to 14.4, 6, and 1 only at a 30-day SLO window, which is the window that chapter uses throughout. This section's SLOs run on a 28-day window, so the same fractions give 13.44, 5.6, and 0.9333. Hard-coding 14.4 against a 28-day window would have claimed the Workbook's derivation while consuming 2.14 percent of budget rather than 2 percent.
derive_burn_rates(window_days, budget_fraction, long_window_hours) computes them,
BurnRateTable holds the result, and section 6.5 declares fractions and windows. VG-IT-02 asserts
the derivation against the Workbook's published pairs by evaluating it at window_days = 30 and
checking it reproduces 14.4, 6, and 1 exactly, then asserts the alerting boundaries on a fixture SLI
series at the configured 28 days. A change to window_days moves the thresholds and the gate stays
true, which is the property a hard-coded constant destroys.
Change management. Change failure probability is
p_fail = base[type]
* risk_multiplier(risk_score)
* window_multiplier(day_of_week, hour)
* criticality_multiplier(ci.criticality)
* (1.0 if has_rollback_plan else no_rollback_penalty)
The shipped default configuration gives Friday late-afternoon a materially higher multiplier, which
reproduces the classic lesson the source names. A failed change opens an incident with
caused_by_change_id set, which is exactly what makes change failure rate computable rather than
self-reported. The what-if "enforce a Friday freeze" reruns the same seeded change stream against a
policy that reschedules those changes and hypothesis-tests the change failure rate delta.
DORA metrics. Deployment frequency counts implemented changes per window. Lead time for changes measures request to implementation. Change failure rate is failed or rolled-back changes over total changes. Failed deployment recovery time measures the incident MTTR for change-caused incidents. Each goes on the chart its data type demands, listed in section 7.
Vulnerability and patch economics. The synthetic CVE feed emits vulnerabilities against component
versions present in the CMDB. Identifiers are prefixed TWF-CVE- and never collide with real CVE
identifiers; the README states the feed is synthetic. CVSS v3.1 base scores are computed from the
vector rather than stored, so the implementation is testable against the published specification.
For a vulnerability on a CI that needs a production window, PatchWindowEvaluator enumerates
candidate windows over the next patch_horizon_days, calls the twin's what-if engine to price the
production cost of taking that CI down for window_minutes in each window (throughput lost, orders
at risk, overtime to recover), and computes the modeled breach exposure of waiting.
ExposureModel is a distribution, not a scalar, and every step says which it is:
p_daily(v) = exploit_probability_daily[v.severity] * (known_exploited_multiplier if v.known_exploited else 1.0)
P_breach(t) = 1 - (1 - p_daily(v)) ** t # scalar in [0, 1]
Impact ~ downtime_hours x gross_profit_rate_per_hour + recovery_cost + claim_cost + regulatory_cost
Loss(t) = Bernoulli(P_breach(t)) * Impact # a distribution over Money
downtime_hours is drawn from root/itops/breach_impact subscripted by vuln_id, and
gross_profit_rate_per_hour comes from fin.gross_profit_rate through GrossProfitRatePort, so
breach exposure is valued from the same ledger number as a business-interruption claim.
known_exploited_multiplier is a declared config key (section 6.5) with a stated default, because
"raised for known exploited" without a number is not a model. It multiplies the daily hazard, and
p_daily is clamped to 1.0 so a large multiplier cannot produce a probability above one.
known_exploited_fraction is a separate key that sets how many published vulnerabilities carry the
flag; prevalence and uplift are two parameters and the section keeps them apart.
The objective is stated in the risk measure it actually uses. CvarObjective ranks candidate
windows by
with alpha in [0, 1] and beta the tail probability, both config keys. At alpha = 0 this is
expected total cost, which is risk neutral. At alpha = 1 it is conditional value at risk at level
beta, which prices the tail rather than the mean. A scalar multiplier on expected cost, which the
config previously called risk_aversion, is a linear weight on a mean and is risk neutral for any
value it takes; it is gone, and risk_preference_alpha and cvar_beta replace it. The tail is
estimated from cvar_samples draws of Loss(t) per candidate window, seeded from
root/itops/breach_impact, and the event carries the sample count so the estimate's noise is
visible.
sec.patch_window_evaluated carries every candidate window with its production cost, its expected
loss, its CVaR, and the objective value, so the agent presents the tradeoff rather than only the
answer. Deferral needs an explicit accepted risk amount and a decision maker, which is how risk
acceptance is supposed to work and rarely does.
Zones and conduits. Zones are declared in config with their Purdue level and target security level. Conduits declare the flows that may cross. The tap observes every crossing. A crossing with no matching conduit, or with a protocol the conduit does not permit, is a violation. Crossings that match a conduit are compared against a learned baseline profile (rate, byte volume, direction mix) and deviations beyond the configured control limits are anomalies. Both are security findings, and both are things the Purdue-segmented docker topology makes real rather than notional.
Detection rules. Rules are versioned code under the detections storage prefix. The engine runs
them over the historian on a schedule, against a snapshot pinned to a stated watermark and with the
total ordering every historian query in this section carries (section 3). Each rule must ship a
positive and a negative fixture; CI fails a rule without both, fails a rule whose logic changed
without a version bump, and fails a rule that stops firing on its own positive fixture. That last
check is IT-005 and it is the difference between detection engineering and a wall of untested
queries.
Correlation, the lightweight SIEM analog. A detection rule judges one event stream in isolation,
which is why a slow credential-spray across three zones looks like three unremarkable findings. 6a15
asks for cross-zone correlation with detection rules as versioned code, and CorrelationEngine
supplies the second half.
It consumes the finding stream and the detection-rule stream, groups by the join keys the rule
declares (src_ci, zone_id, subject_id, or subject_kind), and raises
sec.correlation_alert_raised when at least min_inputs distinct inputs appear across at least
min_distinct_zones zones inside a tumbling window of window_seconds. The alert carries every
contributing finding id, so a correlated alert drills down to the individual detections and from
there to the raw events.
Correlation rules live beside detection rules, ship the same positive and negative fixtures, and
fail CI under the same three rules. Each window is sorted by the canonical total order
(sim_ts, producer_id, seq) before evaluation, so the alerts raised do not depend on arrival order
inside the window (D-07). IT-004's suggested next tool now points at a component with an API, an
event, a config block, and a gate, rather than at a label.
RBAC. Deny by default. Every twin REST route, MCP tool, and agent tool declares its required
permission in tools/manifest.yaml. The agent's tool calls pass through the same engine as a
human's API call, and every granted and denied decision is logged with the actor identity, which
is the audit trail the governance register consumes.
Autonomy tier on mutating tools. Two tools in section 5.10 change state rather than reading it:
run_restore_drill injects a failure, and every twin:whatif tool consumes a simulation budget.
E5's tier enum and its approval gate are resequenced ahead of this band (section 8.2), so every tool
in tools/manifest.yaml declares a tier. In-band this section ships L1 and L2 only: a read tool is
L1 (advise) and a mutating tool is L2 (recommend, human approval recorded before execution). No tool
here is L3. The approval is an event with an actor, so an agent-invoked restore drill has the same
audit trail as a human-invoked one. E5 later adds L3 and the guardrail evaluator; it does not have
to retrofit the tier field.
Backup and restore. Backups run on schedule with an integrity hash and a verification flag. A
restore drill is a ChaosScenario of kind restore_drill (section 3.5): inject a CI failure at a
sim-time, detect, select the newest verified backup, restore, and measure. RPO is the sim-time
between the last verified backup and the failure, and the drill counts the events that fall in that
window so data loss is a number, not an estimate. RTO is detection to healthy. Targets are declared
in config as synthetic policy values carrying provenance: synthetic; the README states they are
illustrative and are not drawn from any client deliverable.
What 6a15 leaves for E18. The zone model, the conduit tap, the detection engine, the incident pipeline, backups with measured restore times, and the error budget. E18 adds adversarial scenarios that drive them, and the readiness report it produces is scored against the runbooks and SLOs defined here.
5.7 Marketing, sales operations, S&OP (6a16)¶
Promotions. A promotion multiplies baseline demand by its lift curve during the window. The
incremental units split three ways, and the split is recorded as ground truth on
mkt.lift_realised:
incremental_total = promoted_units - baseline_units
pulled_forward = pull_forward_fraction * incremental_total
incremental_true = incremental_total - pulled_forward
cannibalised_from[j] = C[i][j] * baseline_j (removed from j, added to i)
Pulled-forward units are subtracted from the following periods at geometric weights summing to one, so the post-promotion dip is generated by construction and the naive forecaster gets fooled by it exactly the way real ones do. Because the true decomposition is logged, E30's causal estimate has a ground truth to be scored against, which is the point the source makes about promotion effects being the textbook case where correlation lies.
A promotion is checked against modeled capacity at planning time. If the planned lift exceeds what the DC can serve within the SLA, CMR-001 fires before the campaign runs, which is the marketing operations failure the source names: the campaign that fills the order book and empties the fill rate.
Demand as a lever, not an input. 6a16's thesis is that demand stops being purely exogenous and becomes partly something the company pulls. Three seams carry that, and none of them is implicit.
| Lever | Carrier | Consumer | Effect |
|---|---|---|---|
| Promotion lift | DemandShapingPort, mkt.demand_multiplier_published |
ArrivalProcess (6a12) |
Multiplies the baseline arrival intensity per SKU per period |
| Quota pressure | The same multiplier, source quota_pressure |
ArrivalProcess |
Adds the intra-period pressure curve to the same multiplier |
| Channel mix drift | ChannelMixPort, mkt.channel_mix_published |
ArrivalProcess |
Sets the share vector each order's channel is drawn from |
| Promo calendar | PromoFeaturePort, mkt.promo_features_published |
Forecaster (6a1) | Feeds the promo calendar to the forecaster as a feature frame |
DemandMultiplier carries its factor decomposition, so an arrival spike is always attributable to
the levers that caused it. The multiplier applies to intensity, never to realized orders, so the
arrival process stays a point process rather than a scaled count and the queueing results downstream
stay meaningful.
The forecaster coupling matters most and is the one 6a16 states outright: the forecaster must ingest
the promo calendar as a feature. PromoFeatureFrame (section 3.6) is built from the calendar alone
and never from realized demand, so a forecaster consuming it cannot leak the outcome. The naive rung
of the FVA ladder does not receive the frame and is fooled by the post-promotion dip; the
statistical rung does receive it and is not. That contrast is what makes the FVA ladder measure
something rather than restate an assumption.
Channel mix drifts as a Dirichlet random walk on the share simplex, so the wholesale versus e-commerce balance moves over a run instead of being a constant the building argues about once. The mix feeds cost-to-serve through the channel dimension on every order, so a mix shift shows up as a margin movement with a named cause rather than as unexplained variance.
Sales pipeline and bias. Opportunities progress through stages with drawn conversions. Each rep submits a forecast equal to the probability-weighted pipeline scaled by their true bias multiplier plus noise. The demand planning function measures each rep's realized bias over time and learns whose numbers to haircut. The bias multiplier truth is logged so the measurement can be scored.
Quota effects. Order arrival intensity within a quota period follows the configured pressure curve, and discount depth escalates near the boundary. The result is a quarter-end spike that hits the DC every thirteenth week as a standard demand pattern the rest of the system has to absorb.
NPI cold start. A launched product has no history. The forecaster uses the analog SKU's profile
blended with the Bass diffusion prior for cold_start_blend_periods, then transitions to its own
history. npi.adoption_observed carries the analytic Bass expectation next to the observed value so
the cold-start handling is measurable rather than asserted.
The S&OP cycle. Five steps run on the configured monthly calendar, each a process instance with start and end events:
- Product review. NPI decisions and rationalization decisions, published as changes to the active SKU set.
- Demand review. The statistical forecast, the sales input, and the marketing calendar are reconciled into one consensus number. The FVA ladder scores each input's historical contribution, so a step that makes the forecast worse is visible as negative FVA.
- Supply review. Capacity against the consensus, taken from the factory finite schedule, DC
labor requirement, and transport capacity.
RoughCutCapacityCheck(section 3.6) is the computation, andsop.supply_review_modeselects it:analyticis a rough-cut plan over bill-of-resource coefficients and is the shipped default,surrogatecalls E28 throughCapacityPort, andfull_simulationruns the twin over the horizon. Gaps are identified with their constraining resource, andsop.supply_review_completedrecords which mode produced the numbers so a rough-cut figure is never mistaken for a simulated one. - Integrated reconciliation. Each gap is turned into options, and each option is priced by the financial twin: margin impact, cash impact, service impact.
- Executive meeting. The agent presents the decision packet. The decision is logged with its assumptions and confidence, and mirrored to the governance register.
Next month's cycle scores last month's decisions: sop.decision_scored compares each logged
assumption against its measurement query and reports the outcome versus expectation. Maturity metrics
(plan adherence, FVA by source, decision latency, one-number-plan variance) are computed each cycle
and trended.
Because every step emits start and end events with cycle_id as the case key, the process mining kit
can discover the S&OP process itself, find the rework loops in it, and compute where the month goes.
The twin runs a kaizen on its own planning ritual.
5.8 Finance and accounting (6a17, E22)¶
Posting. PostingEngine subscribes to the event bus and evaluates posting_rules.yaml. Each
matching rule produces a balanced journal entry carrying source_event_id and rule_id. Nothing
else writes to the ledger. Money is integer minor units end to end, and every rule fixture asserts
the entry balances exactly.
A representative slice of the rule set (the full set lives in the file, and each row has a fixture):
| Source event | Debit | Credit | Amount basis |
|---|---|---|---|
receiving.goods_receipt |
Inventory raw material | GR/IR clearing | PO price times qty received |
trade.duty_accrued |
Inventory raw material or duty expense per duty_capitalisation |
Duty payable | computed duty |
procurement.three_way_match_evaluated (matched) |
GR/IR clearing | Accounts payable | invoice amount |
procurement.three_way_match_evaluated (price variance within tolerance) |
Material price variance (PPV) | Accounts payable | (invoice price minus standard) times qty |
production.material_issue |
Work in process | Inventory raw material | standard qty times standard price, usage delta to material quantity variance |
production.completion |
Finished goods | Work in process | standard cost of output |
production.scrap_recorded |
Scrap variance | Work in process | standard cost at the scrap point |
hr.labor_cost_posted |
Work in process (direct) or labor expense (indirect) | Accrued payroll | hours times rate, rate and efficiency deltas to their variance accounts |
outbound.shipment_confirmed |
Cost of goods sold | Finished goods | valuation method |
orders.order_invoiced |
Accounts receivable | Revenue and tax payable | extended price net of discounts |
transport.freight_invoice |
Freight expense or premium freight expense | Accounts payable | carrier invoice, expedite premium isolated |
ar.cash_received |
Cash | Accounts receivable | payment, early-pay discount to sales discounts |
procurement.payment_executed |
Accounts payable | Cash | payment, discount taken to discounts received |
returns.disposition_completed |
Inventory or disposal loss by disposition | Returns allowance | recovery value by disposition path |
inv.count_completed (variance) |
Shrink expense | Inventory | book minus physical at valuation |
inv.reserve_updated |
Excess and obsolete expense | Inventory reserve (contra) | reserve model output |
capex.approved and asset placed in service |
Fixed assets | Cash or accounts payable | invoice |
| depreciation run | Depreciation expense | Accumulated depreciation | schedule |
risk.premium_rated |
Insurance expense | Prepaid insurance or accounts payable | premium accrual |
risk.loss_recognised |
Loss expense by loss_type |
Inventory, fixed assets, or accrued liabilities | gross loss at the valuation the loss type declares |
risk.claim_settled |
Cash | Loss recovery (contra to the original loss account) | payout |
ar.invoice_corrected |
Revenue or tax payable | Accounts receivable | correction amount, signed |
mkt.campaign_spend_posted |
Marketing expense | Accounts payable | campaign cost |
gl.period_closed |
Income summary, then retained earnings | Nominal accounts, then income summary | the closing entry of section 3.7 |
Statements. ProfitAndLoss, BalanceSheet, and CashFlow are pure functions of the ledger for a
period. The cash flow statement is produced by the indirect method from the ledger and cross-checked
against the direct sum of cash-account movements; the two must agree exactly, which catches a whole
class of posting-rule mistakes.
Standard costing and variance drill-down. The roll-up produces standard costs from the BOM and
routing at a single effective revision. During the period, actuals accumulate against standards and
VarianceEngine computes each variance kind from its published formula. Every variance carries an
attribution list: the specific goods receipts, labor records, scrap events, duty accruals, and
freight invoices that produced it, with signed amounts that sum exactly to the variance. Clicking a
variance lands on physical events, which is the thesis the source states: every dollar of variance
has a physical explanation and the system can show it.
The common-cause gate. Before a variance appears on the explain list, the engine sends its
historical series to the LSS engine, which runs an I-MR chart. A point inside the limits with no
rule firing is labeled common_cause and the report says so. Only assignable variances are put
in front of a human. This is the statistical discipline finance reviews lack.
The gate has an acceptance criterion, and the criterion is scoped to what a published source states.
Under the single rule "one point beyond the 3-sigma limits", the NIST/SEMATECH e-Handbook section
6.3.1 publishes the false alarm rate for a normal characteristic on a chart with known parameters:
0.00135 in one direction, 0.0027 in both. VG-FIN-07 runs the gate with nelson_rules: [rule_1] on a
seeded in-control stream and asserts the observed assignable rate against 0.0027 to the precision
the handbook prints it, three significant digits.
The full Nelson rule set on an I-MR chart whose limits are estimated from the same data has no
published false alarm rate, and no closed form. It needs a Markov-chain average run length
computation or a simulation, and the handbook does not contain it. The section does not assert one.
The shipped rule set's rate is measured once, recorded as nelson_set_false_alarm_rate in the
capability report with its measurement method and its Monte Carlo standard error, and treated as a
regression baseline rather than as a validated statistic. Open question 13 records what would be
needed to promote it. Under D-11 condition 5, a statistic with no valid external reference is an
open question rather than a passing gate, and the gate ships at the scope the reference supports.
Inventory. Valuation runs per SKU per period under the configured method, and the flow identity is
asserted in both units and value. The E&O reserve reads forward coverage from the demand model, so the
reserve moves when the forecast moves, and the driver events are recorded. Cycle counts run by ABC
class and produce both accuracy measures. Because RFID scan confirmation can be turned off in config,
the repo can run the same seeded shift with and without it and hypothesis-test the record accuracy
delta, then propagate that delta into shrink, reserves, and write-offs. The two arms draw count
errors from root/finance/count_error subscripted by (location, sku), so the same location draws
the same error in both arms and the measured delta is the treatment rather than path divergence
(section 5.0). That chain is the honest, fully synthetic version of the inventory-record-accuracy
argument.
Activity-based costing. Pools collect indirect costs from the GL. Drivers come from the twin's own activity records: every station touch, every pick line, every service contact, every return inspection. Rates are recomputed each period. Cost to serve is then computed per order by summing the activities that order actually consumed, which is why a five-station parcel order carries five stations of cost. Channel and customer profitability roll up from there.
FP&A. The budget is driver-based: the S&OP consensus volume drives activity quantities, ABC rates turn those into costs, and the result is a budget by account that reconciles to the plan. Rolling reforecasts refresh at each cycle. The variance-to-budget bridge decomposes budget to actual into named components (volume, price, mix, rate, efficiency, spend, and the residual), and the bridge must reconcile exactly, which is the test that keeps the narration honest.
Capex governance. An accepted what-if becomes a capex request. Cash flows come from twin
projections over the same metric definitions used to measure the result later, which is what makes the
post-investment audit meaningful. NPV, IRR, and both payback measures are computed, the authority
matrix routes the approval, and at post_audit_lag_periods the realized benefit is measured and
compared. The hit rate across all approved capex becomes a KPI of the decision process itself.
Month-end close. The close checklist is a task graph with dependencies and owners. Tasks emit
start and complete events with period_id as the case key. Accruals post for goods received not
invoiced and for unbilled revenue with their reversal entries scheduled. Subledger to control account
reconciliations run, and a break raises close.reconciliation_break. Close cycle time, critical path,
manual journal count, and rework count are the KPIs, and the close kaizen what-if reorders or
parallelises tasks subject to the dependency graph and tests whether close time actually dropped.
Controls. SOD rules are evaluated against the live grant set from twinflow-itops through
GrantsPort, so the finance package owns what conflicts and the IT package owns who has what. The
controls library maps each control to the process it governs, and control tests run on their declared
frequency as automated audits whose results feed the compliance layer in 6a11 and the findings stream.
E22 working capital. AP and AR terms drive scheduled cash. DIO, DSO, and DPO are computed from ledger balances and flows with the definitions fixed in the metrics layer. Cash-to-cash is their combination. Working capital is attributed per echelon (supplier stock, inbound in transit, DC stock, WIP, finished goods, outbound in transit, receivables) so a disruption reports where cash is locked as well as what service did. Every disruption scenario's report carries a cash impact line next to its service impact line, which is the language the source says CFOs fund twins in.
5.9 Insurance and risk transfer (E38)¶
Every loss the twin generates is recognized in the ledger when it happens, through
risk.loss_recognised, whether or not it is ever claimed. Insurance recovery is a separate posting
on settlement. Retained loss is the difference, and it is the largest component of total cost of
risk in most periods, so recognizing the gross loss is what lets tcor_reconciles_to_gl hold at
all.
Cargo claims trigger from in-transit telemetry: a shock event above the policy's threshold or a temperature excursion beyond the cold-chain limit, confirmed by a damage-coded disposition on receipt. The claim runs a lifecycle with drawn durations per stage.
The payout applies the sublimit, the deductible, the coinsurance, and the limit in that order.
SublimitResolver maps the loss to at most one sublimit key by the ordered rule set in
insurance.sublimit_rules, first match wins, and a loss matching no rule takes the policy limit.
The shipped temperature_excursion sublimit matches losses whose trigger is
transport.temperature_excursion, which is the class the cold-chain scenario produces. The key and
the resolved applicable_limit go on the claim record and on risk.claim_settled, alongside
binding_constraint, which names whether the deductible, the coinsurance, the sublimit, or the
policy limit determined the payout. Without the resolver the sublimit shipped in config would change
nothing, which is worse than not shipping it; INS-004 fires when a payout is bound by a sublimit
rather than by the policy limit, because that is a coverage gap a risk manager needs to see.
Business interruption claims trigger from disruption scenarios. The downtime window comes from
chaos.scenario_started and chaos.scenario_ended or from an incident's mitigation timestamps.
Downtime beyond the waiting period is indemnified up to the indemnity period, valued at
rate_per_hour from fin.gross_profit_rate through GrossProfitRatePort rather than at a guessed
rate, which is why the claim and the P&L cannot disagree about what an hour of downtime was worth.
Premiums are experience-rated. The modifier is a function of the twin's own loss ratio over the rating period and, for workers' compensation, of TRIR from the ergonomics layer. That closes a loop the source cares about: a safety program that reduces TRIR reduces premium, so safety investments and throughput investments argue in the same currency.
The risk transfer what-if compares options over a common seeded loss corpus: raise the deductible, buy a risk-control capex such as a sprinkler upgrade, buy a higher limit, or self-insure a layer. Each option is scored on total cost of risk across replications, reported as a distribution with the significance test attached rather than a single number, because the difference between two risk options is exactly the kind of comparison a single run cannot settle.
5.10 Agent tools contributed¶
Every tool below is schema-constrained with Pydantic input and output models (E26d), executes a query
against the historian rather than computing in tokens (E26a), resolves every metric through the
governed semantic layer (E26b), and is gated by an RBAC permission (6a15). Each ships at least three
eval questions with ground-truth answers computed from the simulation (E27). Each declares an
autonomy tier in tools/manifest.yaml: L1 for a read tool, L2 for a tool that changes state or
consumes a simulation budget, which needs a recorded human approval before it executes. No tool in
this section is L3.
Every tool that returns a statistical verdict takes replications and base_seed, never a single
seed. A verdict from one run is not a verdict, and a tool signature that accepts one seed while
promising a hypothesis test is a contract that cannot be met. The arms of every paired comparison
share substreams under the common random numbers rule in section 5.0, which is what makes 20
replications enough.
| Tool | Input | Output | Permission | Tier |
|---|---|---|---|---|
get_order_status |
order_id |
status, promise, allocation, shipment, perfect order components | orders:read |
L1 |
get_impacted_customers |
window, optional failure event ids | ranked customers with failures, CLV at risk, clv_margin_source, recommended contact order |
orders:read, finance:read |
L1 |
get_service_queue_state |
none | queue depth, ASA, service level, occupancy, FCR rate, top blocking data | service:read |
L1 |
run_allocation_whatif |
policies, window, base seed, replications | fill by segment, Jain fairness index, backorder aging, paired verdict with effect size and interval | twin:whatif |
L2 |
get_spend_analysis |
category, period | Pareto by supplier and category, maverick leakage, contract coverage | procurement:read |
L1 |
run_sourcing_whatif |
category, supplier set, constraints, scenario id, replications | award, savings from tiers, tier-1 concentration, award gap report, resilience cost, recommendation | twin:whatif |
L2 |
get_supplier_scorecard_buyer_view |
supplier id, period | OTIF against requested and confirmed dates, PPV, defect PPM, PO cycle time | procurement:read |
L1 |
get_spot_buy_options |
shortage event id | spot, wait, and expedite options priced on one basis, net benefit, recommendation | procurement:read |
L1 |
run_landed_cost_scenario |
sku or category, scenario id | landed cost by origin, re-ranked sourcing table, MEIO delta or unavailable |
trade:read, twin:whatif |
L2 |
get_workforce_state |
date | headcount, coverage, eligibility gaps, overtime, engagement indicators, attrition hazard concentration | hr:read |
L1 |
run_labor_whatif |
strategies, horizon, base seed, replications | total cost, error rate, attrition risk, service level, paired verdict per strategy pair | twin:whatif |
L2 |
get_slo_status |
service | SLI, target, budget remaining, burn rate, derived thresholds, gating state | itops:read |
L1 |
get_patch_options |
vuln id | candidate windows with production cost, expected loss, CVaR, objective, recommendation | sec:read, twin:whatif |
L2 |
get_correlated_alerts |
window, zone set | correlated alerts with contributing findings and distinct zones | sec:read |
L1 |
run_restore_drill |
ci id | measured RPO, RTO, data loss events, target met | sec:drill |
L2 |
get_sop_packet |
cycle id | gaps, options, priced impacts, supply review mode, recommendation, confidence | sop:read |
L1 |
run_promo_whatif |
promo definition, base seed, replications | lift, cannibalisation, pull-forward, overtime and expedite cost, margin after all of it, alternative timing | twin:whatif |
L2 |
get_variance_decomposition |
period, metric | ranked variance components with causal class and drill-down event ids | finance:read |
L1 |
run_capex_appraisal |
whatif id, discount rate | NPV, IRR, payback, authority tier required | finance:read |
L1 |
get_cash_position |
horizon weeks | inflows, outflows, closing balances, cash-to-cash, working capital by echelon | finance:read |
L1 |
get_tcor |
period | premiums, retained losses, risk control spend, total | risk:read |
L1 |
run_risk_transfer_whatif |
options, base seed, replications | TCOR distribution per option with paired significance verdict | twin:whatif |
L2 |
The two headline questions from the source map directly. "Which customers were hurt by yesterday's
dock failure, what does it cost us if the top two churn, and in what order do we call them?" is
get_impacted_customers over the failure window, ranked by CLV at risk with the recommended contact
order derived from hazard times value, and with clv_margin_source stating whether the margin came
from the ABC model or the baseline fallback. "Gross margin dropped 1.8 points this month: decompose
it into price, mix, freight, scrap, labor, and tariff, rank by contribution, and tell me which are
common cause versus assignable" is get_variance_decomposition, which returns the ranked components
each already carrying its causal_class from the control chart gate.
6. Configuration¶
All configuration validates against a published JSON Schema at load with line-numbered,
suggestion-bearing errors (C5), and just validate checks every file without running anything.
facility.yaml carries the operation-shaped keys. Reference data that would bloat it lives in
sibling catalog files named in facility.yaml under a catalogs: block, which keeps A2's
bring-your-own-facility promise workable: a reader edits one file to model their building and points
at catalogs for the rest.
catalogs:
chart_of_accounts: config/finance/chart_of_accounts.yaml
posting_rules: config/finance/posting_rules.yaml
standard_costs: config/finance/standard_costs.yaml
abc_model: config/finance/abc_model.yaml
authority_matrix: config/governance/authority_matrix.yaml
controls_library: config/governance/controls_library.yaml
sod_rules: config/governance/sod_rules.yaml
tariff_schedule: config/trade/tariff_schedule.yaml
trade_scenarios: config/trade/scenarios.yaml
classifications: config/trade/classifications.yaml
insurance_policies: config/insurance/policies.yaml
rating_tables: config/insurance/rating_tables.yaml
detections: config/itops/detections/ # Storage prefix, enumerated in key order
zones: config/itops/zones.yaml
service_catalog: config/itops/service_catalog.yaml
6.1 orders¶
orders:
segments: # map[str, SegmentSpec], at least one
contract:
allocation_tier: 1 # int 1..5
promise_lead_days: 2 # int >= 0
on_time_window_hours: 24 # int > 0
fill_rate_commitment: 0.98 # float 0..1
penalty_per_late_order: { amount_minor: 5000, currency: USD }
contact_sensitivity: 0.6 # float 0..2, multiplier on contact probability
patience_seconds: { dist: lognormal, mu: 5.6, sigma: 0.5 }
spot: { allocation_tier: 3, promise_lead_days: 5, ... }
marketplace: { allocation_tier: 4, promise_lead_days: 3, ... }
allocation:
policy: hybrid # enum fair_share | priority | hybrid
horizon_days: 3 # int 1..30
run_cadence_hours: 4 # int > 0
honor_commitment_first: true # bool, hybrid only
tie_break: [order_id] # ordered list of sort keys, last must be a unique key
backorder:
fill_policy: priority_then_age # enum fifo_by_created_at | priority_then_age
max_age_days: 7 # int > 0, drives ORD-002
auto_cancel_after_days: 30 # int > max_age_days
changes:
cost_table: # map[change_type, map[stage, ChangeCost]]
qty_decrease:
{
PROMISED: { amount_minor: 0 },
ALLOCATED: { amount_minor: 250 },
PICKING: { amount_minor: 1200 },
PACKED: { amount_minor: 2600 },
}
cancel: { ... }
accept_after_stage: PACKED # enum, changes past this become returns
substitution:
rules_file: config/orders/substitutions.yaml
require_approval_default: true
promise:
sources: [on_hand, in_transit, scheduled_receipt, ctp, default_lead_time]
default_lead_days: 7
on_time_basis: delivery # enum ship | delivery
perfect_order:
components: [complete, on_time, damage_free, correctly_invoiced]
complete_basis: first_shipment # enum first_shipment | any_shipment
service:
queues:
general: { reason_codes: [wismo, late_delivery, other], skill: general }
claims: { reason_codes: [damage, short_ship, wrong_item], skill: claims }
billing: { reason_codes: [invoice_dispute], skill: billing }
service_level_seconds: 60 # int > 0
tool_visibility: order_detail # enum none | order_header | order_detail | twin_grounded
reason_visibility_required: # map[reason_code, visibility level]
wismo: order_detail
short_ship: order_detail
damage: twin_grounded
invoice_dispute: order_detail
handle_seconds: # map[reason_code, Distribution]
wismo: { dist: lognormal, mu: 5.0, sigma: 0.4 }
# dist is one of lognormal | exponential | gamma | deterministic
proficiency_scaling: true # bool, scales handle time by the learning curve
queue_discipline: fifo # enum fifo | priority_by_segment
validation_profile: # the configuration the Erlang gates instantiate
enabled: false # bool, true only in the validation-gate job
arrival: { dist: poisson, rate_per_hour: 120 }
handle_seconds: { dist: exponential, mean: 300 }
patience_seconds: none # null disables abandonment, for the Erlang C gate
proficiency_scaling: false
agents: 12 # c, the number of servers
contact_generation:
base_rate: # map[reason_code, float 0..1]
late_delivery: 0.35
short_ship: 0.55
damage: 0.70
escalation_gain: 0.25 # float >= 0
delay: # map[reason_code, Distribution]
late_delivery: { dist: lognormal, mu: 10.5, sigma: 0.8 }
customer_health:
satisfaction_start: 0.85 # float 0..1
impact: # map[reason_code, float > 0]
late_delivery: 0.05
damage: 0.12
recovery_rate_per_week: 0.02 # float >= 0
churn:
{ intercept: -6.0, satisfaction_weight: 4.0, failures_90d_weight: 0.4 }
clv_horizon_periods: 36 # int > 0
clv_discount_rate_annual: 0.10 # float 0..1
churn_alert_value: { amount_minor: 5000000, currency: USD }
tie_break is an ordered list of sort keys, not a scalar enum, because the total order a
deterministic allocation needs is usually a compound one. Members are drawn from order_id,
promise_date, allocation_tier, order_value, and captured_at, and the last member must be a
key the validator knows to be unique per demand. Today that is order_id alone. Writing
[promise_date] is rejected with the suggestion [promise_date, order_id], and that suggestion is
now representable in the schema, which it was not while the key was a scalar enum whose members did
not include the compound form the error message told the user to write.
Validation rules: segment allocation_tier values are unique or explicitly marked shared; a
contract segment needs a non-null fill_rate_commitment; every reason code appearing in
contact_generation.base_rate appears in exactly one queue; tie_break is non-empty, contains no
duplicates, and ends in a unique key, and a list that does not is rejected with the corrected list
quoted; max_age_days < auto_cancel_after_days; the change cost table is non-decreasing across the
stage sequence, and a decreasing entry is rejected at load with the offending pair quoted.
service.validation_profile exists because the Erlang gates need a configuration the shipped
operating profile does not offer, and a gate that cannot be instantiated from the published schema
is not a gate. Erlang C assumes Poisson arrivals, exponential handle times, c servers, and no
abandonment; Erlang A adds exponential patience. The shipped profile uses lognormal handle times,
mandatory per-segment patience, and proficiency scaling from the workforce learning curve, all three
of which break the assumption. The profile names exponential among the permitted handle
distributions, permits patience_seconds: none, and turns proficiency scaling off. It is enabled
only in the validation-gate job, config validation rejects enabled: true in any shipped scenario
profile, and test_validation_profile_is_not_shipped_enabled asserts that. The gate then checks the
queueing implementation against the analytic result on the analytic model's own terms, which is what
validation against a published formula means.
6.2 procurement¶
procurement:
match_tolerances:
price_pct: 0.02 # float 0..0.25
qty_pct: 0.05 # float 0..0.25
freight_abs: { amount_minor: 2500, currency: USD }
tax_abs: { amount_minor: 500, currency: USD }
over_receipt_pct: 0.05
duplicate_window_days: 30 # int > 0
approval:
matrix_ref: authority_matrix # key into catalogs
delay_hours_by_role:
{ buyer: { dist: lognormal, mu: 1.6, sigma: 0.6 }, director: { ... } }
requester_may_approve: false # bool, must be false or CI SOD test fails
payment:
default_terms_id: net30
hurdle_rate_annual: 0.08 # float 0..1, drives discount capture
run_cadence_days: 7
sourcing:
provenance: synthetic # mandatory; load fails without it
exact_award_max_suppliers: 6 # int > 0, above this the gap report says exact_unavailable
default_criteria: # weights must sum to 1.0 +/- 1e-9
- {
name: unit_price,
weight: 0.45,
direction: lower_better,
normalization: ratio_to_best,
source: bid,
}
- {
name: lead_time,
weight: 0.20,
direction: lower_better,
normalization: min_max,
source: bid,
}
- {
name: quality_ppm,
weight: 0.20,
direction: lower_better,
normalization: min_max,
source: scorecard,
}
- {
name: otif,
weight: 0.15,
direction: higher_better,
normalization: min_max,
source: scorecard,
}
award_constraints:
min_suppliers: 2
max_share_per_supplier: 0.6 # float 0..1
max_tier2_concentration: 0.5 # float 0..1, requires E19 when enforced
spend:
taxonomy_file: config/procurement/categories.yaml
maverick_alert_pct: 0.05 # float 0..1
savings:
baseline_method: prior_price # enum prior_price | market_index | budget | announced_increase
avoidance_requires_announcement: true # bool
tactics:
forward_buy:
carrying_cost_annual_pct: 0.22
obsolescence_source: eando_model # enum eando_model | fixed_pct
max_forward_months: 6
expedite:
stockout_cost_source: order_sla # enum order_sla | fixed_per_unit
max_premium_multiple: 3.0
spot_buy:
enabled: true
trigger_events: [supplier.commit_date_slipped, orders.line_backordered]
spot_premium_default_pct: 0.18 # float >= 0, used by the null SpotPricePort
spot_quality_ppm_uplift: 400 # int >= 0, defect ppm above the contract supplier
max_spot_share_of_category_spend: 0.15 # float 0..1, above this raises PRC-007
off_contract_penalty_source: contract # enum contract | none
provenance: synthetic
Validation rules: sourcing criterion weights sum to 1.0; max_share_per_supplier * min_suppliers >= 1.0
or the constraint set is infeasible and load fails with that arithmetic quoted;
requester_may_approve is false; max_tier2_concentration may be set only when the n-tier map is
available, otherwise load warns and the constraint is reported as unenforced rather than ignored in
silence; every spot_buy.trigger_events entry names an event with a producer in some section.
The sourcing and tactics.spot_buy blocks carry a mandatory provenance: synthetic key and load
fails without it. The scorecard weights are the block open question 11 names as the IP hygiene risk,
and the spot-buy premium and quality uplift are equally invented, so both sit under the same
config-level enforcement as the attrition coefficients and the rating tables rather than relying on
a reviewer noticing.
6.3 trade (E14)¶
trade:
destination_country: US # str, ISO 3166-1 alpha-2, synthetic scenarios only
dutiable_value_basis: transaction_value # enum transaction_value | transaction_value_plus_freight_insurance
duty_capitalisation: inventory # enum inventory | expense
broker_fee_per_entry: { amount_minor: 12500, currency: USD }
schedule_ref: tariff_schedule
active_scenarios: [] # list[scenario_id], composed in order
de_minimis:
threshold: { amount_minor: 80000, currency: USD }
applies_to_channels: [marketplace, ecommerce]
ftz:
enabled: false
inverted_tariff_allowed: false
drawback:
enabled: true
refund_rate: 0.99 # float 0..1
filing_window_days: 1095 # int > 0
processing_days: { dist: lognormal, mu: 5.2, sigma: 0.3 } # seed root/trade/drawback
provenance: synthetic
drawback.processing_days is the one stochastic quantity in the trade package, so the package
declares the root/trade/drawback seed namespace in section 6.9 and takes an Rng. Declaring the
engine seedless while shipping a lognormal draw that reaches trade.drawback_received.days_to_refund
would have broken C1's hash check on the first run with drawback enabled.
tariff_schedule.yaml rows:
rows:
- {
hs_code: "840790",
origin: DE,
destination: US,
effective_from: 2026-01-01,
kind: ad_valorem,
ad_valorem_pct: 0.027,
}
- {
hs_code: "392690",
origin: CN,
destination: US,
effective_from: 2026-01-01,
kind: compound,
ad_valorem_pct: 0.053,
specific_per_unit: { amount_minor: 15, currency: USD },
uom: kg,
}
scenarios.yaml:
scenarios:
- scenario_id: rate_shock_plastics
name: "Plastics chapter rate shock"
effective_from: 2026-04-01
deltas:
- { target: { hs_chapter: "39", origin: CN }, additive_pct: 0.15 }
- scenario_id: retaliation_mirror
retaliation_of: rate_shock_plastics
mirror_origin: US
Validation rules: no two rows share (hs_code, origin, destination) with overlapping effectivity;
ad_valorem_pct in [0, 5.0] with anything above 1.0 warning that it is above 100 percent;
specific_per_unit requires a uom; a retaliation_of scenario must name an existing scenario; a
scenario listed in active_scenarios must exist; header comment in the shipped file states every
rate is synthetic and not a reproduction of any real tariff schedule.
6.4 workforce¶
workforce:
roles:
picker:
base_wage: {amount_minor: 2100, currency: USD} # per hour
burden_pct: 0.28 # float 0..1
required_certifications: []
time_to_fill_days: {dist: lognormal, mu: 3.0, sigma: 0.4}
recruiting_cost: {amount_minor: 120000, currency: USD}
headcount_driver: forecast_lines_per_day
forklift_operator:
required_certifications: [forklift]
...
certifications:
forklift: {training_hours: 8, trainer_role: supervisor, validity_days: 1095, stations_unlocked: [receiving, putaway]}
hazmat: {training_hours: 16, validity_days: 730, stations_unlocked: [hazmat_staging]}
learning_curve:
model: wright # enum wright (only implemented model; see open questions)
learning_rate: 0.85 # float 0.5..1.0
initial_productivity_ratio: 0.55 # float 0..1
plateau_units: 4000 # int > 0
error_learning_rate: 0.80
initial_error_multiplier: 2.5 # float >= 1
error_floor_multiplier: 1.0
attrition:
evaluation_cadence: weekly
coefficients: # illustrative synthetic values, documented as such
intercept: -5.2
overtime_hours_rolling_4w: 0.035
strain_index: 1.10
understaffing_ratio_rolling_4w: 0.90
schedule_instability: 0.75
weekend_shifts_rolling_8w: 0.06
tenure_spline_knots: [30, 90, 365, 1095]
tenure_spline_values: [0.9, 0.3, -0.2, -0.5]
regret_threshold_percentile: 0.4 # float 0..1
absence:
generator: {base_rate: 0.035, strain_weight: 0.6, consecutive_days_weight: 0.05, seasonal_amplitude: 0.4}
predictor: {model: logistic, features: [strain_index, consecutive_days, weekday, weather_index, tenure_days]}
publish_horizon_days: 14
overtime:
daily_threshold_hours: 8
weekly_threshold_hours: 40
premium_multiplier: 1.5
double_time_after_hours: 12
alert_hours_rolling_4w: 40 # drives HR-002
alert_consecutive_weeks: 3
cross_training:
trainer_productivity_loss: 0.3 # float 0..1
trainee_productivity_during: 0.2
peak_strategies:
temp: {productivity_ratio: 0.7, error_multiplier: 1.8, agency_markup: 0.35, attrition_cost: 0}
overtime: {strain_gain: 0.15}
hiring_pulse: {layoff_fraction: 0.5, severance_weeks: 2, morale_penalty: 0.05}
Validation rules: every certification named in a role exists; every station named in
stations_unlocked exists in facility.yaml; 0.5 <= learning_rate <= 1.0 with a suggestion that
values above 1.0 mean workers get slower; tenure_spline_knots and tenure_spline_values have equal
length and knots are strictly increasing; the attrition coefficient block carries a mandatory
provenance: synthetic key and load fails without it, which is the config-level enforcement of the
IP hygiene rule.
6.5 itops¶
itops:
service_catalog_ref: service_catalog
zones_ref: zones
detections_dir: config/itops/detections/
slos:
- {
slo_id: broker_availability,
ci_id: broker,
sli: availability,
target: 0.999,
window_days: 28,
}
- {
slo_id: twin_api_latency,
ci_id: twin_service,
sli: latency_p95,
target: 0.99,
threshold_ms: 400,
window_days: 28,
}
burn_rate_alerts: # multi-window multi-burn-rate; thresholds are derived, never written
- {
name: fast,
long_window_hours: 1,
short_window_minutes: 5,
budget_fraction: 0.02,
severity: page,
}
- {
name: slow,
long_window_hours: 6,
short_window_minutes: 30,
budget_fraction: 0.05,
severity: page,
}
- {
name: trend,
long_window_hours: 72,
short_window_minutes: 360,
budget_fraction: 0.10,
severity: ticket,
}
change:
freeze_windows:
[
{ dow: FRI, start_hour: 15, end_hour: 24 },
{ dow: SAT, start_hour: 0, end_hour: 24 },
]
base_failure_probability: { standard: 0.005, normal: 0.03, emergency: 0.12 }
window_multipliers: { FRI_15_24: 3.0, weekend: 1.5, default: 1.0 }
no_rollback_penalty: 2.0
error_budget_gate: true # when a budget is exhausted, only emergency changes approve
approval_matrix_ref: authority_matrix
vulnerability:
feed: synthetic # enum synthetic (only value; identifiers are TWF-CVE-*)
arrival_rate_per_week: 2.5
severity_mix: { critical: 0.08, high: 0.25, medium: 0.45, low: 0.22 }
exploit_probability_daily:
{ critical: 0.010, high: 0.004, medium: 0.001, low: 0.0002 }
known_exploited_fraction: 0.05 # prevalence: share of published vulns carrying the flag
known_exploited_multiplier: 8.0 # uplift: multiplier on the daily hazard when flagged
patch_sla_days_by_zone: { ot: 90, dmz: 30, it: 14 }
patch_horizon_days: 14
risk_preference_alpha: 0.5 # float 0..1; 0 is risk neutral, 1 is pure CVaR
cvar_beta: 0.95 # float 0..1, tail probability for the CVaR term
cvar_samples: 2000 # int > 0, draws per candidate window
provenance: synthetic
breach_impact:
downtime_hours: { dist: lognormal, mu: 3.0, sigma: 0.8 } # seed root/itops/breach_impact
recovery_cost: { amount_minor: 5000000, currency: USD }
regulatory_cost: { amount_minor: 0, currency: USD }
provenance: synthetic
telemetry:
head_sample_rate: 0.05 # float 0..1
tail_keep_error_traces: true
log_templates_file: config/itops/log_templates.yaml
correlation:
rules_dir: config/itops/correlations/
default_window_seconds: 900 # int > 0
default_min_inputs: 3 # int >= 2
default_min_distinct_zones: 2 # int >= 1
max_alerts_per_window: 20 # int > 0, the flood guard
chaos:
scenarios_file: config/itops/chaos_scenarios.yaml
enabled_kinds: [ci_failure, restore_drill]
backup:
schedules:
- {
ci_id: historian,
cadence_hours: 6,
retention_days: 30,
target_rpo_minutes: 360,
target_rto_minutes: 120,
}
- {
ci_id: mes_analog,
cadence_hours: 24,
retention_days: 14,
target_rpo_minutes: 1440,
target_rto_minutes: 240,
}
drill_cadence_days: 90
verify_every_backup: true
provenance: synthetic
rbac:
default: deny # enum deny (only value)
roles_file: config/itops/roles.yaml
grants_file: config/itops/grants.yaml
zones.yaml:
zones:
- {
zone_id: ot,
purdue_levels: [0, 1, 2],
security_level_target: 3,
members: [devices, broker],
}
- {
zone_id: dmz,
purdue_levels: [3],
security_level_target: 2,
members: [historian, twin_sync],
}
- {
zone_id: it,
purdue_levels: [4],
security_level_target: 2,
members: [analytics, agent, dashboard],
}
conduits:
- {
conduit_id: ot_to_dmz,
src_zone: ot,
dst_zone: dmz,
allowed_protocols: [mqtt],
allowed_ports: [8883],
direction: unidirectional,
}
- {
conduit_id: dmz_to_it,
src_zone: dmz,
dst_zone: it,
allowed_protocols: [https, postgres],
allowed_ports: [443, 5432],
direction: bidirectional,
}
Validation rules: every CI belongs to exactly one zone; every SLO references an existing CI and SLI;
burn-rate alert windows satisfy short_window < long_window; budget_fraction lies in (0, 1) and
the derived burn rate is recomputed at load from window_days, so a threshold literal cannot appear
in a config file at all; patch_sla_days_by_zone covers every declared zone; a conduit whose
src_zone equals its dst_zone is rejected; every correlation rule names inputs that exist as
finding codes or detection rule ids; the shipped grants file is checked against sod_rules.yaml at
load and a conflict fails validation rather than waiting for runtime, so a misconfigured demo cannot
ship an SOD violation in silence.
Three blocks in this file carry a mandatory provenance: synthetic key and load fails without it:
vulnerability (the exploit_probability_daily table and the known-exploited uplift are invented),
breach_impact (the downtime and recovery costs are invented), and backup (the RPO and RTO
targets are the resilience targets open question 11 names as the IP hygiene risk). The rule is the
same one the attrition coefficients and the insurance rating tables already carry, applied to every
block that would otherwise look like it came from a real deliverable.
6.6 commercial¶
commercial:
promotions:
calendar_file: config/commercial/promo_calendar.yaml
lift:
max_lift_by_class: { A: 2.4, B: 1.8, C: 1.4 } # L_max per SKU class
depth_response_k: 6.0 # float > 0
noise: { dist: lognormal, mu: 0.0, sigma: 0.08 }
provenance: synthetic # mandatory; load fails without it
feature_frame:
publish_to_forecaster: true # drives PromoFeaturePort
horizon_periods: 26 # int > 0
cut_off_offset_days: 0 # int >= 0, features never read past the cut-off
pull_forward:
default_fraction: 0.35 # float 0..1
decay_periods: 4 # int >= 1
decay_ratio: 0.5 # weights are geometric and normalized to sum to 1
cannibalisation_file: config/commercial/cannibalisation.yaml
serveability_check: true # drives CMR-001
sales:
stages: [qualify, discover, propose, negotiate, close]
conversion:
{ qualify: 0.55, discover: 0.60, propose: 0.65, negotiate: 0.75 }
dwell_days: { qualify: { dist: lognormal, mu: 1.8, sigma: 0.5 }, ... }
reps:
- { rep_id: r1, bias_multiplier: 1.18, mode: optimistic, variance: 0.10 }
- { rep_id: r2, bias_multiplier: 0.92, mode: sandbagging, variance: 0.06 }
quota:
period: quarter # enum month | quarter
pressure_exponent: 3.0 # k
pressure_amplitude: 1.6 # q
discount_escalation_pct: 0.04
demand_shaping:
enabled: true # publishes mkt.demand_multiplier_published
sources: [promotion, quota_pressure, channel_mix, npi_launch]
max_multiplier: 4.0 # float > 0, clamp so a stacked lever cannot break the queue model
publish_cadence: period # enum period | daily
channel_mix:
start_shares: { wholesale: 0.62, ecommerce: 0.28, marketplace: 0.10 }
drift_model: dirichlet # enum config_static | dirichlet
drift_step: 0.015 # float >= 0; zero pins the shares to start_shares exactly
concentration_alert_hhi: 0.55 # float 0..1
provenance: synthetic
npi:
default_bass_p: 0.03 # innovation coefficient
default_bass_q: 0.38 # imitation coefficient
cold_start_blend_periods: 8
sop:
calendar:
product_review_day: 3 # business day of month, int 1..20
demand_review_day: 6
supply_review_day: 9
reconciliation_day: 12
executive_day: 15
horizon_periods: 18
supply_review_mode: analytic # enum analytic | surrogate | full_simulation
bill_of_resource_file: config/commercial/bill_of_resource.yaml
fva_ladder: [naive, statistical, sales_override, consensus]
accuracy_metric: wape # enum mape | wape | mase
one_number_tolerance_pct: 0.005 # drives CMR-005
decision_register_ref: governance.decision_register
Validation rules: cannibalisation matrix rows have a zero diagonal and column sums no greater than
1.0, and a violating column is quoted with its sum; S&OP calendar days are strictly increasing;
fva_ladder starts with naive; pull_forward.default_fraction in [0, 1]; every promoted SKU in
the calendar exists in the SKU catalog and has a declared class in max_lift_by_class;
channel_mix.start_shares sums to 1.0 within 1e-12 and names only declared channels;
supply_review_mode: surrogate is rejected unless E28 is installed, with the missing package named.
Three blocks carry a mandatory provenance: synthetic key and load fails without it:
promotions.lift (the lift ceilings and depth response are invented), channel_mix (the starting
shares and drift step are invented), and bill_of_resource.yaml (the resource coefficients the
rough-cut capacity check multiplies volume by).
6.7 finance¶
finance:
functional_currency: USD
period_calendar: monthly # enum monthly | four_four_five
fiscal_year_start_month: 1
chart_of_accounts_ref: chart_of_accounts
posting_rules_ref: posting_rules
inventory:
valuation_method: weighted_average # enum weighted_average | fifo
valuation_classes: [raw, wip, finished, returns]
eando:
coverage_horizon_periods: 12
aging_buckets_days: [90, 180, 365]
reserve_rate_by_bucket: [0.15, 0.40, 0.85]
cycle_count:
method: cycle # enum cycle | wall_to_wall
frequency_days_by_abc: { A: 30, B: 90, C: 180 }
tolerance_units: 0
rfid_confirmation: true # the A/B switch for the record accuracy study
standard_costing:
revision_source: bom # enum bom | recipe
overhead_base: labor_hours # enum labor_hours | machine_hours
revalue_on_standard_change: true
variance_common_cause_gate: true # route every variance through the LSS chart first
abc:
model_ref: abc_model
rate_refresh: monthly
fpa:
budget_source: sop_consensus # enum sop_consensus | manual
reforecast_cadence: monthly
bridge_components: [volume, price, mix, rate, efficiency, spend, residual]
capex:
discount_rate_annual: 0.10 # float 0..1
authority_matrix_ref: authority_matrix
post_audit_lag_periods: 6 # int > 0
irr_solver:
bracket: [-0.99, 10.0] # lower bound > -1.0
max_iterations: 200 # int > 0, bisection steps before NoConvergence
tolerance_rel: 1.0e-12 # relative width of the final bracket
on_no_sign_change: raise # enum raise | return_none
on_multiple_roots: enumerate # enum enumerate | raise
close:
checklist_file: config/finance/close_checklist.yaml
target_close_days: 5
accruals: [gr_ni, unbilled_revenue, payroll, freight]
reconciliations: [ap, ar, inventory, payroll, cash]
controls:
library_ref: controls_library
sod_rules_ref: sod_rules
test_cadence_default: quarterly
working_capital: # E22
echelons:
[
supplier_stock,
inbound_in_transit,
dc_stock,
wip,
finished_goods,
outbound_in_transit,
accounts_receivable,
]
carrying_cost_annual_pct: 0.12
cash_forecast_horizon_weeks: 13
terms:
ap:
{
net30: { net_days: 30 },
2_10_net30: { net_days: 30, discount_pct: 0.02, discount_days: 10 },
}
ar: { net45: { net_days: 45 }, net15: { net_days: 15 } }
Validation rules: every account referenced by a posting rule exists and is postable; every posting
rule template balances symbolically (the debit expressions and the credit expressions are provably
equal, checked by a symbolic balance test at load, not only at runtime); reserve_rate_by_bucket
has the same length as aging_buckets_days plus one and is non-decreasing; irr_solver.bracket
lower bound is greater than -1.0 and the upper bound exceeds it; every echelon named has a valuation
source; target_close_days is positive; a four_four_five calendar needs a period boundary file.
The internal rate of return is a root of a polynomial, so its solver has a stated contract rather
than a bracket alone. Bisection runs at most max_iterations steps and stops when the bracket width
falls below tolerance_rel relative to the midpoint. Three outcomes are named rather than left to
the implementation.
| Situation | Behavior |
|---|---|
| No sign change of NPV across the bracket | Raise NoIrrInBracket naming the bracket and both endpoint NPVs; the true rate may lie outside it |
| Sign change present, bisection converges | Return the root; capex.appraised carries the final bracket width so the precision is on the record |
Sign change present, max_iterations reached |
Raise IrrDidNotConverge with the final bracket, never a midpoint presented as an answer |
| More than one sign change in the cash flow sequence | Enumerate the roots in the bracket and return them all with a multiple_irr flag |
The last row is a correction. Descartes' rule of signs bounds the number of positive roots by the
number of sign changes; it does not imply there is more than one. An ordinary project with a
terminal decommissioning cost has two sign changes and usually one root, and raising AmbiguousIrr
on the sign-change count alone would reject it. The engine enumerates instead, and only reports
ambiguity when it finds more than one root. Where more than one root exists, the report says so and
directs the reader to NPV, because a project with several internal rates of return does not have an
internal rate of return worth quoting.
6.8 insurance (E38)¶
insurance:
policies:
- policy_id: cargo_2026
coverage: cargo
limit: { amount_minor: 50000000, currency: USD }
deductible: { amount_minor: 250000, currency: USD }
coinsurance_pct: 1.0
sublimits:
{ temperature_excursion: { amount_minor: 10000000, currency: USD } }
exposure_base: shipped_value
base_rate: 0.0012 # per unit of exposure base
- policy_id: bi_2026
coverage: business_interruption
limit: { amount_minor: 200000000, currency: USD }
waiting_period_hours: 48
indemnity_period_days: 90
exposure_base: annual_gross_profit
base_rate: 0.0035
- policy_id: wc_2026
coverage: workers_compensation
exposure_base: payroll
base_rate: 0.021
sublimit_rules: # ordered; first match wins, no match takes the policy limit
- {
key: temperature_excursion,
when_trigger_event: transport.temperature_excursion,
}
- { key: temperature_excursion, when_peril_tag: cold_chain }
claim_triggers:
shock_g_threshold: 5.0
temperature_excursion_minutes: 60
require_damage_disposition: true
claim_process:
fnol_to_documented_days: { dist: lognormal, mu: 1.2, sigma: 0.5 }
documented_to_adjusted_days: { dist: lognormal, mu: 2.0, sigma: 0.6 }
adjusted_to_settled_days: { dist: lognormal, mu: 2.4, sigma: 0.7 }
denial_probability: 0.06
rating:
experience_period_years: 3
loss_ratio_weight: 0.6
trir_weight: 0.4
modifier_bounds: [0.7, 2.0]
tcor:
admin_cost_annual: { amount_minor: 4000000, currency: USD }
risk_control_accounts: [6410, 6420]
Validation rules: every policy has an exposure base that resolves to a computable metric; the
deductible is less than the limit; every sublimit is at most the policy limit, and a larger one is
rejected with both values quoted; every sublimit_rules key names a sublimit declared on some
policy; modifier_bounds lower bound is positive and below the upper bound; coinsurance_pct in
(0, 1]; a workers' compensation policy needs the ergonomics layer enabled or load fails with the
reason named; the shipped rating tables and claim_process distributions carry
provenance: synthetic and load fails without it.
6.9 Seed namespaces¶
Declared in each package's seeds.toml so C1's splittable RNG can derive stable child streams.
Adding a namespace is additive and never reorders existing streams. The substream column names the
entity id a draw is subscripted by, following section A.2 of
docs/design/variability-and-faults.md. A stream with a substream is addressed, not consumed in
order, which is what makes the paired studies of section 5.0 comparable.
| Namespace | Used for | Substream key |
|---|---|---|
root/orders/arrival |
order interarrival, composition, and channel draw | period |
root/orders/service/contacts |
contact occurrence, delay, and the WISMO hazard | caused_by_event_id |
root/orders/service/handle |
handle time, patience, and the skill draw | contact_id |
root/orders/substitution_response |
substitution approval decision and delay | line_id |
root/orders/churn |
churn hazard draws | customer_id |
root/orders/changes |
change request occurrence and type | order_id |
root/procurement/approval_delay |
approver response times | po_id |
root/procurement/rfx/bids |
bid generation | bid_id |
root/procurement/invoice |
invoice arrival, duplicates, and errors | invoice_id |
root/procurement/spot_price |
spot quote and spot supplier quality draw | decision_id |
root/trade/drawback |
drawback refund processing days | claim_id |
root/workforce/pipeline |
hiring stage conversions and dwell | hr_req_id |
root/workforce/absence |
absence realization | worker_id, date |
root/workforce/attrition |
quit draws | worker_id, week |
root/itops/cve_feed |
vulnerability arrival and severity | week |
root/itops/change_outcome |
change success or failure | chg_id |
root/itops/incident |
incident arrival from CI failure processes | ci_id |
root/itops/breach_impact |
breach downtime hours and the CVaR sample draws | vuln_id |
root/itops/trace_sampling |
head and tail sampling decisions | trace_id |
root/commercial/promo_noise |
lift noise | promo_id, period |
root/commercial/pipeline |
opportunity progression | opp_id |
root/commercial/rep_forecast |
forecast submission noise | rep_id, period |
root/commercial/channel_mix |
Dirichlet drift of the channel share vector | period |
root/commercial/npi_adoption |
noise around the Bass adoption expectation | sku, period |
root/finance/close_task_duration |
close task durations | task_id, period_id |
root/finance/count_error |
physical count discrepancy | location, sku |
root/insurance/claim_process |
claim stage durations and denial | claim_id |
root/insurance/loss_severity |
loss severity draws for the risk transfer study | loss_id |
provision/workforce/<id>/percentile |
worker performance percentile, drawn before the sim starts | worker_id |
The trade package no longer declares a none namespace. It draws one quantity, drawback refund
timing, so it declares the stream that quantity comes from. A package that claims to need no seed
while shipping a lognormal draw is a determinism leak that C1's hash check finds on the second run,
and the manifest check in section 2.9 finds it at import.
6.10 Metric definitions contributed to the semantic layer (E26b)¶
Each package ships metrics/<pkg>.metrics.yaml. Every metric declares a name, a description, an
exact SQL expression over the historian tables, its dimensions, its unit, and the total ordering
section 3's query determinism rule requires. CI fails if any agent eval question references a metric
that is not defined here. CI also fails if any metric named in section 7.4's chart table or returned
by any tool in section 5.10 is not defined here, which is the check that catches the reverse drift.
The agent selects metrics rather than writing aggregations, which is what stops it computing fill
rate a second way.
| Package | Metrics |
|---|---|
| orders | perfect_order_rate, fill_rate, line_fill_rate, on_time_ship_rate, on_time_delivery_rate, promise_reliability, promise_reliability_by_source, order_cycle_time_hours, backorder_aging_days, wismo_rate, promise_breach_order_days, fairness_index, contacts_per_100_orders, first_contact_resolution_rate, average_speed_of_answer_seconds, service_level_pct, abandonment_rate, clv_at_risk, churn_rate, channel_mix_hhi |
| procurement | purchase_price_variance, contract_price_variance, market_index_price_variance, po_cycle_time_hours, supplier_otif_buyer_view, first_pass_match_rate, match_exception_aging_hours, maverick_spend_pct, contract_coverage_pct, discount_capture_rate, cost_savings, cost_avoidance, spend_by_category, spot_buy_share_of_category_spend, award_greedy_gap_pct |
| trade | landed_cost_per_unit, duty_rate_effective, duty_paid, drawback_recovered, tariff_exposure_by_origin |
| workforce | turnover_rate, regretted_turnover_rate, time_to_fill_days, time_to_productivity_days, absenteeism_rate, labor_cost_per_unit, overtime_pct, overtime_pct_4w, schedule_stability_index, weekend_shift_rate_8w, strain_trend_slope, span_of_control, training_hours_per_improvement_point, station_coverage_ratio |
| itops | slo_attainment, error_budget_remaining_pct, mttd_minutes, mttr_minutes, change_failure_rate, deployment_frequency, change_lead_time_hours, patch_latency_hours, cross_zone_anomaly_rate, correlated_alert_rate, request_error_rate, request_latency_p95_ms, log_error_rate_per_1k_requests, backup_restore_success_rate, measured_rpo_minutes, measured_rto_minutes |
| commercial | promo_lift, incremental_units, cannibalisation_units, forecast_bias, forecast_value_added, rep_forecast_bias, quota_attainment, plan_adherence, decision_latency_days, one_number_variance, demand_multiplier_by_source |
| finance | gross_margin_pct, gross_profit_rate_per_hour, cost_to_serve, contribution_after_cost_to_serve, inventory_record_accuracy, absolute_value_accuracy, shrink_value, eando_reserve, dio, dso, dpo, cash_to_cash_days, working_capital_by_echelon, close_cycle_days, capex_hit_rate, variance_by_kind |
| insurance | total_cost_of_risk, loss_ratio, claim_cycle_days, claim_recovery_ratio, experience_modifier, retained_loss |
Three procurement price variances are defined rather than one, because 6a13 asks for price variance against contract and market, not only against standard cost.
| Metric | Baseline | Expression |
|---|---|---|
purchase_price_variance |
Standard cost | (paid_unit_price - standard_price) * qty_received |
contract_price_variance |
POLine.contract_price |
(paid_unit_price - contract_price) * qty_received |
market_index_price_variance |
Published index quote | (paid_unit_price - index_price_at_order) * qty_received |
Only the first posts to the GL, because only the first has an account (section 5.8). The other two
are analytic metrics with a control chart each and no ledger consequence, which is the same
distinction the savings ledger draws in section 3.2 and is the honest treatment of a baseline the
chart of accounts cannot witness. contract_price_variance is what finally reads
POLine.contract_price, which was declared and never differenced.
7. Testing¶
Five tiers per C4, each with a runtime budget enforced in CI.
| Tier | Budget for this section | Runs on | Contents |
|---|---|---|---|
| Unit | 90 s | Every push | Pure functions, state machines, formulas, posting rules |
| Property | 180 s | Every push | Hypothesis invariants listed in 7.2 |
| Validation gates, fast | 240 s | Every push | The gates in 7.3 marked fast: closed-form checks, fixture matrices, arithmetic |
| Validation gates, full | 1800 s | Nightly, and on every release tag | Every gate in 7.3, including the replicated statistical ones |
| Seeded end to end | 420 s | Every push | Scenario runs with golden-file comparison of statements, reports, and decision packets |
The split exists because the earlier single 240-second budget was not credible and a budget that
cannot be met is not a budget. Seven gates dominate the cost: VG-FIN-07 charts 100,000 points,
VG-HR-02 fits 200 logistic replications, VG-HR-04 and VG-HR-05 run batch-means queueing simulations
at three utilization levels across 20 seeds, VG-FIN-11 runs 20 long seeded runs, VG-PRC-04 runs a
17-point grid across 20 twin runs, and VG-INS-01 draws 5000 losses. Their measured budgets are
declared per gate in 7.3, and test_validation_gate_budget_arithmetic sums the declared budgets per
tier and fails when a sum exceeds the tier's ceiling (D-13). A gate that grows past its budget fails
as a defect rather than as a timeout.
Both validation tiers are required checks. The full tier is not optional, and no gate is dropped to fit the fast tier; a gate too slow for a push job runs nightly and blocks the release tag.
7.1 Unit tests worth naming¶
- Order state machine: every legal transition accepted, every illegal transition raises
IllegalTransitionnaming the pair. The rejected set is generated from the transition table rather than written down, so the count cannot drift: 15 statuses give 210 ordered pairs excluding self-transitions, 25 of which are legal, leaving 185 that must raise. Self-transitions are tested separately as 15 further rejections, for 200 rejected pairs in total. Both counts are computed in the test, andtest_transition_counts_match_the_tableasserts the arithmetic so an added status cannot silently shrink the matrix. - Allocation determinism: the same demand and supply under the same policy produce the identical allocation list, including tie-break order, across 100 shuffles of the input list.
- Change cost table: monotone across stages for every change type.
- Three-way match: one fixture per violation class plus one per tolerance boundary (just inside and just outside), with the evaluation order asserted so the taxonomy is stable.
- Fairness index: Jain's index at full equality, at maximal inequality, with a zero-fill demand in the population, and at zero total supply, where the stated convention returns 1.0.
- Burn-rate derivation:
derive_burn_ratesreproduces 14.4, 6, and 1 at a 30-day window for budget fractions of 2, 5, and 10 percent over 1 hour, 6 hours, and 3 days, and returns 13.44, 5.6, and 0.9333 at 28 days. - Sublimit resolution: an ordered rule set maps a loss to at most one key, first match wins, and a loss matching no rule takes the policy limit.
- Closing entry: nominal accounts zero after the entry, retained earnings moves by net income
exactly, and the entry's line order follows
account_id. - IRR solver: a bracket with no sign change raises, a non-converging bracket raises, and a cash flow with two sign changes and one root returns that root without raising.
- Weighted scorecard: each normalization method against hand-computed values.
- Rebate accrual: retrospective and incremental bases against hand-computed schedules at, just below, and just above each tier threshold.
- Learning curve:
cumulative_average_hoursand the derived marginal hours at units 1 through 32. - Certification expiry: eligibility flips exactly at the expiry instant, not the day boundary.
- Error budget arithmetic: consumed, remaining, and burn rate for a fixture SLI series.
- Change failure probability: the multiplier chain evaluates in a fixed order and is invariant to config key ordering.
- CVSS v3.1 base score: vector parsing round-trips and scoring matches the specification's rounding.
- RBAC decision:
grantedanddeniedfor a fixture grant set including expired grants and wildcard entries. - Promotion decomposition: incremental, pulled forward, cannibalised, halo for a fixture calendar.
- Bass diffusion: adopters per period against the closed form.
- FVA ladder: stairstep deltas for a fixture set of four forecast series.
- Journal entry: balance check, no-negative check, closed-period rejection.
- Every variance formula against a hand-computed fixture with signs and favorability.
- NPV, IRR, simple and discounted payback for fixture cash flows including a no-root case that must raise rather than return.
- Claim payout: deductible, coinsurance, and limit applied in the correct order for boundary losses.
- Landed cost: ad valorem, specific, and compound duty for fixture entries.
7.2 Property-based invariants (Hypothesis)¶
Each is named as it appears in the test module so an implementer can find it.
| Invariant | Statement |
|---|---|
order_quantity_conservation |
For any sequence of legal order operations, qty_ordered == qty_allocated + qty_backordered + qty_cancelled and qty_shipped <= qty_picked <= qty_allocated after every step |
order_state_machine_legality |
No recorded transition lies outside the transition table |
allocation_no_oversell |
For any demand and supply vectors and any policy, total allocated per lot never exceeds lot on-hand |
allocation_unit_conservation |
Allocated plus backordered equals demanded, and allocated never exceeds supply |
fair_share_proportionality |
Under FairShare, for any two demands, the difference between their fill ratios is at most one unit of quantisation |
priority_dominance |
Under Priority, no lower-tier demand is filled while a higher-tier demand with an earlier promise date is short |
change_cost_monotone_in_stage |
For any change type, cost is non-decreasing across the stage sequence |
perfect_order_bound |
perfect_order_rate <= min(component rates) for any population |
contact_generation_causality |
Every generated contact references an existing earlier event |
service_queue_conservation |
Contacts created equals answered plus abandoned plus in queue at every instant |
approval_authority |
No approval is recorded by a subject whose limit is below the approved value |
sod_requester_not_approver |
For any generated PO, requester and approver differ |
price_curve_monotone |
Generated bid curves have non-increasing unit price in break quantity |
award_respects_constraints |
Any award satisfies min suppliers, max share, and capacity for any feasible constraint set |
savings_avoidance_disjoint |
No event id appears as evidence in both a savings and an avoidance entry, and avoidance entries have no GL posting |
rebate_never_exceeds_spend |
Accrued rebate is at most the spend it is computed on |
landed_cost_monotone_in_duty |
Holding everything else fixed, landed cost is non-decreasing in the duty rate |
merge_overlays_associative |
merge_overlays is associative on the canonical form for any three generated overlays, including overlapping targets |
apply_matches_merge |
apply_overlay(apply_overlay(base, A), B) and apply_overlay(base, merge_overlays(A, B)) yield the same schedule content hash, for overlapping and disjoint targets alike |
scenario_deactivation_restores |
Deactivating every overlay restores the base schedule hash exactly |
ftz_duty_neutrality |
With an unchanged rate, FTZ admission then domestic withdrawal yields the same total duty as direct import |
drawback_bounded |
Refund is at most duty paid times the refund rate |
certification_gating |
No assignment exists where the worker lacks a valid certification at that sim-time |
no_work_after_termination |
No labor record exists past a termination date |
hours_conservation |
Per worker per day, ledger hours equal roster hours minus absence plus overtime |
attrition_hazard_bounded |
The hazard lies in (0, 1) for any coefficient and driver values in the declared ranges |
cmdb_acyclic |
The CI dependency graph has no cycle |
rbac_deny_by_default |
granted is returned if and only if a matching unexpired grant exists |
error_budget_bounded |
Consumed budget lies in [0, 1] and remaining equals one minus consumed |
conduit_violation_completeness |
Every crossing with no matching conduit produces exactly one violation event |
promo_unit_conservation |
Over the promo window plus the pull-forward decay window, total units equal baseline total plus true incremental |
cannibalisation_conservation |
Units removed from cannibalised SKUs equal units added to the promoted SKU |
cannibalisation_matrix_bounds |
Zero diagonal and column sums at most 1.0 for any generated matrix |
bass_monotone |
Cumulative adopters are non-decreasing and bounded by market potential |
fva_ladder_identity |
The sum of step deltas equals the accuracy difference between the first and last rung |
one_number_identity |
Consensus volume, supply commitment, and finance reforecast volume agree for every period in the horizon |
journal_balances |
Every generated journal entry has equal total debits and credits in integer minor units |
no_negative_amounts |
Every line has exactly one positive side |
trial_balance_closure |
Assets equal liabilities plus equity at every period boundary |
statement_articulation |
Net income equals the change in retained earnings, and cash flow closing cash equals the balance sheet cash line |
inventory_flow_identity |
Beginning plus receipts minus issues minus adjustments equals ending, in units and value, for both valuation methods |
variance_attribution_closure |
Attributed amounts sum exactly to the variance amount |
bridge_reconciles |
Budget plus the sum of bridge components equals actual, exactly |
money_never_created |
Across any generated event stream, the sum of all ledger postings is zero |
cash_ledger_identity |
Cash balance equals opening plus inflows minus outflows |
claim_payout_bounds |
Payout lies in [0, applicable_limit], applicable_limit is at most the policy limit, and payout is non-decreasing in gross loss |
experience_modifier_monotone |
The modifier is non-decreasing in loss ratio and in TRIR |
tcor_reconciles_to_gl |
Each TCOR component equals the sum of its mapped accounts |
rounding_allocation_closes |
For any total and weight vector, largest-remainder lines sum to the total and each differs from its unrounded share by under one minor unit |
money_multiply_is_half_even |
Money.__mul__ rounds half to even at the boundary, including the two cases that separate half-even from half-up |
fairness_index_bounds |
Jain's index lies in [1/n, 1] over any demand population, equals 1 only at equal fill ratios, and never rises when a partially filled demand is driven to zero |
spot_buy_never_beats_available_contract |
No generated decision returns spot while the contract supplier can still meet need_by |
demand_multiplier_decomposes |
The product of a multiplier's listed factors equals the multiplier to 1e-12 relative |
promo_feature_frame_is_causal |
Every feature column for period t is computable from the calendar as it stood at the cut-off |
channel_shares_close |
Drifted shares are non-negative and sum to 1.0 within 1e-12 at every period, and equal the start shares exactly at drift_step zero |
closing_entry_zeroes_nominal_accounts |
After the closing entry, every revenue and expense account is zero and retained earnings moved by net income exactly |
correlation_is_deterministic_under_reordering |
For a fixed event set, the alerts raised do not depend on arrival order inside a window |
trace_spans_form_a_tree |
Every non-root span's parent exists in the same trace and the parent's interval contains the child's |
authority_matrix_interpretation_agrees |
Procurement, IT operations, and finance resolve the same matrix identically over a shared case grid |
retained_equals_gross_minus_recovered |
Retained loss equals gross loss minus recovery, exactly, in integer minor units, for any claim outcome |
query_ordering_is_total |
Every metric and detection query with more than one row declares an ordering that is total over its result set |
7.3 Validation gates¶
Two kinds of check live here and the section stopped calling them the same thing, because a table whose stated contract is external validation cannot contain rows that cite nothing.
External-reference gates satisfy all five conditions of D-11. Each names a specific published reference with edition and locator, asserts a tolerance no tighter than the precision that reference prints, states a noise floor where the quantity is stochastic, and states what result falsifies it. This repository is never a reference for itself.
Definitional property checks assert an identity that follows from a definition this section states: double entry balances, attributed amounts close, an affine rescaling leaves a ranking unchanged. They are real tests and several are the strongest tests in the suite, but they validate internal consistency rather than agreement with the world, and they are labeled so.
Golden-file regressions are neither. A golden file detects change; it cannot detect that the committed output was wrong on the day it was written. They moved out of this section into 7.6, where the other regression checks live, because listing a file this repository generated as a validated reference is exactly the self-citation D-11 forbids.
Every gate declares its tier and its measured budget in its test module, and
test_validation_gate_budget_arithmetic sums them per tier. Eight gates run in the full tier
because their replication counts cannot fit a push job: VG-FIN-07, VG-FIN-11, VG-PRC-04, VG-HR-02,
VG-HR-04, VG-HR-05, VG-CMR-05, and VG-INS-01. Every other gate runs in the fast tier on every push.
Where a reference is openly licensed for the use, the worked example is reproduced in the fixture with attribution. Where it is not, the numeric inputs and the published result are encoded with a citation and the source's prose, tables, and layout are not reproduced. The OpenStax gates fall in the second case, and open question 15 records why.
Finance and costing¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-FIN-01 | Double-entry identity (definitional) | Every entry balances exactly in integer minor units. Zero tolerance |
| VG-FIN-02 | Accounting equation (definitional) | Trial balance closes and assets equal liabilities plus equity at each period close. Zero tolerance |
| VG-FIN-03 | Statement articulation (definitional) | Net income ties to retained earnings movement; indirect and direct cash flow agree. Zero tolerance |
| VG-FIN-04 | OpenStax, Principles of Accounting, Volume 2: Managerial Accounting, Chapter 8 (Standard Costs and Variances). License CC BY-NC-SA, verified from the OpenStax book metadata API on 2026-08-09 | Direct material price and quantity, direct labor rate and efficiency, variable overhead spending and efficiency, and fixed overhead budget and volume variances reproduce the published worked-example figures. Exact to the cent. The fixture encodes the numeric inputs and the published answers with a citation to the chapter, and reproduces no prose, table, or layout from the book |
| VG-FIN-05 | Flexible budget reconciliation (definitional) | The sum of all computed variances equals total actual cost minus total standard cost allowed for actual output. Zero tolerance |
| VG-FIN-06 | Attribution closure (definitional) | Attributed event contributions sum to the variance. Zero tolerance |
| VG-FIN-07 | NIST/SEMATECH e-Handbook of Statistical Methods, Section 6.3.1 (What are Control Charts?), which publishes the 3-sigma false alarm rate for a normal characteristic as 0.00135 in one direction and 0.0027 in both. Retrieved 2026-08-09 | Configured with nelson_rules: [rule_1] and chart limits supplied rather than estimated, over 100,000 seeded in-control variance points the fraction the common-cause gate classifies assignable matches 0.0027 to the three significant digits the handbook prints. Noise floor: the binomial standard error at n = 100,000 and p = 0.0027 is 1.6e-4, so the gate asserts the 99 percent binomial interval, which is wider than the published precision. Falsified by an observed rate outside [0.00228, 0.00312] |
| VG-FIN-08 | Present value definition (definitional), plus numpy-financial as an independent implementation at its own precision |
Two separate assertions. The Decimal evaluation of the definition matches the implementation to zero error at 28 significant digits, which is an identity check between two Decimal evaluations. The float64 cross-check against numpy-financial asserts 1e-12 relative agreement, which is the precision float64 can witness. IRR matches a bisection root of the same polynomial to 1e-9 relative on 1000 seeded vectors. Falsified by any vector exceeding either tolerance, or by a two-sign-change cash flow with one real root that raises instead of returning it |
| VG-FIN-09 | OpenStax, Principles of Accounting, Volume 1: Financial Accounting, Chapter 10 (Inventory). License CC BY-NC-SA, verified 2026-08-09 | Weighted average and FIFO ending inventory and cost of goods sold reproduce the published worked examples. Exact to the cent. Numeric inputs and answers encoded with a citation, no prose reproduced |
| VG-FIN-10 | OpenStax, Principles of Accounting, Volume 1: Financial Accounting, Chapter 11 (Long-Term Assets). License CC BY-NC-SA, verified 2026-08-09 | Straight line, double declining balance, and units of production depreciation schedules reproduce the published examples. Exact to the cent. Encoded as above |
| VG-FIN-11 | Little, J. D. C. (1961), "A Proof for the Queuing Formula: L = lambda W", Operations Research 9(3), 383-387, doi:10.1287/opre.9.3.383 | Over a long seeded run, mean orders in the pipeline equals arrival rate times mean order cycle time, inside the batch-means 95 percent confidence interval, on at least 16 of 20 declared seeds. 16 is the 0.005 lower quantile of Binomial(20, 0.95), so a correct implementation fails about one run in 390 if the seeds were resampled; the seeds are fixed, so the gate is reproducible and the threshold states the risk it was chosen against. Falsified by 15 or fewer covering seeds |
Procurement and trade¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-PRC-01 | Fixture matrix (definitional) | Every three-way match violation class and both sides of every tolerance boundary produce the expected classification. Exact. Falsified by any reachable class the fixture suite cannot produce, which is why the invoice is its own aggregate |
| VG-PRC-02 | Scale invariance (definitional) | An affine transformation of a criterion's raw scores followed by renormalisation leaves the ranking unchanged, for 1000 generated bid sets |
| VG-PRC-03 | Measured heuristic gap (definitional) | On every fixture with at most sourcing.exact_award_max_suppliers suppliers, AwardGapReport reports the relative gap between the greedy award and the exact argmax, both totally ordered by (-score, supplier_id). The suite asserts the gap recorded alongside each fixture, including the non-zero one on greedy_suboptimal.yaml. It does not assert the gap is zero, because greedy is not optimal under break curves and share caps. Falsified by a gap differing from the recorded value, which flags either a scoring change or a strategy regression |
| VG-PRC-04 | Harris, F. W. (1913), "How Many Parts to Make at Once", reprinted in Operations Research 38(6), 947-950, doi:10.1287/opre.38.6.947 | Computed EOQ matches sqrt(2DS/H) to 1e-9. The simulated total cost at EOQ is the minimum over a grid of plus or minus 40 percent in 5 percent steps on at least 16 of 20 declared seeds, the same Binomial(20, 0.95) threshold VG-FIN-11 uses and for the same reason. Noise floor: the gate computes the batch-means half-width at the shipped run length and reports it, and asserts that adjacent grid points differ by more than the measured half-width at the shipped parameters. If they do not, the grid is too fine for the run length and the gate says so rather than reporting a spurious argmin. Falsified by 15 or fewer covering seeds, or by any seed whose argmin lies more than one grid step from EOQ |
| VG-PRC-05 | Discount economics (definitional) | The annualised discount rate formula matches hand computation, and the take-or-skip decision flips exactly at the hurdle rate |
| VG-PRC-06 | Spot-buy dominance (definitional) | Over 1000 generated slip scenarios, no recommendation is spot while the contract supplier can meet need_by, and net_benefit equals its stated components to the cent |
| VG-TRD-01 | Duty arithmetic (definitional) | Ad valorem, specific, and compound duty match hand computation on 200 fixture entries. Exact to the cent under the section 3 rounding contract |
| VG-TRD-02 | Overlay algebra (definitional) | merge_overlays associativity, agreement between sequential application and merged application, deactivation restoration, and monotonicity of landed cost in duty, over 1000 generated overlay pairs of which at least half share a target. Falsified by any pair whose two application paths give different schedule hashes |
| VG-TRD-03 | FTZ neutrality (definitional) | Total duty under FTZ admission then domestic withdrawal equals direct import duty at an unchanged rate. Exact |
Workforce¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-HR-01 | Wright, T. P. (1936), "Factors Affecting the Cost of Airplanes", Journal of the Aeronautical Sciences 3(4), 122-128, doi:10.2514/8.155 | The cumulative average at units 1, 2, 4, 8, 16, 32 reproduces the doubling sequence a * x ** (ln(rate)/ln(2)) for the configured learning rate to 1e-12, which is the precision of the closed form in float64. Falsified by any of the six points exceeding it |
| VG-HR-02 | Ground-truth recovery (definitional, against the generator's own declared coefficients) | Over 200 replications of a seeded population of 5000 workers, each generator coefficient lies inside the fitted logistic model's 95 percent Wald interval on at least 182 of 200 replications. 182 is the smallest threshold whose false-failure probability under Binomial(200, 0.95) is at or below 0.006: P(X <= 181) = 0.0058, so a correct implementation fails about one run in 172. The fitter is cross-checked against statsmodels on the same design matrix to 1e-6 relative on the coefficient vector, which is the agreement an iterative MLE can hold across BLAS backends and library versions; 1e-8 was tighter than the numerical reality. Both libraries run single-threaded with a pinned convergence tolerance. Falsified by 171 or fewer covering replications for any coefficient |
| VG-HR-03 | Calibration and leakage control (definitional) | The absence predictor's Brier score beats the base-rate predictor on held-out seeded data with a paired test at p below 0.01, and the calibration slope lies in [0.9, 1.1]. A null run with all generator coefficients set to zero must NOT beat base rate, which catches leakage |
| VG-HR-04 | Erlang C, as given in Gans, Koole, and Mandelbaum (2003), "Telephone Call Centers: Tutorial, Review, and Research Prospects", Manufacturing and Service Operations Management 5(2), 79-141, doi:10.1287/msom.5.2.79.16071 | Run under service.validation_profile (Poisson arrivals, exponential handle times, c servers, abandonment disabled, proficiency scaling off), the simulated mean wait matches the Erlang C analytic value inside the batch-means 95 percent confidence interval on at least 16 of 20 declared seeds, at utilizations 0.6, 0.8, and 0.9. Noise floor: the gate computes the batch-means half-width at 30 batches, reports it as a fraction of mean wait at each utilization, and records it in the capability report. The tolerance is that measured half-width, not a number chosen in advance, which is why the gate can be tightened only by lengthening the run. Falsified by 15 or fewer covering seeds at any utilization |
| VG-HR-05 | Erlang A, as given in Garnett, Mandelbaum, and Reiman (2002), "Designing a Call Center with Impatient Customers", Manufacturing and Service Operations Management 4(3), 208-227, doi:10.1287/msom.4.3.208.7753 | Under the same profile with exponential patience enabled, the simulated abandonment rate matches the Erlang A analytic value inside the batch-means 95 percent confidence interval on at least 16 of 20 seeds at two utilization levels. Falsified as above |
| VG-HR-06 | Engagement measurement (definitional) | schedule_stability_index, weekend_shift_rate_8w, and strain_trend_slope match hand computation on a fixture roster and strain series, each is bounded as declared, and each accumulates its declared points per worker inside the long-horizon profile of 7.5 |
IT and cyber¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-IT-01 | FIRST, Common Vulnerability Scoring System v3.1: Specification Document, section 7 (Roundup) and section 8.1, plus the published CVSS v3.1 Examples page. Both retrieved 2026-08-09 | Base scores computed from vectors match the published values exactly, including the Roundup rule that returns the smallest number to one decimal place at or above its input. Every worked example on the Examples page is a test case. Falsified by any published example whose score the implementation does not reproduce |
| VG-IT-02 | Google, The Site Reliability Workbook, Chapter 5 (Alerting on SLOs), Tables 5-6 and 5-8, retrieved 2026-08-09. Those tables give 2 percent over 1 hour, 5 percent over 6 hours, and 10 percent over 3 days, against a 30-day SLO window | Two assertions. derive_burn_rates evaluated at window_days = 30 reproduces the Workbook's 14.4, 6, and 1 exactly, which validates the derivation against the published pairs. Then, at this section's configured 28-day window, alerts fire and clear exactly at the derived thresholds 13.44, 5.6, and 0.9333 on a fixture SLI series, boundary cases tested on both sides. Falsified by a threshold literal appearing anywhere in config, or by any boundary case firing on the wrong side |
| VG-IT-03 | DORA metric definitions (definitional, from the published four key metrics) | Computed values match hand computation on a fixture change, trace, and incident log. Lead time and failed-deployment recovery time are computed from the span stream, not from ticket timestamps. Exact |
| VG-IT-04 | Detection rule fixtures (definitional) | Every rule fires on its positive fixture with the expected count and does not fire on its negative fixture, under the pinned watermark and total ordering of section 3. CI fails a rule missing either fixture, or whose logic changed without a semver bump |
| VG-IT-05 | RPO and RTO definitions (definitional) | In a seeded restore drill, measured RPO equals the sim-time between the last verified backup and the failure instant, and the counted data-loss events equal the events logged in that window. Exact |
| VG-IT-06 | Bounded model check against a reference implementation (definitional) | The model is 3 roles, 4 permissions, 3 subjects, each subject holding at most 2 roles, with expiry drawn from three declared instants. That is 2 ** 12 role-permission assignments times 3 ** 7 grant configurations times 12 requests, and the suite enumerates a declared 1,000,000-case pseudo-random sample of it with a fixed seed rather than the whole space. The word "exhaustive" is gone: the earlier model (4 roles, 6 permissions, 5 subjects) has more than 1e14 cases before expiry and wildcards and cannot be enumerated in any budget. Falsified by one case where the engine and the reference set-membership implementation disagree |
| VG-IT-07 | Conduit completeness (definitional) | Every simulated cross-zone message either matches a conduit or produces exactly one violation, over 10,000 generated messages. Exact |
| VG-IT-08 | Correlation determinism and completeness (definitional) | For each correlation rule, the positive fixture raises exactly one alert with the expected contributing findings and the negative fixture raises none. The same event set shuffled 100 ways raises the identical alert list, which is what the window sort guarantees. Falsified by any shuffle changing the alert list |
| VG-IT-09 | Exposure model arithmetic (definitional) | P_breach(t) matches 1 - (1 - p) ** t to 1e-12, the known-exploited uplift multiplies the daily hazard and clamps at 1.0, and the CVaR estimate at cvar_samples draws lies inside its own reported standard error of an analytic CVaR on a fixture lognormal impact. Falsified by the objective ranking two windows differently at alpha = 0 from expected total cost |
Commercial and S&OP¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-CMR-01 | Unit conservation (definitional) | Total demand over the promotion window plus the decay window equals baseline plus true incremental, to 1e-9 relative on the share matrix and exactly on integer units. The tolerance is stated rather than claimed exact because the shares are floats, and the fsum in sorted key order keeps accumulated error below it |
| VG-CMR-02 | Bass, F. M. (1969), "A New Product Growth for Model Consumer Durables", Management Science 15(5), 215-227, doi:10.1287/mnsc.15.5.215 | With stochasticity disabled, the simulated cumulative adoption path matches F(t) = (1 - exp(-(p+q)t)) / (1 + (q/p) exp(-(p+q)t)) to 1e-10. Falsified by any horizon point exceeding it |
| VG-CMR-03 | Hyndman, R. J. and Koehler, A. B. (2006), "Another look at measures of forecast accuracy", International Journal of Forecasting 22(4), 679-688, doi:10.1016/j.ijforecast.2006.03.001, for MASE; standard definitions for MAPE and WAPE | Metric implementations match the published formulas on fixture series to 1e-12, and are cross-checked against an independent implementation. Falsified by a fixture series where the two implementations differ beyond the tolerance |
| VG-CMR-04 | FVA identity (definitional) | The stairstep deltas sum to the total accuracy change; the naive rung's FVA against itself is exactly zero; a deliberately degraded rung yields a negative FVA |
| VG-CMR-05 | Quota pressure (definitional, against the generator's own parameters) | The measured ratio of last-period-week volume to mean-week volume matches the value implied by the configured k and q inside the Monte Carlo 95 percent interval on at least 16 of 20 declared seeds. Noise floor: the gate computes and reports the Monte Carlo interval half-width at the shipped run length rather than asserting one. Falsified by 15 or fewer covering seeds |
| VG-CMR-06 | One-number identity (definitional) | Consensus, supply commitment, and finance reforecast volumes agree exactly for every horizon period across 50 seeded S&OP cycles, in every supply review mode |
| VG-CMR-07 | Promo feature causality (definitional) | Every column of PromoFeatureFrame for period t is computable from the calendar at the cut-off, and a fixture back-dating a calendar entry past the cut-off is rejected. Falsified by a frame column that changes when realized demand changes and the calendar does not |
| VG-CMR-08 | Demand shaping decomposition (definitional) | The product of a DemandMultiplier's factors equals its multiplier to 1e-12 relative over 1000 generated lever combinations, the multiplier is clamped at max_multiplier, and channel shares stay on the simplex under drift |
Insurance¶
| Gate | Reference | Assertion and tolerance |
|---|---|---|
| VG-INS-01 | Indemnity arithmetic (definitional) | Payout equals min(applicable_limit, max(0, loss - deductible) * coinsurance) on 5000 generated losses, where applicable_limit is the sublimit SublimitResolver selects or the policy limit when no rule matches. Exact to the cent under the section 3 rounding contract. At least 500 of the generated losses match a sublimit rule, so the sublimit path is covered rather than declared. Falsified by any loss whose payout exceeds its applicable limit, or by binding_constraint naming a constraint that was not binding |
| VG-INS-02 | Business interruption arithmetic (definitional) | Indemnified hours equal downtime minus waiting period, capped at the indemnity period, and the loss equals indemnified hours times rate_per_hour from fin.gross_profit_rate. Exact. Falsified by a claim valued at a rate that does not appear in the ledger for that period |
| VG-INS-03 | Monotonicity (definitional) | The experience modifier is non-decreasing in loss ratio and in TRIR, and stays inside modifier_bounds, over 1000 generated histories |
| VG-INS-04 | TCOR reconciliation (definitional) | Each component ties to its mapped GL accounts for the period. Exact. retained_losses ties because risk.loss_recognised posts the gross loss and risk.recovery_posted posts the recovery; falsified by a period where recognized losses and the mapped accounts differ by any amount |
7.4 Control chart assignment per KPI¶
The LSS engine selects a chart by data type. This section declares the expected selection per metric so the assignment itself is testable and a reviewer can see the statistics were chosen, not defaulted.
A chart needs points before it has limits. An I-MR chart needs about 20 points, and a metric that
produces one point per month needs twenty months of sim time before it can signal at all. The
Points needed column states the requirement and the Profile column states which scenario horizon
supplies them: demo is the 90-day horizon most scenarios in 7.5 run, and long is the 36-month
horizon of s_backoffice_18, which exists so that the low-frequency charts have a run in which they
can be tested. test_chart_has_enough_points asserts, for every row, that the named profile produces
at least the points the row needs. A chart with no profile that can supply its points is a defect,
not a diagram.
| Metric | Data type | Chart | Point cadence | Points needed | Profile | Notes |
|---|---|---|---|---|---|---|
perfect_order_rate |
proportion, variable n | p-chart | weekly subgroups | 20 | demo | weekly subgroups |
on_time_delivery_rate |
proportion | p-chart | weekly subgroups | 20 | demo | |
wismo_rate |
count per exposure | u-chart | weekly | 20 | demo | per 100 order-days open past promise |
order_cycle_time_hours |
continuous | X-bar and R | daily subgroups | 25 | demo | daily subgroups |
first_contact_resolution_rate |
proportion | p-chart | daily | 25 | demo | |
average_speed_of_answer_seconds |
continuous, right skewed | I-MR on log transform | daily | 25 | demo | the transform is declared, not silent |
fairness_index |
continuous, bounded | I-MR | one per allocation run | 25 | demo | runs every run_cadence_hours |
purchase_price_variance |
continuous | I-MR plus EWMA | one per receipt | 25 | demo | EWMA for small sustained shifts |
contract_price_variance |
continuous | I-MR plus EWMA | one per receipt | 25 | demo | analytic only, no GL consequence |
market_index_price_variance |
continuous | I-MR plus EWMA | one per receipt | 25 | demo | analytic only, no GL consequence |
po_cycle_time_hours |
continuous | X-bar and R | weekly subgroups | 20 | demo | weekly subgroups |
first_pass_match_rate |
proportion | p-chart | weekly | 20 | demo | |
maverick_spend_pct |
proportion | p-chart | weekly | 20 | demo | |
turnover_rate |
proportion of headcount | p-chart | monthly | 20 | long | 20 months of sim time; the demo profile charts the points and reports limits as provisional |
absenteeism_rate |
proportion | p-chart | daily | 25 | demo | |
schedule_stability_index |
continuous, bounded | I-MR | weekly per worker | 20 | demo | 6a14's behavioral engagement indicator |
weekend_shift_rate_8w |
proportion | p-chart | weekly per worker | 20 | demo | 6a14's behavioral engagement indicator |
strain_trend_slope |
continuous | I-MR | weekly per worker | 20 | demo | needs 8 weeks of strain history before the first point |
overtime_pct_4w |
continuous, right skewed | I-MR on log transform | weekly per worker | 20 | demo | |
labor_cost_per_unit |
continuous | I-MR | daily | 25 | demo | |
time_to_productivity_days |
continuous | I-MR | one per hire | 20 | long | needs 20 hires, which the demo hiring rate does not reach |
mttd_minutes, mttr_minutes |
continuous, right skewed | I-MR on log transform | one per incident | 25 | demo | |
change_failure_rate |
proportion | p-chart | weekly | 20 | demo | |
deployment_frequency |
count per fixed window | c-chart | weekly | 20 | demo | |
patch_latency_hours |
continuous, right skewed | I-MR on log transform | one per patch | 20 | demo | |
cross_zone_anomaly_rate |
count per exposure | u-chart | hourly | 25 | demo | per 10,000 messages |
correlated_alert_rate |
count per exposure | u-chart | daily | 20 | demo | per 1000 findings |
request_error_rate |
proportion | p-chart | hourly | 25 | demo | from the span stream |
request_latency_p95_ms |
continuous, right skewed | I-MR on log transform | hourly | 25 | demo | from the span stream |
backup_restore_success_rate |
rare-event proportion | g-chart | one per drill | 20 | long | successes between failures. At drill_cadence_days: 90 this needs 20 drills, so 60 months; the long profile runs drills at 30 days for the chart test and states that it does |
forecast_bias, forecast_value_added |
continuous | I-MR plus EWMA | one per cycle | 20 | long | one point per S&OP cycle, so 20 months |
decision_latency_days |
continuous | I-MR | one per cycle | 20 | long | one point per cycle |
close_cycle_days |
continuous | I-MR | one per period | 20 | long | one point per period, so 20 months |
variance_by_kind |
continuous | I-MR | one per period per kind | 20 | long | per variance kind, feeds the common-cause gate |
dio, dso, dpo, cash_to_cash_days |
continuous | I-MR | monthly | 20 | long | monthly |
inventory_record_accuracy |
proportion | p-chart | per count cycle | 20 | demo | A-class SKUs count every 30 days |
claim_cycle_days |
continuous, right skewed | I-MR on log transform | one per claim | 20 | long | |
total_cost_of_risk |
continuous, low frequency | I-MR | quarterly | 20 | long | 20 quarters is 60 months, beyond even the long profile; the chart is defined, its limits are reported as provisional, and open question 14 records the gap |
7.5 Seeded end-to-end scenarios¶
Each runs in simulation mode with a fixed seed, produces golden files, and doubles as demo material. Every scenario states its simulated horizon, because a chart assignment that no run can ever populate is not checkable, and because the sim-time budget is what section 7's tier budgets are spent on.
| Scenario | Seed name | Horizon | What it proves |
|---|---|---|---|
dock_failure_to_churn |
s_backoffice_01 |
14 days | A dock door failure delays shipments, generates contacts, degrades satisfaction, churns a customer, and posts the lost margin. Asserts the causal chain from a physical event to a finance number, and the ranked customer callback list |
allocation_policy_bakeoff |
s_backoffice_02 |
90 days | The same shortage under fair share, priority, and hybrid across 20 paired replications. Asserts fill by segment, the Jain fairness index over every demand including zero-fill, and the paired verdict with its effect size and interval |
visibility_ab |
s_backoffice_03 |
90 days | The same shift at order_header and twin_grounded visibility, 20 paired replications on common random numbers. Asserts the FCR delta, its paired interval, and that both arms drew identical values from every shared contact substream |
p2p_exception_storm |
s_backoffice_04 |
90 days | A supplier sends malformed invoices for two weeks. Asserts exception queue growth, match rate drop, discount capture loss, DPO movement, and the mined procure-to-pay variant map |
sourcing_consolidation |
s_backoffice_05 |
180 days | Five suppliers to three. Asserts tier savings, tier-1 concentration, the greedy-versus-exact award gap, resilience cost from a disruption replay, and the recommendation. Tier-2 concentration is reported as unenforced and is not asserted, because the E19 supplier DAG is Phase 6 |
tariff_shock_forward_buy |
s_backoffice_06 |
180 days | A rate shock scenario activates with 60 days notice. Asserts landed cost re-ranking, the forward-buy breakeven, the cash impact, and the duty variance in the GL |
peak_labor_strategy |
s_backoffice_07 |
8 weeks | Temps versus overtime versus hiring pulse over an eight-week peak, 20 paired replications. Asserts total cost, error rates, attrition, and service level with the paired verdict per strategy pair |
certification_cliff |
s_backoffice_08 |
90 days | Two certifications lapse in the same week. Asserts HR-003 fires before the line stops and the cross-training what-if quantifies the prevention |
friday_change_freeze |
s_backoffice_09 |
90 days | The same change stream with and without a Friday freeze, 20 paired replications. Each change draws its outcome from root/itops/change_outcome subscripted by chg_id, so rescheduling does not desynchronise the arms. Asserts the change failure rate delta, incident count, error budget consumption, and the paired interval |
critical_cve_window |
s_backoffice_10 |
14 days | A critical vulnerability on the broker with a four-hour patch requirement. Asserts the candidate window costs, the breach exposure curve, and the recommendation, and that the agent answer contains only numbers traceable to query results |
restore_drill |
s_backoffice_11 |
7 days | Historian failure and restore. Asserts measured RPO and RTO, counted data-loss events, and the target-met verdict |
promo_that_broke_the_dc |
s_backoffice_12 |
90 days | A 20 percent promotion timed into a capacity peak. Asserts CMR-001 fires at planning time, and the run quantifies overtime, expedite, cannibalisation, forward buy, and margin after all of it, plus the alternative timing recommendation |
sop_cycle_with_scoring |
s_backoffice_13 |
3 months | Three consecutive S&OP cycles. Asserts the one-number identity, the FVA report, the decision packet golden file, and that cycle three scores cycle two's assumptions |
month_end_close_kaizen |
s_backoffice_14 |
6 months | The close run before and after task reordering, paired across 20 replications with task durations drawn per task_id. Asserts the close cycle time reduction, its paired interval, and that the mined close process matches the checklist graph |
margin_drop_decomposition |
s_backoffice_15 |
90 days | Gross margin falls 1.8 points from a mix of tariff, scrap, labor, and freight causes. Asserts the ranked decomposition, the common-cause versus assignable split, and that each assignable component drills to the seeded physical events |
cargo_claim_to_premium |
s_backoffice_16 |
24 months | A cold-chain excursion produces a claim, and the claim history raises next year's premium. Asserts payout arithmetic, TCOR movement, and the risk transfer comparison |
sod_violation_caught |
s_backoffice_17 |
7 days | A grant set is changed to create a create-vendor plus approve-payment conflict. Asserts config validation rejects it at load, and that a runtime-injected grant raises IT-008 and FIN-005 |
long_horizon_charts |
s_backoffice_18 |
36 months | The low-frequency charts of 7.4 accumulate their declared points. Asserts that close_cycle_days, turnover_rate, decision_latency_days, forecast_bias, the working capital series, and backup_restore_success_rate each reach at least the points their row needs and each produces limits. This scenario exists so the chart assignments are checkable rather than aspirational |
spot_buy_versus_contract |
s_backoffice_19 |
60 days | A supplier slips two weeks into a shortage. Asserts the three priced options, the recommendation, the PO carrying buy_mode: spot, and that the justified spot spend is excluded from maverick leakage |
cross_zone_correlation |
s_backoffice_20 |
14 days | A credential-spray pattern crosses three zones over four days. Asserts three separate detection findings, one correlated alert naming all three, IT-009, and that a shuffled event order raises the identical alert |
7.6 Determinism and contract tests¶
- C1, byte-identical tier (D-05): each scenario runs twice in the same CI job on the same platform and the same pinned dependency set, and the SHA-256 of the full event log, the GL table, and the generated statements must match. A mismatch prints the first differing event.
- C1, value-equivalent tier (D-05): the same scenario runs on the two other supported platforms and the job asserts that the business events are identical and reports the observed maximum divergence on continuous fields. The tolerance is derived from measured divergence rather than chosen in advance, and the job names whether an excess is a wrong tolerance or a real defect. Nothing in this section claims byte identity across platforms.
- C1 lint: no module in these packages imports
time,datetime.now,random,secrets, orsocket, and none callsopen(,pathlib.Path,os.path, orshutil, outside an annotated escape hatch. The filesystem entries are as load-bearing as the clock entries, because a model that opens a path is a model that cannot run against an object store or an in-memory fixture. CI enforces the lint; the hash check backstops it. - C1 hash-seed check (D-03): the determinism scenario runs twice under different
PYTHONHASHSEEDvalues and the hashes are compared, which is what catches an unsorted iteration reaching the log. - C3: producer and consumer contract tests per event type. Adding a required field to an event within
a major version fails; adding an optional field passes; removing a field fails. The same job
asserts that every event named in a consumed list has a producer somewhere, and that every
published event has a declared consumer or an explicit
consumers: []marker. - C5: every shipped config file validates, and a corrupted copy of each produces an error message
containing the file name, the line number, and a suggestion. The suggestion text itself is
asserted for the ten most likely mistakes (weights not summing to one, a missing certification, a
tie-break list not ending in a unique key, an unbalanced posting rule, a decreasing change cost
table, an infeasible award constraint set, a missing
provenancekey, a self-referencing conduit, overlapping tariff effectivity, and a reserve rate vector of the wrong length). Each suggestion is asserted to be representable in the schema it suggests, so an error message can never tell a user to write something the config cannot express. - A1: a CI job creates a fresh virtualenv per package, installs only that package from the built wheel, and runs its standalone example. If a package needs a sibling to run its own example, the job fails, which is the mechanical enforcement of take-one-brick.
- Budget arithmetic (D-13):
test_validation_gate_budget_arithmeticsums the declared budget of every test in a tier and fails when the sum exceeds the tier ceiling in section 7. A scenario or gate that grows past its budget fails as a defect with the offending total quoted, rather than as a CI timeout with no explanation. - Golden-file regressions: the P&L, balance sheet, cash flow, working capital snapshot, decision
packet, and maturity snapshot for the reference seeded scenarios byte-match their committed
files. Regenerating needs an explicit
just regoldenand a diff review. These detect change; they do not validate correctness, which is why they sit here and not in 7.3.
7.7 Agent evaluation contributions (E27)¶
Each tool ships at least three eval questions whose ground truth is computed from the simulation. Refusal cases are included by design: a question about a customer that does not exist, a question requiring a metric not in the semantic layer, and a question asked while the underlying tool is failing must all produce an abstention or a refusal, never a number. The grounding checker (E26f) must reject any answer sentence containing a number without a matching query-result id, and the back-office eval set includes three adversarial questions that invite the model to do arithmetic in tokens (percentage changes, per-unit divisions, and a currency conversion) to prove it does not.
Indirect prompt injection cases contributed to the E43 red-team suite from this section: an instruction embedded in a supplier name, in a service contact free-text field, in a detection rule title, and in a capex request description. The agent must treat all four as data.
8. Phase placement¶
8.1 What the source says¶
The constraints paragraph places the safety and ergonomics layer (6a10) "after 3i", then runs 6a11 through 6a17 in order, then Phase 4. So the band this section occupies is:
... P3i upstream production -> 6a10 -> 6a11 -> [6a12 -> 6a13 -> 6a14 -> 6a15 -> 6a16 -> 6a17] -> P4 -> P5 -> P6
That ordering is already dependency-sound for the bulk of this section, because every physical flow this section reads (outbound, e-commerce, returns, cross-dock, transport, MEIO, production) has landed by the time 6a12 starts. Nothing here needs to move because it arrived too early.
8.2 Resequencing applied¶
Ten E-items are upstream dependencies of work in this band. The agreed rule (an E-item that is an upstream dependency of earlier work moves ahead of its dependent) is applied to each. Nothing is dropped, and nothing already scheduled is delayed past its dependents. Every row states the smallest part that moves, because moving a whole Phase 6 item to satisfy one dependency would be resequencing by sledgehammer.
| Item | Default position | Moved to | Why |
|---|---|---|---|
| E16 ATP/CTP order promising | Phase 6 | Immediately before 6a12 | 6a12's source text requires allocation "against ATP/CTP". The order lifecycle cannot be built without the promise engine it calls. E16 also needs only on-hand, in-transit, scheduled receipts, and the finite scheduler, all of which exist after 3i |
| E14 tariff and trade-policy scenario engine | Phase 6 | Immediately before 6a13 | Already agreed. 6a13's forward-buy-versus-tariff decision and landed-cost sourcing are named requirements of 6a13, not additions |
| E22 financial twin overlay | Phase 6 | Immediately after 6a13 | 6a17's source text says it "graduates the financial twin (E22) from an overlay into a functioning finance department", so E22 precedes it. AP terms need procurement and AR terms need orders, so the earliest sound slot is after 6a13. Placing it here also lets 6a14 payroll and 6a15 patch-window costs be quoted in cash from the moment they exist |
| E23 labor rostering optimization | Phase 6 | Immediately before 6a14 | Already agreed. 6a14's absenteeism prediction "feeds the rostering optimizer (E23)", and the skills matrix gates the roster |
| Decision register (the append-only schema and store from E21, not multi-agent negotiation) | Phase 6 | Immediately before 6a16 | 6a16's executive step ends in "the decision is logged to the governance register". The register is a schema plus an append-only table; E21 later adds role agents and a supervisor on top of it. Splitting the register out is sequencing, not descoping: E21 keeps every remaining part |
| E30 causal inference | Phase 6 | Immediately before 6a16 | Already agreed. Marketing-mix ROI is "measured honestly by the causal layer (E30)" inside 6a16's own text |
| E38 insurance and risk transfer | Phase 6 | Immediately after 6a17 | Cargo telemetry (3h), TRIR (6a10), and gross profit (6a17) all exist at that point, so E38 is close to free there, and the source invites pulling an E item forward when it is nearly free. This is a decision, not a candidacy: E38 ships in-band as slice 24, section 1 owns it entire, sections 3.8, 4.9, and 6.8 are in-band content, and scenario s_backoffice_16 is an in-band test |
| E26 accuracy stack, layers a, b, d, and f | Phase 6 | Immediately before 6a12 | Every tool in section 5.10 executes a query rather than computing in tokens (layer a), resolves metrics through the governed semantic layer (layer b), is schema-constrained (layer d), and is grounding-checked (layer f). Section 7.7 tests all four, so they cannot arrive after the tools do. Layers c, e, and g stay in E26 |
| E27 agent evaluation harness, the runner and the suite file format | Phase 6 | Immediately before 6a12 | Every tool ships at least three eval questions with ground truth computed from the simulation. The questions are this section's content; the runner that executes them is E27's, and it has to exist first. Scoring across the whole tool surface stays in E27 |
| E5 autonomy tiers, the tier enum and the approval gate | Phase 6 | Immediately before 6a15 | Two tools here change state, and an agent-invoked restore drill with no approval gate is a guardrail hole in the section that defines RBAC. In-band this section ships L1 and L2 only. E5 keeps L3 auto-apply and the guardrail evaluator |
| E43 AI security evals | Phase 6 | Not moved | The four indirect prompt-injection fixtures ship as this section's own tool tests now, so nothing here waits on E43. E43 later adopts them and adds scoring across the whole tool surface |
| E19 n-tier supplier mapping | Phase 6 | Not moved | Nothing in-band asserts a tier-2 number. SupplierDagPort returns unavailable, max_tier2_concentration is reported as unenforced, PRC-005 asserts tier-1 only, and s_backoffice_05 asserts tier-1 only. The seam is built now so E19 lands as a port implementation |
8.3 Ordering inside the band¶
Each package is delivered in slices so that every phase boundary leaves the repo shippable with the five-minute quickstart intact. A slice is only "done" when its tests, its metrics file, its findings catalog, and its standalone example all pass.
| Order | Slice | Contents | Depends on |
|---|---|---|---|
| 1 | E26 layers a, b, d, f | Query execution, governed semantic layer, tool schema constraint, grounding checker | Historian, schema registry (Phase 0) |
| 2 | E27 harness | Eval runner and the eval-suite file format | Slice 1 |
| 3 | E16 promise engine | ATP cascade, CTP fallback, promise reliability tracking by source | Planning (3d), finite scheduler (3i) |
| 4 | 6a12-A order core | Order model, state machine, capture, validation, credit stub, holds, promise integration, arrival process with both shaping ports bound to null implementations | Slice 3, outbound (3e), e-commerce (3g) |
| 5 | 6a12-B allocation | Three policies, backorders, Jain fairness index, substitution, change and cancellation costs | Slice 4, inventory availability |
| 6 | 6a12-C service and customers | Contact generation, promise breach, queue, FCR and visibility, satisfaction, churn, CLV on BaselineCostToServe, perfect order, external-failure COPQ classification |
Slice 5, returns (3f), QMS (6a11) for COPQ routing |
| 7 | E14 trade engine | Classification, schedule, overlay apply and merge, landed cost, FTZ, drawback, MEIO port | Supplier network (3e), transport (3h) |
| 8 | 6a13-A procure to pay | Requisition, PO, approval, acknowledgment, receipt, the invoice aggregate, three-way match, payment | Slice 7, planning reorder signals (3d) |
| 9 | Contract publication | The Contract schema, tier schedule, and contract.created published as the standing contract set, with no sourcing engine yet |
Slice 8 |
| 10 | 6a13-B sourcing | RFX, scoring, greedy and exact award strategies, gap report, tier achievement, rebates, renegotiation triggers | Slice 9, supplier scorecards (3e) |
| 11 | 6a13-C spend and tactics | Spend taxonomy, maverick detection, savings and avoidance ledgers, forward buy, spot buy, expedite, EOQ validation | Slices 7, 8, 10 |
| 12 | E22 financial overlay | AP and AR terms, invoice and payment streams, DSO, DPO, DIO, cash to cash, working capital by echelon, cash forecast, and the FP&A stub that publishes fpa.reforecast_published from a fixed driver set |
Slices 6 and 8 |
| 13 | 6a14-A workforce core | Worker registry, roles, wage table, skills matrix, certifications, eligibility gating, labor ledger, engagement snapshots | Ergonomics (6a10) for strain, slice 9 for the contract set the roster prices against |
| 14 | E23 rostering | Constraint-solver roster consuming the labor requirement and absence forecast | Slice 13 |
| 15 | 6a14-B lifecycle | Hiring pipeline, learning curves, cross-training, attrition, absence generation and prediction, peak strategies | Slices 13 and 14 |
| 16 | 6a15-A ITSM, SRE, telemetry | CMDB, spans, traces, logs, golden signals, SLOs, derived burn rates, error budgets, incidents, problems, changes, DORA | The twin's own services already exist from P1 onward |
| 17 | E5 tiers | Autonomy tier enum and the approval gate for mutating tools | Slice 16 for the actor identity on the approval event |
| 18 | 6a15-B security operations | Synthetic CVE feed, CVSS scoring, exposure model and CVaR patch economics, zones and conduits, detection rules, correlation engine, RBAC, chaos scenarios, backups and drills | Slices 16 and 17, twin what-if engine |
| 19 | Decision register | Append-only decision schema, store, and query surface | Slice 18 for RBAC on the register |
| 20 | E30-A causal pipeline | DoWhy and EconML estimator pipeline, seeded and pinned, run against fixture data with known effects | Historian, slice 1 |
| 21 | 6a16-A demand shaping | Promotions, lift, pull forward, cannibalisation, NPI and Bass, channel mix drift, demand multiplier, promo feature frame, serveability check | Slice 20, forecaster (3d), slice 4 for the arrival ports |
| 22 | E30-B causal scoring | Scoring the estimator against the ground-truth parameters slice 21 logs on mkt.lift_realised |
Slices 20 and 21 |
| 23 | 6a16-B sales operations | Pipeline, rep bias, quota effects, FVA ladder | Slice 21 |
| 24 | 6a16-C S&OP cycle | Five steps as events, rough-cut capacity check, consensus, priced gaps, decision packet, scoring, maturity metrics | Slices 12, 19, 21, 23, factory schedule (3i), transport capacity (3h) |
| 25 | 6a17-A ledger | Chart of accounts, posting engine, subledgers, statements, closing entries, period close mechanics, gross profit rate | Slice 12 |
| 26 | 6a17-B costing | Standard cost roll-up, variance engine, attribution, common-cause gate, inventory valuation, E&O, cycle counts and record accuracy | Slice 25, production (3i) |
| 27 | 6a17-C ABC and FP&A | Activity pools, cost to serve, AbcCostToServe wired behind CostToServePort, driver-based budget replacing the slice 12 stub, rolling reforecast, variance bridge |
Slice 26, slice 24 for the budget source |
| 28 | 6a17-D governance | Capex appraisal and post-audit, month-end close as a process, close kaizen, SOD and controls testing | Slices 25 to 27, authority matrix, RBAC grants from slice 18 |
| 29 | E38 insurance | Policies, sublimits, cargo and BI claims, loss recognition, experience rating, TCOR, risk transfer what-if | Slice 28, transport telemetry (3h), TRIR (6a10) |
Four orderings changed, and each is recorded rather than corrected in silence.
E23 no longer depends on a later slice. The previous table had slice 10 depending on "slice 11's contracts published first as stubs", which is a slice depending on its own successor. Contract publication is now slice 9, ahead of everything that reads a contract, and the roster lands at 14 after the workforce core it staffs. Nothing was descoped to achieve it; one slice was split out and its dependents follow it.
The FP&A stub is named in the slice that owns it. The previous table had the S&OP slice depending on an "FP&A stub from slice 9" that slice 9's contents did not mention. Slice 12 now lists it: a reforecast publisher over a fixed driver set, enough for the S&OP cycle to reconcile against. Slice 27 replaces it with the driver-based budget once activity-based costing exists.
E30 is split so the generator precedes the scoring. The previous table ran the whole causal layer before the ground-truth generator, which scored an estimator against data that did not yet exist. The pipeline lands at 20 and is developed against fixture data with known effects, the generator that logs the true decomposition lands at 21, and the scoring lands at 22.
Customer lifetime value ships in two stages. Slice 6 computes it through BaselineCostToServe,
and slice 27 wires AbcCostToServe behind the same port. Every churn event records which one
produced its margin. That is a config change at slice 27 rather than a rewrite, and it is why the
churn model does not wait twenty slices for the costing model.
8.4 Phase 0 obligations this section relies on¶
Nothing here can be retrofitted, which is why these sit in Phase 0 and this section simply consumes them: C1 determinism and the splittable RNG tree, C2 the sim clock, C3 the schema registry with additive-only evolution, C5 config validation, C10 monorepo tooling, and A1's package topology. Two additions this section depends on that are worth naming explicitly at Phase 0 because retrofitting them is expensive:
- The event envelope carries
actorandcausation_idfrom day one. The audit trail, the SOD evidence, the decision register, and the variance drill-down all read them. Addingactorlater means every historical run is unauditable. - Money is integer minor units from the first event that carries an amount. A float amount anywhere upstream poisons the ledger's exactness invariants, and the CI lint that bans float money fields has to exist before the first amount is emitted.
8.5 What this band leaves for Phase 6¶
| Later item | What this section leaves ready for it |
|---|---|
| E18 OT cyberattack drill | Zones, conduits, tap, detection engine, incident pipeline, backups with measured restore, error budgets, runbook format |
| E21 collaborative multi-agent with governance | The decision register, RBAC on every tool, and the S&OP decision packet as the first real decision type |
| E19 n-tier illumination | SupplierDagPort, the award engine's max_tier2_concentration constraint hook, and the consolidation what-if that reports it as unenforced until the DAG exists |
| E20 reverse stress testing | Service and cash thresholds already defined (SLA penalties, working capital, TCOR) so the optimizer has objectives to break |
| E37 PLM | Standard cost roll-up reads a single effective revision; E37 adds effectivity dates, ECO propagation into open POs and standard costs, and revision in genealogy |
| E39 ESG and CSRD | Social metrics (turnover, training hours) and the GL structure the report aggregates from |
| E40 weather | The absence model already accepts a weather_index feature and the promo baseline accepts a weather driver |
| E45 AI operations economics | The GL, ABC model, and posting rules that absorb a monthly AI P&L as one more cost pool |
| E48 field-grade failure artifacts | The runbook reference field on itops.known_error_published and the CMMS analog linkage |
9. Open questions¶
These are genuine ambiguities in the source or genuine cross-section boundaries. None is resolved by inventing an answer here.
-
Multi-currency. The source requires tariffs, imports, cross-border shipments, and CBAM-style declarations, all of which imply foreign exchange, but never names FX exposure, translation, or revaluation. This section ships
Moneywith a currency code and anFxRatePortwhose default is a fixed-rate table, so the seam exists, but the decision on whether the finance layer models FX gain and loss, translation of a foreign subsidiary, and hedging is not made here. Version 1 is single-currency and config validation rejects any document whose currency differs fromfinance.functional_currency, so the ledger invariants hold today. The multi-currency milestone is recorded in ROADMAP with its dependency on E14, and what it must still decide is the rate selection rule, the rounding point for a translated amount, and whether revaluation posts. -
Ownership of the authority matrix. Procurement (PO approval), IT operations (change approval), and finance (journal and capex approval) all need it, and A1 forbids cross-package imports. This section treats it as a shared config artifact validated by the config layer with each package reading it independently, and
authority_matrix_interpretation_agreesproves the three packages read it identically. If another section defines a governance package that owns it as code, this section consumes that instead. The semantics are settled and tested; the ownership is not. -
Where the decision register lives. 6a16 needs it, E5 and E21 extend it. This section assumes it is pulled forward as a standalone append-only store with its own schema. Whether it belongs to the agent section or to a governance package is a boundary decision for whoever owns E5.
-
Standard cost revision source before E37. 6a17 requires standard costing with material usage and yield variances, which needs a BOM. 6a9 provides ISA-88 recipes for the factory, but the DC's purchased SKUs may have no BOM at all. This section reads
revision_source: bom | recipeand rolls up from whichever exists, treating a purchased SKU's standard as a single material line. Whether that is the intended treatment for purchased-for-resale items, or whether they carry landed-cost standards instead, is not stated in the source. -
Whether the AP exception queue and the service center share a staffing model. Both are staffed office queues. 6a12 explicitly makes service agents "a staffed resource with queues, handle times, and their own rostering". 6a13 describes AP exceptions as consuming "real AP departments" but does not say they are rostered. This section models both as SimPy resources but routes only service agents through E23's roster. If AP handlers are also to be rostered, the config gains a queue and the workforce package gains a role, which is additive.
-
Definition of on-time for the perfect order. The source names "on time" without saying whether it is measured at ship or at delivery, and the two give materially different rates. This section makes it a config key (
perfect_order.on_time_basis) defaulting to delivery, and reports both in the capability report. If the intent was ship date, the default flips; the metric definition in the semantic layer changes in one place. -
Whether cost avoidance appears in any published KPI headline. The source is explicit that savings and avoidance are tracked separately "the way real procurement organizations defend their existence", which this section implements as two ledgers with a hard test that avoidance never posts to the GL. What is not stated is whether the README's measured headline may ever include an avoidance number. The recommendation from this section is that it must not, because an unauditable number in a repo whose thesis is auditable numbers is a self-inflicted wound, but that is a presentation decision for the README owner.
-
Severity ordering between security findings and safety findings. 6a10 establishes that safety findings outrank throughput findings by definition. 6a15 produces high-severity security findings (unpatched critical, SOD conflict, conduit violation). The relative ordering of a safety finding and a security finding at the same nominal severity is not stated anywhere in the source. This section assigns severity floors but does not assert a total order across classes. The alarm rationalization layer needs one, and it must come from an explicit policy config rather than from an accident of enum ordering.
-
Which supply review mode the shipped demo profile selects. The computation is specified:
RoughCutCapacityCheckin section 3.6 defines all three modes,sop.supply_review_modeselects one,analyticis the default, andsop.supply_review_completedrecords which mode produced its numbers. What is not decided is which mode the shipped demo profile runs. A full finite-capacity run of the factory over an 18-period horizon every month is expensive in simulation time, and the choice determines whether the S&OP cycle replays inside the viewer's budget. That belongs with whoever owns the sim-time budget. -
Insurance policy realism boundary. E38 names cargo claims, business interruption with waiting periods, and premiums as functions of loss history and TRIR. It does not say whether to model reinsurance, captives, parametric covers, or claims-made versus occurrence triggers. This section implements occurrence-triggered indemnity policies with limits, deductibles, coinsurance, sublimits, and waiting periods, and stops there. Anything beyond that is a ROADMAP addition rather than an assumption.
-
Provenance of the vendor scorecard weights and the resilience RPO and RTO targets. The source describes both as coming from the author's professional deliverables. Hard rule 2 forbids that. Every config block that could carry invented parameters now needs a
provenance: synthetickey and load fails without it:sourcing,tactics.spot_buy,attrition.coefficients,vulnerability,breach_impact,backup,promotions.lift,channel_mix,bill_of_resource.yaml, the insurance rating tables, andclaim_process. A schema check proves the key is present; it cannot prove the values behind it were re-derived from public practice. Whoever populates the shipped configs has to confirm that, and the capability report has to say who did. -
Whether HS codes and country codes may be plausible real values. This section uses the public HS nomenclature structure with synthetic codes and ISO 3166 country codes, with a header comment stating every rate is invented. Real HS chapter numbers are public reference data rather than proprietary information, so using them appears safe, but a reviewer who wants zero resemblance to any real tariff schedule may prefer entirely fictional codes. The choice affects only readability.
-
The false alarm rate of the full Nelson rule set on an estimated-limits I-MR chart. VG-FIN-07 validates the common-cause gate against the one rate the NIST/SEMATECH e-Handbook publishes: 0.0027 for a single point outside 3-sigma limits, with the limits supplied rather than estimated. The shipped configuration runs several Nelson rules on limits estimated from the same data, and that combined rate has no published value and no closed form. Getting one needs a Markov-chain average run length computation for the specific rule set, or a large simulation whose result is this repository's own number and so cannot be a reference for itself under D-11. In the meantime the shipped rule set's rate is measured, recorded with its method and Monte Carlo standard error, and used as a regression baseline rather than as a validated statistic. Promoting it needs an external published derivation for the exact rule set, or an accepted argument that a per-rule-1-only gate is enough to validate the classifier.
-
Whether an annual metric can have a control chart in any demoable run. Section 7.4 assigns
total_cost_of_riskan I-MR chart at quarterly cadence. Twenty points is five years of sim time, which is beyond even the 36-month long-horizon profile. The chart is defined and its limits are reported as provisional until the point count is reached. The options are a longer profile that costs sim-time budget, a shorter TCOR period that changes the accounting meaning, or accepting that some charts are defined for production use and are never exercised in a demo run. This section does not pick one, because the choice belongs with whoever owns the sim-time budget and the demo story. -
Whether the OpenStax worked examples may be encoded at all. VG-FIN-04, VG-FIN-09, and VG-FIN-10 check the variance, inventory, and depreciation implementations against worked examples in the OpenStax Principles of Accounting volumes. Both volumes are licensed CC BY-NC-SA, verified from the OpenStax book metadata API on 2026-08-09, not CC BY as this section previously stated. The non-commercial term conflicts with this repository's commercial license option and the share-alike term would reach a derivative work, so the fixtures encode the numeric inputs and the published answers with a citation and reproduce no prose, table, or layout. Individual facts and figures are not themselves copyrightable, which is the basis for that treatment, but it is the same class of question D-14 settled for PM4Py and it deserves the same treatment: a legal read before release. If the answer is no, the gates need a differently licensed source of worked examples, and they are not dropped.