aps-agent/tests/golden/test_scheduling_validator_v...

1020 lines
36 KiB
Python

from __future__ import annotations
from datetime import date, datetime, timezone
from types import MappingProxyType
import pytest
from server.aps_domain.closed_loop_problem import build_closed_loop_problem
from server.aps_domain.scheduling_problem_v2 import (
ConstraintPolicy,
ObjectivePolicy,
OperationActivity,
PeggingAllocation,
Requirement,
Resource,
ResourceKind,
RoutingStatus,
ScheduledActivity,
SchedulingProblemV2,
SchedulingSolutionV2,
SolutionProvenance,
SolveStatus,
SourceFingerprint,
SupplyDecision,
SupplyEvent,
SupplySource,
SourcingType,
TimeInterval,
operation_activity_identity,
scheduling_problem_hash,
)
from server.aps_domain.scheduling_validator import (
validate_scheduling_solution,
validate_solution,
)
BUSINESS_DATE = date(2026, 8, 2)
SOURCE_ID = "factory-pack"
SOURCE_REVISION = "ruiyang-2026-08-02-r1"
SOURCE_HASH = "a" * 64
def _dt(hour: int, minute: int = 0) -> datetime:
return datetime(2026, 8, 2, hour, minute, tzinfo=timezone.utc)
def _source_fields() -> dict[str, str]:
return {
"sourceRef": SOURCE_ID,
"sourceRevision": SOURCE_REVISION,
"sourceHash": SOURCE_HASH,
}
def _operation(
*,
activity_id: str,
requirement_id: str,
operation_code: str,
sequence: int,
eligible_resource_id: str,
predecessor_ids: tuple[str, ...] = (),
input_requirement_ids: tuple[str, ...] = (),
) -> OperationActivity:
raw = {
"activityId": activity_id,
"requirementId": requirement_id,
"routingId": f"route-{requirement_id}",
"routingVersion": "confirmed-r1",
"operationId": f"op-{activity_id}",
"operationCode": operation_code,
"sequence": sequence,
"durationMin": 60.0,
"predecessorActivityIds": predecessor_ids,
"inputRequirementIds": input_requirement_ids,
"eligibleResourceIds": (eligible_resource_id,),
"requiredCapabilities": (operation_code,),
"requiredResourceUnits": 1.0,
"continuous": False,
**_source_fields(),
}
return OperationActivity(
activityIdentity=operation_activity_identity(raw),
**raw,
)
def _scheduled(
activity: OperationActivity,
*,
start: datetime,
end: datetime,
resource_id: str,
) -> ScheduledActivity:
return ScheduledActivity(
activityId=activity.activityId,
activityIdentity=activity.activityIdentity,
requirementId=activity.requirementId,
operationId=activity.operationId,
sequence=activity.sequence,
resourceId=resource_id,
start=start,
end=end,
resourceUnits=1.0,
)
def _rebind(problem: SchedulingProblemV2, solution: SchedulingSolutionV2) -> SchedulingSolutionV2:
provenance = solution.provenance.model_copy(
update={
"problemHash": scheduling_problem_hash(problem),
"sourceRevision": problem.sourceRevision,
"sourceFingerprints": problem.sourceFingerprints,
"businessDate": problem.businessDate,
}
)
return solution.model_copy(update={"problemId": problem.problemId, "provenance": provenance})
def _build_valid_fixture() -> tuple[SchedulingProblemV2, SchedulingSolutionV2, MappingProxyType]:
calendar = (TimeInterval(start=_dt(8), end=_dt(18), label="day-shift"),)
resources = (
Resource(
resourceId="factory-1",
code="F1",
name="Factory 1",
kind=ResourceKind.FACTORY,
calendarIntervals=calendar,
cumulativeCapacity=4,
**_source_fields(),
),
Resource(
resourceId="workshop-1",
code="WS1",
name="Workshop 1",
kind=ResourceKind.WORKSHOP,
parentResourceId="factory-1",
calendarIntervals=calendar,
cumulativeCapacity=2,
**_source_fields(),
),
Resource(
resourceId="res-cut",
code="CUT-01",
name="Cut machine",
kind=ResourceKind.EQUIPMENT,
parentResourceId="workshop-1",
capabilities=("CUT",),
calendarIntervals=calendar,
cumulativeCapacity=1,
**_source_fields(),
),
Resource(
resourceId="res-weld",
code="WELD-01",
name="Weld machine",
kind=ResourceKind.EQUIPMENT,
parentResourceId="workshop-1",
capabilities=("WELD",),
calendarIntervals=calendar,
cumulativeCapacity=1,
**_source_fields(),
),
Resource(
resourceId="res-asm",
code="ASM-01",
name="Assembly station",
kind=ResourceKind.EQUIPMENT,
parentResourceId="workshop-1",
capabilities=("ASM",),
calendarIntervals=calendar,
cumulativeCapacity=1,
**_source_fields(),
),
)
child_cut = _operation(
activity_id="act-child-cut",
requirement_id="req-child-make",
operation_code="CUT",
sequence=10,
eligible_resource_id="res-cut",
)
child_weld = _operation(
activity_id="act-child-weld",
requirement_id="req-child-make",
operation_code="WELD",
sequence=20,
eligible_resource_id="res-weld",
predecessor_ids=(child_cut.activityId,),
)
root_asm = _operation(
activity_id="act-root-asm",
requirement_id="req-root",
operation_code="ASM",
sequence=10,
eligible_resource_id="res-asm",
input_requirement_ids=("req-child-make", "req-buy", "req-sub"),
)
requirements = (
Requirement(
requirementId="req-root",
orderId="MOM--00280",
orderLineId="MOM--00280:1",
materialId="FG-001",
quantity=1,
sourcingType=SourcingType.MAKE,
requiredAt=_dt(17),
routingStatus=RoutingStatus.CONFIRMED,
routingId="route-req-root",
routingVersion="confirmed-r1",
activityIds=(root_asm.activityId,),
**_source_fields(),
),
Requirement(
requirementId="req-child-make",
orderId="MOM--00280",
orderLineId="MOM--00280:1",
materialId="SEMI-001",
quantity=1,
sourcingType=SourcingType.MAKE,
requiredAt=_dt(10),
parentRequirementId="req-root",
requiredByActivityId=root_asm.activityId,
routingStatus=RoutingStatus.CONFIRMED,
routingId="route-req-child-make",
routingVersion="confirmed-r1",
activityIds=(child_cut.activityId, child_weld.activityId),
**_source_fields(),
),
Requirement(
requirementId="req-buy",
orderId="MOM--00280",
orderLineId="MOM--00280:1",
materialId="RAW-001",
quantity=2,
sourcingType=SourcingType.BUY,
requiredAt=_dt(10),
parentRequirementId="req-root",
requiredByActivityId=root_asm.activityId,
**_source_fields(),
),
Requirement(
requirementId="req-sub",
orderId="MOM--00280",
orderLineId="MOM--00280:1",
materialId="SUB-001",
quantity=1,
sourcingType=SourcingType.SUBCONTRACT,
requiredAt=_dt(10),
parentRequirementId="req-root",
requiredByActivityId=root_asm.activityId,
**_source_fields(),
),
)
supply_events = (
SupplyEvent(
supplyEventId="supply-buy",
requirementId="req-buy",
materialId="RAW-001",
source=SupplySource.PURCHASE,
quantity=2,
availableAt=_dt(9),
**_source_fields(),
),
SupplyEvent(
supplyEventId="supply-sub",
requirementId="req-sub",
materialId="SUB-001",
source=SupplySource.SUBCONTRACT,
quantity=1,
availableAt=_dt(9, 30),
**_source_fields(),
),
)
fingerprints = (
SourceFingerprint(sourceId=SOURCE_ID, revision=SOURCE_REVISION, sha256=SOURCE_HASH),
)
problem = SchedulingProblemV2(
problemId="problem-round65-w3",
businessDate=BUSINESS_DATE,
planningStart=_dt(8),
planningEnd=_dt(18),
requirements=requirements,
supplyEvents=supply_events,
activities=(child_cut, child_weld, root_asm),
resources=resources,
objectivePolicy=ObjectivePolicy(
priorities=("weightedTardiness", "changeover", "loadBalance"),
weights={"weightedTardiness": 1000.0, "changeover": 10.0, "loadBalance": 1.0},
),
constraintPolicy=ConstraintPolicy(
enabled={"routing": True, "material": True, "resource": True},
hardConstraints=("routing", "material", "resource", "calendar", "maintenance"),
),
sourceRevision=SOURCE_REVISION,
sourceFingerprints=fingerprints,
metadata={"factory": "閿愭壃", "businessDate": "2026-08-02"},
)
scheduled_activities = (
_scheduled(child_cut, start=_dt(8), end=_dt(9), resource_id="res-cut"),
_scheduled(child_weld, start=_dt(9), end=_dt(10), resource_id="res-weld"),
_scheduled(root_asm, start=_dt(10), end=_dt(11), resource_id="res-asm"),
)
solution = SchedulingSolutionV2(
problemId=problem.problemId,
solveStatus=SolveStatus.FEASIBLE,
objectiveValues={"weightedTardiness": 0.0, "changeover": 0.0, "loadBalance": 0.25},
activities=scheduled_activities,
supplyDecisions=(
SupplyDecision(
decisionId="decision-buy",
supplyEventId="supply-buy",
requirementId="req-buy",
materialId="RAW-001",
source=SupplySource.PURCHASE,
quantity=2,
availableAt=_dt(9),
consumedByActivityId=root_asm.activityId,
),
SupplyDecision(
decisionId="decision-sub",
supplyEventId="supply-sub",
requirementId="req-sub",
materialId="SUB-001",
source=SupplySource.SUBCONTRACT,
quantity=1,
availableAt=_dt(9, 30),
consumedByActivityId=root_asm.activityId,
),
),
pegging=(
PeggingAllocation(
peggingId="peg-make",
childRequirementId="req-child-make",
parentRequirementId="req-root",
quantity=1,
),
PeggingAllocation(
peggingId="peg-buy",
childRequirementId="req-buy",
parentRequirementId="req-root",
quantity=2,
),
PeggingAllocation(
peggingId="peg-sub",
childRequirementId="req-sub",
parentRequirementId="req-root",
quantity=1,
),
),
provenance=SolutionProvenance(
runId="skill-run-valid",
solverId="fixture-skill",
solverVersion="2.0.0",
generatedAt=_dt(12),
businessDate=BUSINESS_DATE,
problemHash=scheduling_problem_hash(problem),
sourceRevision=SOURCE_REVISION,
sourceFingerprints=fingerprints,
),
)
world = MappingProxyType(
{
"sourceRevision": SOURCE_REVISION,
"problemHash": scheduling_problem_hash(problem),
"sourceFingerprints": MappingProxyType(
{SOURCE_ID: (SOURCE_REVISION, SOURCE_HASH)}
),
}
)
return problem, solution, world
@pytest.fixture
def valid_skill_fixture():
return _build_valid_fixture()
@pytest.fixture
def malicious_skill_fixture(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
orphan = ScheduledActivity(
activityId="act-orphan",
activityIdentity="f" * 64,
requirementId="req-root",
operationId="op-orphan",
sequence=99,
resourceId="res-asm",
start=_dt(12),
end=_dt(13),
)
duplicate_orphan = solution.model_copy(
update={"activities": (*solution.activities, solution.activities[0], orphan)}
)
child_cut, child_weld, root_asm = solution.activities
reversed_routing = solution.model_copy(
update={
"activities": (
child_cut.model_copy(update={"start": _dt(9), "end": _dt(10)}),
child_weld.model_copy(update={"start": _dt(8), "end": _dt(9)}),
root_asm,
)
}
)
overlap_wrong_resource = solution.model_copy(
update={
"activities": (
child_cut,
child_weld,
root_asm.model_copy(
update={"resourceId": "res-weld", "start": _dt(9, 30), "end": _dt(10, 30)}
),
)
}
)
return {
"problem": problem,
"world": world,
"duplicate_orphan": duplicate_orphan,
"reversed_routing": reversed_routing,
"overlap_wrong_resource": overlap_wrong_resource,
}
def _codes(report) -> set[str]:
return {issue.code for issue in report.hardViolations}
def test_valid_skill_fixture_passes_and_contract_round_trips(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
world_before = {
"sourceRevision": world["sourceRevision"],
"problemHash": world["problemHash"],
"sourceFingerprints": dict(world["sourceFingerprints"]),
}
report = validate_solution(problem, solution, world=world)
assert report.valid is True
assert report.hardViolations == ()
assert len(report.operationIdentityHash) == 64
assert report.recomputedKpi["scheduledActivityCount"] == 3
assert report.recomputedKpi["requirementCount"] == 4
assert problem.businessDate == BUSINESS_DATE
assert world_before == {
"sourceRevision": world["sourceRevision"],
"problemHash": world["problemHash"],
"sourceFingerprints": dict(world["sourceFingerprints"]),
}
dumped = problem.model_dump(mode="json")
restored = SchedulingProblemV2.model_validate(dumped)
assert restored == problem
assert dumped["requirements"][1]["requirementId"] == "req-child-make"
assert dumped["supplyEvents"][0]["supplyEventId"] == "supply-buy"
alias_report = validate_scheduling_solution(problem, solution, world=world)
assert alias_report == report
def test_operation_identity_hash_is_order_independent(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
first = validate_solution(problem, solution, world=world)
reordered = solution.model_copy(update={"activities": tuple(reversed(solution.activities))})
second = validate_solution(problem, reordered, world=world)
assert first.valid and second.valid
assert first.operationIdentityHash == second.operationIdentityHash
def test_duplicate_and_orphan_skill_activities_are_rejected(malicious_skill_fixture):
report = validate_solution(
malicious_skill_fixture["problem"],
malicious_skill_fixture["duplicate_orphan"],
world=malicious_skill_fixture["world"],
)
assert report.valid is False
assert {"DUPLICATE_ACTIVITY", "ORPHAN_ACTIVITY"} <= _codes(report)
def test_wrong_routing_order_is_rejected(malicious_skill_fixture):
report = validate_solution(
malicious_skill_fixture["problem"],
malicious_skill_fixture["reversed_routing"],
world=malicious_skill_fixture["world"],
)
assert report.valid is False
assert {"ROUTING_ORDER_VIOLATION", "ROUTING_PRECEDENCE_VIOLATION"} <= _codes(report)
def test_overlap_wrong_resource_and_capability_are_rejected(malicious_skill_fixture):
report = validate_solution(
malicious_skill_fixture["problem"],
malicious_skill_fixture["overlap_wrong_resource"],
world=malicious_skill_fixture["world"],
)
assert report.valid is False
assert {
"RESOURCE_CAPACITY_OVERLAP",
"INELIGIBLE_RESOURCE",
"RESOURCE_CAPABILITY_MISMATCH",
"PARENT_STARTS_BEFORE_CHILD_COMPLETE",
} <= _codes(report)
def test_calendar_and_maintenance_violations_are_rejected(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
child_cut, child_weld, root_asm = solution.activities
outside_calendar = solution.model_copy(
update={
"activities": (
child_cut,
child_weld,
root_asm.model_copy(update={"start": _dt(7), "end": _dt(8)}),
)
}
)
calendar_report = validate_solution(problem, outside_calendar, world=world)
assert {"RESOURCE_CALENDAR_VIOLATION", "ACTIVITY_OUTSIDE_HORIZON"} <= _codes(calendar_report)
maintenance = TimeInterval(start=_dt(10, 15), end=_dt(10, 45), label="planned-maintenance")
changed_resources = tuple(
resource.model_copy(update={"maintenanceIntervals": (maintenance,)})
if resource.resourceId == "res-asm"
else resource
for resource in problem.resources
)
maintenance_problem = problem.model_copy(update={"resources": changed_resources})
maintenance_solution = _rebind(maintenance_problem, solution)
maintenance_world = MappingProxyType(
{
"sourceRevision": SOURCE_REVISION,
"problemHash": scheduling_problem_hash(maintenance_problem),
"sourceFingerprints": world["sourceFingerprints"],
}
)
maintenance_report = validate_solution(
maintenance_problem,
maintenance_solution,
world=maintenance_world,
)
assert "RESOURCE_MAINTENANCE_VIOLATION" in _codes(maintenance_report)
def test_material_readiness_and_parent_start_gate_are_rejected(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
late_events = tuple(
event.model_copy(update={"availableAt": _dt(10, 30)})
if event.supplyEventId == "supply-buy"
else event.model_copy(update={"availableAt": _dt(10, 45)})
for event in problem.supplyEvents
)
late_problem = problem.model_copy(update={"supplyEvents": late_events})
late_decisions = tuple(
decision.model_copy(
update={
"availableAt": _dt(10, 30)
if decision.supplyEventId == "supply-buy"
else _dt(10, 45)
}
)
for decision in solution.supplyDecisions
)
late_solution = _rebind(
late_problem,
solution.model_copy(update={"supplyDecisions": late_decisions}),
)
late_world = MappingProxyType(
{
"sourceRevision": SOURCE_REVISION,
"problemHash": scheduling_problem_hash(late_problem),
"sourceFingerprints": world["sourceFingerprints"],
}
)
report = validate_solution(late_problem, late_solution, world=late_world)
assert report.valid is False
assert {"MATERIAL_NOT_READY", "PARENT_STARTS_BEFORE_SUPPLY_READY"} <= _codes(report)
material_refs = {
ref
for issue in report.hardViolations
if issue.code == "MATERIAL_NOT_READY"
for ref in issue.entityRefs
}
assert {"decision-buy", "decision-sub"} <= material_refs
def test_supply_and_pegging_quantity_conservation_is_enforced(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
short_buy = solution.supplyDecisions[0].model_copy(update={"quantity": 1.0})
short_peg = solution.pegging[1].model_copy(update={"quantity": 1.0})
malicious = solution.model_copy(
update={
"supplyDecisions": (short_buy, solution.supplyDecisions[1]),
"pegging": (solution.pegging[0], short_peg, solution.pegging[2]),
}
)
report = validate_solution(problem, malicious, world=world)
assert report.valid is False
assert {"SUPPLY_QUANTITY_NOT_CONSERVED", "PEGGING_QUANTITY_NOT_CONSERVED"} <= _codes(report)
def test_identity_provenance_and_world_hash_drift_are_rejected(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
tampered_activity = solution.activities[0].model_copy(update={"activityIdentity": "b" * 64})
drifted_fingerprint = SourceFingerprint(
sourceId=SOURCE_ID,
revision="stale-r0",
sha256="c" * 64,
)
drifted_provenance = solution.provenance.model_copy(
update={
"problemHash": "d" * 64,
"sourceRevision": "stale-r0",
"sourceFingerprints": (drifted_fingerprint,),
}
)
malicious = solution.model_copy(
update={
"activities": (tampered_activity, *solution.activities[1:]),
"provenance": drifted_provenance,
}
)
stale_world = MappingProxyType(
{
"sourceRevision": "stale-world",
"problemHash": "e" * 64,
"sourceFingerprints": MappingProxyType(
{SOURCE_ID: ("stale-world", "f" * 64)}
),
}
)
before = {
"sourceRevision": stale_world["sourceRevision"],
"problemHash": stale_world["problemHash"],
"sourceFingerprints": dict(stale_world["sourceFingerprints"]),
}
report = validate_solution(problem, malicious, world=stale_world)
assert report.valid is False
assert {
"ACTIVITY_IDENTITY_DRIFT",
"PROBLEM_HASH_DRIFT",
"SOURCE_REVISION_DRIFT",
"SOURCE_FINGERPRINT_DRIFT",
"WORLD_SOURCE_REVISION_DRIFT",
"WORLD_PROBLEM_HASH_DRIFT",
"WORLD_SOURCE_FINGERPRINT_DRIFT",
} <= _codes(report)
assert before == {
"sourceRevision": stale_world["sourceRevision"],
"problemHash": stale_world["problemHash"],
"sourceFingerprints": dict(stale_world["sourceFingerprints"]),
}
def test_template_make_routing_is_fail_closed(valid_skill_fixture):
problem, solution, world = valid_skill_fixture
changed_requirements = tuple(
requirement.model_copy(update={"routingStatus": RoutingStatus.TEMPLATE})
if requirement.requirementId == "req-child-make"
else requirement
for requirement in problem.requirements
)
template_problem = problem.model_copy(update={"requirements": changed_requirements})
template_solution = _rebind(template_problem, solution)
template_world = MappingProxyType(
{
"sourceRevision": SOURCE_REVISION,
"problemHash": scheduling_problem_hash(template_problem),
"sourceFingerprints": world["sourceFingerprints"],
}
)
report = validate_solution(template_problem, template_solution, world=template_world)
assert report.valid is False
assert "ROUTING_NOT_CONFIRMED" in _codes(report)
def _w1_multi_parent_world() -> dict:
return {
"materials": [
{"id": 1, "code": "FG", "name": "Finished", "type": "FINISHED_PRODUCT", "unit": "PCS"},
{"id": 2, "code": "SFA", "name": "Semi A", "type": "SEMI_FINISHED", "unit": "PCS"},
{"id": 3, "code": "SFB", "name": "Semi B", "type": "SEMI_FINISHED", "unit": "PCS"},
{"id": 4, "code": "RM", "name": "Shared raw", "type": "RAW_MATERIAL", "unit": "KG", "stock": 5},
],
"flexMaterials": [],
"boms": [
{"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE"},
{"id": 2, "productId": 2, "isDefault": True, "status": "ACTIVE"},
{"id": 3, "productId": 3, "isDefault": True, "status": "ACTIVE"},
],
"bomItems": [
{"id": 1, "bomId": 1, "materialId": 2, "quantity": 1},
{"id": 2, "bomId": 1, "materialId": 3, "quantity": 1},
{"id": 3, "bomId": 2, "materialId": 4, "quantity": 2},
{"id": 4, "bomId": 3, "materialId": 4, "quantity": 3},
],
"flexBom": [],
"routings": [
{"id": 1, "productId": 1, "isDefault": True, "status": "ACTIVE"},
{"id": 2, "productId": 2, "isDefault": True, "status": "ACTIVE"},
{"id": 3, "productId": 3, "isDefault": True, "status": "ACTIVE"},
],
"routingSteps": [
{"id": 1, "routingId": 1, "operationId": 1, "sequenceNo": 1, "isExternal": False},
{"id": 2, "routingId": 2, "operationId": 2, "sequenceNo": 1, "isExternal": False},
{"id": 3, "routingId": 3, "operationId": 3, "sequenceNo": 1, "isExternal": False},
],
"routingOperations": [],
"operations": [
{"id": 1, "code": "ASM", "name": "Assembly"},
{"id": 2, "code": "WELD", "name": "Weld A"},
{"id": 3, "code": "CUT", "name": "Cut B"},
],
"flexRoutings": [],
"workstations": [],
"workstationOperations": [],
"equipment": [],
"flexEquipment": [
{"id": 1, "code": "EQ-ASM", "status": "RUNNING", "capabilities": ["ASM"]},
{"id": 2, "code": "EQ-WELD", "status": "RUNNING", "capabilities": ["WELD"]},
{"id": 3, "code": "EQ-CUT", "status": "RUNNING", "capabilities": ["CUT"]},
],
"salesOrders": [
{
"id": 1,
"orderNo": "SO-W1-MULTI",
"status": "APPROVED",
"deliveryDate": "2026-08-05",
"items": [
{"id": 1, "productId": 1, "productCode": "FG", "quantity": 1, "unit": "PCS"}
],
}
],
"purchaseOrders": [],
"outsourceOrders": [],
}
def _w1_payload_to_v2(payload: dict) -> tuple[SchedulingProblemV2, SchedulingSolutionV2, MappingProxyType]:
source_hash = payload["sourceHash"]
source_id = "w1-closed-loop"
source = {
"sourceRef": source_id,
"sourceRevision": source_hash,
"sourceHash": source_hash,
}
fingerprint = SourceFingerprint(sourceId=source_id, revision=source_hash, sha256=source_hash)
rows = {row["requirement_id"]: row for row in payload["requirements"]}
activity_id_by_requirement = {
requirement_id: f"w1-act:{requirement_id}"
for requirement_id, row in rows.items()
if row["sourcing_type"] == "MAKE"
}
consumer_ids_by_child = {
requirement_id: tuple(
activity_id_by_requirement[parent_id]
for parent_id in row["parent_requirement_ids"]
if parent_id in activity_id_by_requirement
)
for requirement_id, row in rows.items()
}
requirement_models = []
activity_models = []
for requirement_id, row in rows.items():
parent_ids = tuple(row["parent_requirement_ids"])
consumer_ids = consumer_ids_by_child[requirement_id]
activity_ids = (
(activity_id_by_requirement[requirement_id],)
if requirement_id in activity_id_by_requirement
else ()
)
routing_status = {
"READY": RoutingStatus.CONFIRMED,
"TEMPLATE": RoutingStatus.TEMPLATE,
"MISSING": RoutingStatus.MISSING,
"NOT_APPLICABLE": RoutingStatus.CONFIRMED,
}[row["routing_status"]]
requirement_models.append(
Requirement(
requirementId=requirement_id,
orderId=str(row["sales_order_id"]),
orderLineId=str(row["sales_order_line_id"]),
materialId=str(row["material_key"]),
quantity=row["quantity"],
sourcingType=SourcingType(row["sourcing_type"]),
requiredAt=datetime.fromisoformat(row["required_at"]).replace(tzinfo=timezone.utc),
parentRequirementId=parent_ids[0] if len(parent_ids) == 1 else None,
parentRequirementIds=parent_ids,
requiredByActivityId=consumer_ids[0] if len(consumer_ids) == 1 else None,
requiredByActivityIds=consumer_ids,
routingStatus=routing_status,
routingId=f"w1-route:{requirement_id}" if activity_ids else None,
routingVersion=source_hash if activity_ids else None,
activityIds=activity_ids,
**source,
)
)
make_rows = sorted(
(row for row in rows.values() if row["sourcing_type"] == "MAKE"),
key=lambda row: (-row["bom_depth"], row["requirement_id"]),
)
scheduled_models = []
for index, row in enumerate(make_rows):
requirement_id = row["requirement_id"]
start = _dt(8 + index)
end = _dt(9 + index)
raw = {
"activityId": activity_id_by_requirement[requirement_id],
"requirementId": requirement_id,
"routingId": f"w1-route:{requirement_id}",
"routingVersion": source_hash,
"operationId": f"w1-op:{requirement_id}",
"operationCode": "GENERAL",
"sequence": 10,
"durationMin": 60.0,
"predecessorActivityIds": (),
"inputRequirementIds": tuple(row["child_requirement_ids"]),
"eligibleResourceIds": ("w1-equipment",),
"requiredCapabilities": ("GENERAL",),
"requiredResourceUnits": 1.0,
"continuous": False,
**source,
}
definition = OperationActivity(
activityIdentity=operation_activity_identity(raw),
**raw,
)
activity_models.append(definition)
scheduled_models.append(
_scheduled(definition, start=start, end=end, resource_id="w1-equipment")
)
calendar = (TimeInterval(start=_dt(8), end=_dt(18), label="W1 mapped shift"),)
resources = (
Resource(
resourceId="w1-factory",
code="W1-F",
kind=ResourceKind.FACTORY,
calendarIntervals=calendar,
cumulativeCapacity=1,
**source,
),
Resource(
resourceId="w1-equipment",
code="W1-EQ",
kind=ResourceKind.EQUIPMENT,
parentResourceId="w1-factory",
capabilities=("GENERAL",),
calendarIntervals=calendar,
cumulativeCapacity=1,
**source,
),
)
supply_models = []
supply_decisions = []
supply_event_id_by_requirement = {}
for event in payload["supplyEvents"]:
if event["source"] == "MAKE":
continue
if not event["available_at"]:
continue
available_at = datetime.fromisoformat(event["available_at"]).replace(tzinfo=timezone.utc)
for allocation in event["allocations"]:
requirement_id = allocation["requirement_id"]
row = rows[requirement_id]
mapped_event_id = f"w1:{event['event_id']}:{requirement_id}"
supply_event_id_by_requirement[requirement_id] = mapped_event_id
supply_models.append(
SupplyEvent(
supplyEventId=mapped_event_id,
requirementId=requirement_id,
materialId=str(row["material_key"]),
source=SupplySource(event["source"]),
quantity=allocation["quantity"],
availableAt=available_at,
**source,
)
)
for peg in row["pegging_refs"]:
parent_id = peg["parent_requirement_id"]
supply_decisions.append(
SupplyDecision(
decisionId=f"w1-decision:{requirement_id}:{parent_id}",
supplyEventId=mapped_event_id,
requirementId=requirement_id,
materialId=str(row["material_key"]),
source=SupplySource(event["source"]),
quantity=peg["quantity"],
availableAt=available_at,
consumedByActivityId=activity_id_by_requirement[parent_id],
)
)
pegging = tuple(
PeggingAllocation(
peggingId=f"w1-peg:{row['requirement_id']}:{peg['parent_requirement_id']}",
childRequirementId=row["requirement_id"],
parentRequirementId=peg["parent_requirement_id"],
quantity=peg["quantity"],
)
for row in rows.values()
for peg in row["pegging_refs"]
)
problem = SchedulingProblemV2(
problemId=payload["problemId"],
businessDate=date.fromisoformat(payload["businessDate"]),
planningStart=_dt(8),
planningEnd=_dt(18),
requirements=tuple(requirement_models),
supplyEvents=tuple(supply_models),
activities=tuple(activity_models),
resources=resources,
sourceRevision=source_hash,
sourceFingerprints=(fingerprint,),
metadata={"adapter": "explicit-w1-to-v2-test", "w1SchemaVersion": payload["schemaVersion"]},
)
solution = SchedulingSolutionV2(
problemId=problem.problemId,
solveStatus=SolveStatus.FEASIBLE,
activities=tuple(scheduled_models),
supplyDecisions=tuple(supply_decisions),
pegging=pegging,
provenance=SolutionProvenance(
runId="w1-map-valid",
solverId="w1-map-fixture",
solverVersion="2.0",
generatedAt=_dt(14),
businessDate=BUSINESS_DATE,
problemHash=scheduling_problem_hash(problem),
sourceRevision=source_hash,
sourceFingerprints=(fingerprint,),
),
)
world = MappingProxyType(
{
"sourceRevision": source_hash,
"problemHash": scheduling_problem_hash(problem),
"sourceFingerprints": MappingProxyType(
{source_id: (source_hash, source_hash)}
),
}
)
return problem, solution, world
def test_w1_snake_case_payload_maps_to_v2_and_preserves_all_parents():
w1_problem = build_closed_loop_problem(
_w1_multi_parent_world(),
business_date="2026-08-02",
)
payload = w1_problem.to_dict()
shared = next(
row
for row in payload["requirements"]
if row["material_code"] == "RM"
)
assert len(shared["parent_requirement_ids"]) == 2
assert sum(peg["quantity"] for peg in shared["pegging_refs"]) == shared["quantity"]
problem, solution, world = _w1_payload_to_v2(payload)
mapped = next(row for row in problem.requirements if row.requirementId == shared["requirement_id"])
report = validate_solution(problem, solution, world=world)
assert mapped.parentRequirementId is None
assert set(mapped.parentRequirementIds) == set(shared["parent_requirement_ids"])
assert len(mapped.requiredByActivityIds) == 2
assert report.valid is True, report.hardViolations
def test_multi_parent_pegging_checks_every_parent_not_only_compatibility_parent():
payload = build_closed_loop_problem(
_w1_multi_parent_world(),
business_date="2026-08-02",
).to_dict()
problem, solution, world = _w1_payload_to_v2(payload)
shared = next(row for row in problem.requirements if len(row.parentRequirementIds) == 2)
shared_pegs = [peg for peg in solution.pegging if peg.childRequirementId == shared.requirementId]
assert len(shared_pegs) == 2
first = shared_pegs[0].model_copy(update={"quantity": shared.quantity})
malicious = solution.model_copy(
update={
"pegging": tuple(
peg
for peg in solution.pegging
if peg.childRequirementId != shared.requirementId
) + (first,)
}
)
report = validate_solution(problem, malicious, world=world)
assert report.valid is False
assert "PEGGING_PARENT_MISSING" in _codes(report)
assert "PEGGING_QUANTITY_NOT_CONSERVED" not in _codes(report)