Build the per-window x per-party concurrent-delay attribution
matrix from a chronological list of XER snapshots.
Implements the per-window concurrency view per AACE RP 29R-03
§4.2.B (concurrent delay apportionment). Where
``forensic_windows_analysis`` answers "how many days does each
party own across the whole project?", this tool answers "how did
each window distribute its shift across the parties?" — useful
when defending or attacking concurrency findings on a
window-by-window basis.
Conservation rule (AACE 29R-03 §4.1): the sum of per-party
column totals equals the sum of per-window completion shifts
within ±1 day of rounding. The ``conservation_check`` field on
the response reflects this; ``conservation_diff_days`` carries
the exact gap.
IMPORTANT — conservation is NOT attribution. ``conservation_check``
can be True (the columns sum to the grand total) even when 100% of
the shift lands in the Unattributed column, i.e. no party owns any
of the drift. Read ``unattributed_share_pct`` and
``high_unattributed_share_warning`` to know whether a meaningful
apportionment actually occurred. A fully-unattributed matrix
conserves perfectly but attributes nothing — never present its
green conservation check as a validated apportionment.
Use this tool when you only need the matrix view; use
``forensic_windows_analysis`` for the full claim.
Args:
schedules: chronologically ordered list of dicts — the SAME
shape ``forensic_windows_analysis`` accepts. Each dict
carries ``label`` (optional) and EXACTLY ONE of
``xer_content`` (full XER text, hosted/remote use) or
``xer_path`` (server-side path, local use). This is the
preferred input for hosted/remote clients.
xer_paths: legacy chronologically ordered list of server-side
XER file paths (local-server use).
xer_contents: legacy chronologically ordered list of XER text
contents. Each element is the full text of one XER.
Supply EXACTLY ONE of schedules / xer_paths / xer_contents
(lists must have at least 2 entries either way).
Returns:
{
"parties": ["Owner", "Contractor", "Concurrent",
"Force Majeure", "Unattributed"],
# Unit for every shift_* field and the grand totals. Always
# "working_days" — the matrix measures the completion shift
# in working days (Dana default). The *_calendar_days twins
# express the SAME shift in calendar days so an unlabeled
# "11" can never be mistaken for the 15-calendar-day value.
"shift_unit": "working_days",
"rows": [{ "window_label", "period_start", "period_end",
# shift_days == shift_workdays (working days,
# legacy alias). shift_calendar_days is the same
# shift in calendar days; shift_basis names the
# finish driver the shift was measured on.
"shift_days", "shift_unit", "shift_workdays",
"shift_calendar_days", "shift_basis",
"parties": {party: days},
"cascade_inferred": bool }, ...],
"column_totals": {party: days},
"grand_total_shift": int, # working days (legacy)
"grand_total_shift_workdays": int,
"grand_total_shift_calendar_days": int | None,
"conservation_check": bool,
"conservation_diff_days": int,
# Disambiguates "conserved AND attributed" from "conserved
# but entirely Unattributed". unattributed_share_pct is
# |Unattributed| / sum|shift| as a percent; the warning
# flips True when that share is dominant (>= 50%).
"unattributed_share_pct": float,
"high_unattributed_share_warning": bool,
"standard": "AACE RP 29R-03 §4.2.B (concurrent delay apportionment)"
}