from __future__ import annotations import csv import hashlib import json import math import shutil from collections import Counter, defaultdict from datetime import datetime, timedelta from itertools import pairwise from pathlib import Path from typing import Any import pytest from server.shipyard_synthetic.alternatives import ALTERNATIVE_MODES from server.shipyard_synthetic.config import DATA_DISCLAIMER, GeneratorConfig from server.shipyard_synthetic.constraints import validate_schedule_constraints from server.shipyard_synthetic.exporter import export_bundle from server.shipyard_synthetic.models import DatasetBundle from server.shipyard_synthetic.pipeline import build_synthetic_dataset from server.shipyard_synthetic.rag_skills import RAG_CATEGORIES, SKILL_IDS from server.shipyard_synthetic.registry import TABLE_ORDER, TABLE_SPECS from server.shipyard_synthetic.scenarios import CANONICAL_FILES, EVENT_TYPES from server.shipyard_synthetic.validation import validate_bundle REPO_ROOT = Path(__file__).resolve().parents[2] REAL_DATA_FILES = ( REPO_ROOT / "server/data/sap_mirror.json", REPO_ROOT / "server/data/approvals.json", REPO_ROOT / "server/data/world.json", REPO_ROOT / "server/data/world-proj_712276ba.json", ) EXPECTED_TABLES = ( "organizations", "workshops", "workcenters", "resources", "resource-groups", "calendars", "shifts", "skills", "teams", "employees", "suppliers", "warehouses", "locations", "contracts", "ship-projects", "milestones", "wbs", "blocks", "zones", "work-packages", "drawings", "ebom", "pbom", "mbom", "routings", "routing-operations", "engineering-releases", "materials", "inventory", "inventory-allocations", "substitutes", "planned-receipts", "material-requirements", "kit-readiness", "production-orders", "work-orders", "operations", "purchase-suggestions", "outsource-suggestions", "capacity-demands", "schedule-versions", "schedule-slots", "resource-loads", "conflicts", "kpis", "mes-orders", "operation-reports", "material-issues", "quality-inspections", "nonconformities", "rework-orders", ) EXPECTED_EVENT_TYPES = ( "KEY_STEEL_DELAY_14D", "MAIN_ENGINE_DELAY_30D", "GANTRY_CRANE_FAILURE_7D", "PAINT_SHOP_OUTAGE_5D", "STRONG_WIND_LIFT_SUSPENSION_3D", "OUTSOURCE_CAPACITY_REDUCTION_40PCT", "WELDER_TEAM_SHORTAGE_20PCT", "CRITICAL_SECTION_REWORK", "OWNER_DESIGN_CHANGE", "SISTER_SHIP_PRIORITY_INCREASE", "DOCK_RELEASE_DELAY_10D", "SEA_TRIAL_WINDOW_DELAY", "CABLE_SHORTAGE", "PIPE_PRESSURE_TEST_FAILURE", "URGENT_REPAIR_PROJECT_INSERT", ) EXPECTED_SKILL_IDS = ( "ship-project-master-planning", "ship-wbs-network-planning", "ship-bom-explosion", "ship-mrp-netting", "ship-make-buy-outsource-decision", "ship-material-readiness", "ship-block-production-scheduling", "ship-dock-erection-scheduling", "ship-crane-lift-scheduling", "ship-yard-space-scheduling", "ship-outfitting-zone-scheduling", "ship-workforce-scheduling", "ship-procurement-planning", "ship-outsourcing-planning", "ship-quality-hold-planning", "ship-scenario-simulation", "ship-schedule-repair", "ship-schedule-explanation", "ship-bottleneck-detection", "ship-risk-prediction", ) EXPECTED_RAG_CATEGORIES = ( "船舶建造流程", "分段制造工艺规则", "总组和搭载规则", "船坞使用规则", "龙门吊吊装规则", "分段运输规则", "涂装环境规则", "舾装前移规则", "托盘集配规则", "焊接工艺规则", "无损检测规则", "管系试压规则", "电气调试规则", "船级社检验规则", "供应商交付经验", "姊妹船历史工时", "设备故障经验", "典型延期案例", "排产策略说明", "异常重排处置规则", ) REQUIRED_SKILL_FIELDS = { "skillId", "name", "description", "inputSchema", "outputSchema", "requiredData", "algorithmCandidates", "hardConstraints", "softConstraints", "fallbackAlgorithm", "timeoutSeconds", "validationRules", "evidenceFields", "version", } ZERO_VALIDATION_METRICS = { "foreignKeyErrorCount", "requiredFieldErrorCount", "duplicatePrimaryKeyCount", "missingTableCount", "scaleCountMismatchCount", "syntheticBoundaryViolationCount", "missingMakeRoutingCount", "makeDesignReleaseViolationCount", "missingBuyOutsourceSupplierCount", "sourcingModeViolationCount", "fulfillmentModeViolationCount", "nettingEquationViolationCount", "purchaseTraceViolationCount", "productionRoutingCoverageViolationCount", "productionResourceCoverageViolationCount", "activeOperationCoverageViolationCount", "duplicateOperationScheduleCount", "resourceOverlapCount", "teamOverlapCount", "precedenceViolationCount", "materialReadinessViolationCount", "illegalFrozenChangeCount", "maintenanceCalendarViolationCount", "calendarViolationCount", "resourceTypeViolationCount", "resourceCapabilityViolationCount", "resourceWeightDimensionViolationCount", "teamSkillViolationCount", "teamCrewViolationCount", "teamQualificationViolationCount", "dockConflictCount", "berthConflictCount", "fixtureConflictCount", "paintBoothConflictCount", "liftingCapacityViolationCount", "transportConstraintViolationCount", "weatherWindowViolationCount", "hotWorkDensityViolationCount", "qualityHoldPointViolationCount", "reworkClosureViolationCount", "reinspectionViolationCount", "scheduleVersionEvidenceViolationCount", "unexplainedScheduleDecisionCount", "unresolvedEvidenceRefCount", "skillRegistryViolationCount", "skillSchemaViolationCount", "ragKnowledgeViolationCount", "alternativeViolationCount", "scenarioViolationCount", "infeasibleCoreViolationCount", "executionClosureViolationCount", "supplierCapacityViolationCount", "supplierBlackoutViolationCount", "outsourceTimeChainViolationCount", "outsourceSupplierEligibilityViolationCount", "constraintCoverageViolationCount", "alternativeScheduleViolationCount", "scenarioDiffViolationCount", "unmarkedHardConstraintViolationCount", } def _sha256(path: Path) -> str: digest = hashlib.sha256() with path.open("rb") as stream: for chunk in iter(lambda: stream.read(1024 * 1024), b""): digest.update(chunk) return digest.hexdigest() def _existing_hashes(paths: tuple[Path, ...]) -> dict[str, str]: return { str(path.relative_to(REPO_ROOT)): _sha256(path) for path in paths if path.exists() } def _payload_hashes(root: Path) -> dict[str, str]: return { path.relative_to(root).as_posix(): _sha256(path) for path in sorted(root.rglob("*")) if path.is_file() } def _assert_report_valid(report: dict[str, Any], label: str) -> None: if report.get("valid"): return issues = ( report.get("blockingIssues") or report.get("violations") or ["unknown validation failure"] ) pytest.fail( f"{label} validation failed:\n" + json.dumps(list(issues)[:40], ensure_ascii=False, indent=2, default=str), pytrace=False, ) def _csv_data_row_count(path: Path) -> int: with path.open("r", encoding="utf-8", newline="") as stream: return max(0, sum(1 for _ in csv.reader(stream)) - 1) @pytest.fixture(scope="session", autouse=True) def preserve_real_runtime_data() -> Any: before = _existing_hashes(REAL_DATA_FILES) yield before assert _existing_hashes(REAL_DATA_FILES) == before, ( "W69 synthetic tests modified real server/data runtime files" ) @pytest.fixture(scope="session") def small_config() -> GeneratorConfig: return GeneratorConfig.for_scale("small") @pytest.fixture(scope="session") def small_bundle(small_config: GeneratorConfig) -> DatasetBundle: return build_synthetic_dataset(small_config, validate=True) @pytest.fixture(scope="session") def full_config() -> GeneratorConfig: return GeneratorConfig.for_scale("full") @pytest.fixture(scope="session") def full_bundle(full_config: GeneratorConfig) -> DatasetBundle: # Full is intentionally generated once for this entire module. return build_synthetic_dataset(full_config, validate=True) @pytest.fixture(scope="session") def deterministic_small_exports( tmp_path_factory: pytest.TempPathFactory, small_config: GeneratorConfig, small_bundle: DatasetBundle, ) -> tuple[Path, Path, dict[str, Any], dict[str, Any]]: root = tmp_path_factory.mktemp("bh-syn-deterministic") first = root / "first" second = root / "second" first_manifest = export_bundle(small_bundle, small_config, first) same_bundle = build_synthetic_dataset(small_config, validate=True) second_manifest = export_bundle(same_bundle, small_config, second) return first, second, first_manifest, second_manifest @pytest.fixture(scope="session") def full_export( tmp_path_factory: pytest.TempPathFactory, full_config: GeneratorConfig, full_bundle: DatasetBundle, ) -> tuple[Path, dict[str, Any]]: output = tmp_path_factory.mktemp("bh-syn-full") / "dataset" manifest = export_bundle(full_bundle, full_config, output) return output, manifest def test_small_pipeline_validation_passes(small_config: GeneratorConfig) -> None: bundle: DatasetBundle | None = None try: bundle = build_synthetic_dataset(small_config, validate=True) except (TypeError, ValueError) as exc: pytest.fail(f"small pipeline validate=True did not pass: {exc}", pytrace=False) assert bundle is not None report = bundle.artifacts["validation-report"] _assert_report_valid(report, "small pipeline") assert report["solveStatus"] == "FEASIBLE" assert report["metrics"]["unmarkedHardConstraintViolationCount"] == 0 def test_small_pipeline_exports_complete_deterministic_payload( deterministic_small_exports: tuple[Path, Path, dict[str, Any], dict[str, Any]], ) -> None: first, second, first_manifest, second_manifest = deterministic_small_exports assert first_manifest["datasetType"] == "SYNTHETIC" assert first_manifest["organizationScenario"] == "BEIHAI_SHIPYARD_APS" assert first_manifest["csvFileCount"] == 51 assert first_manifest["businessDigest"] == second_manifest["businessDigest"] assert _payload_hashes(first) == _payload_hashes(second) assert (first / "README.md").is_file() assert (first / "data-dictionary.md").is_file() assert (first / "world.json").is_file() assert (first / "validation/validation-report.json").is_file() def test_same_seed_business_digest_is_stable_and_different_seed_changes_it( small_config: GeneratorConfig, small_bundle: DatasetBundle, ) -> None: same = build_synthetic_dataset(small_config, validate=False) different = build_synthetic_dataset( GeneratorConfig.for_scale("small", seed=small_config.seed + 1), validate=False, ) assert small_bundle.metadata["businessDigest"] == same.metadata["businessDigest"] assert ( small_bundle.metadata["businessDigest"] != different.metadata["businessDigest"] ) def test_full_profile_exact_counts(full_bundle: DatasetBundle) -> None: counts = full_bundle.counts() block_counts = Counter(str(row["blockType"]) for row in full_bundle.rows("blocks")) wbs_task_count = sum( row.get("wbsType") == "TASK" for row in full_bundle.rows("wbs") ) assert counts["ship-projects"] == 4 assert counts["milestones"] == 72 assert block_counts == {"GRAND_BLOCK": 72, "SECTION": 280} assert counts["work-packages"] == 1000 assert wbs_task_count == 4000 assert counts["materials"] == 12000 assert counts["ebom"] + counts["pbom"] + counts["mbom"] == 50000 assert counts["routings"] == 220 assert counts["production-orders"] == 2400 assert counts["operations"] == 12000 assert counts["resources"] == 150 assert counts["teams"] == 60 assert counts["suppliers"] == 40 assert counts["purchase-suggestions"] == 550 assert counts["outsource-suggestions"] == 140 assert counts["schedule-slots"] == 12000 assert counts["conflicts"] == 180 assert len(full_bundle.artifacts["knowledge-assets"]) == 120 def test_all_51_csv_and_manifest_rows_bytes_and_hashes( full_bundle: DatasetBundle, full_export: tuple[Path, dict[str, Any]], ) -> None: output, manifest = full_export assert EXPECTED_TABLES == tuple(TABLE_ORDER) assert len(EXPECTED_TABLES) == 51 assert manifest["csvFileCount"] == 51 assert manifest["manifestSelfIncluded"] is False assert manifest["generatedAt"] == "2026-08-04T00:00:00+08:00" assert manifest["generatedAtBasis"] == "DETERMINISTIC_BUILD_DATE" assert manifest["planningBaseDate"] == "2026-09-01" assert datetime.fromisoformat(manifest["generatedAt"]).date() < datetime.fromisoformat( manifest["planningBaseDate"] ).date() entries = {str(row["path"]): row for row in manifest["files"]} for table_name in EXPECTED_TABLES: relative_path = TABLE_SPECS[table_name].path path = output / relative_path entry = entries[relative_path] assert path.is_file(), relative_path assert entry["rowCount"] == len(full_bundle.rows(table_name)) assert _csv_data_row_count(path) == len(full_bundle.rows(table_name)) assert entry["bytes"] == path.stat().st_size assert entry["sha256"] == _sha256(path) for relative_path, entry in entries.items(): path = output / relative_path assert path.is_file(), relative_path assert entry["bytes"] == path.stat().st_size assert entry["sha256"] == _sha256(path) validation_report = json.loads( (output / "validation/validation-report.json").read_text(encoding="utf-8") ) referential = json.loads( (output / "validation/referential-integrity.json").read_text(encoding="utf-8") ) constraints = json.loads( (output / "validation/constraint-validation.json").read_text(encoding="utf-8") ) quality_markdown = (output / "validation/data-quality-report.md").read_text( encoding="utf-8" ) assert validation_report["status"] == "PASS" assert referential["valid"] is True assert constraints["valid"] is True assert constraints["summary"]["hardViolationCount"] == 0 assert "NOT_GENERATED" not in json.dumps( {"referential": referential, "constraints": constraints} ) assert "NOT_RUN" not in quality_markdown assert len( json.loads( (output / "execution/rework-operations.json").read_text(encoding="utf-8") ) ) == 50 assert len( json.loads( (output / "execution/rework-schedule-slots.json").read_text( encoding="utf-8" ) ) ) == 50 def test_exact_20_skills_required_fields_and_schemas( full_bundle: DatasetBundle, full_export: tuple[Path, dict[str, Any]], ) -> None: output, _ = full_export registry = full_bundle.artifacts["skill-registry"] schemas = full_bundle.artifacts["skill-schemas"] skills = registry["skills"] assert EXPECTED_SKILL_IDS == tuple(SKILL_IDS) assert registry["skillCount"] == 20 assert tuple(row["skillId"] for row in skills) == EXPECTED_SKILL_IDS assert set(schemas) == set(EXPECTED_SKILL_IDS) for skill in skills: skill_id = skill["skillId"] assert REQUIRED_SKILL_FIELDS <= set(skill) assert all( skill[field] not in (None, "", [], {}) for field in REQUIRED_SKILL_FIELDS ) assert skill["evidenceRef"] == f"skill:{skill_id}" assert skill["inputSchema"].endswith(f"/{skill_id}.schema.json#/$defs/input") assert skill["outputSchema"].endswith(f"/{skill_id}.schema.json#/$defs/output") schema = schemas[skill_id] assert schema["$defs"]["input"]["required"] assert schema["$defs"]["output"]["required"] exported = output / "skills/skill-schemas" / f"{skill_id}.schema.json" assert exported.is_file() assert json.loads(exported.read_text(encoding="utf-8")) == schema def test_full_rag_has_20_categories_six_each_and_resolvable_evidence( full_bundle: DatasetBundle, ) -> None: assets = full_bundle.artifacts["knowledge-assets"] category_counts = Counter(str(row["category"]) for row in assets) skill_ids = set(EXPECTED_SKILL_IDS) knowledge_ids = {str(row["knowledgeId"]) for row in assets} assert EXPECTED_RAG_CATEGORIES == tuple(RAG_CATEGORIES) assert category_counts == {category: 6 for category in EXPECTED_RAG_CATEGORIES} assert len(knowledge_ids) == 120 for asset in assets: knowledge_id = str(asset["knowledgeId"]) assert asset["evidenceRef"] == knowledge_id assert asset["sourceType"] == "SYNTHETIC_KNOWLEDGE" assert asset["relatedSkillId"] in skill_ids assert asset["relatedSkillEvidenceRef"] == f"skill:{asset['relatedSkillId']}" assert "not a real Beihai Shipyard" in asset["content"] for artifact_name in ("schedule-alternatives", "scenario-events"): artifact = full_bundle.artifacts[artifact_name] rows = artifact["alternatives"] if isinstance(artifact, dict) else artifact for row in rows: for reference in row.get("evidenceRefs") or []: assert ( reference in knowledge_ids or reference.removeprefix("skill:") in skill_ids ) def test_sourcing_fulfillment_routing_supplier_and_netting_contracts( full_bundle: DatasetBundle, ) -> None: materials = full_bundle.rows("materials") routings = {str(row["routingId"]) for row in full_bundle.rows("routings")} suppliers = {str(row["supplierId"]) for row in full_bundle.rows("suppliers")} sourcing_counts = Counter(str(row["sourcingMode"]) for row in materials) fulfillment_counts = Counter(str(row["fulfillmentMode"]) for row in materials) assert all( sourcing_counts[mode] > 0 for mode in ("MAKE", "BUY", "OUTSOURCE", "OWNER_SUPPLIED") ) assert all( fulfillment_counts[mode] > 0 for mode in ( "STOCK", "TRANSFER", "PLANNED_RECEIPT", "NEW_SUPPLY", "DESIGN_PENDING", ) ) for material in materials: if ( material["sourcingMode"] == "MAKE" and material["fulfillmentMode"] != "DESIGN_PENDING" ): assert material["routingId"] in routings if material["sourcingMode"] in {"BUY", "OUTSOURCE"}: assert material["primarySupplierId"] in suppliers outsource_required = { "outsourceOrderId", "projectId", "wbsId", "materialId", "operationId", "supplierId", "sendDate", "expectedReturnDate", "transportOutboundDays", "processDays", "inspectionDays", "transportReturnDays", "quantity", "status", "qualityStatus", "previousOperationId", "nextOperationId", "requiredCertificate", "riskLevel", } material_ids = {str(row["materialId"]) for row in materials} for suggestion in full_bundle.rows("outsource-suggestions"): assert outsource_required <= set(suggestion) assert suggestion["outsourceOrderId"] == suggestion["outsourceSuggestionId"] assert suggestion["recordType"] == "SUGGESTION" assert suggestion["isActualOrder"] is False assert suggestion["materialId"] in material_ids assert suggestion["expectedReturnDate"] == suggestion["returnDate"] assert float(suggestion["quantity"]) > 0 assert suggestion["qualityStatus"] == "PENDING_INSPECTION" assert str(suggestion["requiredCertificate"]).startswith("SHIP-OUTSOURCE-") assert suggestion["riskLevel"] in {"LOW", "MEDIUM", "HIGH"} for requirement in full_bundle.rows("material-requirements"): expected = ( float(requirement["grossRequirement"]) + float(requirement["safetyStockRequirement"]) + float(requirement["scrapRequirement"]) - float(requirement["stockUsed"]) - float(requirement["releasedAllocationUsed"]) - float(requirement["plannedReceiptUsed"]) - float(requirement["substituteUsed"]) ) assert math.isclose( float(requirement["netRequirement"]), max(0.0, expected), abs_tol=0.002 ) def test_production_orders_have_routings_and_operations_have_finite_resources( full_bundle: DatasetBundle, ) -> None: routings = {str(row["routingId"]) for row in full_bundle.rows("routings")} resources = {str(row["resourceId"]) for row in full_bundle.rows("resources")} teams = {str(row["teamId"]): row for row in full_bundle.rows("teams")} work_orders = { str(row["workOrderId"]): row for row in full_bundle.rows("work-orders") } demands = { str(row["operationId"]): row for row in full_bundle.rows("capacity-demands") } slots = {str(row["operationId"]): row for row in full_bundle.rows("schedule-slots")} operations_by_work_order: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) for order in full_bundle.rows("production-orders"): assert order["routingId"] in routings assert order["designReleaseStatus"] == "RELEASED" for operation in full_bundle.rows("operations"): operation_id = str(operation["operationId"]) work_order_id = str(operation["workOrderId"]) operations_by_work_order[work_order_id].append(operation) assert work_order_id in work_orders assert operation["routingId"] in routings assert operation_id in demands assert operation_id in slots assert demands[operation_id]["capacityMode"] == "FINITE" assert slots[operation_id]["resourceId"] in resources assert slots[operation_id]["teamId"] in teams assert set(operation["requiredSkillCodes"]) <= set( teams[slots[operation_id]["teamId"]]["skillCodes"] ) assert int(operation["crewSize"]) <= int( teams[slots[operation_id]["teamId"]]["crewSize"] ) assert set(operations_by_work_order) == set(work_orders) assert {len(rows) for rows in operations_by_work_order.values()} == {5} def test_full_schedule_constraints_and_validation_metrics_are_clean( full_config: GeneratorConfig, full_bundle: DatasetBundle, ) -> None: operations = full_bundle.rows("operations") slots = full_bundle.rows("schedule-slots") assert len(operations) == len(slots) == 12000 assert len({row["operationId"] for row in slots}) == len(operations) schedule_report = validate_schedule_constraints(full_bundle, full_config) _assert_report_valid(schedule_report, "full finite-capacity schedule") assert schedule_report["hardViolationCount"] == 0 assert schedule_report["unmarkedHardViolationCount"] == 0 assert schedule_report["checks"]["activeOperationCoverage"] == 12000 assert schedule_report["checks"]["scheduleSlotCount"] == 12000 assert schedule_report["checks"]["precedenceArcCount"] > 0 assert schedule_report["checks"]["materialReadyCheckCount"] == 12000 assert schedule_report["checks"]["holdPointCheckCount"] > 0 report = validate_bundle(full_bundle, full_config) _assert_report_valid(report, "full dataset") metrics = report["metrics"] assert {name: metrics[name] for name in ZERO_VALIDATION_METRICS} == { name: 0 for name in ZERO_VALIDATION_METRICS } assert report["constraintValidation"]["hardViolationCount"] == 0 coverage_keys = { "constraintCoverage.precedenceFS", "constraintCoverage.precedenceSS", "constraintCoverage.precedenceFF", "constraintCoverage.precedenceSF", "constraintCoverage.positiveLag", "constraintCoverage.negativeLag", "constraintCoverage.qualification", "constraintCoverage.weightDimension", "constraintCoverage.transport", "constraintCoverage.hazard", "constraintCoverage.areaDensity", "constraintCoverage.maintenanceBlackout", "constraintCoverage.supplierBlackout", "constraintCoverage.supplierCapacityUnit", "constraintCoverage.weatherWindow", } assert all(report["checks"][name] > 0 for name in coverage_keys) relation_counts = Counter( str(row["relationType"]) for row in operations if row.get("predecessorOperationId") ) assert all(relation_counts[name] > 0 for name in ("FS", "SS", "FF", "SF")) assert any(float(row["lagHours"]) > 0 for row in operations) assert any(float(row["lagHours"]) < 0 for row in operations) assert any(row.get("requiredQualificationCodes") for row in operations) assert any(row.get("transportWindowStatus") == "OPEN" for row in operations) assert any(row.get("weatherSensitive") for row in operations) assert any(row.get("hazardClass") for row in operations) assert any(float(row.get("maxAreaDensityUnits") or 0) > 0 for row in operations) @pytest.mark.parametrize( ("business_constraint_name", "case", "expected_code"), ( ("\u5de5\u5e8f\u524d\u540e\u5173\u7cfb", "PROCESS_PRECEDENCE", "FS_PRECEDENCE"), ("\u7269\u6599\u9f50\u5957", "MATERIAL_KIT", "MATERIAL_READY"), ("\u8bbe\u8ba1\u56fe\u7eb8\u5df2\u91ca\u653e", "DESIGN_RELEASE", "DESIGN_RELEASE"), ("\u8d44\u6e90\u6709\u9650\u80fd\u529b", "FINITE_RESOURCE", "RESOURCE_CAPABILITY"), ("\u8bbe\u5907\u7ef4\u62a4\u7a97\u53e3", "MAINTENANCE_WINDOW", "MAINTENANCE_BLACKOUT"), ("\u4eba\u5458\u6280\u80fd\u548c\u4eba\u6570", "TEAM_SKILL_CREW", "TEAM_SKILLS"), ("\u8239\u575e\u72ec\u5360", "DOCK_EXCLUSIVE", "DOCK_EXCLUSIVITY"), ("\u7801\u5934\u6cca\u4f4d\u72ec\u5360", "BERTH_EXCLUSIVE", "BERTH_EXCLUSIVITY"), ("\u80ce\u67b6\u548c\u573a\u5730\u5360\u7528", "SUPPORT_FRAME_SITE", "SUPPORT_FRAME_SITE_OCCUPANCY"), ("\u540a\u88c5\u80fd\u529b\u548c\u540a\u88c5\u7a97\u53e3", "LIFT_CAPACITY_WINDOW", "LIFT_WINDOW"), ("\u5206\u6bb5\u8fd0\u8f93\u8def\u5f84", "TRANSPORT_PATH", "TRANSPORT_PATH"), ("\u6d82\u88c5\u73af\u5883\u6761\u4ef6", "PAINT_ENVIRONMENT", "PAINT_ENVIRONMENT"), ("\u8d28\u91cf Hold Point", "QUALITY_HOLD", "QUALITY_HOLD"), ("\u59d4\u5916\u5230\u8d27", "OUTSOURCE_ARRIVAL", "OUTSOURCE_TIME_CHAIN"), ("\u4f9b\u5e94\u5546\u4ea7\u80fd", "SUPPLIER_CAPACITY", "SUPPLIER_MONTHLY_CAPACITY"), ("\u9879\u76ee\u51bb\u7ed3\u533a", "PROJECT_FREEZE_ZONE", "FROZEN_ZONE"), ("\u5df2\u5f00\u5de5\u4efb\u52a1\u4e0d\u53ef\u968f\u610f\u8fc1\u79fb", "STARTED_IMMUTABLE", "STARTED_TASK_IMMUTABLE"), ("\u5df2\u4e0b\u53d1 MES \u4efb\u52a1\u4e0d\u53ef\u81ea\u52a8\u5220\u9664", "MES_NO_DELETE", "MES_DISPATCH_DELETE"), ("\u51bb\u7ed3\u5de5\u5355\u4e0d\u5141\u8bb8\u81ea\u52a8\u6539\u671f", "FROZEN_ORDER_NO_RESCHEDULE", "FROZEN_WORK_ORDER_RESCHEDULE"), ("\u5206\u6bb5\u642d\u8f7d\u987a\u5e8f", "SECTION_ERECTION_SEQUENCE", "SECTION_ERECTION_SEQUENCE"), ("\u603b\u6bb5\u7a7a\u95f4\u548c\u5e72\u6d89\u5173\u7cfb", "GRAND_BLOCK_SPATIAL", "GRAND_BLOCK_SPATIAL_INTERFERENCE"), ("\u5371\u9669\u4f5c\u4e1a\u4e92\u65a5", "HAZARD_MUTEX", "HAZARD_INCOMPATIBILITY"), ("\u540c\u533a\u57df\u591a\u4e13\u4e1a\u65bd\u5de5\u5bc6\u5ea6", "AREA_DENSITY", "AREA_DENSITY"), ("\u5173\u952e\u8bbe\u5907\u5230\u8d27\u6761\u4ef6", "CRITICAL_EQUIPMENT", "CRITICAL_EQUIPMENT_ARRIVAL"), ("\u8239\u4e1c\u548c\u8239\u7ea7\u793e\u89c1\u8bc1\u65e5\u5386", "WITNESS_CALENDAR", "WITNESS_CALENDAR"), ("\u4e0b\u6c34\u548c\u8bd5\u822a\u5929\u6c14\u7a97\u53e3", "LAUNCH_TRIAL_WEATHER", "LAUNCH_TRIAL_WEATHER"), ), ids=[f"{index:02d}-{name}" for index, name in enumerate(( "\u5de5\u5e8f\u524d\u540e\u5173\u7cfb", "\u7269\u6599\u9f50\u5957", "\u8bbe\u8ba1\u56fe\u7eb8\u5df2\u91ca\u653e", "\u8d44\u6e90\u6709\u9650\u80fd\u529b", "\u8bbe\u5907\u7ef4\u62a4\u7a97\u53e3", "\u4eba\u5458\u6280\u80fd\u548c\u4eba\u6570", "\u8239\u575e\u72ec\u5360", "\u7801\u5934\u6cca\u4f4d\u72ec\u5360", "\u80ce\u67b6\u548c\u573a\u5730\u5360\u7528", "\u540a\u88c5\u80fd\u529b\u548c\u540a\u88c5\u7a97\u53e3", "\u5206\u6bb5\u8fd0\u8f93\u8def\u5f84", "\u6d82\u88c5\u73af\u5883\u6761\u4ef6", "\u8d28\u91cf Hold Point", "\u59d4\u5916\u5230\u8d27", "\u4f9b\u5e94\u5546\u4ea7\u80fd", "\u9879\u76ee\u51bb\u7ed3\u533a", "\u5df2\u5f00\u5de5\u4efb\u52a1\u4e0d\u53ef\u968f\u610f\u8fc1\u79fb", "\u5df2\u4e0b\u53d1 MES \u4efb\u52a1\u4e0d\u53ef\u81ea\u52a8\u5220\u9664", "\u51bb\u7ed3\u5de5\u5355\u4e0d\u5141\u8bb8\u81ea\u52a8\u6539\u671f", "\u5206\u6bb5\u642d\u8f7d\u987a\u5e8f", "\u603b\u6bb5\u7a7a\u95f4\u548c\u5e72\u6d89\u5173\u7cfb", "\u5371\u9669\u4f5c\u4e1a\u4e92\u65a5", "\u540c\u533a\u57df\u591a\u4e13\u4e1a\u65bd\u5de5\u5bc6\u5ea6", "\u5173\u952e\u8bbe\u5907\u5230\u8d27\u6761\u4ef6", "\u8239\u4e1c\u548c\u8239\u7ea7\u793e\u89c1\u8bc1\u65e5\u5386", "\u4e0b\u6c34\u548c\u8bd5\u822a\u5929\u6c14\u7a97\u53e3", ), 1)], ) def test_hard_constraint_mutation_matrix_rejects_each_family( small_config: GeneratorConfig, small_bundle: DatasetBundle, business_constraint_name: str, case: str, expected_code: str, ) -> None: """Reject one explicit illegal mutation for each of the 26 named business constraints.""" bundle = small_bundle.clone() operations = {str(row["operationId"]): row for row in bundle.rows("operations")} slots_by_operation = { str(row["operationId"]): row for row in bundle.rows("schedule-slots") } resources = {str(row["resourceId"]): row for row in bundle.rows("resources")} def overlap(first: dict[str, Any], second: dict[str, Any]) -> None: duration = datetime.fromisoformat(str(second["end"])) - datetime.fromisoformat( str(second["start"]) ) second["start"] = first["start"] second["end"] = ( datetime.fromisoformat(str(first["start"])) + duration ).isoformat() if case == "PROCESS_PRECEDENCE": operation = next(row for row in operations.values() if row.get("predecessorOperationId")) operation["relationType"] = "FS" operation["lagHours"] = 100000.0 elif case == "MATERIAL_KIT": next(iter(operations.values()))["materialReadyAt"] = "2099-12-31" elif case == "DESIGN_RELEASE": next(iter(operations.values()))["drawingReleased"] = False elif case == "FINITE_RESOURCE": next(iter(operations.values()))["requiredCapabilityTags"] = ["CAP-SYN-NOT-AVAILABLE"] elif case == "MAINTENANCE_WINDOW": slot = next(iter(slots_by_operation.values())) resources[str(slot["resourceId"])].setdefault("maintenanceBlackoutDates", []).append( str(slot["start"])[:10] ) elif case == "TEAM_SKILL_CREW": next(iter(operations.values()))["requiredSkillCodes"] = ["SK-SYN-NOT-AVAILABLE"] elif case in {"DOCK_EXCLUSIVE", "BERTH_EXCLUSIVE", "SUPPORT_FRAME_SITE"}: field = { "DOCK_EXCLUSIVE": "dockId", "BERTH_EXCLUSIVE": "berthId", "SUPPORT_FRAME_SITE": "supportFrameSiteId", }[case] grouped: defaultdict[str, list[dict[str, Any]]] = defaultdict(list) for operation in operations.values(): if operation.get(field): grouped[str(operation[field])].append(slots_by_operation[str(operation["operationId"])]) first, second = next(rows[:2] for rows in grouped.values() if len(rows) >= 2) overlap(first, second) elif case == "LIFT_CAPACITY_WINDOW": operation = next(row for row in operations.values() if row.get("liftingRequired")) operation["liftWindowStatus"] = "CLOSED" elif case == "TRANSPORT_PATH": operation = next(row for row in operations.values() if row.get("transportPathRequired")) operation["transportPathStatus"] = "BLOCKED" elif case == "PAINT_ENVIRONMENT": operation = next(row for row in operations.values() if row.get("paintingEnvironmentRequired")) operation["paintingEnvironmentStatus"] = "UNACCEPTABLE" elif case == "QUALITY_HOLD": operation = next(row for row in operations.values() if row.get("holdPoint")) operation["holdStatus"] = "HOLD" elif case == "OUTSOURCE_ARRIVAL": suggestion = bundle.rows("outsource-suggestions")[0] suggestion["returnDate"] = suggestion["sendDate"] elif case == "SUPPLIER_CAPACITY": suggestion = bundle.rows("outsource-suggestions")[0] supplier = next( row for row in bundle.rows("suppliers") if row["supplierId"] == suggestion["supplierId"] ) supplier["monthlyCapacityUnit"] = "OPERATION" supplier["monthlyCapacity"] = 0.5 elif case == "PROJECT_FREEZE_ZONE": slot = next(row for row in slots_by_operation.values() if row.get("timeFence") == "FROZEN") slot["baselineStart"] = ( datetime.fromisoformat(str(slot["start"])) - timedelta(hours=1) ).isoformat() slot["changeAuthorized"] = False elif case == "STARTED_IMMUTABLE": operation = next(row for row in operations.values() if row.get("startedTaskImmutable")) slot = slots_by_operation[str(operation["operationId"])] slot["start"] = (datetime.fromisoformat(str(slot["start"])) + timedelta(minutes=30)).isoformat() slot["end"] = (datetime.fromisoformat(str(slot["end"])) + timedelta(minutes=30)).isoformat() elif case == "MES_NO_DELETE": work_order = next(row for row in bundle.rows("work-orders") if row.get("mesDispatchProtected")) operation = next( row for row in operations.values() if row.get("workOrderId") == work_order["workOrderId"] ) bundle.set_rows( "schedule-slots", [row for row in bundle.rows("schedule-slots") if row["operationId"] != operation["operationId"]], ) elif case == "FROZEN_ORDER_NO_RESCHEDULE": operation = next( row for row in operations.values() if row.get("frozenWorkOrderProtected") and not row.get("startedTaskImmutable") ) slot = slots_by_operation[str(operation["operationId"])] slot["end"] = (datetime.fromisoformat(str(slot["end"])) + timedelta(minutes=30)).isoformat() elif case == "SECTION_ERECTION_SEQUENCE": operation = next(row for row in operations.values() if row.get("erectionPredecessorOperationId")) predecessor = slots_by_operation[str(operation["erectionPredecessorOperationId"])] overlap(predecessor, slots_by_operation[str(operation["operationId"])]) elif case == "GRAND_BLOCK_SPATIAL": grouped = defaultdict(list) for operation in operations.values(): if operation.get("spatialInterferenceGroup"): grouped[str(operation["spatialInterferenceGroup"])].append( slots_by_operation[str(operation["operationId"])] ) first, second = next(rows[:2] for rows in grouped.values() if len(rows) >= 2) overlap(first, second) elif case in {"HAZARD_MUTEX", "AREA_DENSITY"}: first_operation, second_operation = list(operations.values())[:2] first_operation["zoneId"] = "ZONE-SYN-MUTATION" second_operation["zoneId"] = "ZONE-SYN-MUTATION" first_operation["areaDensityUnits"] = 2 second_operation["areaDensityUnits"] = 2 first_operation["maxAreaDensityUnits"] = 2 second_operation["maxAreaDensityUnits"] = 2 if case == "HAZARD_MUTEX": first_operation["hazardClass"] = "HOT_WORK" second_operation["hazardClass"] = "PAINT_VOC" else: first_operation["hazardClass"] = None second_operation["hazardClass"] = None overlap( slots_by_operation[str(first_operation["operationId"])], slots_by_operation[str(second_operation["operationId"])], ) elif case == "CRITICAL_EQUIPMENT": operation = next(row for row in operations.values() if row.get("criticalEquipmentRequired")) operation["criticalEquipmentArrivalAt"] = "2099-12-31" elif case == "WITNESS_CALENDAR": operation = next(row for row in operations.values() if row.get("witnessRequired")) operation["witnessCalendarStatus"] = "CLOSED" elif case == "LAUNCH_TRIAL_WEATHER": operation = next(row for row in operations.values() if row.get("launchTrialWeatherRequired")) operation["launchTrialWeatherStatus"] = "CLOSED" else: # pragma: no cover - exhaustive parametrization guard raise AssertionError((business_constraint_name, case)) report = validate_schedule_constraints(bundle, small_config) codes = {str(row["constraint"]) for row in report["violations"]} assert expected_code in codes, (business_constraint_name, expected_code, sorted(codes)) def test_exact_26_business_constraints_have_positive_baseline_coverage( full_bundle: DatasetBundle, full_config: GeneratorConfig, ) -> None: expected_names = ( "\u5de5\u5e8f\u524d\u540e\u5173\u7cfb", "\u7269\u6599\u9f50\u5957", "\u8bbe\u8ba1\u56fe\u7eb8\u5df2\u91ca\u653e", "\u8d44\u6e90\u6709\u9650\u80fd\u529b", "\u8bbe\u5907\u7ef4\u62a4\u7a97\u53e3", "\u4eba\u5458\u6280\u80fd\u548c\u4eba\u6570", "\u8239\u575e\u72ec\u5360", "\u7801\u5934\u6cca\u4f4d\u72ec\u5360", "\u80ce\u67b6\u548c\u573a\u5730\u5360\u7528", "\u540a\u88c5\u80fd\u529b\u548c\u540a\u88c5\u7a97\u53e3", "\u5206\u6bb5\u8fd0\u8f93\u8def\u5f84", "\u6d82\u88c5\u73af\u5883\u6761\u4ef6", "\u8d28\u91cf Hold Point", "\u59d4\u5916\u5230\u8d27", "\u4f9b\u5e94\u5546\u4ea7\u80fd", "\u9879\u76ee\u51bb\u7ed3\u533a", "\u5df2\u5f00\u5de5\u4efb\u52a1\u4e0d\u53ef\u968f\u610f\u8fc1\u79fb", "\u5df2\u4e0b\u53d1 MES \u4efb\u52a1\u4e0d\u53ef\u81ea\u52a8\u5220\u9664", "\u51bb\u7ed3\u5de5\u5355\u4e0d\u5141\u8bb8\u81ea\u52a8\u6539\u671f", "\u5206\u6bb5\u642d\u8f7d\u987a\u5e8f", "\u603b\u6bb5\u7a7a\u95f4\u548c\u5e72\u6d89\u5173\u7cfb", "\u5371\u9669\u4f5c\u4e1a\u4e92\u65a5", "\u540c\u533a\u57df\u591a\u4e13\u4e1a\u65bd\u5de5\u5bc6\u5ea6", "\u5173\u952e\u8bbe\u5907\u5230\u8d27\u6761\u4ef6", "\u8239\u4e1c\u548c\u8239\u7ea7\u793e\u89c1\u8bc1\u65e5\u5386", "\u4e0b\u6c34\u548c\u8bd5\u822a\u5929\u6c14\u7a97\u53e3", ) report = full_bundle.artifacts["baseline-results"]["constraintReport"] coverage = report["businessConstraintCoverage"] assert tuple(coverage) == expected_names assert len(coverage) == 26 assert all(row["positiveSampleCount"] > 0 for row in coverage.values()) assert all(row["violationCount"] == 0 for row in coverage.values()) frozen_slots = [ row for row in full_bundle.rows("schedule-slots") if row.get("timeFence") == "FROZEN" ] assert frozen_slots frozen_limit = full_config.planning_base_date + timedelta(days=13) assert max(datetime.fromisoformat(str(row["start"])).date() for row in frozen_slots) <= frozen_limit assert len(full_bundle.rows("schedule-slots")) == 12000 def test_shipyard_synthetic_text_fields_have_no_question_mark_corruption( small_bundle: DatasetBundle, ) -> None: source_files = ( "orders.py", "scheduler.py", "constraints.py", "alternatives.py", "scenarios.py", ) for filename in source_files: source = (REPO_ROOT / "server/shipyard_synthetic" / filename).read_text(encoding="utf-8") assert "????" not in source, filename generated = json.dumps( {"tables": small_bundle.tables, "artifacts": small_bundle.artifacts}, ensure_ascii=False, sort_keys=True, ) assert "????" not in generated assert all( row.get("explanation") and "????" not in str(row["explanation"]) for row in small_bundle.rows("schedule-slots") ) assert all(name and "????" not in name for _mode, name, _weights in ALTERNATIVE_MODES) assert all( action and "????" not in action for event in small_bundle.artifacts["scenario-events"] for action in event["input"]["recommendedActions"] ) def test_seven_alternatives_and_fifteen_scenarios_with_infeasible_cores( full_bundle: DatasetBundle, full_export: tuple[Path, dict[str, Any]], ) -> None: """Alternatives and events must contain real, revalidated schedule changes.""" output, _ = full_export alternative_artifact = full_bundle.artifacts["schedule-alternatives"] alternatives = alternative_artifact["alternatives"] expected_modes = tuple(row[0] for row in ALTERNATIVE_MODES) assert len(alternatives) == 7 assert tuple(row["mode"] for row in alternatives) == expected_modes assert len({row["scheduleVersionId"] for row in alternatives}) == 7 recommended = [row for row in alternatives if row["mode"] == "RECOMMENDED"] assert len(recommended) == 1 assert ( alternative_artifact["recommendedAlternativeId"] == recommended[0]["alternativeId"] ) for alternative in alternatives: assert alternative["scheduleVersionId"] != alternative["baseScheduleVersionId"] assert alternative["changedSlotCount"] > 0 assert alternative["changedSlotCount"] == len(alternative["slotDeltas"]) assert alternative["changedSlotCount"] == len(alternative["alternativeSlots"]) assert alternative["constraintValidation"]["valid"] is True assert alternative["constraintValidation"]["hardViolationCount"] == 0 actual_counts: Counter[str] = Counter() for delta in alternative["slotDeltas"]: before, after = delta["before"], delta["after"] changed = [ field for field in ("start", "end", "resourceId", "teamId") if before[field] != after[field] ] assert changed assert set(changed) == set(delta["changedFields"]) assert after["scheduleVersionId"] == alternative["scheduleVersionId"] actual_counts.update(changed) assert dict(actual_counts) == alternative["changedFieldCounts"] events = full_bundle.artifacts["scenario-events"] assert len(events) == 15 assert tuple(EVENT_TYPES) == EXPECTED_EVENT_TYPES assert tuple(row["eventType"] for row in events) == EXPECTED_EVENT_TYPES assert all(row["eventName"] for row in events) assert len({row["scenarioId"] for row in events}) == 15 assert len({row["afterScheduleVersionId"] for row in events}) == 15 infeasible = [] required_input_fields = { "occurredAt", "affectedProjectIds", "affectedWbsIds", "affectedProductionOrderIds", "affectedResourceIds", "parameters", "originalImpact", "recommendedActions", "requiresReschedule", "frozenImpact", } for event in events: assert required_input_fields <= set(event["input"]) assert event["input"]["synthetic"] is True assert event["input"]["datasetType"] == "SYNTHETIC" assert event["input"]["requiresReschedule"] is True assert event["input"]["parameters"] assert event["input"]["recommendedActions"] assert event["input"]["frozenImpact"]["timeFenceField"] == "timeFence" assert event["afterScheduleVersionId"] != event["baseScheduleVersionId"] assert isinstance(event["before"], list) assert isinstance(event["after"], list) assert isinstance(event["diff"], dict) assert event["diff"]["unauthorizedFrozenChangeCount"] == 0 if event["solveStatus"] == "INFEASIBLE": infeasible.append(event) assert event["conflictCore"] assert event["reliefSuggestions"] continue assert event["solveStatus"] == "FEASIBLE" assert event["constraintValidation"]["valid"] is True assert event["constraintValidation"]["hardViolationCount"] == 0 before_by_operation = {row["operationId"]: row for row in event["before"]} after_by_operation = {row["operationId"]: row for row in event["after"]} changed_counts: Counter[str] = Counter() actual_changed = 0 for operation_id, before in before_by_operation.items(): after = after_by_operation[operation_id] changed = [ field for field in ("start", "end", "resourceId", "teamId") if before[field] != after[field] ] if changed: actual_changed += 1 changed_counts.update(changed) assert after["scheduleVersionId"] == event["afterScheduleVersionId"] assert actual_changed == event["diff"]["changedSlotCount"] assert len(event["before"]) == len(event["after"]) == actual_changed assert dict(changed_counts) == event["diff"]["changedFieldCounts"] assert infeasible for file_name, event_type in CANONICAL_FILES.items(): assert full_bundle.artifacts[file_name]["eventType"] == event_type exported = output / "scenarios" / f"{file_name}.json" assert exported.is_file() assert ( json.loads(exported.read_text(encoding="utf-8"))["eventType"] == event_type ) events_root = output / "scenarios/events" assert events_root.is_dir() event_dirs = sorted(path for path in events_root.iterdir() if path.is_dir()) assert len(event_dirs) == 15 assert all( {"input.json", "before.json", "after.json", "diff.json"} <= {path.name for path in event_dir.iterdir()} for event_dir in event_dirs ) def test_execution_quality_six_tables_and_rework_reinspection_chain( full_bundle: DatasetBundle, ) -> None: """NCR rework must consume a distinct finite slot and obey its timeline.""" table_names = ( "mes-orders", "operation-reports", "material-issues", "quality-inspections", "nonconformities", "rework-orders", ) assert all(full_bundle.rows(name) for name in table_names) operations = {str(row["operationId"]) for row in full_bundle.rows("operations")} resources = {str(row["resourceId"]) for row in full_bundle.rows("resources")} teams = {str(row["teamId"]) for row in full_bundle.rows("teams")} baseline_slots = { str(row["scheduleSlotId"]): row for row in full_bundle.rows("schedule-slots") } rework_operations = { str(row["reworkOperationId"]): row for row in full_bundle.rows("rework-operations") } rework_slots = { str(row["reworkScheduleSlotId"]): row for row in full_bundle.rows("rework-schedule-slots") } inspections = { str(row["inspectionId"]): row for row in full_bundle.rows("quality-inspections") } rework_orders = { str(row["reworkOrderId"]): row for row in full_bundle.rows("rework-orders") } nonconformities = full_bundle.rows("nonconformities") assert len(nonconformities) == len(rework_orders) == len(rework_slots) == 50 assert len(rework_operations) == 50 assert any( row.get("holdPoint") and "HOLD" in row["stateHistory"] and "RELEASED" in row["stateHistory"] for row in inspections.values() ) for nonconformity in nonconformities: assert nonconformity["status"] == "CLOSED" initial = inspections[nonconformity["inspectionId"]] reinspection = inspections[nonconformity["reinspectionId"]] rework = rework_orders[nonconformity["reworkOrderId"]] rework_slot = rework_slots[rework["reworkScheduleSlotId"]] assert initial["nonconformityId"] == nonconformity["nonconformityId"] assert initial["reworkOrderId"] == rework["reworkOrderId"] assert reinspection["inspectionType"] == "REINSPECTION" assert reinspection["status"] == "CLOSED" assert {"REINSPECTION", "PASSED", "CLOSED"} <= set( reinspection["stateHistory"] ) assert rework["status"] == "CLOSED" assert rework["sourceOperationId"] in operations assert rework["reworkOperationId"] in rework_operations assert rework["resourceId"] in resources assert rework["teamId"] in teams assert rework["sourceScheduleSlotId"] in baseline_slots assert rework["reworkScheduleSlotId"] not in baseline_slots assert rework_slot["resourceId"] == rework["resourceId"] assert rework_slot["teamId"] == rework["teamId"] assert rework_slot["finiteCapacityReserved"] is True assert math.isclose( float(rework["plannedHours"]), float(rework_slot["durationHours"]), abs_tol=0.01, ) assert rework["reinspectionRequired"] is True assert "REINSPECTION_PASSED" in rework["stateHistory"] timeline = [ initial["heldAt"], initial["inspectedAt"], nonconformity["openedAt"], rework_slot["start"], rework_slot["end"], reinspection["inspectedAt"], reinspection["releasedAt"], nonconformity["closedAt"], ] parsed = [datetime.fromisoformat(value) for value in timeline] assert all(left <= right for left, right in pairwise(parsed)) baseline_intervals: defaultdict[tuple[str, str], list[tuple[datetime, datetime]]] = defaultdict(list) for slot in baseline_slots.values(): interval = (datetime.fromisoformat(slot["start"]), datetime.fromisoformat(slot["end"])) baseline_intervals[("resource", str(slot["resourceId"]))].append(interval) baseline_intervals[("team", str(slot["teamId"]))].append(interval) for slot in rework_slots.values(): start, end = datetime.fromisoformat(slot["start"]), datetime.fromisoformat(slot["end"]) for owner in (("resource", str(slot["resourceId"])), ("team", str(slot["teamId"]))): assert all(end <= left or right <= start for left, right in baseline_intervals[owner]) def test_incremental_export_skips_unchanged_and_tombstones_removed_file( tmp_path: Path, small_config: GeneratorConfig, small_bundle: DatasetBundle, ) -> None: output = tmp_path / "dataset" first_manifest = export_bundle(small_bundle, small_config, output) obsolete = output / "obsolete-synthetic-artifact.txt" obsolete.write_text("obsolete", encoding="utf-8") manifest_path = output / "manifest.json" persisted = json.loads(manifest_path.read_text(encoding="utf-8")) persisted["files"].append( { "path": obsolete.name, "kind": "text", "bytes": obsolete.stat().st_size, "rowCount": None, "sha256": _sha256(obsolete), } ) persisted["fileCount"] += 1 manifest_path.write_text( json.dumps(persisted, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8", ) second_manifest = export_bundle( small_bundle, small_config, output, incremental=True ) assert second_manifest["businessDigest"] == first_manifest["businessDigest"] assert second_manifest["incremental"]["enabled"] is True assert second_manifest["incremental"]["skippedFiles"] assert obsolete.name in second_manifest["incremental"]["tombstones"] assert not obsolete.exists() def test_isolated_worldstore_loads_consumable_flex_projection_without_runtime_pollution( tmp_path: Path, monkeypatch: pytest.MonkeyPatch, deterministic_small_exports: tuple[Path, Path, dict[str, Any], dict[str, Any]], preserve_real_runtime_data: dict[str, str], ) -> None: first, _, _, _ = deterministic_small_exports isolated_world = tmp_path / "world.json" shutil.copy2(first / "world.json", isolated_world) source_hash = _sha256(first / "world.json") monkeypatch.setenv("APS_DB_DISABLED", "1") from server.aps_domain.flex import flex_gantt_view, flex_overview from server.aps_domain.mrp import list_mrp from server.aps_domain.sourcing import infer_material_sourcing from server.state.store import WorldStore store = WorldStore( path=str(isolated_world), world_key="default", tenant_uuid="synthetic", ) world = store.data for key in ( "flexMaterials", "flexBom", "flexOrders", "flexRoutings", "flexEquipment", "flexScheduleVersions", "flexWorkOrders", ): assert world.get(key), key overview = flex_overview(world) assert overview["materials"] assert overview["orders"] assert overview["equipment"] assert overview["latestVersion"] assert overview["workOrders"] gantt = flex_gantt_view(world) assert gantt["versionNo"] assert gantt["equipment"] assert gantt["workOrders"] assert all( { "productCode", "quantity", "operationName", "seq", "equipmentCode", "equipmentName", "start", "end", } <= set(row) for row in gantt["workOrders"] ) mrp_projection = list_mrp(world) assert mrp_projection["make"] assert set(mrp_projection) == { "make", "purchaseOrders", "outsourceOrders", "productionOrders", } first_material = world["flexMaterials"][0] assert infer_material_sourcing(first_material) == first_material["sourcingMode"] material_ids = {str(row["id"]) for row in world["flexMaterials"]} order_nos = {str(row["orderNo"]) for row in world["flexOrders"]} version_ids = {str(row["id"]) for row in world["flexScheduleVersions"]} assert all(str(row["childCode"]) in material_ids for row in world["flexBom"]) assert all(str(row["flexOrderNo"]) in order_nos for row in world["flexWorkOrders"]) assert all(str(row["versionId"]) in version_ids for row in world["flexWorkOrders"]) assert _sha256(first / "world.json") == source_hash assert _existing_hashes(REAL_DATA_FILES) == preserve_real_runtime_data def test_synthetic_disclaimer_and_naming_boundaries(full_bundle: DatasetBundle) -> None: assert full_bundle.metadata["datasetType"] == "SYNTHETIC" assert full_bundle.metadata["organizationScenario"] == "BEIHAI_SHIPYARD_APS" assert full_bundle.metadata["timezone"] == "Asia/Shanghai" assert full_bundle.metadata["generatedFor"] == "APS development and validation" assert full_bundle.metadata["dataDisclaimer"] == DATA_DISCLAIMER assert full_bundle.metadata["planningBaseDate"] == "2026-09-01" assert full_bundle.metadata["planningHorizonEnd"] == "2027-12-31" assert all( str(row["projectCode"]).startswith("BH-SYN-") for row in full_bundle.rows("ship-projects") ) assert all( str(row["shipOwner"]).startswith("船东-SYN-") for row in full_bundle.rows("ship-projects") ) assert all( str(row["supplierName"]).startswith("供应商-SYN-") for row in full_bundle.rows("suppliers") ) assert all("模拟" in str(row["name"]) for row in full_bundle.rows("employees")) assert all( row.get("datasetType") == "SYNTHETIC" for row in full_bundle.rows("ship-projects") )