Procházet zdrojové kódy

增加了方案大于价格的逻辑

dongwu před 4 dny
rodič
revize
ebbf3f87a6

+ 10 - 0
.idea/.gitignore

@@ -0,0 +1,10 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Ignored default folder with query files
+/queries/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/

+ 14 - 0
.idea/Eric-ontology.iml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="PYTHON_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.venv" />
+    </content>
+    <orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+  <component name="PyDocumentationSettings">
+    <option name="format" value="PLAIN" />
+    <option name="myDocStringFormat" value="Plain" />
+  </component>
+</module>

+ 20 - 0
.idea/inspectionProfiles/Project_Default.xml

@@ -0,0 +1,20 @@
+<component name="InspectionProjectProfileManager">
+  <profile version="1.0">
+    <option name="myName" value="Project Default" />
+    <inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
+    <inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
+      <option name="ignoredErrors">
+        <list>
+          <option value="N803" />
+        </list>
+      </option>
+    </inspection_tool>
+    <inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
+      <option name="ignoredIdentifiers">
+        <list>
+          <option value="yagmail" />
+        </list>
+      </option>
+    </inspection_tool>
+  </profile>
+</component>

+ 6 - 0
.idea/inspectionProfiles/profiles_settings.xml

@@ -0,0 +1,6 @@
+<component name="InspectionProjectProfileManager">
+  <settings>
+    <option name="USE_PROJECT_PROFILE" value="false" />
+    <version value="1.0" />
+  </settings>
+</component>

+ 4 - 0
.idea/misc.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/Eric-ontology.iml" filepath="$PROJECT_DIR$/.idea/Eric-ontology.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 8 - 3
README.md

@@ -23,6 +23,7 @@ docs/
 - `ontology/ai-market-demo-extension.ttl`:询价约束、候选评估与推荐审计扩展
 - `data/qingdao-export-sample.ttl`:青岛出口样例数据
 - `data/ai-market-demo.ttl`:同路线多产品、多供应商和有效/失效报价场景
+- `data/ai-market-india-mock.ttl`:印度那瓦舍瓦港、金奈港的本地方案优先样本,含船公司直营与订舱代理合作关系画像
 - `data/ai-market-scenarios.ttl`:汉堡海运、法兰克福空运、危险品和新增客户画像
 - `queries/order-status.rq`:查询订单当前状态
 - `queries/risk-orders.rq`:查询有风险的订单
@@ -43,6 +44,7 @@ docs/
 - `queries/supplier-share-governance.rq`:查询预留的平台供应商份额治理策略及启用状态
 - `docs/customer-supplier-profile-mapping.md`:客户画像与平台供应商画像对应关系
 - `docs/product-supplier-market-model.md`:产品、供应商、组合方案和AI市场推荐价格模型
+- `docs/ai-market-solution-priority-change-log.md`:方案优先改造的字段、排序口径、页面输出与验证记录
 - `app/main.py`:简单接口服务
 - `app/market/`:过滤、评分、定价和解释型推荐引擎
 - `app/mock_data.py`:本地数据目录、动态 Mock 生成和内存重置
@@ -93,13 +95,16 @@ http://localhost:8000
 
 ```text
 硬条件过滤过程
-经济、均衡和时效候选方案
-客户、价格、时效、供应商和产品策略评分
+方案优选、关系优选与价格参考候选方案
+方案保障、合作关系、现舱、履约、客户适配、时效与价格七项评分
+与具体供应商的合作关系、低价获取概率、优先舱位概率、现舱状态和可订 TEU
 AI市场推荐价格、客户调整和最终报价
 淘汰原因与决策审计
 ```
 
-顶部可以切换到 Mock 数据台和原有履约操作台。Mock 数据台可以查看客户、路线、产品、供应商、报价和策略,也可以向当前进程临时添加供应商报价和客户画像;动态数据不会写入 TTL,服务重启或点击“重置内存数据”后消失。
+当前排序以可执行方案为首要目标:方案保障 27%、合作关系优势 23%、现舱保障 15%、供应履约 18%、客户适配 10%、时效 4%、价格 3%。价格仍参与利润校验和候选比较,但不会单独主导推荐。印度那瓦舍瓦港和金奈港样本用于直观验证该口径:战略合作直营且有现舱的方案可以高于最低采购价方案排序。
+
+顶部可以切换到 Mock 数据台和原有履约操作台。Mock 数据台可以查看客户、路线、产品、供应商、报价和策略,也可以向当前进程临时添加印度目的港的供应商报价和客户画像。单次可生成 1–8 家供应商、0–5 个客户;动态数据不会写入 TTL,服务重启或点击“重置内存数据”后消失。
 
 Fuseki 和 `docker-compose.yml` 继续保留,仅用于后续远程数据服务实验,不是本地 Demo 的运行前置条件。
 

binární
app/__pycache__/main.cpython-312.pyc


binární
app/__pycache__/mock_data.cpython-312.pyc


binární
app/__pycache__/repository.cpython-312.pyc


binární
app/market/__pycache__/__init__.cpython-312.pyc


binární
app/market/__pycache__/filters.cpython-312.pyc


binární
app/market/__pycache__/models.cpython-312.pyc


binární
app/market/__pycache__/pricing.cpython-312.pyc


binární
app/market/__pycache__/scoring.cpython-312.pyc


binární
app/market/__pycache__/service.cpython-312.pyc


+ 9 - 0
app/market/models.py

@@ -57,3 +57,12 @@ class CandidateRecord(BaseModel):
     agency_level: str = ""
     offering_channel: str = ""
     capacity_commitment: str = ""
+    spot_space_status: str = "需确认"
+    spot_space_teu: float = 0
+    solution_assurance: float = 0.72
+    service_support_score: float = 0.72
+    relationship_tier: str = "基础合作"
+    relationship_strength: float = 0.60
+    low_price_access_probability: float = 0.50
+    priority_space_probability: float = 0.60
+    relationship_evidence: str = "按标准合作机制执行"

+ 68 - 44
app/market/scoring.py

@@ -5,6 +5,24 @@ from dataclasses import dataclass
 from app.market.models import CandidateRecord, InquiryRequest
 
 
+DECISION_WEIGHTS = {
+    "solution_assurance": 0.27,
+    "relationship_advantage": 0.23,
+    "spot_space": 0.15,
+    "supplier_execution": 0.18,
+    "customer_fit": 0.10,
+    "transit": 0.04,
+    "price": 0.03,
+}
+
+SPOT_SPACE_STATUS_SCORES = {
+    "可立即订舱": 100.0,
+    "现舱有限": 70.0,
+    "需确认": 35.0,
+    "无现舱": 0.0,
+}
+
+
 @dataclass(frozen=True)
 class CustomerSignals:
     price_sensitivity: float
@@ -27,22 +45,12 @@ def score_candidates(
     min_cost, max_cost = min(costs), max(costs)
     min_transit = min(candidate.transit_days for candidate in candidates)
 
-    raw_weights = {
-        "price": 1 + customer.price_sensitivity,
-        "transit": 0.8 + customer.time_sensitivity,
-        "supplier": 1 + customer.reliability_sensitivity,
-        "customer": 0.9,
-        "strategy": 0.5,
-    }
-    weight_total = sum(raw_weights.values())
-    weights = {name: value / weight_total for name, value in raw_weights.items()}
-
     scored: list[dict] = []
     for candidate in candidates:
         if max_cost == min_cost:
             price_score = 100.0
         else:
-            price_score = 100 - ((candidate.supplier_cost - min_cost) / (max_cost - min_cost) * 35)
+            price_score = 100 - ((candidate.supplier_cost - min_cost) / (max_cost - min_cost) * 28)
 
         transit_penalty = 12 if candidate.transport_mode == "空运" else 5
         transit_score = max(
@@ -57,25 +65,38 @@ def score_candidates(
         ) / 2
         supplier_score = (offering_reliability * 0.7 + product_reliability * 0.3) * 100
 
-        is_brand_unit = "品牌本地分支机构" in candidate.agency_level
-        customer_score = 94.0 if is_brand_unit else 83.0
+        route_assurance = 100.0 if candidate.route_type in {"直航", "直达"} else 72.0
+        solution_score = (
+            candidate.solution_assurance * 100 * 0.55
+            + route_assurance * 0.25
+            + candidate.service_support_score * 100 * 0.20
+        )
+        relationship_score = (
+            candidate.relationship_strength * 0.55
+            + candidate.low_price_access_probability * 0.25
+            + candidate.priority_space_probability * 0.20
+        ) * 100
+        spot_space_score = (
+            SPOT_SPACE_STATUS_SCORES.get(candidate.spot_space_status, 35.0) * 0.85
+            + min(candidate.spot_space_teu / 16, 1.0) * 100 * 0.15
+        )
+
+        customer_score = 55.0
         if candidate.route_type == inquiry.preferred_route_type:
-            customer_score += 4
-            if customer.time_sensitivity >= 0.85:
-                customer_score += 8
-        if customer.price_sensitivity >= 0.7 and not is_brand_unit:
-            customer_score += 7
-        if customer.reliability_sensitivity >= 0.85 and is_brand_unit:
-            customer_score += 2
+            customer_score += 15
+        customer_score += candidate.service_support_score * customer.reliability_sensitivity * 15
+        customer_score += (transit_score / 100) * customer.time_sensitivity * 10
+        customer_score += (price_score / 100) * customer.price_sensitivity * 5
         customer_score = min(100.0, customer_score)
 
-        strategy_score = 92.0 if candidate.route_type == "直航" else 72.0
         total_score = (
-            price_score * weights["price"]
-            + transit_score * weights["transit"]
-            + supplier_score * weights["supplier"]
-            + customer_score * weights["customer"]
-            + strategy_score * weights["strategy"]
+            solution_score * DECISION_WEIGHTS["solution_assurance"]
+            + relationship_score * DECISION_WEIGHTS["relationship_advantage"]
+            + spot_space_score * DECISION_WEIGHTS["spot_space"]
+            + supplier_score * DECISION_WEIGHTS["supplier_execution"]
+            + customer_score * DECISION_WEIGHTS["customer_fit"]
+            + transit_score * DECISION_WEIGHTS["transit"]
+            + price_score * DECISION_WEIGHTS["price"]
         )
 
         scored.append({
@@ -84,7 +105,9 @@ def score_candidates(
             "transit_score": round(transit_score, 1),
             "supplier_execution_score": round(supplier_score, 1),
             "customer_fit_score": round(customer_score, 1),
-            "product_strategy_score": round(strategy_score, 1),
+            "solution_assurance_score": round(solution_score, 1),
+            "relationship_advantage_score": round(relationship_score, 1),
+            "spot_space_score": round(spot_space_score, 1),
             "total_score": round(total_score, 1),
         })
 
@@ -92,24 +115,25 @@ def score_candidates(
     scored.sort(key=lambda item: item["total_score"], reverse=True)
     for rank, item in enumerate(scored, start=1):
         item["rank"] = rank
-    return scored, {name: round(value, 4) for name, value in weights.items()}
+    return scored, DECISION_WEIGHTS.copy()
 
 
 def assign_solution_types(scored: list[dict]) -> None:
-    economy = min(scored, key=lambda item: item["candidate"].supplier_cost)
-    fastest = min(
-        (item for item in scored if item is not economy),
-        key=lambda item: item["candidate"].transit_days,
-        default=economy,
-    )
-    balanced = max(
-        (item for item in scored if item not in (economy, fastest)),
-        key=lambda item: item["total_score"],
-        default=max(scored, key=lambda item: item["total_score"]),
-    )
-
     for item in scored:
-        item["solution_type"] = "候选方案"
-    economy["solution_type"] = "经济方案"
-    fastest["solution_type"] = "时效方案"
-    balanced["solution_type"] = "均衡方案"
+        item["solution_type"] = "可执行方案"
+
+    primary = max(scored, key=lambda item: item["total_score"])
+    primary["solution_type"] = "方案优选"
+    remaining = [item for item in scored if item is not primary]
+
+    if remaining:
+        relationship = max(remaining, key=lambda item: item["relationship_advantage_score"])
+        relationship["solution_type"] = "关系优选"
+        remaining = [item for item in remaining if item is not relationship]
+    if remaining:
+        price = min(remaining, key=lambda item: item["candidate"].supplier_cost)
+        price["solution_type"] = "价格参考"
+        remaining = [item for item in remaining if item is not price]
+    if remaining:
+        fastest = min(remaining, key=lambda item: item["candidate"].transit_days)
+        fastest["solution_type"] = "时效参考"

+ 117 - 8
app/market/service.py

@@ -225,6 +225,14 @@ class MarketService:
         run_uri = RUN[run_id]
         with self._lock:
             triples = list(self.audit_graph.triples((run_uri, None, None)))
+            evaluations = [
+                evaluation
+                for _, _, evaluation in self.audit_graph.triples(
+                    (run_uri, GESLI.hasCandidateEvaluation, None)
+                )
+            ]
+            for evaluation in evaluations:
+                triples.extend(self.audit_graph.triples((evaluation, None, None)))
         return {
             "run_id": run_id,
             "generated_at": run["generated_at"],
@@ -235,6 +243,7 @@ class MarketService:
             "rejected_count": len(run["rejected_candidates"]),
             "rdf_assertions": len(triples),
             "sources": self.repository.loaded_file_names,
+            "business_explanation": self._business_audit(run),
         }
 
     def _candidate_records(self) -> list[CandidateRecord]:
@@ -282,6 +291,21 @@ class MarketService:
                 agency_level=row.get("agencyLevel", ""),
                 offering_channel=row.get("offeringChannel", ""),
                 capacity_commitment=row.get("capacityCommitment", ""),
+                spot_space_status=row.get("spotSpaceStatus") or "需确认",
+                spot_space_teu=_as_float(row.get("spotSpaceTeu")),
+                solution_assurance=_as_float(row.get("solutionAssuranceScore"), 0.72),
+                service_support_score=_as_float(row.get("serviceSupportScore"), 0.72),
+                relationship_tier=row.get("relationshipTier") or "基础合作",
+                relationship_strength=_as_float(row.get("relationshipStrength"), 0.60),
+                low_price_access_probability=_as_float(
+                    row.get("lowPriceAccessProbability"), 0.50
+                ),
+                priority_space_probability=_as_float(
+                    row.get("prioritySpaceProbability"), 0.60
+                ),
+                relationship_evidence=(
+                    row.get("relationshipEvidence") or "按标准合作机制执行"
+                ),
             )
             for row in rows
         ]
@@ -382,16 +406,20 @@ class MarketService:
             2,
         )
         reasons = [
+            f"我方与{candidate.supplier_name}为{candidate.relationship_tier}:{candidate.relationship_evidence}",
+            _spot_space_summary(candidate.spot_space_status, candidate.spot_space_teu),
+            f"低价获取概率{candidate.low_price_access_probability:.0%},优先舱位概率{candidate.priority_space_probability:.0%}",
             f"供应商订舱与放舱综合成功率{reliability:.0%}",
             f"预计运输时间{candidate.transit_days:g}天",
             f"{candidate.route_type or '当前'}路由,{candidate.capacity_commitment or '运力已确认'}",
         ]
-        if scores["solution_type"] == "经济方案":
-            reasons.insert(0, "当前可执行候选中采购价格最有竞争力")
-        elif scores["solution_type"] == "时效方案":
-            reasons.insert(0, "当前可执行候选中预计运输时间最短")
-        else:
-            reasons.insert(0, "客户偏好、供应商可靠性与价格综合表现最均衡")
+        focus_reasons = {
+            "方案优选": "方案保障、供应商合作与履约表现综合最优",
+            "关系优选": "船公司或订舱代理的合作优势最突出",
+            "价格参考": "当前可执行候选中采购价格最具参考性",
+            "时效参考": "当前可执行候选中预计运输时间最短",
+        }
+        reasons.insert(0, focus_reasons.get(scores["solution_type"], "可执行的备选运输方案"))
 
         return {
             "id": candidate.quote_uri,
@@ -414,9 +442,27 @@ class MarketService:
             "transit_score": scores["transit_score"],
             "supplier_execution_score": scores["supplier_execution_score"],
             "customer_fit_score": scores["customer_fit_score"],
-            "product_strategy_score": scores["product_strategy_score"],
+            "solution_assurance_score": scores["solution_assurance_score"],
+            "relationship_advantage_score": scores["relationship_advantage_score"],
+            "spot_space_score": scores["spot_space_score"],
             "total_score": scores["total_score"],
             "confidence_score": None,
+            "relationship": {
+                "supplier_name": candidate.supplier_name,
+                "supplier_channel": candidate.offering_channel,
+                "tier": candidate.relationship_tier,
+                "strength": candidate.relationship_strength,
+                "low_price_access_probability": candidate.low_price_access_probability,
+                "priority_space_probability": candidate.priority_space_probability,
+                "evidence": candidate.relationship_evidence,
+            },
+            "spot_space": {
+                "status": candidate.spot_space_status,
+                "teu": candidate.spot_space_teu,
+                "summary": _spot_space_summary(
+                    candidate.spot_space_status, candidate.spot_space_teu
+                ),
+            },
             "reasons": reasons,
             "pricing": prices,
         }
@@ -440,9 +486,50 @@ class MarketService:
             f"为{customer_row['customerName']}推荐{candidate['solution_type']}:"
             f"{candidate['supplier_name']}的{candidate['product_name']}。"
             f"综合得分{candidate['total_score']:.1f},采用“{strategy['name']}”,"
-            f"在时效、履约可靠性和客户价格接受度之间取得当前最优平衡。"
+            f"当前{candidate['spot_space']['summary']},"
+            f"在方案保障、供应商合作、现舱与履约可靠性之间取得当前最优平衡。"
         )
 
+    @staticmethod
+    def _business_audit(run: dict) -> dict:
+        top = run["candidates"][0]
+        alternatives = []
+        for candidate in run["candidates"][1:]:
+            alternatives.append({
+                "title": f"{candidate['solution_type']}:{candidate['supplier_name']}",
+                "detail": (
+                    f"{candidate['spot_space']['summary']};"
+                    f"我方与该供应商为{candidate['relationship']['tier']};"
+                    f"预计{candidate['transit_days']:g}天到达。"
+                ),
+            })
+        rejected = [
+            f"{candidate['supplier_name']}:{';'.join(candidate['reasons'])}"
+            for candidate in run["rejected_candidates"][:3]
+        ]
+        return {
+            "decision": (
+                f"本次为{run['normalized_inquiry']['customer_name']}的"
+                f"{run['normalized_inquiry']['route']}询价,在"
+                f"{len(run['candidates'])}个可执行方案中选择了{top['supplier_name']}。"
+            ),
+            "why_recommended": [
+                f"{top['spot_space']['summary']},可直接支持本次订舱安排。",
+                (
+                    f"我方与{top['supplier_name']}为{top['relationship']['tier']},"
+                    f"{top['relationship']['evidence']}。"
+                ),
+                (
+                    f"方案为{top['route_type']},预计{top['transit_days']:g}天,"
+                    f"方案保障得分{top['solution_assurance_score']}分。"
+                ),
+                f"建议向客户报价{top['pricing']['final_quote_amount']:,.0f} {top['pricing']['currency']}。",
+            ],
+            "alternatives": alternatives,
+            "rejected": rejected,
+            "selection_basis": "优先看方案保障、与船公司或订舱代理的合作深度和现舱;再比较履约、客户需求、时效与价格。价格用于利润校验和横向比较,但不单独决定首选。",
+        }
+
     def _store_run(
         self, run_id: str, result: dict, inquiry: InquiryRequest, strategy: dict
     ) -> None:
@@ -463,6 +550,21 @@ class MarketService:
                 self.audit_graph.add((evaluation_uri, GESLI.evaluatedQuote, URIRef(candidate["id"])))
                 self.audit_graph.add((evaluation_uri, GESLI.recommendationRank, Literal(candidate["rank"])))
                 self.audit_graph.add((evaluation_uri, GESLI.totalScore, Literal(candidate["total_score"])))
+                self.audit_graph.add((
+                    evaluation_uri,
+                    GESLI.solutionAssuranceEvaluationScore,
+                    Literal(candidate["solution_assurance_score"]),
+                ))
+                self.audit_graph.add((
+                    evaluation_uri,
+                    GESLI.relationshipAdvantageScore,
+                    Literal(candidate["relationship_advantage_score"]),
+                ))
+                self.audit_graph.add((
+                    evaluation_uri,
+                    GESLI.spotSpaceScore,
+                    Literal(candidate["spot_space_score"]),
+                ))
             top_evaluation = RUN[f"{run_id}/candidate/1"]
             self.audit_graph.add((run_uri, GESLI.recommendedEvaluation, top_evaluation))
 
@@ -474,6 +576,13 @@ def _as_float(value: str | None, default: float = 0.0) -> float:
         return default
 
 
+def _spot_space_summary(status: str, teu: float) -> str:
+    status_label = status if status.startswith("现舱") else f"现舱{status}"
+    if teu > 0:
+        return f"{status_label},当前可订{teu:g} TEU"
+    return f"{status_label},需向供应商确认"
+
+
 def _as_int(value: str | None, default: int = 0) -> int:
     try:
         return int(value) if value not in (None, "") else default

+ 163 - 19
app/mock_data.py

@@ -18,8 +18,8 @@ MOCK = Namespace("https://gesli.example/runtime-mock/")
 class MockBatchRequest(BaseModel):
     origin: str
     destination: str
-    supplier_count: int = Field(default=2, ge=1, le=5)
-    customer_count: int = Field(default=1, ge=0, le=3)
+    supplier_count: int = Field(default=4, ge=1, le=8)
+    customer_count: int = Field(default=2, ge=0, le=5)
     evaluation_date: date = date(2026, 7, 20)
 
 
@@ -103,6 +103,19 @@ class LocalMockDataService:
                 "valid_to": valid_to,
                 "status": status,
                 "dangerous_goods": row.get("supportsDangerousGoods", "false") == "true",
+                "relationship_tier": row.get("relationshipTier") or "基础合作",
+                "relationship_strength": _as_float(row.get("relationshipStrength"), 0.60),
+                "low_price_access_probability": _as_float(
+                    row.get("lowPriceAccessProbability"), 0.50
+                ),
+                "priority_space_probability": _as_float(
+                    row.get("prioritySpaceProbability"), 0.60
+                ),
+                "relationship_evidence": (
+                    row.get("relationshipEvidence") or "按标准合作机制执行"
+                ),
+                "spot_space_status": row.get("spotSpaceStatus") or "需确认",
+                "spot_space_teu": _as_float(row.get("spotSpaceTeu")),
                 "runtime_mock": row.get("quote", "").startswith(str(MOCK)),
             })
         quote_rows.sort(
@@ -172,31 +185,32 @@ class LocalMockDataService:
             schedule = MOCK[f"schedule-{suffix}"]
             quote = MOCK[f"quote-{suffix}"]
             performance = MOCK[f"performance-{suffix}"]
-            supplier_name = f"内存模拟供应商 {token[-3:].upper()}{index + 1}"
+            profile = self._supplier_profile(index)
+            supplier_name = (
+                f"印度航线Mock{profile['supplier_label']} {token[-3:].upper()}{index + 1}"
+            )
             product_name = (
-                f"{request.origin}-{request.destination}动态Mock方案{index + 1}"
+                f"{request.origin}-{request.destination}{profile['product_label']}方案{index + 1}"
             )
             departure = request.evaluation_date + timedelta(days=8 + index)
             transit_days = self._transit_days(transport_mode, index)
             arrival = departure + timedelta(days=transit_days)
-            cost = round(benchmark * (0.76 + index * 0.025), -2)
-            success_rate = min(0.98, 0.87 + index * 0.035)
+            cost = round(benchmark * profile["cost_ratio"], -2)
+            success_rate = profile["booking_success_rate"]
             product_class = (
                 GESLI.AirFreightProduct
                 if transport_mode == "空运"
                 else GESLI.ContainerOceanProduct
             )
-            route_type = "直达" if transport_mode == "空运" and index == 0 else (
-                "直航" if index % 2 == 0 else "中转"
-            )
+            route_type = "直达" if transport_mode == "空运" and index < 2 else profile["route_type"]
 
             triples.extend([
                 (brand, RDF.type, GESLI.ServiceBrand),
                 (brand, GESLI.brandCode, Literal(f"MOCK-{token}-{index + 1}")),
                 (brand, GESLI.brandName, Literal(f"Mock品牌 {index + 1}")),
-                (supplier, RDF.type, GESLI.FreightForwarderSupplier),
+                (supplier, RDF.type, profile["supplier_class"]),
                 (supplier, RDFS.label, Literal(supplier_name, lang="zh")),
-                (supplier, GESLI.platformSupplierType, Literal("内存Mock供应商")),
+                (supplier, GESLI.platformSupplierType, Literal(profile["supplier_type"])),
                 (supplier, GESLI.isActivePartner, Literal(True)),
                 (product, RDF.type, product_class),
                 (product, GESLI.productCode, Literal(f"MOCK-PRODUCT-{suffix}")),
@@ -204,7 +218,7 @@ class LocalMockDataService:
                 (product, GESLI.hasServiceBrand, brand),
                 (product, GESLI.hasProductPerformanceProfile, performance),
                 (product, GESLI.transportMode, Literal(transport_mode)),
-                (product, GESLI.serviceForm, Literal("Mock标准服务")),
+                (product, GESLI.serviceForm, Literal(profile["service_form"])),
                 (product, GESLI.routeType, Literal(route_type)),
                 (product, GESLI.originLocation, Literal(request.origin)),
                 (product, GESLI.destinationLocation, Literal(request.destination)),
@@ -216,13 +230,22 @@ class LocalMockDataService:
                 (offering, GESLI.offeringCode, Literal(f"MOCK-OFFER-{suffix}")),
                 (offering, GESLI.offeredBySupplier, supplier),
                 (offering, GESLI.offeringProduct, product),
-                (offering, GESLI.offeringChannel, Literal("本地动态Mock")),
-                (offering, GESLI.agencyLevel, Literal("一级模拟代理")),
-                (offering, GESLI.capacityCommitment, Literal(f"模拟可用运力{10 + index * 5}单位")),
+                (offering, GESLI.offeringChannel, Literal(profile["offering_channel"])),
+                (offering, GESLI.agencyLevel, Literal(profile["agency_level"])),
+                (offering, GESLI.capacityCommitment, Literal(profile["capacity_commitment"])),
                 (offering, GESLI.capacityAvailable, Literal(True)),
+                (offering, GESLI.spotSpaceStatus, Literal(profile["spot_space_status"])),
+                (offering, GESLI.spotSpaceTeu, Literal(profile["spot_space_teu"], datatype=XSD.decimal)),
                 (offering, GESLI.supportsDangerousGoods, Literal(index == 0)),
+                (offering, GESLI.solutionAssuranceScore, Literal(profile["solution_assurance"], datatype=XSD.decimal)),
+                (offering, GESLI.serviceSupportScore, Literal(profile["service_support_score"], datatype=XSD.decimal)),
+                (offering, GESLI.relationshipTier, Literal(profile["relationship_tier"])),
+                (offering, GESLI.relationshipStrength, Literal(profile["relationship_strength"], datatype=XSD.decimal)),
+                (offering, GESLI.lowPriceAccessProbability, Literal(profile["low_price_access_probability"], datatype=XSD.decimal)),
+                (offering, GESLI.prioritySpaceProbability, Literal(profile["priority_space_probability"], datatype=XSD.decimal)),
+                (offering, GESLI.relationshipEvidence, Literal(profile["relationship_evidence"])),
                 (offering, GESLI.bookingSuccessRate, Literal(success_rate, datatype=XSD.decimal)),
-                (offering, GESLI.spaceReleaseSuccessRate, Literal(success_rate - 0.01, datatype=XSD.decimal)),
+                (offering, GESLI.spaceReleaseSuccessRate, Literal(profile["space_release_success_rate"], datatype=XSD.decimal)),
                 (schedule, RDF.type, GESLI.ScheduledServiceInstance),
                 (schedule, GESLI.scheduleCode, Literal(f"MOCK-SCHEDULE-{suffix}")),
                 (schedule, GESLI.scheduleOfProduct, product),
@@ -247,14 +270,14 @@ class LocalMockDataService:
             customer = MOCK[f"customer-{suffix}"]
             trade = MOCK[f"customer-trade-{suffix}"]
             credit = MOCK[f"customer-credit-{suffix}"]
-            customer_name = f"内存模拟客户 {token[-3:].upper()}{index + 1}"
+            customer_name = f"印度目的港模拟客户 {token[-3:].upper()}{index + 1}"
             triples.extend([
                 (customer, RDF.type, GESLI.Customer),
                 (customer, RDFS.label, Literal(customer_name, lang="zh")),
                 (customer, GESLI.hasTradeProfile, trade),
                 (customer, GESLI.hasCreditProfile, credit),
                 (trade, RDF.type, GESLI.CustomerTradeProfile),
-                (trade, GESLI.industry, Literal("动态Mock行业")),
+                (trade, GESLI.industry, Literal("印度市场Mock行业")),
                 (trade, GESLI.routeName, Literal(f"{request.origin}-{request.destination}")),
                 (trade, GESLI.repeatPurchaseCount, Literal(4 + index * 6)),
                 (trade, GESLI.quoteWinRate, Literal(0.5 + index * 0.08, datatype=XSD.decimal)),
@@ -297,7 +320,128 @@ class LocalMockDataService:
 
     @staticmethod
     def _transit_days(transport_mode: str, index: int) -> int:
-        return (3 + index) if transport_mode == "空运" else (14 + index * 2)
+        return (3 + index) if transport_mode == "空运" else (16 + index * 2)
+
+    @staticmethod
+    def _supplier_profile(index: int) -> dict:
+        profiles = [
+            {
+                "supplier_label": "船公司直营",
+                "supplier_class": GESLI.Carrier,
+                "supplier_type": "船公司",
+                "product_label": "优先直航",
+                "route_type": "直航",
+                "service_form": "优先舱位包柜",
+                "offering_channel": "船公司直营",
+                "agency_level": "产品品牌本地分支机构",
+                "capacity_commitment": "年度协议优先保舱20TEU",
+                "spot_space_status": "可立即订舱",
+                "spot_space_teu": 18,
+                "relationship_tier": "战略合作",
+                "relationship_strength": 0.96,
+                "low_price_access_probability": 0.90,
+                "priority_space_probability": 0.95,
+                "relationship_evidence": "年度协议、价格窗口直连与优先放舱额度",
+                "solution_assurance": 0.98,
+                "service_support_score": 0.94,
+                "booking_success_rate": 0.97,
+                "space_release_success_rate": 0.96,
+                "cost_ratio": 0.91,
+            },
+            {
+                "supplier_label": "一级订舱代理",
+                "supplier_class": GESLI.ShippingAgencyCompany,
+                "supplier_type": "订舱代理",
+                "product_label": "稳定直航",
+                "route_type": "直航",
+                "service_form": "稳定舱位包柜",
+                "offering_channel": "一级订舱代理",
+                "agency_level": "一级代理",
+                "capacity_commitment": "月度保舱12TEU",
+                "spot_space_status": "现舱有限",
+                "spot_space_teu": 8,
+                "relationship_tier": "核心合作",
+                "relationship_strength": 0.87,
+                "low_price_access_probability": 0.82,
+                "priority_space_probability": 0.86,
+                "relationship_evidence": "核心代理授权与固定订舱窗口",
+                "solution_assurance": 0.90,
+                "service_support_score": 0.90,
+                "booking_success_rate": 0.94,
+                "space_release_success_rate": 0.92,
+                "cost_ratio": 0.86,
+            },
+            {
+                "supplier_label": "稳定合作代理",
+                "supplier_class": GESLI.FreightForwarderSupplier,
+                "supplier_type": "货代公司",
+                "product_label": "常规直航",
+                "route_type": "直航",
+                "service_form": "标准包柜",
+                "offering_channel": "长期合作货代",
+                "agency_level": "稳定合作代理",
+                "capacity_commitment": "周度滚动确认8TEU",
+                "spot_space_status": "现舱有限",
+                "spot_space_teu": 4,
+                "relationship_tier": "稳定合作",
+                "relationship_strength": 0.74,
+                "low_price_access_probability": 0.70,
+                "priority_space_probability": 0.72,
+                "relationship_evidence": "长期出货记录与周度保舱协同",
+                "solution_assurance": 0.83,
+                "service_support_score": 0.82,
+                "booking_success_rate": 0.90,
+                "space_release_success_rate": 0.88,
+                "cost_ratio": 0.79,
+            },
+            {
+                "supplier_label": "市场订舱代理",
+                "supplier_class": GESLI.ShippingAgencyCompany,
+                "supplier_type": "订舱代理",
+                "product_label": "中转经济",
+                "route_type": "中转",
+                "service_form": "经济包柜",
+                "offering_channel": "市场订舱代理",
+                "agency_level": "二级代理",
+                "capacity_commitment": "按单票确认舱位",
+                "spot_space_status": "需确认",
+                "spot_space_teu": 0,
+                "relationship_tier": "常规合作",
+                "relationship_strength": 0.52,
+                "low_price_access_probability": 0.46,
+                "priority_space_probability": 0.48,
+                "relationship_evidence": "按市场舱位和临时报价执行",
+                "solution_assurance": 0.67,
+                "service_support_score": 0.68,
+                "booking_success_rate": 0.84,
+                "space_release_success_rate": 0.82,
+                "cost_ratio": 0.72,
+            },
+            {
+                "supplier_label": "区域货代",
+                "supplier_class": GESLI.FreightForwarderSupplier,
+                "supplier_type": "货代公司",
+                "product_label": "加班船直航",
+                "route_type": "直航",
+                "service_form": "加班船包柜",
+                "offering_channel": "区域货代",
+                "agency_level": "合作货代",
+                "capacity_commitment": "加班船窗口优先确认6TEU",
+                "spot_space_status": "现舱有限",
+                "spot_space_teu": 3,
+                "relationship_tier": "稳定合作",
+                "relationship_strength": 0.69,
+                "low_price_access_probability": 0.63,
+                "priority_space_probability": 0.70,
+                "relationship_evidence": "区域市场协同与加班船资源预留",
+                "solution_assurance": 0.80,
+                "service_support_score": 0.79,
+                "booking_success_rate": 0.89,
+                "space_release_success_rate": 0.88,
+                "cost_ratio": 0.82,
+            },
+        ]
+        return profiles[index % len(profiles)]
 
 
 def _as_float(value: str | None, default: float = 0.0) -> float:

+ 58 - 28
app/static/app.js

@@ -143,11 +143,13 @@ function applyRouteDefaults(updateValues) {
 
 function populateMockRouteSelect() {
   if (!marketOptions) return;
-  const routes = marketOptions.routes.map((route, index) => ({
+  const allRoutes = marketOptions.routes.map((route, index) => ({
     id: String(index),
     name: `${route.origin} → ${route.destination}`,
   }));
-  populateSelect("mockRoute", routes, "id", "name", "0");
+  const indiaRoutes = allRoutes.filter(route => route.name.includes("印度"));
+  const routes = indiaRoutes.length ? indiaRoutes : allRoutes;
+  populateSelect("mockRoute", routes, "id", "name", routes[0]?.id || "");
 }
 
 function populateSelect(id, values, valueKey, labelKey, selectedValue) {
@@ -250,7 +252,9 @@ function renderRecommendation(result) {
       <div class="recommendation-tags">
         <span>${escapeHtml(top.route_type)}</span>
         <span>${escapeHtml(top.transit_days)} 天</span>
-        <span>可靠性 ${percent(top.reliability_rate)}</span>
+        <span>与 ${escapeHtml(top.supplier_name)} ${escapeHtml(top.relationship.tier)}</span>
+        <span>${escapeHtml(top.spot_space.summary)}</span>
+        <span>低价机会 ${percent(top.relationship.low_price_access_probability)}</span>
         <span>${escapeHtml(context.strategy_name)}</span>
       </div>
     </div>
@@ -266,11 +270,13 @@ function renderRecommendation(result) {
 }
 
 function renderCandidates(candidates) {
-  document.getElementById("candidateMeta").textContent = `${candidates.length} 套可执行方案 · 按综合得分排序`;
+  document.getElementById("candidateMeta").textContent = `${candidates.length} 套可执行方案 · 方案、供应商合作与现舱优先`;
   document.getElementById("candidateGrid").innerHTML = candidates.map((candidate, index) => {
-    const typeClass = candidate.solution_type === "经济方案"
-      ? "type-economy"
-      : candidate.solution_type === "时效方案" ? "type-speed" : "type-balanced";
+    const typeClass = candidate.solution_type === "关系优选"
+      ? "type-relationship"
+      : candidate.solution_type === "价格参考"
+        ? "type-price"
+        : candidate.solution_type === "时效参考" ? "type-speed" : "type-primary";
     const riskClass = candidate.pricing.cost_risk_status.includes("底线")
       ? "is-danger"
       : candidate.pricing.cost_risk_status.includes("安全") ? "is-safe" : "";
@@ -286,17 +292,27 @@ function renderCandidates(candidates) {
         </div>
         <div class="candidate-facts">
           <div><span>预计时效</span><strong>${escapeHtml(candidate.transit_days)} 天</strong></div>
-          <div><span>预计开航</span><strong>${shortDate(candidate.departure_date)}</strong></div>
-          <div><span>综合得分</span><strong>${candidate.total_score}</strong></div>
+          <div><span>方案保障</span><strong>${candidate.solution_assurance_score}</strong></div>
+          <div><span>供应商合作等级</span><strong>${escapeHtml(candidate.relationship.tier)}</strong></div>
+          <div><span>现舱</span><strong>${escapeHtml(candidate.spot_space.status)}${candidate.spot_space.teu ? ` · ${escapeHtml(candidate.spot_space.teu)} TEU` : ""}</strong></div>
         </div>
         <div class="score-list">
-          ${scoreRow("客户匹配", candidate.customer_fit_score)}
-          ${scoreRow("价格竞争", candidate.price_score)}
-          ${scoreRow("运输时效", candidate.transit_score)}
+          ${scoreRow("方案保障", candidate.solution_assurance_score)}
+          ${scoreRow("关系优势", candidate.relationship_advantage_score)}
+          ${scoreRow("现舱保障", candidate.spot_space_score)}
           ${scoreRow("供应履约", candidate.supplier_execution_score)}
-          ${scoreRow("产品策略", candidate.product_strategy_score)}
+          ${scoreRow("客户适配", candidate.customer_fit_score)}
+          ${scoreRow("运输时效", candidate.transit_score)}
+          ${scoreRow("价格参考", candidate.price_score)}
         </div>
-        <div class="candidate-reason">${candidate.reasons.slice(0, 2).map(escapeHtml).join(";")}</div>
+        <div class="relationship-panel">
+          <div class="relationship-supplier"><span>我方与供应商 ${escapeHtml(candidate.relationship.supplier_name)}(${escapeHtml(candidate.relationship.supplier_channel)})的合作关系</span><strong>${escapeHtml(candidate.relationship.tier)} · 供应商合作亲密度 ${percent(candidate.relationship.strength)}</strong></div>
+          <div class="relationship-metric"><span>低价获取概率</span><strong>${percent(candidate.relationship.low_price_access_probability)}</strong></div>
+          <div class="relationship-metric"><span>优先舱位概率</span><strong>${percent(candidate.relationship.priority_space_probability)}</strong></div>
+          <div class="relationship-metric"><span>现舱状态</span><strong>${escapeHtml(candidate.spot_space.status)}</strong></div>
+          <p>${escapeHtml(candidate.relationship.evidence)}</p>
+        </div>
+        <div class="candidate-reason">${candidate.reasons.slice(0, 4).map(escapeHtml).join(";")}</div>
         <div class="candidate-price">
           <div><span>建议客户报价</span><strong>${money(candidate.pricing.final_quote_amount)}</strong></div>
           <em class="risk-label ${riskClass}">${escapeHtml(candidate.pricing.cost_risk_status)}</em>
@@ -341,19 +357,25 @@ async function openAudit() {
   dialog.showModal();
   try {
     const audit = await fetchJson(`/api/market/recommendations/${encodeURIComponent(currentRunId)}/audit`);
+    const explanation = audit.business_explanation;
     target.innerHTML = `
-      <div class="audit-grid">
-        ${auditItem("推荐任务", audit.run_id)}
-        ${auditItem("生成时间", audit.generated_at)}
-        ${auditItem("客户与路线", `${audit.input.customer_name} · ${audit.input.route}`)}
-        ${auditItem("候选结果", `${audit.candidate_count} 个通过 / ${audit.rejected_count} 个淘汰`)}
-        ${auditItem("市场策略", audit.market_context.strategy_name)}
-        ${auditItem("RDF决策断言", `${audit.rdf_assertions} 条`)}
-      </div>
-      <div class="audit-sources">
-        <h3>本次决策数据来源</h3>
-        ${audit.sources.map(source => `<code>${escapeHtml(source)}</code>`).join("")}
-      </div>`;
+      <section class="audit-decision">
+        <span>本次怎么选</span>
+        <p>${escapeHtml(explanation.decision)}</p>
+      </section>
+      <section class="audit-section">
+        <h3>为什么优先推荐</h3>
+        <ol class="audit-reason-list">${explanation.why_recommended.map(reason => `<li>${escapeHtml(reason)}</li>`).join("")}</ol>
+      </section>
+      <section class="audit-section">
+        <h3>其他方案为什么保留</h3>
+        <div class="audit-alternatives">${explanation.alternatives.map(item => `<div><strong>${escapeHtml(item.title)}</strong><p>${escapeHtml(item.detail)}</p></div>`).join("") || "<p>本次没有其他可执行备选方案。</p>"}</div>
+      </section>
+      <section class="audit-section">
+        <h3>没有采用的方案</h3>
+        <ul class="audit-rejection-list">${explanation.rejected.map(reason => `<li>${escapeHtml(reason)}</li>`).join("") || "<li>本次没有方案因硬条件被淘汰。</li>"}</ul>
+      </section>
+      <section class="audit-basis"><strong>判断口径</strong><p>${escapeHtml(explanation.selection_basis)}</p></section>`;
   } catch (error) {
     target.innerHTML = `<div class="error">${escapeHtml(error.message)}</div>`;
   }
@@ -429,7 +451,11 @@ function renderActiveCatalogTable() {
       columns: [
         ["quote_no", "报价编号"], ["route", "路线"], ["transport_mode", "方式"],
         ["product_name", "产品"], ["supplier_name", "供应商"],
-        ["supplier_cost_amount", "采购金额"], ["valid_to", "有效至"], ["status", "状态"],
+        ["relationship_tier", "供应商合作等级"], ["relationship_strength", "供应商合作亲密度"],
+        ["low_price_access_probability", "低价获取"], ["priority_space_probability", "优先舱位"],
+        ["spot_space_status", "现舱状态"], ["spot_space_teu", "现舱 TEU"],
+        ["relationship_evidence", "合作依据"], ["supplier_cost_amount", "采购金额"],
+        ["valid_to", "有效至"], ["status", "状态"],
       ],
       rows: mockCatalog.quotes,
     },
@@ -458,13 +484,17 @@ function renderCatalogTable(rows, columns) {
 }
 
 function formatCatalogCell(key, value, row) {
-  if (["price_sensitivity", "time_sensitivity", "reliability_sensitivity"].includes(key)) {
+  if ([
+    "price_sensitivity", "time_sensitivity", "reliability_sensitivity",
+    "relationship_strength", "low_price_access_probability", "priority_space_probability",
+  ].includes(key)) {
     const width = Math.round(Number(value || 0) * 100);
     return `<div class="sensitivity-cell"><span>${width}%</span><div class="sensitivity-track"><span style="width:${width}%"></span></div></div>`;
   }
   if (key === "supplier_cost_amount") {
     return `${money(value)} ${escapeHtml(row.currency || "CNY")}${row.runtime_mock ? ` <span class="runtime-mark">内存</span>` : ""}`;
   }
+  if (key === "spot_space_teu") return value ? `${Number(value)} TEU` : "待确认";
   if (key === "status") {
     const className = value === "当前有效" ? "status-current" : value === "已过期" ? "status-expired" : "";
     return `<span class="${className}">${escapeHtml(value)}</span>`;

+ 7 - 7
app/static/index.html

@@ -155,7 +155,7 @@
         <div>
           <span class="section-kicker">LOCAL RDF / VOLATILE DATA</span>
           <h1 id="mockTitle">Mock 数据台</h1>
-          <p>当前内存图中的客户、路线、产品、供应商、报价与市场策略</p>
+          <p>当前内存图中的客户、路线、产品、供应商、报价与供应商合作信息</p>
         </div>
         <div class="mock-actions">
           <span class="volatile-label">重启即清空动态数据</span>
@@ -170,7 +170,7 @@
           <span class="section-index">01</span>
           <div>
             <h2 id="mockGeneratorTitle">动态补充样本</h2>
-            <p id="mockStatus">仅写入当前进程的 RDF Graph</p>
+            <p id="mockStatus">印度目的港样本,仅写入当前进程的 RDF Graph</p>
           </div>
         </div>
         <div class="generator-controls">
@@ -180,11 +180,11 @@
           </label>
           <label class="compact-field">
             <span>供应商数量</span>
-            <input id="mockSupplierCount" type="number" min="1" max="5" value="2">
+            <input id="mockSupplierCount" type="number" min="1" max="8" value="4">
           </label>
           <label class="compact-field">
             <span>客户数量</span>
-            <input id="mockCustomerCount" type="number" min="0" max="3" value="1">
+            <input id="mockCustomerCount" type="number" min="0" max="5" value="2">
           </label>
           <button id="generateMockBtn" class="primary-command" type="button">
             <span>添加一批 Mock 数据</span><span aria-hidden="true">+</span>
@@ -259,14 +259,14 @@
   <dialog id="auditDialog" class="audit-dialog">
     <div class="dialog-heading">
       <div>
-        <span class="section-kicker">DECISION TRACE</span>
-        <h2>决策审计</h2>
+        <span class="section-kicker">DECISION EXPLAINED</span>
+        <h2>这次方案为什么这样选</h2>
       </div>
       <button id="closeAuditBtn" class="icon-command" type="button" aria-label="关闭">×</button>
     </div>
     <div id="auditContent" class="audit-content"></div>
   </dialog>
 
-  <script src="/static/app.js?v=7"></script>
+  <script src="/static/app.js?v=8"></script>
 </body>
 </html>

+ 34 - 12
app/static/styles.css

@@ -327,18 +327,20 @@ h1, h2, h3, p { margin: 0; }
   border-radius: 5px;
   animation: rise-in 280ms both;
 }
-.candidate-card.type-economy { border-top-color: var(--blue); }
+.candidate-card.type-relationship { border-top-color: var(--blue); }
+.candidate-card.type-price { border-top-color: var(--ink-soft); }
 .candidate-card.type-speed { border-top-color: var(--amber); }
 @keyframes rise-in { from { opacity: 0; transform: translateY(8px); } }
 .candidate-head { padding: 16px 16px 13px; border-bottom: 1px solid var(--line); }
 .candidate-type-row { display: flex; justify-content: space-between; gap: 10px; align-items: center; }
 .candidate-type { color: var(--teal); font-size: 11px; font-weight: 800; }
-.type-economy .candidate-type { color: var(--blue); }
+.type-relationship .candidate-type { color: var(--blue); }
+.type-price .candidate-type { color: var(--ink-soft); }
 .type-speed .candidate-type { color: var(--amber); }
 .candidate-rank { color: var(--muted); font-family: Georgia, serif; font-size: 12px; }
 .candidate-head h3 { margin-top: 9px; min-height: 40px; font-size: 14px; line-height: 1.45; }
 .candidate-supplier { margin-top: 5px; color: var(--muted); font-size: 11px; }
-.candidate-facts { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--line); }
+.candidate-facts { display: grid; grid-template-columns: repeat(4, 1fr); border-bottom: 1px solid var(--line); }
 .candidate-facts div { padding: 11px 10px; border-right: 1px solid var(--line); }
 .candidate-facts div:last-child { border-right: 0; }
 .candidate-facts span { display: block; color: var(--muted); font-size: 9px; }
@@ -348,7 +350,22 @@ h1, h2, h3, p { margin: 0; }
 .score-track { height: 4px; background: #e5eae7; }
 .score-track span { display: block; height: 100%; background: var(--teal); }
 .score-row strong { text-align: right; font-size: 10px; }
-.candidate-reason { min-height: 72px; padding: 12px 16px; background: var(--surface-soft); color: var(--ink-soft); font-size: 11px; line-height: 1.55; }
+.relationship-panel {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(0, 1fr));
+  gap: 0;
+  border-top: 1px solid var(--line);
+  border-bottom: 1px solid var(--line);
+  background: #f2f7f5;
+}
+.relationship-panel > div { min-width: 0; }
+.relationship-supplier { grid-column: 1 / -1; padding: 10px 11px; border-bottom: 1px solid var(--line); }
+.relationship-metric { padding: 10px 11px; border-right: 1px solid var(--line); }
+.relationship-metric:nth-of-type(4) { border-right: 0; }
+.relationship-panel span { display: block; color: var(--muted); font-size: 9px; }
+.relationship-panel strong { display: block; margin-top: 4px; color: var(--teal-dark); font-family: Georgia, serif; font-size: 16px; font-weight: 500; }
+.relationship-panel p { grid-column: 1 / -1; margin: 0; padding: 9px 11px 10px; border-top: 1px solid var(--line); color: var(--ink-soft); font-size: 10px; line-height: 1.5; }
+.candidate-reason { min-height: 86px; padding: 12px 16px; background: var(--surface-soft); color: var(--ink-soft); font-size: 11px; line-height: 1.55; }
 .candidate-price {
   display: flex;
   align-items: flex-end;
@@ -553,14 +570,18 @@ th { background: var(--surface-soft); color: var(--ink-soft); font-weight: 700;
 .dialog-heading h2 { margin-top: 5px; font-family: Georgia, "Songti SC", serif; font-size: 22px; font-weight: 500; }
 .icon-command { width: 34px; height: 34px; background: var(--surface-soft); border: 1px solid var(--line); color: var(--ink-soft); font-size: 22px; }
 .audit-content { padding: 22px; }
-.audit-grid { display: grid; grid-template-columns: repeat(2, 1fr); border: 1px solid var(--line); }
-.audit-item { min-height: 72px; padding: 12px; border-right: 1px solid var(--line); border-bottom: 1px solid var(--line); }
-.audit-item:nth-child(2n) { border-right: 0; }
-.audit-item span { color: var(--muted); font-size: 9px; }
-.audit-item strong { display: block; margin-top: 5px; font-size: 12px; overflow-wrap: anywhere; }
-.audit-sources { margin-top: 20px; }
-.audit-sources h3 { font-size: 12px; }
-.audit-sources code { display: block; margin-top: 8px; padding: 9px 10px; background: var(--surface-soft); color: var(--ink-soft); font-size: 10px; overflow-wrap: anywhere; }
+.audit-decision { padding: 16px; border-left: 3px solid var(--teal); background: #eef7f4; }
+.audit-decision span, .audit-basis strong { color: var(--teal-dark); font-size: 10px; font-weight: 800; }
+.audit-decision p { margin: 7px 0 0; font-family: Georgia, "Songti SC", serif; font-size: 17px; line-height: 1.55; }
+.audit-section { margin-top: 22px; }
+.audit-section h3 { margin: 0 0 10px; font-size: 13px; }
+.audit-reason-list, .audit-rejection-list { display: grid; gap: 9px; margin: 0; padding-left: 20px; color: var(--ink-soft); font-size: 12px; line-height: 1.6; }
+.audit-reason-list li::marker { color: var(--teal); font-weight: 800; }
+.audit-alternatives { display: grid; gap: 8px; }
+.audit-alternatives > div { padding: 11px 12px; border: 1px solid var(--line); background: var(--surface-soft); }
+.audit-alternatives strong { font-size: 12px; }
+.audit-alternatives p, .audit-basis p { margin: 5px 0 0; color: var(--ink-soft); font-size: 11px; line-height: 1.6; }
+.audit-basis { margin-top: 22px; padding: 13px; border: 1px solid var(--line); }
 
 @media (max-width: 1120px) {
   #marketView { grid-template-columns: 290px minmax(0, 1fr); }
@@ -570,6 +591,7 @@ th { background: var(--surface-soft); color: var(--ink-soft); font-weight: 700;
   .candidate-head { grid-row: 1 / span 2; }
   .candidate-facts { border-left: 1px solid var(--line); }
   .score-list { border-left: 1px solid var(--line); }
+  .relationship-panel { grid-column: 1 / -1; }
   .candidate-reason { grid-column: 1 / -1; min-height: 0; }
   .candidate-price { grid-column: 1 / -1; }
   .data-summary { grid-template-columns: repeat(4, minmax(0, 1fr)); }

+ 5 - 3
config/market-strategies.json

@@ -1,8 +1,10 @@
 {
   "solution_price_adjustments": {
-    "经济方案": -700,
-    "均衡方案": 0,
-    "时效方案": 1400
+    "方案优选": 0,
+    "关系优选": 300,
+    "价格参考": -700,
+    "时效参考": 1400,
+    "可执行方案": 0
   },
   "quote_rounding": 100,
   "allowed_lower_delta": 1000,

+ 283 - 0
data/ai-market-india-mock.ttl

@@ -0,0 +1,283 @@
+@prefix gesli: <https://gesli.example/ontology#> .
+@prefix sample: <https://gesli.example/sample#> .
+@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
+
+# Local-only India mock market. Names and relationship data are fictional demo samples.
+sample:IndiaMockRetail a gesli:Customer ;
+    rdfs:label "印度目的港模拟客户A"@zh ;
+    gesli:hasTradeProfile sample:IndiaMockRetailTrade ;
+    gesli:hasCreditProfile sample:IndiaMockRetailCredit ;
+    gesli:hasServiceProfile sample:IndiaMockRetailService .
+
+sample:IndiaMockRetailTrade a gesli:CustomerTradeProfile ;
+    gesli:industry "消费品" ;
+    gesli:routeName "青岛-印度那瓦舍瓦港" ;
+    gesli:repeatPurchaseCount 22 ;
+    gesli:customerPriceSensitivity "0.42"^^xsd:decimal ;
+    gesli:customerTimeSensitivity "0.76"^^xsd:decimal ;
+    gesli:customerReliabilitySensitivity "0.93"^^xsd:decimal .
+
+sample:IndiaMockRetailCredit a gesli:CustomerCreditProfile ;
+    gesli:overdueDays 0 .
+
+sample:IndiaMockRetailService a gesli:CustomerServiceProfile ;
+    gesli:notificationRequirement "装船、到港和放货节点即时通知" ;
+    gesli:documentRequirement "印度进口单证预审" ;
+    gesli:exceptionResponseRequirement "2小时内响应" .
+
+sample:IndiaMockAutoParts a gesli:Customer ;
+    rdfs:label "印度目的港模拟客户B"@zh ;
+    gesli:hasTradeProfile sample:IndiaMockAutoPartsTrade ;
+    gesli:hasCreditProfile sample:IndiaMockAutoPartsCredit .
+
+sample:IndiaMockAutoPartsTrade a gesli:CustomerTradeProfile ;
+    gesli:industry "汽车零部件" ;
+    gesli:routeName "青岛-印度金奈港" ;
+    gesli:repeatPurchaseCount 16 ;
+    gesli:customerPriceSensitivity "0.58"^^xsd:decimal ;
+    gesli:customerTimeSensitivity "0.70"^^xsd:decimal ;
+    gesli:customerReliabilitySensitivity "0.88"^^xsd:decimal .
+
+sample:IndiaMockAutoPartsCredit a gesli:CustomerCreditProfile ;
+    gesli:overdueDays 0 .
+
+sample:MarketBenchmark_QD_IN_Nhava_202607 a gesli:MarketPriceBenchmark ;
+    rdfs:label "青岛-印度那瓦舍瓦港2026年7月Mock市场基准"@zh ;
+    gesli:marketBenchmarkAmount "46000"^^xsd:decimal ;
+    gesli:currency "CNY" ;
+    gesli:originLocation "青岛港" ;
+    gesli:destinationLocation "印度那瓦舍瓦港" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-20"^^xsd:date .
+
+sample:Strategy_QD_IN_Nhava_SolutionFirst a gesli:MarketShareStrategy ;
+    rdfs:label "青岛-印度那瓦舍瓦方案保障策略"@zh ;
+    gesli:strategyCode "STR-QD-IN-NHAVA-SOLUTION-202607" ;
+    gesli:strategyName "青岛-印度那瓦舍瓦方案保障策略" ;
+    gesli:strategyObjective "优先保障直航、保舱和稳定的船公司及订舱代理协作关系" ;
+    gesli:strategyEnabled true ;
+    gesli:strategyStatus "已启用" ;
+    gesli:strategyAdjustmentRate "-0.015"^^xsd:decimal ;
+    gesli:strategyScopeOrigin "青岛港" ;
+    gesli:strategyScopeDestination "印度那瓦舍瓦港" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-20"^^xsd:date .
+
+sample:MarketBenchmark_QD_IN_Chennai_202607 a gesli:MarketPriceBenchmark ;
+    rdfs:label "青岛-印度金奈港2026年7月Mock市场基准"@zh ;
+    gesli:marketBenchmarkAmount "43800"^^xsd:decimal ;
+    gesli:currency "CNY" ;
+    gesli:originLocation "青岛港" ;
+    gesli:destinationLocation "印度金奈港" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-20"^^xsd:date .
+
+sample:Strategy_QD_IN_Chennai_SolutionFirst a gesli:MarketShareStrategy ;
+    rdfs:label "青岛-印度金奈方案保障策略"@zh ;
+    gesli:strategyCode "STR-QD-IN-CHENNAI-SOLUTION-202607" ;
+    gesli:strategyName "青岛-印度金奈方案保障策略" ;
+    gesli:strategyObjective "优先保障汽车零部件客户的直航资源、文件支持和优先订舱" ;
+    gesli:strategyEnabled true ;
+    gesli:strategyStatus "已启用" ;
+    gesli:strategyAdjustmentRate "-0.010"^^xsd:decimal ;
+    gesli:strategyScopeOrigin "青岛港" ;
+    gesli:strategyScopeDestination "印度金奈港" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-20"^^xsd:date .
+
+# Qingdao to Nhava Sheva: a higher-cost direct carrier should lead because the solution and relationship are stronger.
+sample:IndiaMockCarrierDirect a gesli:Carrier ;
+    rdfs:label "印度航线Mock船公司直营"@zh ;
+    gesli:isActivePartner true .
+
+sample:IndiaMockProductNhavaDirect a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-NHAVA-DIRECT" ;
+    gesli:productName "青岛-印度那瓦舍瓦直航优先舱40HQ" ;
+    gesli:transportMode "海运" ;
+    gesli:serviceForm "优先舱位包柜" ;
+    gesli:routeType "直航" ;
+    gesli:originLocation "青岛港" ;
+    gesli:destinationLocation "印度那瓦舍瓦港" ;
+    gesli:estimatedTransitDays "19"^^xsd:decimal .
+
+sample:IndiaMockOfferingNhavaDirect a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-NHAVA-DIRECT" ;
+    gesli:offeredBySupplier sample:IndiaMockCarrierDirect ;
+    gesli:offeringProduct sample:IndiaMockProductNhavaDirect ;
+    gesli:offeringChannel "船公司直营" ;
+    gesli:agencyLevel "产品品牌本地分支机构" ;
+    gesli:capacityCommitment "年度协议优先保舱20TEU" ;
+    gesli:capacityAvailable true ;
+    gesli:spotSpaceStatus "可立即订舱" ;
+    gesli:spotSpaceTeu "18"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.97"^^xsd:decimal ;
+    gesli:spaceReleaseSuccessRate "0.96"^^xsd:decimal ;
+    gesli:solutionAssuranceScore "0.98"^^xsd:decimal ;
+    gesli:serviceSupportScore "0.94"^^xsd:decimal ;
+    gesli:relationshipTier "战略合作" ;
+    gesli:relationshipStrength "0.96"^^xsd:decimal ;
+    gesli:lowPriceAccessProbability "0.90"^^xsd:decimal ;
+    gesli:prioritySpaceProbability "0.95"^^xsd:decimal ;
+    gesli:relationshipEvidence "年度协议、价格窗口直连与优先放舱额度" .
+
+sample:IndiaMockScheduleNhavaDirect a gesli:ScheduledServiceInstance ;
+    gesli:scheduleCode "SCH-MOCK-IN-NHAVA-20260729" ;
+    gesli:scheduleOfProduct sample:IndiaMockProductNhavaDirect ;
+    gesli:estimatedDepartureTime "2026-07-29T18:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedArrivalTime "2026-08-17T08:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedTransitDays "19"^^xsd:decimal .
+
+sample:IndiaMockQuoteNhavaDirect a gesli:SupplierQuote ;
+    gesli:quoteNo "SQ-MOCK-IN-NHAVA-DIRECT-001" ;
+    gesli:quotedBySupplier sample:IndiaMockCarrierDirect ;
+    gesli:quotedForOffering sample:IndiaMockOfferingNhavaDirect ;
+    gesli:quoteAppliesToSchedule sample:IndiaMockScheduleNhavaDirect ;
+    gesli:supplierCostAmount "40100"^^xsd:decimal ;
+    gesli:currency "CNY" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-15"^^xsd:date .
+
+sample:IndiaMockCoreAgent a gesli:ShippingAgencyCompany ;
+    rdfs:label "印度航线Mock核心订舱代理"@zh ;
+    gesli:isActivePartner true .
+
+sample:IndiaMockProductNhavaCore a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-NHAVA-CORE" ;
+    gesli:productName "青岛-印度那瓦舍瓦直航稳定舱40HQ" ;
+    gesli:transportMode "海运" ;
+    gesli:serviceForm "稳定舱位包柜" ;
+    gesli:routeType "直航" ;
+    gesli:originLocation "青岛港" ;
+    gesli:destinationLocation "印度那瓦舍瓦港" ;
+    gesli:estimatedTransitDays "20"^^xsd:decimal .
+
+sample:IndiaMockOfferingNhavaCore a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-NHAVA-CORE" ;
+    gesli:offeredBySupplier sample:IndiaMockCoreAgent ;
+    gesli:offeringProduct sample:IndiaMockProductNhavaCore ;
+    gesli:offeringChannel "一级订舱代理" ;
+    gesli:agencyLevel "一级代理" ;
+    gesli:capacityCommitment "月度保舱12TEU" ;
+    gesli:capacityAvailable true ;
+    gesli:spotSpaceStatus "现舱有限" ;
+    gesli:spotSpaceTeu "8"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.94"^^xsd:decimal ;
+    gesli:spaceReleaseSuccessRate "0.92"^^xsd:decimal ;
+    gesli:solutionAssuranceScore "0.90"^^xsd:decimal ;
+    gesli:serviceSupportScore "0.90"^^xsd:decimal ;
+    gesli:relationshipTier "核心合作" ;
+    gesli:relationshipStrength "0.87"^^xsd:decimal ;
+    gesli:lowPriceAccessProbability "0.82"^^xsd:decimal ;
+    gesli:prioritySpaceProbability "0.86"^^xsd:decimal ;
+    gesli:relationshipEvidence "核心代理授权与固定订舱窗口" .
+
+sample:IndiaMockScheduleNhavaCore a gesli:ScheduledServiceInstance ;
+    gesli:scheduleCode "SCH-MOCK-IN-NHAVA-20260730" ;
+    gesli:scheduleOfProduct sample:IndiaMockProductNhavaCore ;
+    gesli:estimatedDepartureTime "2026-07-30T18:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedArrivalTime "2026-08-19T08:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedTransitDays "20"^^xsd:decimal .
+
+sample:IndiaMockQuoteNhavaCore a gesli:SupplierQuote ;
+    gesli:quoteNo "SQ-MOCK-IN-NHAVA-CORE-001" ;
+    gesli:quotedBySupplier sample:IndiaMockCoreAgent ;
+    gesli:quotedForOffering sample:IndiaMockOfferingNhavaCore ;
+    gesli:quoteAppliesToSchedule sample:IndiaMockScheduleNhavaCore ;
+    gesli:supplierCostAmount "37200"^^xsd:decimal ;
+    gesli:currency "CNY" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-15"^^xsd:date .
+
+sample:IndiaMockMarketAgent a gesli:ShippingAgencyCompany ;
+    rdfs:label "印度航线Mock市场订舱代理"@zh ;
+    gesli:isActivePartner true .
+
+sample:IndiaMockProductNhavaEconomy a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-NHAVA-ECONOMY" ;
+    gesli:productName "青岛-印度那瓦舍瓦中转经济舱40HQ" ;
+    gesli:transportMode "海运" ;
+    gesli:serviceForm "经济包柜" ;
+    gesli:routeType "中转" ;
+    gesli:originLocation "青岛港" ;
+    gesli:destinationLocation "印度那瓦舍瓦港" ;
+    gesli:estimatedTransitDays "25"^^xsd:decimal .
+
+sample:IndiaMockOfferingNhavaEconomy a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-NHAVA-ECONOMY" ;
+    gesli:offeredBySupplier sample:IndiaMockMarketAgent ;
+    gesli:offeringProduct sample:IndiaMockProductNhavaEconomy ;
+    gesli:offeringChannel "市场订舱代理" ;
+    gesli:agencyLevel "二级代理" ;
+    gesli:capacityCommitment "按单票确认舱位" ;
+    gesli:capacityAvailable true ;
+    gesli:spotSpaceStatus "需确认" ;
+    gesli:spotSpaceTeu "0"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.84"^^xsd:decimal ;
+    gesli:spaceReleaseSuccessRate "0.82"^^xsd:decimal ;
+    gesli:solutionAssuranceScore "0.67"^^xsd:decimal ;
+    gesli:serviceSupportScore "0.68"^^xsd:decimal ;
+    gesli:relationshipTier "常规合作" ;
+    gesli:relationshipStrength "0.52"^^xsd:decimal ;
+    gesli:lowPriceAccessProbability "0.46"^^xsd:decimal ;
+    gesli:prioritySpaceProbability "0.48"^^xsd:decimal ;
+    gesli:relationshipEvidence "按市场舱位和临时报价执行" .
+
+sample:IndiaMockScheduleNhavaEconomy a gesli:ScheduledServiceInstance ;
+    gesli:scheduleCode "SCH-MOCK-IN-NHAVA-20260731" ;
+    gesli:scheduleOfProduct sample:IndiaMockProductNhavaEconomy ;
+    gesli:estimatedDepartureTime "2026-07-31T18:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedArrivalTime "2026-08-25T08:00:00+08:00"^^xsd:dateTime ;
+    gesli:estimatedTransitDays "25"^^xsd:decimal .
+
+sample:IndiaMockQuoteNhavaEconomy a gesli:SupplierQuote ;
+    gesli:quoteNo "SQ-MOCK-IN-NHAVA-ECONOMY-001" ;
+    gesli:quotedBySupplier sample:IndiaMockMarketAgent ;
+    gesli:quotedForOffering sample:IndiaMockOfferingNhavaEconomy ;
+    gesli:quoteAppliesToSchedule sample:IndiaMockScheduleNhavaEconomy ;
+    gesli:supplierCostAmount "33300"^^xsd:decimal ;
+    gesli:currency "CNY" ;
+    gesli:validFrom "2026-07-20"^^xsd:date ;
+    gesli:validTo "2026-08-15"^^xsd:date .
+
+# Qingdao to Chennai contains three additional local mock offers.
+sample:IndiaMockChennaiCarrier a gesli:Carrier ; rdfs:label "印度金奈Mock船公司直营"@zh ; gesli:isActivePartner true .
+sample:IndiaMockChennaiCoreAgent a gesli:ShippingAgencyCompany ; rdfs:label "印度金奈Mock核心订舱代理"@zh ; gesli:isActivePartner true .
+sample:IndiaMockChennaiMarketAgent a gesli:FreightForwarderSupplier ; rdfs:label "印度金奈Mock市场货代"@zh ; gesli:isActivePartner true .
+
+sample:IndiaMockProductChennaiDirect a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-CHENNAI-DIRECT" ; gesli:productName "青岛-印度金奈直航优先舱40HQ" ;
+    gesli:transportMode "海运" ; gesli:serviceForm "优先舱位包柜" ; gesli:routeType "直航" ;
+    gesli:originLocation "青岛港" ; gesli:destinationLocation "印度金奈港" ; gesli:estimatedTransitDays "18"^^xsd:decimal .
+sample:IndiaMockProductChennaiCore a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-CHENNAI-CORE" ; gesli:productName "青岛-印度金奈稳定直航舱40HQ" ;
+    gesli:transportMode "海运" ; gesli:serviceForm "稳定舱位包柜" ; gesli:routeType "直航" ;
+    gesli:originLocation "青岛港" ; gesli:destinationLocation "印度金奈港" ; gesli:estimatedTransitDays "20"^^xsd:decimal .
+sample:IndiaMockProductChennaiEconomy a gesli:ContainerOceanProduct ;
+    gesli:productCode "MOCK-IN-CHENNAI-ECONOMY" ; gesli:productName "青岛-印度金奈中转经济舱40HQ" ;
+    gesli:transportMode "海运" ; gesli:serviceForm "经济包柜" ; gesli:routeType "中转" ;
+    gesli:originLocation "青岛港" ; gesli:destinationLocation "印度金奈港" ; gesli:estimatedTransitDays "26"^^xsd:decimal .
+
+sample:IndiaMockOfferingChennaiDirect a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-CHENNAI-DIRECT" ; gesli:offeredBySupplier sample:IndiaMockChennaiCarrier ; gesli:offeringProduct sample:IndiaMockProductChennaiDirect ;
+    gesli:offeringChannel "船公司直营" ; gesli:agencyLevel "产品品牌本地分支机构" ; gesli:capacityCommitment "年度协议优先保舱16TEU" ; gesli:capacityAvailable true ; gesli:spotSpaceStatus "可立即订舱" ; gesli:spotSpaceTeu "14"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.96"^^xsd:decimal ; gesli:spaceReleaseSuccessRate "0.95"^^xsd:decimal ; gesli:solutionAssuranceScore "0.96"^^xsd:decimal ; gesli:serviceSupportScore "0.92"^^xsd:decimal ;
+    gesli:relationshipTier "战略合作" ; gesli:relationshipStrength "0.94"^^xsd:decimal ; gesli:lowPriceAccessProbability "0.87"^^xsd:decimal ; gesli:prioritySpaceProbability "0.93"^^xsd:decimal ; gesli:relationshipEvidence "年度协议与优先放舱额度" .
+sample:IndiaMockOfferingChennaiCore a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-CHENNAI-CORE" ; gesli:offeredBySupplier sample:IndiaMockChennaiCoreAgent ; gesli:offeringProduct sample:IndiaMockProductChennaiCore ;
+    gesli:offeringChannel "一级订舱代理" ; gesli:agencyLevel "一级代理" ; gesli:capacityCommitment "月度保舱10TEU" ; gesli:capacityAvailable true ; gesli:spotSpaceStatus "现舱有限" ; gesli:spotSpaceTeu "6"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.93"^^xsd:decimal ; gesli:spaceReleaseSuccessRate "0.91"^^xsd:decimal ; gesli:solutionAssuranceScore "0.88"^^xsd:decimal ; gesli:serviceSupportScore "0.88"^^xsd:decimal ;
+    gesli:relationshipTier "核心合作" ; gesli:relationshipStrength "0.85"^^xsd:decimal ; gesli:lowPriceAccessProbability "0.80"^^xsd:decimal ; gesli:prioritySpaceProbability "0.84"^^xsd:decimal ; gesli:relationshipEvidence "核心代理授权和固定订舱窗口" .
+sample:IndiaMockOfferingChennaiEconomy a gesli:SupplierProductOffering ;
+    gesli:offeringCode "OFF-MOCK-IN-CHENNAI-ECONOMY" ; gesli:offeredBySupplier sample:IndiaMockChennaiMarketAgent ; gesli:offeringProduct sample:IndiaMockProductChennaiEconomy ;
+    gesli:offeringChannel "市场货代" ; gesli:agencyLevel "二级代理" ; gesli:capacityCommitment "按单票确认舱位" ; gesli:capacityAvailable true ; gesli:spotSpaceStatus "需确认" ; gesli:spotSpaceTeu "0"^^xsd:decimal ;
+    gesli:bookingSuccessRate "0.83"^^xsd:decimal ; gesli:spaceReleaseSuccessRate "0.81"^^xsd:decimal ; gesli:solutionAssuranceScore "0.65"^^xsd:decimal ; gesli:serviceSupportScore "0.66"^^xsd:decimal ;
+    gesli:relationshipTier "常规合作" ; gesli:relationshipStrength "0.48"^^xsd:decimal ; gesli:lowPriceAccessProbability "0.42"^^xsd:decimal ; gesli:prioritySpaceProbability "0.45"^^xsd:decimal ; gesli:relationshipEvidence "按市场舱位和临时报价执行" .
+
+sample:IndiaMockScheduleChennaiDirect a gesli:ScheduledServiceInstance ; gesli:scheduleCode "SCH-MOCK-IN-CHENNAI-20260729" ; gesli:scheduleOfProduct sample:IndiaMockProductChennaiDirect ; gesli:estimatedDepartureTime "2026-07-29T18:00:00+08:00"^^xsd:dateTime ; gesli:estimatedArrivalTime "2026-08-16T08:00:00+08:00"^^xsd:dateTime ; gesli:estimatedTransitDays "18"^^xsd:decimal .
+sample:IndiaMockScheduleChennaiCore a gesli:ScheduledServiceInstance ; gesli:scheduleCode "SCH-MOCK-IN-CHENNAI-20260730" ; gesli:scheduleOfProduct sample:IndiaMockProductChennaiCore ; gesli:estimatedDepartureTime "2026-07-30T18:00:00+08:00"^^xsd:dateTime ; gesli:estimatedArrivalTime "2026-08-19T08:00:00+08:00"^^xsd:dateTime ; gesli:estimatedTransitDays "20"^^xsd:decimal .
+sample:IndiaMockScheduleChennaiEconomy a gesli:ScheduledServiceInstance ; gesli:scheduleCode "SCH-MOCK-IN-CHENNAI-20260731" ; gesli:scheduleOfProduct sample:IndiaMockProductChennaiEconomy ; gesli:estimatedDepartureTime "2026-07-31T18:00:00+08:00"^^xsd:dateTime ; gesli:estimatedArrivalTime "2026-08-26T08:00:00+08:00"^^xsd:dateTime ; gesli:estimatedTransitDays "26"^^xsd:decimal .
+
+sample:IndiaMockQuoteChennaiDirect a gesli:SupplierQuote ; gesli:quoteNo "SQ-MOCK-IN-CHENNAI-DIRECT-001" ; gesli:quotedBySupplier sample:IndiaMockChennaiCarrier ; gesli:quotedForOffering sample:IndiaMockOfferingChennaiDirect ; gesli:quoteAppliesToSchedule sample:IndiaMockScheduleChennaiDirect ; gesli:supplierCostAmount "39000"^^xsd:decimal ; gesli:currency "CNY" ; gesli:validFrom "2026-07-20"^^xsd:date ; gesli:validTo "2026-08-15"^^xsd:date .
+sample:IndiaMockQuoteChennaiCore a gesli:SupplierQuote ; gesli:quoteNo "SQ-MOCK-IN-CHENNAI-CORE-001" ; gesli:quotedBySupplier sample:IndiaMockChennaiCoreAgent ; gesli:quotedForOffering sample:IndiaMockOfferingChennaiCore ; gesli:quoteAppliesToSchedule sample:IndiaMockScheduleChennaiCore ; gesli:supplierCostAmount "35800"^^xsd:decimal ; gesli:currency "CNY" ; gesli:validFrom "2026-07-20"^^xsd:date ; gesli:validTo "2026-08-15"^^xsd:date .
+sample:IndiaMockQuoteChennaiEconomy a gesli:SupplierQuote ; gesli:quoteNo "SQ-MOCK-IN-CHENNAI-ECONOMY-001" ; gesli:quotedBySupplier sample:IndiaMockChennaiMarketAgent ; gesli:quotedForOffering sample:IndiaMockOfferingChennaiEconomy ; gesli:quoteAppliesToSchedule sample:IndiaMockScheduleChennaiEconomy ; gesli:supplierCostAmount "31900"^^xsd:decimal ; gesli:currency "CNY" ; gesli:validFrom "2026-07-20"^^xsd:date ; gesli:validTo "2026-08-15"^^xsd:date .

+ 63 - 0
docs/ai-market-solution-priority-change-log.md

@@ -0,0 +1,63 @@
+# AI市场中枢方案优先改造记录
+
+## 1. 改造目标
+
+将推荐逻辑从“报价竞争优先”调整为“可交付方案与合作关系优先,价格用于报价决策和参考”。
+
+业务前提:船公司直营、核心订舱代理和一般市场代理即使提供同一目的港产品,也会因年度协议、价格窗口、优先放舱额度和历史协作深度而具有不同的可订舱性与低价获取把握。
+
+## 2. 评分口径变更
+
+| 评分维度 | 权重 | 口径 |
+| --- | ---: | --- |
+| 方案保障 | 27% | 产品方案保障度、直航/直达属性、服务支持能力 |
+| 合作关系优势 | 23% | 与具体供应商的合作深度、低价获取概率、优先舱位概率 |
+| 现舱保障 | 15% | 现舱状态和当前可订 TEU;可立即订舱优先,需确认方案降权保留 |
+| 供应履约 | 18% | 订舱成功率、放舱成功率、ETD/ETA准点率 |
+| 客户适配 | 10% | 路由偏好、客户时效/可靠性/价格敏感度与服务支持能力 |
+| 运输时效 | 4% | 当前可执行候选中的相对运输时间 |
+| 价格参考 | 3% | 当前可执行候选中的相对采购成本 |
+
+价格继续用于客户报价、授权区间与毛利风险判断,但不再是决定推荐顺序的主导维度。
+
+## 3. 数据模型变更
+
+新增到 `gesli:SupplierProductOffering` 的字段:
+
+- `solutionAssuranceScore`:方案保障度
+- `serviceSupportScore`:服务支持能力
+- `relationshipTier`:供应商合作等级。供应商指船公司或订舱代理,例如战略合作、核心合作、稳定合作、常规合作
+- `relationshipStrength`:我方与具体供应商的合作亲密度,不是抽象方案评分
+- `lowPriceAccessProbability`:通过该渠道获取低价的概率
+- `prioritySpaceProbability`:优先取得舱位的概率
+- `relationshipEvidence`:关系依据,例如年度协议、固定订舱窗口、优先放舱额度
+- `spotSpaceStatus`:现舱状态,枚举为可立即订舱、现舱有限、需确认或无现舱
+- `spotSpaceTeu`:当前可直接订用的 TEU 数量
+
+推荐审计增加方案保障、合作关系优势和现舱保障得分,并以业务语言说明推荐理由、备选方案和淘汰原因。
+
+## 4. 数据样本变更
+
+新增 `data/ai-market-india-mock.ttl`:
+
+- 目的港:印度那瓦舍瓦港、印度金奈港
+- 预置两类客户、两套市场基准和策略、六个可执行报价
+- 同时覆盖船公司直营、核心订舱代理与市场代理
+- 那瓦舍瓦场景故意提供“价格更高但战略合作、优先保舱且有18 TEU现舱”的直营方案,以及“价格最低但关系、方案保障和现舱均较弱”的市场代理方案,用于验证方案优先口径
+
+动态 Mock 生成器默认显示印度目的港路线,单批可添加 1–8 家供应商和 0–5 个客户。生成的供应商会按战略合作、核心合作、稳定合作、常规合作等画像生成关系字段。
+
+## 5. 页面输出变更
+
+候选方案卡新增:
+
+- 方案保障、现舱状态与当前可订 TEU
+- 方案保障、关系优势、现舱保障、履约、客户适配、时效、价格七项得分
+- 我方与具体供应商的合作关系、低价获取概率、优先舱位概率和合作依据
+- 客户建议报价、授权区间和毛利风险仍完整保留
+
+Mock 数据台的供应商报价目录增加关系等级、与供应商合作度、低价获取、优先舱位、现舱状态、现舱 TEU 和合作依据列。
+
+## 6. 验证标准
+
+印度那瓦舍瓦港场景中,船公司直营方案的采购成本高于市场代理方案,但应因为方案保障、关系强度、优先舱位能力和18 TEU现舱而取得“方案优选”推荐;最低采购成本方案仅作为“价格参考”保留。

+ 13 - 1
ontology/ai-market-demo-extension.ttl

@@ -30,8 +30,17 @@ gesli:preferredRouteType a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:l
 gesli:requiresDangerousGoodsService a owl:DatatypeProperty ; rdfs:range xsd:boolean ; rdfs:label "是否需要危险品服务"@zh .
 
 gesli:capacityAvailable a owl:DatatypeProperty ; rdfs:range xsd:boolean ; rdfs:label "运力是否可用"@zh .
+gesli:spotSpaceStatus a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "现舱状态"@zh .
+gesli:spotSpaceTeu a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "当前可订舱位(TEU)"@zh .
 gesli:supportsDangerousGoods a owl:DatatypeProperty ; rdfs:range xsd:boolean ; rdfs:label "是否支持危险品"@zh .
 gesli:dangerousGoodsOnly a owl:DatatypeProperty ; rdfs:range xsd:boolean ; rdfs:label "是否为危险品专用产品"@zh .
+gesli:solutionAssuranceScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "方案保障度"@zh .
+gesli:serviceSupportScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "服务支持能力"@zh .
+gesli:relationshipTier a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "供应商合作等级(船公司或订舱代理)"@zh .
+gesli:relationshipStrength a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "供应商合作亲密度(船公司或订舱代理)"@zh .
+gesli:lowPriceAccessProbability a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "低价获取概率"@zh .
+gesli:prioritySpaceProbability a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "优先舱位获取概率"@zh .
+gesli:relationshipEvidence a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "供应商合作依据"@zh .
 gesli:strategyAdjustmentRate a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "策略价格调整率"@zh .
 gesli:strategyScopeOrigin a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "策略适用起运地"@zh .
 gesli:strategyScopeDestination a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "策略适用目的地"@zh .
@@ -47,7 +56,10 @@ gesli:customerFitScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:la
 gesli:priceScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "价格得分"@zh .
 gesli:transitScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "时效得分"@zh .
 gesli:supplierExecutionScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "供应商执行得分"@zh .
-gesli:productStrategyScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "产品策略得分"@zh .
+gesli:solutionAssuranceEvaluationScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "方案保障评估得分"@zh .
+gesli:relationshipAdvantageScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "供应商合作优势得分"@zh .
+gesli:spotSpaceScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "现舱保障得分"@zh .
+gesli:productStrategyScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "产品策略得分(兼容字段)"@zh .
 gesli:totalScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "综合得分"@zh .
 gesli:confidenceScore a owl:DatatypeProperty ; rdfs:range xsd:decimal ; rdfs:label "推荐置信度"@zh .
 gesli:recommendationReason a owl:DatatypeProperty ; rdfs:range xsd:string ; rdfs:label "推荐理由"@zh .

+ 12 - 1
queries/market-candidates.rq

@@ -3,7 +3,9 @@ PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
 
 SELECT ?quote ?quoteNo ?validFrom ?validTo ?currency ?supplierCostAmount
        ?offering ?offeringCode ?offeringChannel ?agencyLevel ?capacityCommitment
-       ?capacityAvailable ?supportsDangerousGoods ?dangerousGoodsOnly ?bookingSuccessRate ?spaceReleaseSuccessRate
+       ?capacityAvailable ?spotSpaceStatus ?spotSpaceTeu ?supportsDangerousGoods ?dangerousGoodsOnly ?bookingSuccessRate ?spaceReleaseSuccessRate
+       ?solutionAssuranceScore ?serviceSupportScore ?relationshipTier ?relationshipStrength
+       ?lowPriceAccessProbability ?prioritySpaceProbability ?relationshipEvidence
        ?supplier ?supplierName ?supplierActive
        ?product ?productCode ?productName ?brandName ?transportMode ?serviceForm ?routeType
        ?originLocation ?destinationLocation ?estimatedTransitDays
@@ -30,10 +32,19 @@ WHERE {
   OPTIONAL { ?offering gesli:agencyLevel ?agencyLevel . }
   OPTIONAL { ?offering gesli:capacityCommitment ?capacityCommitment . }
   OPTIONAL { ?offering gesli:capacityAvailable ?capacityAvailable . }
+  OPTIONAL { ?offering gesli:spotSpaceStatus ?spotSpaceStatus . }
+  OPTIONAL { ?offering gesli:spotSpaceTeu ?spotSpaceTeu . }
   OPTIONAL { ?offering gesli:supportsDangerousGoods ?supportsDangerousGoods . }
   OPTIONAL { ?offering gesli:dangerousGoodsOnly ?dangerousGoodsOnly . }
   OPTIONAL { ?offering gesli:bookingSuccessRate ?bookingSuccessRate . }
   OPTIONAL { ?offering gesli:spaceReleaseSuccessRate ?spaceReleaseSuccessRate . }
+  OPTIONAL { ?offering gesli:solutionAssuranceScore ?solutionAssuranceScore . }
+  OPTIONAL { ?offering gesli:serviceSupportScore ?serviceSupportScore . }
+  OPTIONAL { ?offering gesli:relationshipTier ?relationshipTier . }
+  OPTIONAL { ?offering gesli:relationshipStrength ?relationshipStrength . }
+  OPTIONAL { ?offering gesli:lowPriceAccessProbability ?lowPriceAccessProbability . }
+  OPTIONAL { ?offering gesli:prioritySpaceProbability ?prioritySpaceProbability . }
+  OPTIONAL { ?offering gesli:relationshipEvidence ?relationshipEvidence . }
   OPTIONAL { ?supplier gesli:isActivePartner ?directSupplierActive . }
   OPTIONAL {
     ?supplier gesli:hasBusinessPartnerBasicInfo ?supplierBasic .

+ 42 - 12
tests/test_market_engine.py

@@ -10,6 +10,7 @@ from app.repository import RDFRepository
 ROOT = Path(__file__).resolve().parents[1]
 HAIER = "https://gesli.example/sample#Haier"
 HISENSE = "https://gesli.example/sample#Hisense"
+INDIA_RETAIL = "https://gesli.example/sample#IndiaMockRetail"
 
 
 class MarketEngineTest(unittest.TestCase):
@@ -28,27 +29,29 @@ class MarketEngineTest(unittest.TestCase):
                 "generated_solutions": 3,
             },
         )
-        self.assertEqual(result["recommendation"]["solution_type"], "均衡方案")
-        self.assertEqual(result["recommendation"]["supplier_name"], "马士基青岛分支机构")
-        self.assertEqual(
-            {candidate["solution_type"] for candidate in result["candidates"]},
-            {"经济方案", "均衡方案", "时效方案"},
-        )
+        self.assertEqual(result["recommendation"]["solution_type"], "方案优选")
+        self.assertIn("方案优选", {candidate["solution_type"] for candidate in result["candidates"]})
+        self.assertIn("relationship", result["candidates"][0])
+        self.assertEqual(result["weights"]["price"], 0.03)
+        self.assertEqual(result["weights"]["spot_space"], 0.15)
 
     def test_form_only_exposes_routes_with_market_context(self):
         options = self.service.form_options()
 
         self.assertEqual(options["origins"], ["青岛港", "青岛胶东机场"])
-        self.assertEqual(options["destinations"], ["汉堡港", "法兰克福机场", "洛杉矶港"])
+        self.assertEqual(
+            options["destinations"],
+            ["印度那瓦舍瓦港", "印度金奈港", "汉堡港", "法兰克福机场", "洛杉矶港"],
+        )
         self.assertEqual(options["route_types"], ["中转", "直航", "直达"])
-        self.assertEqual(len(options["routes"]), 3)
+        self.assertEqual(len(options["routes"]), 5)
 
     def test_customer_profile_changes_recommendation(self):
         haier = self.service.recommend(InquiryRequest(customer=HAIER))
         hisense = self.service.recommend(InquiryRequest(customer=HISENSE))
 
-        self.assertEqual(haier["recommendation"]["solution_type"], "均衡方案")
-        self.assertEqual(hisense["recommendation"]["solution_type"], "经济方案")
+        self.assertEqual(haier["recommendation"]["solution_type"], "方案优选")
+        self.assertEqual(hisense["recommendation"]["solution_type"], "方案优选")
         self.assertNotEqual(
             haier["recommendation"]["final_quote_amount"],
             hisense["recommendation"]["final_quote_amount"],
@@ -98,9 +101,34 @@ class MarketEngineTest(unittest.TestCase):
             preferred_route_type="直达",
         ))
 
-        self.assertEqual(result["recommendation"]["solution_type"], "时效方案")
         self.assertEqual(result["recommendation"]["supplier_name"], "汉莎货运青岛代理")
 
+    def test_india_solution_and_relationship_outweigh_lowest_purchase_price(self):
+        result = self.service.recommend(InquiryRequest(
+            customer=INDIA_RETAIL,
+            destination="印度那瓦舍瓦港",
+            requested_departure_date="2026-07-28",
+            max_transit_days=30,
+            target_amount=46000,
+        ))
+
+        top = result["candidates"][0]
+        lowest_price = max(result["candidates"], key=lambda item: item["price_score"])
+
+        self.assertEqual(top["solution_type"], "方案优选")
+        self.assertEqual(top["supplier_name"], "印度航线Mock船公司直营")
+        self.assertLess(top["price_score"], lowest_price["price_score"])
+        self.assertGreater(
+            top["relationship_advantage_score"],
+            lowest_price["relationship_advantage_score"],
+        )
+        self.assertEqual(top["relationship"]["tier"], "战略合作")
+        self.assertEqual(top["relationship"]["supplier_name"], "印度航线Mock船公司直营")
+        self.assertEqual(top["relationship"]["supplier_channel"], "船公司直营")
+        self.assertEqual(top["spot_space"]["status"], "可立即订舱")
+        self.assertEqual(top["spot_space"]["teu"], 18)
+        self.assertGreater(top["spot_space_score"], lowest_price["spot_space_score"])
+
     def test_sales_response_does_not_expose_supplier_cost(self):
         result = self.service.recommend(InquiryRequest(customer=HAIER))
         serialized = json.dumps(result, ensure_ascii=False)
@@ -115,7 +143,9 @@ class MarketEngineTest(unittest.TestCase):
 
         self.assertIsNotNone(audit)
         self.assertEqual(audit["candidate_count"], 3)
-        self.assertGreaterEqual(audit["rdf_assertions"], 9)
+        self.assertGreaterEqual(audit["rdf_assertions"], 20)
+        self.assertIn("business_explanation", audit)
+        self.assertIn("现舱", audit["business_explanation"]["selection_basis"])
 
 
 if __name__ == "__main__":

+ 21 - 10
tests/test_mock_data.py

@@ -21,30 +21,41 @@ class MockDataTest(unittest.TestCase):
         catalog = self.mock_data.catalog()
 
         self.assertEqual(catalog["summary"]["in_memory_triple_count"], 0)
-        self.assertGreaterEqual(catalog["summary"]["customer_count"], 7)
-        self.assertGreaterEqual(catalog["summary"]["quote_count"], 18)
-        self.assertGreaterEqual(catalog["summary"]["strategy_count"], 3)
+        self.assertGreaterEqual(catalog["summary"]["customer_count"], 9)
+        self.assertGreaterEqual(catalog["summary"]["quote_count"], 24)
+        self.assertGreaterEqual(catalog["summary"]["strategy_count"], 5)
+        self.assertTrue(any(row["destination"] == "印度那瓦舍瓦港" for row in catalog["routes"]))
+        india_direct = next(row for row in catalog["quotes"] if row["quote_no"] == "SQ-MOCK-IN-NHAVA-DIRECT-001")
+        self.assertEqual(india_direct["spot_space_status"], "可立即订舱")
+        self.assertEqual(india_direct["spot_space_teu"], 18)
 
     def test_generated_batch_immediately_joins_recommendation_pool(self):
         before = self.market.recommend(InquiryRequest(customer=HAIER))
         result = self.mock_data.generate(MockBatchRequest(
             origin="青岛港",
-            destination="洛杉矶港",
-            supplier_count=2,
-            customer_count=1,
+            destination="印度那瓦舍瓦港",
+            supplier_count=4,
+            customer_count=2,
+        ))
+        after = self.market.recommend(InquiryRequest(
+            customer=HAIER,
+            destination="印度那瓦舍瓦港",
+            requested_departure_date="2026-07-28",
+            max_transit_days=30,
+            target_amount=46000,
         ))
-        after = self.market.recommend(InquiryRequest(customer=HAIER))
 
         self.assertGreater(result["added_triples"], 100)
         self.assertEqual(result["catalog"]["summary"]["in_memory_triple_count"], result["added_triples"])
-        self.assertEqual(after["pipeline"]["discovered"], before["pipeline"]["discovered"] + 2)
-        self.assertEqual(result["catalog"]["summary"]["customer_count"], 8)
+        self.assertEqual(after["pipeline"]["discovered"], 7)
+        self.assertEqual(result["catalog"]["summary"]["customer_count"], 11)
+        self.assertTrue(any("印度航线Mock" in name for name in result["suppliers"]))
 
     def test_reset_discards_only_runtime_mock_data(self):
         baseline = self.repository.triple_count
         self.mock_data.generate(MockBatchRequest(
             origin="青岛港",
-            destination="汉堡港",
+            destination="印度金奈港",
             supplier_count=1,
             customer_count=1,
         ))

+ 1 - 0
tests/test_repository.py

@@ -20,6 +20,7 @@ class RepositoryTest(unittest.TestCase):
                 "ontology/global-shipment.ttl",
                 "ontology/ai-market-demo-extension.ttl",
                 "data/ai-market-demo.ttl",
+                "data/ai-market-india-mock.ttl",
                 "data/ai-market-scenarios.ttl",
                 "data/qingdao-export-sample.ttl",
             ],