diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9528b02 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +本项目遵循语义化版本。日期使用北京时间。 + +## [0.5.0] - 2026-08-13 + +### Added + +- 合肥—蔚来六步实时典型博弈回放与共同随机数财政冲击分支; +- 联合产业基金实体、独立审核、分期承诺和资金池结算; +- 15%—35%财政可动用比例 × 联合基金开关敏感性; +- 首页实验报告中心,支持策略、消融、seed、失败案例和归档世界下钻; +- 演示、报告仓库和真实案例机制的自动化测试与复现命令。 + +### Changed + +- 产品文档、路线图和案例参数卡统一到v0.5实际能力; +- Web端版本和API健康信息升级到0.5.0; +- 财政成本与外部股权资金来源分账展示,避免把联合基金误算成本级财政现金。 + +### Fixed + +- 移除Web入口未使用的分支/干预函数,修复GitHub Actions ESLint失败; +- 联合基金回合只在有外部融资目标时生成,保持通用世界的协商协议兼容。 + +## [0.4.0] - 2026-08-12 + +- P2.2—P2.4反事实、解释、材料闭环; +- 统一政企双边协商与政府内部协调; +- 合肥—蔚来公开案例参数卡和独立可视化实验报告。 diff --git a/Makefile b/Makefile index 6223022..c892744 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,22 @@ -.PHONY: install api demo test lint web-dev web-build +.PHONY: install api demo demo-stack test lint web-dev web-build web-check verify install: python3 -m pip install -e '.[dev]' api: - uvicorn insidegov.api:app --reload --port 8000 + uv run uvicorn insidegov.api:app --reload --port 8000 demo: - PYTHONPATH=src python3 -m insidegov.cli run --quarters 16 + uv run insidegov demo --seed 42 --fiscal-multiplier 0.5 + +demo-stack: + ./scripts/start-demo.sh test: - PYTHONPATH=src python3 -m pytest -q + uv run pytest -q lint: - ruff check src tests + uv run ruff check src tests web-dev: cd apps/web && npm run dev @@ -21,3 +24,7 @@ web-dev: web-build: cd apps/web && npm run build +web-check: + cd apps/web && npm run lint && npm run typecheck && npm test && npm run build + +verify: lint test web-check diff --git a/README.md b/README.md index ed083ab..b2c19d0 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,11 @@ InsideGov 是一个面向政企互动研究与政策演示的多智能体实验 推荐使用 `uv` 安装与运行: ```bash +# 五分钟评委演示:实时生成基线、反事实、审计链和敏感性结果 +uv run insidegov demo --seed 42 --fiscal-multiplier 0.5 +# 真实案例校准与敏感性 +uv run insidegov case-hefei-nio --seed 42 --quarters 16 +uv run insidegov case-hefei-nio-sensitivity --seed 42 --quarters 16 uv run insidegov run --quarters 16 uv run insidegov compare uv run insidegov matrix --no-llm @@ -55,6 +60,10 @@ npm install npm run dev ``` +Web 端要求 Node.js 22 或更高版本。也可以运行 `make demo-stack` 同时启动 API 与前端, +然后在首页点击“进入五分钟演示”。演示使用确定性认知层,断网时也能完成,不消耗 +模型 Token。详细讲解词和验收步骤见 [docs/DEMO.md](docs/DEMO.md)。 + 默认使用确定性认知层。需使用 DeepSeek 时,复制 `.env.example` 的变量到本地 `.env`,将凭据放在 `DEEPSEEK_API_KEY`,然后在控制台“研究说明”中创建 LLM 世界。密钥不应提交到 Git。 ## 仓库结构 @@ -79,6 +88,10 @@ tests/ 可复现性和关键约束测试 - DeepSeek OpenAI 兼容接口,支持 `deepseek-v4-flash` / `deepseek-v4-pro`,异常时单步自动降级; - 四类 Agent 独立校准任务;确定性与两种 DeepSeek 的多种子矩阵;五组机制消融;均值、方差和失败案例报告; - 私有信息、内部治理、信用扩散、供应链溢出均进入状态转移公式;消融会改变入园门槛、履约数和集群规模,不只是更改提示词。 +- **五分钟典型博弈回放**:从公开事实与参数来源,到企业表达、政府内部审核、联合基金、分期合同、跨期履约和财政冲击反事实;所有 ID 和结果均由本次运行生成,不是前端预写结局。 +- **实验报告中心**:策略与消融均值、单个 seed、失败/降级原因和归档世界下钻,支持 JSON 导出。 +- **P2 产品闭环**:历史季度复制、自然语言干预人工确认、证据约束的 Agent 访谈、HTML/JSON 自动报告、文本材料参数抽取与溯源。 +- **真实案例校准**:合肥—蔚来公开参数卡,本级财政与三类联合产业基金分账结算,以及15%—35%财政空间敏感性。 - **人才对接场景**:企业需求向量化表达、政策工具包与语言模式(官话/人话)设计、人才解读协商(理解度/信任/覆盖度)、可选平台翻译撮合、合同分期兑现与知识外溢,以及 2x2 反事实矩阵。 - **政企协商机制实验室**:企业双层需求(真实/表达错位)与政府信念分离、七种协商机制严格反事实对比、理解差距与语义/激励对齐度量、低质项目签约前识别(veto)、分阶段履约结算,以及多种子矩阵(均值/方差 + 帕累托前沿 + H1-H7 假设检验 + 语言探针)。 @@ -94,4 +107,5 @@ tests/ 可复现性和关键约束测试 代码采用 MIT License。提交贡献前请阅读 [CONTRIBUTING.md](CONTRIBUTING.md) 和 [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md)。书籍 PDF 等受版权保护材料不会纳入公开仓库。 -公开前可先运行 `uv run pytest` 和 `uv run ruff check .`。`.env`、`.insidegov/`、本地检查点、运行日志与参考文献 PDF 均已默认排除。如果密钥曾在聊天、终端或截图中暴露,应在开源前到服务商控制台轮换;仅从 Git 删除它不等于失效。 +公开前可运行 `make verify`,一次检查 Python 测试/Ruff 与 Web lint/typecheck/test/build。 +`.env`、`.insidegov/`、本地检查点、运行日志与参考文献 PDF 均已默认排除。如果密钥曾在聊天、终端或截图中暴露,应在开源前到服务商控制台轮换;仅从 Git 删除它不等于失效。 diff --git a/apps/web/README.md b/apps/web/README.md index f14fa39..43d30d3 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -1,6 +1,6 @@ # InsideGov Console -InsideGov 的交互式推演控制台。页面数据全部来自 Python API,支持推进、干预、分支、恢复与导出。 +InsideGov 的交互式推演控制台。页面数据全部来自 Python API,支持推进、干预、分支、恢复与导出。v0.5 首页提供“进入五分钟演示”和“实验结果”两个主入口:前者实时生成典型博弈与反事实,后者支持矩阵均值、seed、消融和失败案例下钻。 ```bash npm install @@ -9,4 +9,6 @@ npm run dev 启动前需在仓库根目录运行 `uv run uvicorn insidegov.api:app --reload`。如 API 不在 `http://localhost:8000`,设置 `NEXT_PUBLIC_INSIDEGOV_API_URL`。 +也可在仓库根目录运行 `make demo-stack` 一键启动前后端。确定性演示不需要API Key。 + 需要 Node.js 22.13 或更高版本。 diff --git a/apps/web/app/components/DemoReplay.tsx b/apps/web/app/components/DemoReplay.tsx new file mode 100644 index 0000000..a0efcf3 --- /dev/null +++ b/apps/web/app/components/DemoReplay.tsx @@ -0,0 +1,152 @@ +"use client"; + +import { useState } from "react"; +import { API_BASE, api, type DemoBundle, type World } from "../lib/api"; + +type Props = { + remember: (world: World) => void; + refreshList: () => Promise; +}; + +export function DemoReplay({ remember, refreshList }: Props) { + const [bundle, setBundle] = useState(null); + const [step, setStep] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + async function runDemo() { + setLoading(true); + setError(""); + try { + const result = await api.createDemo(); + setBundle(result); + setStep(0); + remember(result.baseline); + await refreshList(); + } catch (caught) { + setError(`演示生成失败:${String(caught)}`); + } finally { + setLoading(false); + } + } + + if (!bundle) { + return
+ LIVE · FIVE-MINUTE EVIDENCE CHAIN +

五分钟典型政企博弈回放

+

系统将现场运行合肥—蔚来校准世界:先推进共同历史,再从 Q3 复制基线与财政冲击分支,保持随机状态一致并运行至 Q16。

+
    +
  • 不是前端预写剧情,每次都会生成新的世界 ID 和完整快照。
  • +
  • 展示企业表达、政府内部会商、独立产业基金、条件承诺与规则结算。
  • +
  • 确定性模式无需网络和模型 Key,比赛现场可稳定复现。
  • +
+ + {error &&

{error}

} +
; + } + + const current = bundle.replay.steps[step]; + const meeting = bundle.replay.internal_negotiation; + const outward = bundle.replay.external_negotiation; + const comparison = bundle.comparison.delta; + + return
+
+ 实时规则运行 + {bundle.baseline.id} + {bundle.branch.id} + Seed {bundle.seed} · 共同祖先 Q{bundle.common_ancestor_quarter} +
+
    + {bundle.replay.steps.map((item, index) =>
  1. + +
  2. )} +
+ +
+
STEP {step + 1} / 6

{current.title}

+ {current.id === "world" &&
+
公开资料

合肥一般公共预算收入、企业资产与现金投入、12 万辆年产能、70 亿元历史股权投资均保留来源。

+
专家映射

可调度财力默认取财政收入 25%;供应链、行政能力和信用指数均明确标为映射,不冒充公开事实。

+
模型假设

就业容量和供应商进入窗口缺少企业级公开数据,报告中始终标注外推边界。

+
} + {current.id === "external" &&
+

企业表面诉求:{outward.stated_need}

+

政府追问:{outward.government_questions.join("、")}

+

认知更新:{compact(outward.belief_before)} → {compact(outward.belief_after)}

+

企业回应:{outward.enterprise_response} · {outward.enterprise_rationale}

+
} + {current.id === "internal" && <> +
+
招商局提案现金 {money(meeting.proposal_tools.subsidy)}本级股权 {money(meeting.proposal_tools.equity)}联合基金申请 {money(meeting.proposal_tools.external_equity)}
+ → +
财政局硬约束现金上限 {money(meeting.finance_tool_limits.subsidy)}本级股权上限 {money(meeting.finance_tool_limits.equity)}{meeting.finance_approved ? "直接通过" : "要求重组"}
+ → +
市领导协调后现金 {money(meeting.final_tools.subsidy)}总股权 {money(meeting.final_tools.total_equity_support)}{meeting.payment_schedule.length} 个条件性支付节点
+
+
+

演示世界私有真值审计

+ {bundle.replay.synthetic_private_audit.map((item) =>
+ {item.agent_id} · {item.provider} · {item.fallback ? "发生降级" : "原生执行"} +

私有信息:{compact(item.private_context_used)}

+

当时观察:{compact(item.observation)}

+

认知建议:{compact(item.llm_suggestion)}

+

规则修正:{compact(item.rule_adjustment)}

+

最终执行:{compact(item.executed_action)}

+
)} +
+ } + {current.id === "contract" && <> +
{bundle.replay.funding_partners.map((fund) =>
{fund.source_level}{fund.name}

独立风控阈值 {(fund.due_diligence_threshold * 100).toFixed(0)}%

)}
+
{bundle.replay.promises.map((promise) =>
{promise.funding_source_id ? "联合基金股权" : toolName(promise.item)}{money(promise.amount)}Q{promise.due_quarter} · {promise.condition}{promise.status}
)}
+ } + {current.id === "delivery" &&
+ {bundle.replay.causal_events.slice(0, 12).map((event, index) =>
{event.title}

{event.detail}

)} +
} + {current.id === "counterfactual" && <> +
+
基线 Q16{bundle.replay.baseline_outcome.cluster_size} 家集群{bundle.replay.baseline_outcome.total_employment.toLocaleString()} 个就业
+
Q4 财力减半{bundle.replay.branch_outcome.cluster_size} 家集群{bundle.replay.branch_outcome.total_employment.toLocaleString()} 个就业
+
规则产生的差异{signed(comparison.cluster_size)} 家{signed(comparison.total_employment)} 个就业
+
+

财力冲击 → 本级承诺延期 → 客观信用与企业感知下降 → 边缘供应商未达到进入门槛。差值是模型内部机制结果,不是现实政策因果效应。

+ + } +
+ +
+ + + + 导出分支 JSON + {step < bundle.replay.steps.length - 1 + ? + : } +
+
; +} + +function SensitivityTable({ bundle }: { bundle: DemoBundle }) { + return
+

财政空间 × 联合基金敏感性

+
可调度财力联合基金总股权支持与历史 70 亿差距Q16 集群
+ {bundle.sensitivity.runs.map((run) =>
+ {(run.available_budget_share * 100).toFixed(0)}% + {run.joint_investment ? "开启" : "关闭"} + {money(run.total_equity_support)} + {money(run.historical_equity_gap)} + {run.cluster_size} 家 +
)} +
; +} + +function compact(value: unknown) { + const text = JSON.stringify(value); + return text.length > 420 ? `${text.slice(0, 417)}…` : text; +} + +function money(value?: number) { return value == null ? "—" : `${value.toFixed(2)} 亿`; } +function signed(value?: number) { return value == null ? "—" : `${value > 0 ? "+" : ""}${value.toLocaleString()}`; } +function toolName(item: string) { return ({ subsidy: "现金补贴", equity: "本级股权", credit_support_cost: "信贷支持成本" } as Record)[item] ?? item; } diff --git a/apps/web/app/components/ReportCenter.tsx b/apps/web/app/components/ReportCenter.tsx new file mode 100644 index 0000000..7523b46 --- /dev/null +++ b/apps/web/app/components/ReportCenter.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { API_BASE, api, type ExperimentReport, type ExperimentReportSummary, type ExperimentRun, type World } from "../lib/api"; + +export function ReportCenter({ remember }: { remember: (world: World) => void }) { + const [reports, setReports] = useState([]); + const [selectedId, setSelectedId] = useState(""); + const [report, setReport] = useState(null); + const [group, setGroup] = useState<"strategy" | "ablation">("strategy"); + const [run, setRun] = useState(null); + const [error, setError] = useState(""); + + useEffect(() => { + let alive = true; + void api.listExperimentReports().then((items) => { + if (!alive) return; + setReports(items); + const preferred = items.find((item) => item.kind === "p1_matrix") ?? items[0]; + if (preferred) setSelectedId(preferred.id); + }).catch((caught) => setError(String(caught))); + return () => { alive = false; }; + }, []); + + useEffect(() => { + if (!selectedId) return; + let alive = true; + void api.getExperimentReport(selectedId).then((result) => { + if (alive) setReport(result); + }).catch((caught) => setError(String(caught))); + return () => { alive = false; }; + }, [selectedId]); + + const summaries = group === "strategy" ? report?.strategy_summary ?? [] : report?.ablation_summary ?? []; + const runs = group === "strategy" ? report?.strategy_runs ?? [] : report?.ablation_runs ?? []; + const maxEmployment = Math.max(1, ...summaries.map((item) => metric(item, "total_employment", "employment"))); + + function selectReport(id: string) { + setSelectedId(id); + setReport(null); + setRun(null); + } + + async function openRunWorld(selected: ExperimentRun) { + if (!report || !selected.world_available) return; + remember(await api.getExperimentWorld(report.report_id, selected.world_id)); + } + + return
+ EXPERIMENT EVIDENCE CENTER +

可视化实验报告与 seed 下钻

+
+ +
+ {report && 导出 JSON} +
+ {error &&

{error}

} + {!report &&

正在读取实验报告…

} + {report && <> +
{String(report.generated_at ?? "发布内置报告")}{runs.length} 个可下钻运行{report.failure_cases.length} 个失败案例
+
{summaries.map((item, index) => { + const employment = metric(item, "total_employment", "employment"); + const cluster = metric(item, "cluster_size", "cluster"); + const label = item.name ?? item.strategy ?? item.mechanism ?? item.id ?? `实验 ${index + 1}`; + return ; + })}
+ {runs.length > 0 ?
{runs.map((item) => )}
:

内置发布报告只包含聚合数据。运行 uv run insidegov matrix 后,这里会自动出现每个 seed 及其完整世界。

} + {run &&
+
SINGLE RUN

{run.strategy} · Seed {run.seed}

{run.successful ? "成功样本" : "失败样本"}
+

选址{run.selected_city ?? "未落地"}

就业{run.total_employment}

集群{run.cluster_size}

兑现{run.fulfilled_promises}

信用{(run.average_credibility * 100).toFixed(1)}%

方差来源Seed {run.seed}

+ +
} +

失败与降级案例

{report.failure_cases.length ? report.failure_cases.map((item, index) =>
{item.strategy} · Seed {item.seed}{item.stage}

{item.reason}

) :

本报告没有失败记录。

}
+ } +
; +} + +function metric(item: ExperimentReport["strategy_summary"][number], key: string, fallback: "employment" | "cluster") { + return item.metrics?.[key]?.mean ?? item[fallback] ?? 0; +} + +function sampleSize(item: ExperimentReport["strategy_summary"][number]) { + return item.metrics?.total_employment?.n ?? item.seeds ?? item.successful ?? 0; +} diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css index d403301..364f015 100644 --- a/apps/web/app/globals.css +++ b/apps/web/app/globals.css @@ -16,9 +16,10 @@ button:disabled { opacity:.5;cursor:not-allowed!important; } .dashboard-grid { display:grid;grid-template-columns:290px minmax(610px,1fr) 330px;gap:12px;padding:12px;max-width:1720px;margin:0 auto }.panel { background:linear-gradient(150deg,#122226,#0d191c);border:1px solid var(--line);box-shadow:0 12px 40px #0003 }.panel-heading { justify-content:space-between;padding:16px 17px;border-bottom:1px solid var(--line) }.panel-heading small { color:var(--cyan);font:9px var(--font-geist-mono);letter-spacing:.16em }.panel-heading h2 { font-size:15px;margin:4px 0 0 }.panel-heading>span,.legend { color:var(--muted);font-size:10px }.legend { display:flex;gap:12px }.left-rail { align-self:start }.city-list { padding:9px }.city-card { padding:13px;border:1px solid transparent;border-bottom-color:#203438;background:#0d1a1d;margin-bottom:7px }.city-card.selected { border-color:#3aa7a4;background:linear-gradient(110deg,#153033,#102024) }.city-head { gap:9px }.city-mark { width:32px;height:32px;display:grid;place-items:center;border-radius:4px;font-family:serif;font-size:17px;background:#233 }.city-mark.amber{color:var(--amber);background:#382e20}.city-mark.cyan{color:var(--cyan);background:#173b3b}.city-mark.violet{color:#ac9ce5;background:#2b2740}.city-head h3{font-size:14px;margin:0}.city-head p{font-size:9px;color:var(--muted);margin:3px 0 0}.city-head b{margin-left:auto;color:#071315;background:var(--cyan);font-size:9px;padding:4px 7px}.mini-metrics{display:grid;grid-template-columns:1fr 1fr;gap:5px;margin:12px 0 10px}.mini-metrics span{font-size:9px;color:var(--muted);background:#0a1517;padding:7px}.mini-metrics strong{display:block;color:var(--ink);font:12px var(--font-geist-mono);margin-top:3px}.city-card label{display:grid;grid-template-columns:1fr auto;gap:5px;font-size:9px;color:var(--muted);margin:8px 0}.city-card label em{font-style:normal;color:#b9c7ca}.city-card label i{grid-column:1/-1;height:3px;background:#26393d}.city-card label i b{display:block;height:100%;background:var(--cyan)}.offer{border-top:1px dashed #2b4146;margin-top:11px;padding-top:9px;font-size:9px;color:var(--muted)}.offer strong{float:right;color:var(--amber);font:12px var(--font-geist-mono)}.wide-button{width:calc(100% - 18px);margin:0 9px 10px;font-size:11px}.wide-button.accent{border-color:#477b79;color:var(--cyan);margin:12px 0 0;width:100%} .center-stage { display:grid;gap:12px;align-content:start }.metric-row { display:grid;grid-template-columns:repeat(4,1fr);gap:8px }.metric-card { border:1px solid var(--line);background:#101f22;padding:13px 14px;position:relative;overflow:hidden }.metric-card:after { content:"";position:absolute;top:0;left:0;right:0;height:2px;background:var(--cyan) }.metric-card small{display:block;color:var(--muted);font-size:9px}.metric-card strong{display:block;font:24px var(--font-geist-mono);margin:7px 0 4px}.metric-card span{font-size:9px;color:#82989c}.metric-card.amber:after{background:var(--amber)}.metric-card.green:after{background:var(--green)}.metric-card.red:after{background:var(--red)}.metric-card.violet:after{background:#a28be2}.world-map{min-height:440px}.map-canvas{height:384px;position:relative;overflow:hidden;background:radial-gradient(circle at 56% 48%,#183b3d 0,#0b181b 38%,#081315 72%)}.grid-lines{position:absolute;inset:0;opacity:.28;background-image:linear-gradient(#284348 1px,transparent 1px),linear-gradient(90deg,#284348 1px,transparent 1px);background-size:42px 42px;transform:perspective(500px) rotateX(58deg) scale(1.7);transform-origin:center bottom}.map-city,.cluster-core{position:absolute;z-index:4;display:flex;flex-direction:column;align-items:center}.map-city span{width:38px;height:38px;border:1px solid #5b777b;background:#142a2e;display:grid;place-items:center;border-radius:50%;font-family:serif}.map-city b,.cluster-core b{font-size:11px;margin-top:7px}.map-city small,.cluster-core small{font-size:8px;color:var(--muted);margin-top:3px}.map-hai{left:13%;top:20%}.map-yun{right:12%;top:22%}.cluster-core{left:50%;top:48%;transform:translate(-50%,-50%)}.anchor{font-size:38px;color:var(--amber);text-shadow:0 0 18px #e2b56a88;z-index:3}.rings i{position:absolute;border:1px solid #55bbb84d;border-radius:50%;left:50%;top:39%;transform:translate(-50%,-50%);animation:pulse 3s infinite}.rings i:nth-child(1){width:110px;height:110px}.rings i:nth-child(2){width:190px;height:190px;animation-delay:.6s}.rings i:nth-child(3){width:270px;height:270px;animation-delay:1.2s}@keyframes pulse{50%{opacity:.3;transform:translate(-50%,-50%) scale(1.06)}}.supplier{position:absolute;width:7px;height:7px;background:var(--cyan);border-radius:50%;box-shadow:0 0 8px var(--cyan)}.s1{transform:translate(-88px,-45px)}.s2{transform:translate(82px,-40px)}.s3{transform:translate(-110px,15px)}.s4{transform:translate(105px,22px)}.s5{transform:translate(-68px,67px)}.s6{transform:translate(64px,73px)}.s7{transform:translate(-135px,-76px)}.s8{transform:translate(139px,-70px)}.s9{transform:translate(-148px,65px)}.s10{transform:translate(151px,72px)}.flow{position:absolute;height:1px;background:linear-gradient(90deg,transparent,var(--cyan),transparent);width:35%;top:39%;opacity:.45}.flow-a{left:17%;transform:rotate(17deg)}.flow-b{right:17%;transform:rotate(-18deg)}.map-caption{position:absolute;bottom:17px;left:17px;right:17px;border-left:2px solid var(--cyan);padding:8px 11px;background:#0a1517cc;display:flex;align-items:center;gap:12px}.map-caption strong{font-size:11px;color:var(--cyan)}.map-caption span{font-size:10px;color:#9aadb1}.chart-panel{height:250px}.alert{color:var(--amber)!important;border:1px solid #58472c;padding:4px 7px}.chart-area{height:145px;display:flex;align-items:flex-end;gap:5px;padding:18px 18px 0;border-bottom:1px solid #2b3e43}.chart-area button{height:100%;flex:1;border:0;background:transparent;position:relative;display:flex;align-items:flex-end;justify-content:center;gap:2px;cursor:pointer}.chart-area button i{display:block;width:35%;opacity:.7;min-height:1px}.chart-area button .demand{background:var(--cyan)}.chart-area button .capacity{background:var(--amber)}.chart-area button span{position:absolute;bottom:-17px;color:#597076;font:8px var(--font-geist-mono)}.chart-area button.active{background:#20343880}.chart-area button.active i{opacity:1}.chart-legend{padding:25px 17px 0;gap:14px;font-size:9px;color:var(--muted)}.chart-legend i{width:8px;height:8px;display:inline-block;margin-right:5px}.cyan-box{background:var(--cyan)}.amber-box{background:var(--amber)}.chart-legend strong{margin-left:auto;color:#6e858a;font-weight:400} .right-rail{display:grid;gap:12px;align-content:start}.experiment-panel>p{font-size:10px;line-height:1.6;color:var(--muted);margin:14px 16px}.branch-list{padding:0 11px}.branch-list button{width:100%;display:flex;align-items:center;text-align:left;border:1px solid transparent;background:#0c181a;color:var(--ink);padding:9px;margin-bottom:6px;cursor:pointer}.branch-list button.active{border-color:var(--cyan);background:#152d30}.branch-list button>i{width:25px;height:25px;border:1px solid #3a5055;display:grid;place-items:center;font-style:normal;font:10px var(--font-geist-mono);margin-right:9px}.branch-list button.active>i{background:var(--cyan);color:#071315}.branch-list span{display:flex;flex-direction:column}.branch-list strong{font-size:11px}.branch-list small{font-size:8px;color:var(--muted);margin-top:3px}.branch-list b{margin-left:auto;color:#617a7f}.event-panel{height:500px}.events{padding:7px 15px;max-height:438px;overflow:auto}.events article{display:grid;grid-template-columns:31px 1fr;gap:10px;border-left:1px solid #294046;padding:9px 0 9px 10px;position:relative}.events article:before{content:"";position:absolute;width:6px;height:6px;border-radius:50%;background:#5a7075;left:-4px;top:15px}.events time{font:9px var(--font-geist-mono);color:#71878c}.events small{font-size:8px;color:var(--cyan)}.events h3{font-size:10px;margin:3px 0}.events p{font-size:8px;color:var(--muted);margin:0;line-height:1.5}.events article.success:before{background:var(--green)}.events article.warning:before{background:var(--amber)}.events article.danger:before{background:var(--red)} -.intervention-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:5px;padding:0 11px 12px}.intervention-grid button,.intervention-grid a{border:1px solid #3b4c50;background:#0c181a;color:var(--muted);font-size:8px;padding:8px 2px;cursor:pointer;text-align:center;text-decoration:none}.intervention-grid button:hover,.intervention-grid a:hover{color:var(--amber);border-color:var(--amber)} +.intervention-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:5px;padding:0 11px 12px}.intervention-grid button,.intervention-grid a{border:1px solid #3b4c50;background:#0c181a;color:var(--muted);font-size:8px;padding:8px 2px;cursor:pointer;text-align:center;text-decoration:none}.intervention-grid button:hover,.intervention-grid a:hover{color:var(--amber);border-color:var(--amber)} .modal textarea,.modal input{width:100%;box-sizing:border-box;background:#091315;border:1px solid #31474c;color:var(--ink);padding:10px;margin:8px 0;font:11px var(--font-geist-mono);resize:vertical}.modal article{margin-top:12px}.audit-list b{color:var(--cyan)} .trace-drawer{margin:0 12px 12px;max-width:1696px;margin-inline:auto}.trace-grid{padding:9px}.trace-grid article{display:grid;grid-template-columns:35px 135px 1.1fr 1.4fr 85px 120px;align-items:center;gap:12px;padding:11px;border-bottom:1px solid #203438}.trace-grid small{font-size:8px;color:var(--muted)}.trace-grid h3{font-size:11px;margin:3px 0}.trace-grid p{font-size:9px;margin:3px 0;color:#9fb0b4}.trace-no{font:11px var(--font-geist-mono);color:#496167}.trace-grid>article>strong{font:13px var(--font-geist-mono);color:var(--amber)}.trace-grid button{border:0;background:transparent;color:var(--cyan);font-size:9px;cursor:pointer}footer{height:42px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:center;padding:0 22px;color:#50676c;font-size:9px;max-width:1720px;margin:auto} .empty-chart{width:100%;align-self:center;text-align:center;color:var(--muted);font-size:11px}.modal-backdrop{position:fixed;inset:0;z-index:60;background:#020708d9;display:grid;place-items:center;padding:40px}.modal{width:min(760px,90vw);max-height:82vh;padding:28px;position:relative;overflow:auto}.modal>small{color:var(--cyan);font:9px var(--font-geist-mono);letter-spacing:.16em}.modal>h2{margin:7px 0 18px}.modal>p{color:#a0b0b4;line-height:1.7;font-size:12px}.modal-close{position:absolute;right:14px;top:12px;background:transparent;border:0;color:var(--muted);font-size:24px;cursor:pointer}.modal-list{display:grid;gap:8px}.modal-list article{background:#0a1517;border:1px solid var(--line);padding:13px;display:grid;grid-template-columns:repeat(3,1fr);gap:7px}.modal-list article strong,.modal-list article p{grid-column:1/-1}.modal-list article span{font-size:10px;color:#a7b9bd}.modal-list article p{font-size:10px;color:var(--muted);margin:3px 0}.settings{display:grid;grid-template-columns:1fr 1fr;gap:12px}.settings label{display:grid;gap:7px;color:var(--muted);font-size:10px}.settings select{background:#0b181a;border:1px solid var(--line);color:var(--ink);padding:10px}.hint{color:var(--amber)!important;font-size:10px!important} .tranches{grid-column:1/-1;display:flex;flex-wrap:wrap;gap:5px;border-top:1px dashed var(--line);padding-top:8px}.tranches i{font-style:normal;font-size:8px;color:var(--cyan);background:#102427;padding:5px 7px} +.demo-banner{max-width:1696px;margin:12px auto 0;border:1px solid #4f6653;background:linear-gradient(100deg,#142723,#0d1c1d 70%);min-height:72px;padding:12px 18px;display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:10px}.demo-banner div{display:grid;gap:3px}.demo-banner small{color:var(--green);font:9px var(--font-geist-mono);letter-spacing:.14em}.demo-banner strong{font-size:14px}.demo-banner span{color:var(--muted);font-size:9px}.demo-banner button{border:1px solid #42675a;background:#112723;color:var(--green);padding:10px 15px;cursor:pointer}.demo-banner button:first-of-type{background:var(--green);color:#07130f;font-weight:700}.modal-wide{width:min(1160px,94vw);max-height:90vh}.demo-intro{max-width:720px;margin:auto}.demo-intro ul{padding-left:20px;color:#9eb0b5;font-size:11px;line-height:1.8}.error-text{color:#ff9f97!important}.demo-replay{display:grid;gap:15px}.replay-proof{display:flex;align-items:center;gap:8px;flex-wrap:wrap;border:1px solid #31554b;background:#0b1b18;padding:10px}.replay-proof span{color:var(--green);font-weight:700;font-size:10px}.replay-proof code{font-size:9px;color:#9fc0b4;background:#071310;padding:5px 7px}.replay-proof b{margin-left:auto;color:var(--muted);font-size:9px}.replay-steps{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(6,1fr);gap:5px}.replay-steps button{width:100%;height:58px;border:1px solid var(--line);background:#0a1618;color:#71868b;display:flex;align-items:center;text-align:left;gap:7px;padding:8px;cursor:pointer}.replay-steps i{width:24px;height:24px;border:1px solid #34494e;border-radius:50%;display:grid;place-items:center;font-style:normal;font-size:9px}.replay-steps span{font-size:9px}.replay-steps small{display:block;margin-top:3px;color:#52676c}.replay-steps .active button{border-color:var(--cyan);color:var(--cyan);background:#11272a}.replay-steps .done button{border-color:#31544a;color:var(--green)}.replay-stage{min-height:440px;background:#091416;border:1px solid var(--line);padding:18px;overflow:auto}.replay-stage>header{border-bottom:1px solid var(--line);margin-bottom:14px}.replay-stage>header small{font:9px var(--font-geist-mono);color:var(--cyan)}.replay-stage>header h3{margin:5px 0 13px}.replay-grid,.fund-grid,.comparison-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.replay-grid article,.fund-grid article,.comparison-cards article{border:1px solid var(--line);background:#0d1c1f;padding:14px}.replay-grid strong,.fund-grid strong,.comparison-cards strong{display:block;color:var(--amber);font-size:14px}.replay-grid p,.fund-grid p{color:var(--muted);font-size:10px;line-height:1.7}.evidence-stack{display:grid;gap:8px}.evidence-stack p{margin:0;border-left:2px solid var(--cyan);background:#0d1b1e;padding:12px;font-size:11px;line-height:1.7;color:#9eafb3}.offer-chain{display:grid;grid-template-columns:1fr 24px 1fr 24px 1fr;gap:7px;align-items:center}.offer-chain>article{border:1px solid var(--line);background:#0d1d20;padding:12px;display:grid;gap:6px}.offer-chain>article small{color:var(--cyan)}.offer-chain>article strong{font-size:12px}.offer-chain>article span{font-size:9px;color:var(--muted)}.offer-chain>b{text-align:center;color:var(--amber)}.private-audit{margin-top:14px}.private-audit h4,.sensitivity-table h4{font-size:11px;color:var(--amber)}.private-audit details{border:1px solid var(--line);background:#0b181a;margin:6px 0;padding:10px}.private-audit summary{cursor:pointer;color:#a8babd;font-size:10px}.private-audit p{font-size:9px;color:var(--muted);overflow-wrap:anywhere}.fund-grid article small{display:block;color:var(--cyan);font-size:8px;margin-bottom:7px}.fund-grid article strong{color:var(--ink);font-size:11px}.promise-table{margin-top:12px;border:1px solid var(--line)}.promise-table>div{display:grid;grid-template-columns:1fr 100px 1.4fr 80px;gap:8px;padding:8px 11px;border-bottom:1px solid var(--line);align-items:center;font-size:9px}.promise-table b{color:var(--amber)}.promise-table em{font-style:normal;text-transform:uppercase;color:var(--green)}.event-chain{display:grid;grid-template-columns:repeat(2,1fr);gap:7px}.event-chain article{display:grid;grid-template-columns:35px 1fr;gap:9px;border:1px solid var(--line);background:#0c191b;padding:10px}.event-chain time{color:var(--cyan);font:9px var(--font-geist-mono)}.event-chain strong{font-size:10px}.event-chain p{font-size:9px;color:var(--muted);margin:5px 0 0}.comparison-cards article small{display:block;color:var(--muted);font-size:9px}.comparison-cards article strong{font:19px var(--font-geist-mono);margin:8px 0}.comparison-cards article span{font-size:10px}.comparison-cards .warning{border-color:#6d5932}.boundary{border-left:2px solid var(--amber);background:#1c1911;color:#b8aa8e;padding:11px;font-size:10px;line-height:1.7}.sensitivity-table{margin-top:14px}.table-row{display:grid;grid-template-columns:repeat(5,1fr);border-bottom:1px solid var(--line);padding:7px 10px;font-size:9px}.table-row.header{color:var(--muted);background:#102024}.table-row b{color:var(--amber)}.replay-actions{display:flex;gap:7px;height:auto;padding:0;border:0;max-width:none}.replay-actions button,.replay-actions a{border:1px solid #355056;background:#0c191b;color:#9fb2b6;padding:9px 12px;cursor:pointer;font-size:9px}.replay-actions .primary{margin-left:auto;background:var(--cyan);border-color:var(--cyan);color:#071315;font-weight:700}.report-center{display:grid;gap:12px}.report-center>small{color:var(--cyan);font:9px var(--font-geist-mono);letter-spacing:.16em}.report-center>h2{margin:0}.report-toolbar{display:grid;grid-template-columns:1fr auto auto;gap:10px;align-items:end}.report-toolbar label{display:grid;gap:5px;color:var(--muted);font-size:9px}.report-toolbar select{background:#091517;border:1px solid var(--line);color:var(--ink);padding:9px}.report-toolbar div{display:flex}.report-toolbar button,.report-toolbar a{border:1px solid var(--line);background:#0b181a;color:var(--muted);padding:9px 11px;font-size:9px;cursor:pointer}.report-toolbar button.active{border-color:var(--cyan);color:var(--cyan)}.report-meta{display:flex;gap:10px;color:var(--muted);font-size:9px}.report-meta span{border:1px solid var(--line);padding:5px 8px}.summary-chart{display:grid;gap:6px}.summary-chart button{display:grid;grid-template-columns:150px 1fr 105px 120px;align-items:center;gap:10px;border:1px solid var(--line);background:#0b181a;color:var(--ink);padding:9px;text-align:left;cursor:pointer}.summary-chart button>span{font-size:10px}.summary-chart i{height:8px;background:#172b2f}.summary-chart i b{display:block;height:100%;background:linear-gradient(90deg,var(--cyan),var(--green))}.summary-chart strong{font:10px var(--font-geist-mono);color:var(--amber)}.summary-chart small{font-size:8px;color:var(--muted)}.seed-grid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px;max-height:180px;overflow:auto}.seed-grid button{display:grid;text-align:left;gap:3px;border:1px solid var(--line);background:#0b181a;color:var(--ink);padding:8px;cursor:pointer}.seed-grid button.active{border-color:var(--cyan);background:#11272a}.seed-grid button.failed{border-color:#693b37}.seed-grid strong{font-size:10px}.seed-grid span,.seed-grid small{font-size:8px;color:var(--muted)}.run-detail{border:1px solid #38565b;background:#0c1a1d;padding:12px}.run-detail header{display:flex;justify-content:space-between;align-items:center}.run-detail h3{margin:4px 0}.run-detail header span{font-size:9px;color:var(--green)}.run-detail>div{display:grid;grid-template-columns:repeat(6,1fr);gap:6px}.run-detail p{display:grid;color:var(--muted);font-size:8px;background:#091416;padding:7px}.run-detail p strong{color:var(--ink);font-size:11px;margin-top:4px}.run-detail>button{border:1px solid var(--cyan);background:#11282a;color:var(--cyan);padding:9px 12px;cursor:pointer}.failure-panel{border-top:1px solid var(--line);padding-top:10px}.failure-panel h3{font-size:11px}.failure-panel article{display:grid;grid-template-columns:180px 100px 1fr;gap:8px;font-size:9px;color:var(--muted);padding:7px;background:#191313;border-bottom:1px solid #472a27}.failure-panel article p{margin:0}.failure-panel>p{color:var(--muted);font-size:9px} @media(max-width:1250px){.dashboard-grid{grid-template-columns:270px 1fr 310px}.metric-card strong{font-size:20px}} diff --git a/apps/web/app/lib/api.ts b/apps/web/app/lib/api.ts index 85b408e..ffc99ba 100644 --- a/apps/web/app/lib/api.ts +++ b/apps/web/app/lib/api.ts @@ -3,6 +3,8 @@ export const API_BASE = process.env.NEXT_PUBLIC_INSIDEGOV_API_URL ?? "http://loc export type Offer = { subsidy: number; equity: number; + external_equity: number; + fund_allocations: Record; land_discount: number; credit_support: number; }; @@ -40,6 +42,16 @@ export type World = { phase: string; cities: Record; firms: Record; + investment_funds: Record; + promises: Array<{ + id: string; item: string; amount: number; due_quarter: number; condition: string; + status: string; paid_amount: number; delayed_quarters: number; + funding_source_id: string | null; + }>; events: Array<{ quarter: number; kind: string; title: string; detail: string; severity: string }>; traces: Array<{ id: string; actor_id: string; action: string; evidence: string[]; @@ -55,7 +67,7 @@ export type World = { proposal_tools: Record; finance_tool_limits: Record; final_tools: Record; - payment_schedule: Array<{ item: string; amount: number; due_offset: number; condition: string }>; + payment_schedule: Array<{ item: string; amount: number; due_offset: number; condition: string; funding_source_id?: string | null }>; turns: Array<{ actor_id: string; act: string; summary: string; amount?: number; approved?: boolean }>; }>; external_negotiations: Array<{ @@ -126,6 +138,75 @@ export type CandidateWorld = { }>; }; +export type DemoBundle = { + schema_version: string; generated_at: string; source: string; seed: number; + common_ancestor_quarter: number; + intervention: { quarter: number; target: string; operation: string; value: number }; + baseline: World; branch: World; + comparison: { baseline_world_id: string; branch_world_id: string; delta: Record }; + replay: { + selected_city_id: string; selected_city_name: string; + external_negotiation: World["external_negotiations"][number]; + internal_negotiation: World["negotiations"][number]; + synthetic_private_audit: Array<{ + audit_id: string; agent_id: string; private_context_used: Record; + observation: Record; llm_suggestion: Record; + rule_adjustment: Record; executed_action: Record; + rationale: string; reflection: string; provider: string; fallback: boolean; + }>; + funding_partners: Array; + promises: World["promises"]; + baseline_outcome: Metric; branch_outcome: Metric; + causal_events: World["events"]; + steps: Array<{ id: string; title: string; quarter: number }>; + }; + sensitivity: HefeiSensitivity; +}; + +export type HefeiSensitivity = { + case: string; seed: number; quarters: number; historical_equity_investment: number; + budget_shares: number[]; interpretation_boundary: string; + runs: Array<{ + available_budget_share: number; joint_investment: boolean; selected_city: string | null; + municipal_equity: number; external_equity: number; total_equity_support: number; + historical_equity_gap: number; fulfilled_promises: number; cluster_size: number; + total_employment: number; average_credibility: number; + }>; +}; + +export type ExperimentReportSummary = { + id: string; title: string; kind: string; source: string; generated_at: string | null; + strategy_runs: number; ablation_runs: number; failures: number; + drilldown_available: boolean; +}; + +export type ExperimentRun = { + strategy: string; seed: number; successful: boolean; selected_city: string | null; + fulfilled_promises: number; total_employment: number; total_committed_expenditure: number; + average_credibility: number; cluster_size: number; utilization: number; + world_id: string; world_available: boolean; +}; + +export type ExperimentMetricSummary = { + id?: string; name?: string; strategy?: string; mechanism?: string; + attempted?: number; successful?: number; success_rate?: number; + metrics?: Record; + employment?: number; employment_variance?: number; cluster?: number; + fulfilled_promises?: number; seeds?: number; +}; + +export type ExperimentReport = { + report_id: string; generated_at?: string; + report_summary: Partial; + configuration: Record; + strategy_summary: ExperimentMetricSummary[]; + ablation_summary: ExperimentMetricSummary[]; + strategy_runs: ExperimentRun[]; + ablation_runs: ExperimentRun[]; + failure_cases: Array<{ strategy: string; seed: number; stage: string; reason: string }>; + llm_quality?: Array>; +}; + async function request(path: string, init?: RequestInit): Promise { const response = await fetch(`${API_BASE}${path}`, { ...init, @@ -186,4 +267,11 @@ export const api = { seed: 42, }), }), + createDemo: (seed = 42, fiscalMultiplier = 0.5) => request("/demos/hefei-nio", { + method: "POST", body: JSON.stringify({ seed, fiscal_multiplier: fiscalMultiplier }), + }), + hefeiSensitivity: (seed = 42) => request(`/cases/hefei-nio/sensitivity?seed=${seed}`), + listExperimentReports: () => request("/experiment-reports"), + getExperimentReport: (id: string) => request(`/experiment-reports/${id}`), + getExperimentWorld: (reportId: string, worldId: string) => request(`/experiment-reports/${reportId}/worlds/${worldId}`), }; diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 0c45d6c..287de13 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,6 +1,8 @@ "use client"; import { useCallback, useEffect, useMemo, useState } from "react"; +import { DemoReplay } from "./components/DemoReplay"; +import { ReportCenter } from "./components/ReportCenter"; import { API_BASE, api, type AgentActionAudit, type CandidateWorld, type Capability, type InterventionPlan, type Interview, type Metric, type World } from "./lib/api"; const phaseNames: Record = { @@ -17,7 +19,7 @@ export default function Home() { const [error, setError] = useState(""); const [policyMode, setPolicyMode] = useState("deterministic"); const [model, setModel] = useState("deepseek-v4-flash"); - const [detail, setDetail] = useState<"negotiations" | "unified" | "trace" | "audit" | "counterfactual" | "interview" | "material" | "about" | null>(null); + const [detail, setDetail] = useState<"demo" | "reports" | "negotiations" | "unified" | "trace" | "audit" | "counterfactual" | "interview" | "material" | "about" | null>(null); const remember = useCallback((next: World) => { setWorld(next); @@ -72,22 +74,6 @@ export default function Home() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [playing, world?.quarter]); - async function createBranch() { - if (!world) return; - setBusy(true); - try { remember(await api.branch(world.id)); await refreshList(); } - finally { setBusy(false); } - } - - async function intervene(kind: string, target: string, value: number) { - if (!world) return; - setBusy(true); - try { - await api.intervene(world.id, kind, target, value); - remember(await api.step(world.id)); - } finally { setBusy(false); } - } - const latest = world?.history.at(-1); const cities = world ? Object.values(world.cities) : []; const selected = world?.selected_city_id ? world.cities[world.selected_city_id] : null; @@ -102,10 +88,16 @@ export default function Home() {
内
InsideGov政企互动推演场
{world.name}{world.id}
- +
{error &&
{error}
} +
+
COMPETITION DEMO · RULE-GENERATED从真实案例到反事实结果,六步验证博弈不是预写剧情合肥—蔚来公开参数卡 · 联合产业基金 · Agent 私有信息 · 跨期履约
+ + +
+
模拟时钟2030 Q{((world.quarter || 1) - 1) % 4 + 1}第 {world.quarter} / 16 季度
{["招商竞争", "承诺履约", "产业演化"].map((item, index) =>
{index + 1}{item}
)}
@@ -146,16 +138,16 @@ export default function Home() {
DECISION TRACE

可追溯决策链

{world.traces.slice(-4).reverse().map((trace, index) =>
0{index + 1}
{trace.actor_id}

{trace.action}

调用证据

{trace.evidence.join(" · ")}

内部权衡

{trace.constraints.join(" · ")}

{Object.values(trace.expected_effects)[0]?.toFixed?.(1) ?? "—"}
)}
-
InsideGov v0.3 · 合成世界,不构成现实政策预测{world.policy_mode === "llm" ? world.model_name : "确定性基线"} · Seed {world.seed} · 自动持久化
+
InsideGov v0.5 · 政策实验与机制探索,不构成现实政策预测{world.policy_mode === "llm" ? world.model_name : "确定性基线"} · Seed {world.seed} · 自动持久化
- {detail &&
{detail === "about" ? : detail === "negotiations" ? : detail === "unified" ? : detail === "audit" ? : detail === "counterfactual" ? : detail === "interview" ? : detail === "material" ? : }
} + {detail &&
{detail === "demo" ? : detail === "reports" ? : detail === "about" ? : detail === "negotiations" ? : detail === "unified" ? : detail === "audit" ? : detail === "counterfactual" ? : detail === "interview" ? : detail === "material" ? : }
} ); } @@ -168,8 +160,8 @@ function ChartBar({ point }: { point: Metric }) { return ; } -function Negotiations({ world }: { world: World }) { return <>INTERNAL GOVERNANCE

招商局提案 → 财政局否决 → 市领导协调

{world.negotiations.slice().reverse().map((item) =>
Q{item.quarter} · {world.cities[item.city_id].name}招商局:现金 {money(item.proposal_tools?.subsidy)},股权 {money(item.proposal_tools?.equity)}财政局:现金上限 {money(item.finance_tool_limits?.subsidy)},股权上限 {money(item.finance_tool_limits?.equity)}市领导:现金 {money(item.final_tools?.subsidy)} + 股权 {money(item.final_tools?.equity)}

{item.concerns.join(";")}

{item.payment_schedule?.map((row, index) => Q+{row.due_offset} · {toolName(row.item)} {row.amount.toFixed(2)} 亿 · {row.condition})}
)}
; } -function UnifiedNegotiations({ world }: { world: World }) { const internal = Object.fromEntries(world.negotiations.map((item) => [item.id, item])); return <>NESTED NEGOTIATION

企业表达 → 政府澄清 → 内部会商 → 企业回应

{world.external_negotiations.slice().reverse().map((item) => { const meeting = internal[item.internal_negotiation_id]; return
Q{item.quarter} · {world.cities[item.city_id].name} · {item.enterprise_response}

企业表面诉求:{item.stated_need}

政府追问:{item.government_questions.join("、") || "沿用已有理解"}

理解更新:{brief(item.belief_before)} → {brief(item.belief_after)}(信心 {(item.belief_confidence * 100).toFixed(0)}%)

{meeting && <>招商局:现金 {money(meeting.proposal_tools.subsidy)}+股权 {money(meeting.proposal_tools.equity)}财政硬上限:现金 {money(meeting.finance_tool_limits.subsidy)}+股权 {money(meeting.finance_tool_limits.equity)}领导协调:现金 {money(meeting.final_tools.subsidy)}+股权 {money(meeting.final_tools.equity)}}

企业回应:{item.enterprise_rationale}

{Object.keys(item.counter_terms).length > 0 &&

还价条件:{brief(item.counter_terms)}

}效用 {item.utility.toFixed(1)} / 最低门槛 {item.minimum_utility.toFixed(1)} · 内部记录 {item.internal_negotiation_id}
; })}
; } +function Negotiations({ world }: { world: World }) { return <>INTERNAL GOVERNANCE

招商局提案 → 产业基金审核 → 财政局否决 → 市领导协调

{world.negotiations.slice().reverse().map((item) =>
Q{item.quarter} · {world.cities[item.city_id].name}招商局:现金 {money(item.proposal_tools?.subsidy)},本级股权 {money(item.proposal_tools?.equity)}联合基金:独立审核 {money(item.proposal_tools?.external_equity)}财政局:现金上限 {money(item.finance_tool_limits?.subsidy)},本级股权上限 {money(item.finance_tool_limits?.equity)}最终:现金 {money(item.final_tools?.subsidy)} + 总股权 {money(item.final_tools?.total_equity_support)}

{item.concerns.join(";")}

{item.payment_schedule?.map((row, index) => Q+{row.due_offset} · {toolName(row.item)} {row.amount.toFixed(2)} 亿 · {row.condition})}
)}
; } +function UnifiedNegotiations({ world }: { world: World }) { const internal = Object.fromEntries(world.negotiations.map((item) => [item.id, item])); return <>NESTED NEGOTIATION

企业表达 → 政府澄清 → 内部会商 → 企业回应

{world.external_negotiations.slice().reverse().map((item) => { const meeting = internal[item.internal_negotiation_id]; return
Q{item.quarter} · {world.cities[item.city_id].name} · {item.enterprise_response}

企业表面诉求:{item.stated_need}

政府追问:{item.government_questions.join("、") || "沿用已有理解"}

理解更新:{brief(item.belief_before)} → {brief(item.belief_after)}(信心 {(item.belief_confidence * 100).toFixed(0)}%)

{meeting && <>招商局:现金 {money(meeting.proposal_tools.subsidy)}+本级股权 {money(meeting.proposal_tools.equity)}联合基金:{money(meeting.proposal_tools.external_equity)}财政硬上限:现金 {money(meeting.finance_tool_limits.subsidy)}+本级股权 {money(meeting.finance_tool_limits.equity)}领导协调:现金 {money(meeting.final_tools.subsidy)}+总股权 {money(meeting.final_tools.total_equity_support)}}

企业回应:{item.enterprise_rationale}

{Object.keys(item.counter_terms).length > 0 &&

还价条件:{brief(item.counter_terms)}

}效用 {item.utility.toFixed(1)} / 最低门槛 {item.minimum_utility.toFixed(1)} · 内部记录 {item.internal_negotiation_id}
; })}
; } function Traces({ world }: { world: World }) { return <>AUDIT LOG

完整决策链

{world.traces.slice().reverse().map((trace) =>
{trace.actor_id} · {trace.action}

{trace.evidence.join(";")}

{trace.outcome}
)}
; } function AuditTimeline({ world }: { world: World }) { const [quarter, setQuarter] = useState("all"); @@ -180,7 +172,7 @@ function AuditTimeline({ world }: { world: World }) { const visible = audits.filter((item) => (quarter === "all" || item.quarter === quarter) && (agent === "all" || item.agent_id === agent)); return <>P2.1 · AUDITABLE AGENTS

观察 → 建议 → 规则修正 → 执行 → 复盘

{visible.slice().reverse().map((item) => )}{!visible.length &&

当前筛选条件下暂无认知行动。

}
; } -function AuditCard({ item }: { item: AgentActionAudit }) { return
Q{item.quarter} · {item.agent_id} · {item.action_type}{item.provider}{item.fallback ? " · 已降级" : " · 原生执行"}

当时观察:{brief(item.observation)}

私有信息:{"redacted" in item.private_context_used ? `默认脱敏;使用字段 ${(item.private_context_used.fields_used ?? []).join("、")}` : "已记录"}

模型/策略建议:{brief(item.llm_suggestion)}

规则修正:{brief(item.rule_adjustment)}

最终执行:{brief(item.executed_action)}

理由:{item.rationale}

复盘:{item.reflection}

诊断记录 {item.diagnostics.length} 条 · {item.outcome}
; } +function AuditCard({ item }: { item: AgentActionAudit }) { const fields = item.private_context_used.fields_used; const fieldNames = Array.isArray(fields) ? fields.join("、") : ""; return
Q{item.quarter} · {item.agent_id} · {item.action_type}{item.provider}{item.fallback ? " · 已降级" : " · 原生执行"}

当时观察:{brief(item.observation)}

私有信息:{"redacted" in item.private_context_used ? `默认脱敏;使用字段 ${fieldNames}` : "已记录"}

模型/策略建议:{brief(item.llm_suggestion)}

规则修正:{brief(item.rule_adjustment)}

最终执行:{brief(item.executed_action)}

理由:{item.rationale}

复盘:{item.reflection}

诊断记录 {item.diagnostics.length} 条 · {item.outcome}
; } function brief(value: unknown) { const text = JSON.stringify(value); return text.length > 240 ? `${text.slice(0, 237)}…` : text; } function CounterfactualLab({ world, remember, refreshList }: { world: World; remember: (world: World) => void; refreshList: () => Promise }) { const [quarter, setQuarter] = useState(Math.max(0, world.quarter - 1)); @@ -199,4 +191,4 @@ function CounterfactualLab({ world, remember, refreshList }: { world: World; rem function InterviewLab({ world }: { world: World }) { const agents = Object.keys(world.action_audits.reduce((acc, item) => ({ ...acc, [item.agent_id]: true }), {} as Record)); const [agent, setAgent] = useState(agents[0] ?? "city_lin_finance"); const [quarter, setQuarter] = useState(Math.min(1, world.quarter)); const [question, setQuestion] = useState("为什么否决现金加码?"); const [result, setResult] = useState(null); return <>P2.3 · GROUNDED INTERVIEW

采访指定历史节点的 Agent