Disclosure evidence and reproducible claim audits

updatesupport-finance can preserve financial facts, compile them to explicit constraints, and show feasible allocations supporting or opposing a claim. The APIs accept vendor-neutral records; retrieval and filing-version selection remain the responsibility of an adapter or analyst.

Analyst workflow

The APIs extend the low-level solver with five optional layers. They use the same linear feasibility engine, precision checks, claim audits, conflict reports and offline snapshots. Existing hand-built problems still work.

Measurement contracts and numerical units

MeasurementDefinition(name, kind, unit, currency, scope, sign) declares the meaning of an observation. Kinds are stock, flow, schedule and lifetime. Wrap facts in MeasurementObservation, including an explicit completeness flag, component fact IDs and source-to-cash multiplier when needed. Supply DisclosureRequirement contracts with requested periods, maximum age and expected components to compile_disclosure_evidence.

The requirement ledger records satisfied, missing, unavailable, incompatible, ambiguous, stale and incomplete inputs. Missing values never become zero. A derived total with an incomplete or unavailable dependency is quarantined. .require_valid() rejects an incomplete contract; .fact_for(name) returns a satisfied modeled fact. Coverage is only as strong as the supplied requirements. No helper discovers undocumented provider omissions from a field name.

unit_scales={"USD": ("USD million", 1e6)} normalizes numerical units while retaining each original DisclosureFact in normalizations. Powers of ten keep the original decimals exactly expressible; currency is unchanged. An explicit cash_multiplier=-1 records a verified sign conversion. Neither conversion loosens the solver’s feasibility tolerance. Save .snapshot_context() in the snapshot context to validate that the original and modeled facts still agree.

CashMeasure(name, components, scope) declares a signed cash measure over named primitives or other cash measures. cash_measure_constraints expands the shared primitives, rejecting cycles and unknown inputs. Alternative measures therefore reuse the same advance or financing movement. This prevents separate copies of the same modeled input from drifting; it cannot infer whether a declared cash definition is economically appropriate. cash_bridge_constraint requires cash scopes at both endpoints; differing scopes need an explicit signed adjustment (closing scope minus opening scope). Declare the adjustment’s domain yourself.

Percentage margins and threshold claims

Use AllocationShareMargin beside amount margins:

share_margin = usf.AllocationShareMargin(
    "customer_share", percentage_fact, denominator="company_revenue",
    columns=["selected_direct_buyer"], bounds=(.155, .165),
    policy="inclusive half-percentage-point allowance for displayed 16%",
)

The denominator names an amount margin, not an interchangeable revenue label. The compiler checks entity, period and numerator containment. Share facts use ratio or percent units without currency. Explicit bounds are ratios and require a named policy; otherwise source decimals or explicit exact=True are required. Qualitative words such as approximately do not select numeric bounds.

table.share_target(..., denominator="compute_revenue", threshold=1/3) produces the excess numerator - denominator/3, together with a source-supported, strictly positive denominator floor. table.problem retains the floor in every tier, including custom tiers. Audit a lower_at_least=0 claim on the excess. This tests a share threshold; it does not optimize a general ratio.

The standalone percentage_constraints supports explicitly declared stock to future-recognition relationships. Its denominator_nonnegative=True argument is a caller assertion; the caller must establish that domain in the model. varying_fields records intentional linked evidence context differences.

Dated windows and selected timing

time_window_allocation partitions a DisclosureWindow into contiguous, disjoint inclusive-date buckets. It retains the source cohort, its as-of date, measurement contract and schedule evidence role. Each bucket is nonnegative; without a timing policy it may contain any share of the total. Subtotal helpers sum unique buckets for cumulative windows.

Call .even_schedule_constraints(basis="months", policy=...) to explicitly choose even monthly conversion. Whole calendar months are required: a twelve month September–August schedule split at May yields nine and three months, with 75% allocated before the fiscal year end only in the selected policy tier. Daily schedules use inclusive day counts. Lifetime commitments and instant book liabilities are rejected as flows; first supply a separately declared schedule. Year-end balances do not certify intrayear liquidity.

Policies, stresses and break-even targets

ConstraintPolicy(name, EvidenceRole, constraints) attaches a structured role and group name to constraints. Roles distinguish reported facts, declared accounting relationships, management expectations and analyst policies. disclosure_support_basis(problem, tier) reports whether support uses only reported evidence and accounting declarations or is conditional on other roles. Unclassified constraints make the result conditional.

case = usf.DisclosureStressCase(
    "prepayment_delay", "cash_plan",
    {"prepayment_assumption": revised_prepayment_constraint},
    "Delay a declared amount while retaining the other cash-plan constraints",
)
analysis = usf.run_disclosure_scenarios(problem, [case])

Cases replace or remove named active constraints and re-solve every shared identity. Inconsistent combinations return infeasible intervals and conflict reports. Changes to reported history require historical_counterfactual=True; changes to accounting relationships require model_counterfactual=True. Counterfactual observations are labeled analyst policies and retain original fact IDs as lineage, rather than pretending to be reported observations.

break_even_analysis bounds an existing target under explicit conditions. For required issuance, release the fixed issuance policy and require ending cash to equal opening cash. A cash-floor inequality instead often gives a finite minimum and an unbounded maximum. It only releases policies/expectations. The helper does not invent a cash limit or assign probabilities to stresses.

Reuse attribute_disclosure_constraints(..., groups={policy.name: [c.name for c in policy.constraints]}) for leave-one-policy-out bound changes. This measures dependence on supplied constraints, not causal effects or expected information value. Minimal combined policy changes require a separately chosen perturbation metric and are outside this API.

Decision briefs and comparison guards

AnalystFinding combines a question, DisclosureAuditPack, DisclosureScope, optional AnalystBaseline, decision impacts, missing evidence and unanswered scope. Baselines must use the target’s display unit. AnalystDecisionBrief renders compact Markdown, JSON and tables with readable target/claim labels, bounded results and tier support. It contains no issuer-specific interpretation. Keep different cash, revenue, customer, geography and cohort meanings explicit.

Save scope.as_dict() under snapshot context["scope"]. compare_disclosure_snapshots checks target expression, unit and scale alongside compare_disclosure_scopes. Different window kinds or declared cohorts are flagged; anonymous customer identities cannot be linked across periods by label. Missing scope metadata remains unassessed, including older snapshots, which continue to replay. A comparable result is descriptive, not a causal trend.

The complete provider-neutral example is analyst_decision_workflow.py.

Record the financial context

import updatesupport_finance as usf

fact = usf.DisclosureFact(
    fact_id="example-revenue-v1",
    entity="example-cik",
    concept="revenue",
    value=123,
    scale=1_000_000,
    unit="USD",
    currency="USD",
    decimals=-6,
    period_start="2025-01-01",
    period_end="2025-12-31",
    available_at="2026-02-01T12:00:00Z",
    source_id="example-filing",
    source_url="https://example.com/filing",
)

constraint = usf.disclosure_fact_constraint("reported_revenue", "revenue", fact)

The amount is value * scale in base units. decimals=-6 means nearest-million rounding in those base units; this example compiles to [122500000, 123500000]. Inclusive rounding bounds deliberately retain both endpoints. An unknown precision (decimals=None) requires an explicit exact=True choice before compilation. Variable units must match the fact’s base unit; no automatic currency conversion occurs.

period_start=None denotes an instant rather than a duration. Availability is an explicit timestamp with a timezone, never inferred from the accounting period. Derived facts require reported=False, a description in derivation, and stable derivation_inputs IDs. The derivation text records the recipe; this API does not execute or independently validate its arithmetic.

Financial relationship helpers

  • rounded_amount_constraint(..., increment=...) encodes a declared rounding convention in the same units as its expression.

  • reconciliation_constraint(total=..., components=..., residual=...) declares that signed components and an optional residual exhaust the total.

  • stock_flow_constraint(opening=..., closing=..., movements=..., residual=...) declares a stock-flow bridge with signed movement coefficients.

  • Existing containment_constraint(...) expresses a declared component capacity.

Residual variables must use lower=None when they can be negative. The finance variable helper otherwise defaults to nonnegative. These constructors do not infer accounting identities or classify unexplained residuals as data errors.

Show what supports or opposes a claim

report = usf.triangulate_disclosure(spec)
claim = usf.disclosure_claim(
    target="revenue", tier="reported", lower_at_least=120_000_000
)
pack = usf.disclosure_audit_pack(report, claim=claim, evidence=[fact])
print(pack.to_markdown())

Audit packs include structured evidence and feasible-allocation tables by default. include_allocations=False avoids the additional solves. Without a claim, the table uses finite endpoint assignments. With a claim, supporting allocations satisfy the complete claim, including both bounds of a two-sided claim. Existing extrema are reused when suitable, with additional solves for interior or unbounded cases. Opposing allocations cross a bound by at least a recorded positive separation. An interior supporting allocation can exist even when both extrema fail.

The disclosure_allocation_checks table includes original constraints and variable-bound sides, their values, violations, and numerical tolerances. disclosure_allocation_attempts records failed and infeasible witness solves; an unavailable witness is not a claim verdict. Witnesses are numerical feasible examples, not predictions about the undisclosed allocation.

Numerical claims and conflicting disclosures

One finite endpoint can establish a one-sided claim: [10, infinity) supports x >= 5. Infeasibility, solver failure, and failed residual checks produce inconclusive claims, with the solve status preserved separately.

feasibility_tolerance on the problem controls HiGHS primal/dual feasibility settings and post-solve residual checks in original expression units. It defaults to 1e-7 and must be at least 1e-10. Scale mixed-magnitude models deliberately. Assignment checks include variable bounds as well as constraints.

Claims use a numerical buffer of absolute_tolerance + relative_tolerance * abs(threshold), with defaults 1e-7 and 1e-9. Bounds within that buffer do not resolve the claim. Setting both to zero opts into raw comparisons, including equality. These settings and solver diagnostics are numerical evidence, not exact-arithmetic certificates or financial reporting tolerances. Rounded disclosures must still be modeled as intervals separately.

conflict = usf.find_disclosure_conflict(spec, tier="reported", max_checks=1000)
print(conflict.to_markdown())

The deletion filter includes variable-bound sides and preserves source provenance. irreducible means no single member can be removed while retaining infeasibility; it does not promise the globally smallest conflict. Other statuses are feasible, budget_exhausted, and inconclusive. No constraints are silently relaxed. The core equivalents are find_named_linear_conflict(...) and check_named_linear_assignment(...).

Capture and replay a snapshot

snapshot = usf.capture_disclosure_snapshot(
    spec, facts=[fact], as_of="2026-02-02T00:00:00Z",
    target="revenue", tier="reported", claim=claim,
    assumptions=["Nearest-million rounding is represented by inclusive bounds."],
)
saved_json = snapshot.to_json(indent=2)
restored = usf.DisclosureSnapshot.from_json(saved_json)
replayed = restored.replay()
print(restored.fingerprint)
print(restored.runtime_differences())

A versioned bundle includes the input facts, full problem, selected claim, assumptions, results, Python/package versions, and source-code hashes for both packages (including editable checkouts). Canonical JSON provides a stable SHA-256 content fingerprint and detaches records from mutable caller objects. The fingerprint identifies contents; it does not authenticate sources. Loading validates the schema, evidence, and agreement between stored input records and the report. Replay recomputes using the installed runtime; exact reproduction of solver assignments or duals is not promised across versions or multiple optimal solutions.

compare_disclosure_snapshots(before, after) separates fact changes, constraint changes, other model changes, assumptions, claim selection, runtime changes, intervals, and verdicts. Records match on stable fact IDs; an amendment assigned a new ID appears as removal/addition. These differences do not establish a unique causal explanation for the changed result.

The full invented-data workflow is executable without feed access:

uv run --package updatesupport-finance python \
  packages/updatesupport-finance/examples/disclosure_evidence_workflow.py \
  --output /tmp/disclosure-evidence-demo

It demonstrates a supported allocation claim becoming inconclusive after a revision, with saved original/revised snapshots and their comparison.