From 3890514896f57cd4b96d67400d271ef882c14362 Mon Sep 17 00:00:00 2001 From: Yige Date: Mon, 31 Aug 2026 01:22:53 -0400 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=B0=86=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E4=B9=A6=20Web=20=E7=A7=BB=E5=87=BA=E6=A0=B8=E5=BF=83=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93=20(#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: yige --- README.md | 5 + README.zh-CN.md | 5 + agent.json | 2 +- web/solver-report/.gitignore | 3 - web/solver-report/README.md | 72 - web/solver-report/README.zh-CN.md | 72 - web/solver-report/decision-tree.schema.json | 97 -- web/solver-report/package.json | 15 - .../public-release-attestation.schema.json | 56 - .../scripts/audit-screenshot-privacy.swift | 99 -- .../scripts/build-report-web.mjs | 882 ---------- .../scripts/create-privacy-contact-sheets.mjs | 75 - .../scripts/create-public-screenshots.mjs | 143 -- .../scripts/public-release-policy.mjs | 257 --- .../scripts/validate-deployed-report.mjs | 100 -- .../scripts/validate-report-web.mjs | 388 ----- .../validate-screenshot-privacy-ocr.mjs | 80 - .../scripts/verify-static-release.py | 66 - web/solver-report/source/index.html | 107 -- web/solver-report/source/report.css | 1430 ----------------- web/solver-report/source/report.js | 583 ------- .../tests/public-release-policy.test.mjs | 132 -- .../tests/source-contract.test.mjs | 137 -- 23 files changed, 11 insertions(+), 4795 deletions(-) create mode 100644 README.md create mode 100644 README.zh-CN.md delete mode 100644 web/solver-report/.gitignore delete mode 100644 web/solver-report/README.md delete mode 100644 web/solver-report/README.zh-CN.md delete mode 100644 web/solver-report/decision-tree.schema.json delete mode 100644 web/solver-report/package.json delete mode 100644 web/solver-report/public-release-attestation.schema.json delete mode 100644 web/solver-report/scripts/audit-screenshot-privacy.swift delete mode 100644 web/solver-report/scripts/build-report-web.mjs delete mode 100644 web/solver-report/scripts/create-privacy-contact-sheets.mjs delete mode 100644 web/solver-report/scripts/create-public-screenshots.mjs delete mode 100644 web/solver-report/scripts/public-release-policy.mjs delete mode 100644 web/solver-report/scripts/validate-deployed-report.mjs delete mode 100644 web/solver-report/scripts/validate-report-web.mjs delete mode 100644 web/solver-report/scripts/validate-screenshot-privacy-ocr.mjs delete mode 100644 web/solver-report/scripts/verify-static-release.py delete mode 100644 web/solver-report/source/index.html delete mode 100644 web/solver-report/source/report.css delete mode 100644 web/solver-report/source/report.js delete mode 100644 web/solver-report/tests/public-release-policy.test.mjs delete mode 100644 web/solver-report/tests/source-contract.test.mjs diff --git a/README.md b/README.md new file mode 100644 index 0000000..8e37180 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# DesireCore Core Agent + +This repository contains the core DesireCore Agent definition, persona, principles, and skills. [中文说明](./README.zh-CN.md). + +The Solver Report Web is maintained independently in [desirecore-agent/solver-report-web](https://github.com/desirecore-agent/solver-report-web), under the **desirecore-agent organization**. Web source, build tools, dependencies, and report evidence do not belong in this Agent repository or its bootstrap archive. The former Web-only history is preserved in the new repository's `archive/web-history` branch. diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..b46dc2f --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,5 @@ +# DesireCore 核心智能体 + +本仓库仅管理 DesireCore 核心智能体定义、人格、原则和技能。[English](./README.md)。 + +求解器验证书 Web 由 **desirecore-agent 组织**下的 [desirecore-agent/solver-report-web](https://github.com/desirecore-agent/solver-report-web) 独立管理。Web 源码、构建工具、依赖和报告证据不属于本智能体仓库或其 bootstrap 压缩包。原 Web 专属历史保留在新仓库的 `archive/web-history` 分支。 diff --git a/agent.json b/agent.json index 66725f7..68a5610 100644 --- a/agent.json +++ b/agent.json @@ -2,7 +2,7 @@ "name": "DesireCore", "$schema": "http://desirecore/schemas/agent-seed.json", "id": "7da73b7f-bb08-4e7b-a3cf-5d4af6e22c7f", - "version": "1.12.0", + "version": "1.12.1", "requiredClientVersion": "10.0.100", "description": "系统中枢调度器,负责任务分发、Agent 编排与全局状态监控", "author": "DesireCore Team", diff --git a/web/solver-report/.gitignore b/web/solver-report/.gitignore deleted file mode 100644 index 7b85b0e..0000000 --- a/web/solver-report/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -deployment-validation.json -validation.json -dist/ diff --git a/web/solver-report/README.md b/web/solver-report/README.md deleted file mode 100644 index 97099ff..0000000 --- a/web/solver-report/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Solver Report Web - -Version-controlled source and build tooling for the public Solver Agent Team evidence report. - -This directory owns the Web application, interaction behavior, build contract, validators, and deployment verification scripts. Scenario packs, runtime receipts, and original screenshots remain external build inputs so the core DesireCore Agent bootstrap does not grow by hundreds of megabytes. - -## Image viewer - -The screenshot viewer supports: - -- fit-to-window and 1:1 pixel modes; -- bounded zoom buttons and `+` / `-` / `0` / `1` keyboard shortcuts, including 1:1 source pixels on mobile; -- `Ctrl`/`Command` + wheel or trackpad pinch zoom around the pointer; -- double-click zoom and mouse/touch drag panning; -- two-pointer touch pinch zoom; -- an original-file download link and accessible live zoom status. - -Each newly opened image starts in fit mode. Closing and reopening the viewer does not retain stale zoom or pan state. - -## Human + Agent scenario decision trees - -Every scenario derives a structured decision tree from its Scenario Pack, accepted Run, and independent validation receipts. The tree shows: - -- the business facts, hard rules, and objective priorities confirmed by a human; -- Agent clarification, modeling, and compatible-engine solve or diagnosis steps; -- independent recomputation of variable domains, constraints, objectives, and evidence; -- fail-closed branches for missing facts, incomplete rule mapping, or failed verification; -- the observed delivery, conflict diagnosis, rejection, or recovery outcome. - -Tree data follows [`decision-tree.schema.json`](./decision-tree.schema.json) and binds the Scenario Pack, messages, session, governed tool receipts, result payload, OptimizationSpec, and validation-report hashes. The observed path comes only from settled summaries and receipts in the accepted Run; oracle data is used only to detect expectation drift. Optimization, validation-only, and recovery scenarios use different topologies, so the report never invents modeling or solve steps that did not occur. The page can switch between all branches and the observed path, and can download an SVG snapshot of the current scenario. UI code remains a generic renderer and contains no scenario-specific business tree. - -## Build - -The build consumes an evidence directory with `scenario-packs/`, `evidence/`, and `report-web/dist/` output. Rendering dependencies are resolved from a DesireCore application checkout. - -Public builds also require `evidence/public-release-attestation.json`. The [Draft-07 Schema](./public-release-attestation.schema.json) binds the review to the content roots of every public message and all 174 screenshots, together with the review methods, evidence ownership, and public-release verdict. Any message or screenshot change invalidates the old attestation and fails the build. - -The site consumes `evidence/screenshots/public-screenshot-plan.json`, a derivation plan whose `sourceSha256` values bind every public image to the immutable raw evidence. Raw screenshots containing local paths must never be used as public originals. - -```bash -export SOLVER_REPORT_INPUT_ROOT=/absolute/path/to/scenario-book -export DESIRECORE_APP_ROOT=/absolute/path/to/desirecore -node scripts/build-report-web.mjs -node scripts/validate-report-web.mjs -``` - -Optional environment variables: - -- `SOLVER_REPORT_OUTPUT_ROOT`: override the generated directory. To make recursive cleanup safe, it must remain below `/report-web/` and must not overlap the Agent source or DesireCore application checkout. -- `LATEST_PLATFORM_REGRESSION_FILE`: override the latest regression summary. -- `SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE`: override the public-release attestation path. -- `SOLVER_REPORT_VALIDATION_FILE`: override the validation report path. -- `REPORT_BASE_URL`: public URL used by `validate-deployed-report.mjs`. -- `SOLVER_REPORT_DEPLOYMENT_VALIDATION_FILE`: override deployment validation output. - -Run the source contract tests with `node --test tests/*.test.mjs`. - -Before signing a public-release attestation, review every original screenshot at readable resolution. Contact sheets are only a hash-labelled coverage index; they are not sufficient for reading small credentials. On macOS, run the original-resolution Apple Vision OCR gate as a second, independent check: - -```bash -export SOLVER_REPORT_SCREENSHOT_OCR_FILE=/private/tmp/solver-report-ocr.json -export SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE=/private/tmp/solver-report-ocr-validation.json -npm run privacy:ocr -``` - -The validator requires all 174 planned files in order, recomputes every source hash, and applies the same fail-closed sensitive-text policy to recognized text. OCR and full-resolution visual review are both required before updating the attestation. Review-only artifacts never enter the site output or Agent bootstrap. - -## Deployment boundary - -The builder projects only allowlisted public provenance fields and rejects local paths, email addresses, private URLs, common key formats, and named credentials. Deployment validation also requires `build.json#gates.publicReleasePrivacy=pass` and an attestation digest matching the manifest. - -Generate a new immutable release, verify every `integrity.json` entry before switching, and atomically update the server's `current` symlink. Never overwrite an existing release or relabel historical evidence as output from a newer platform commit. diff --git a/web/solver-report/README.zh-CN.md b/web/solver-report/README.zh-CN.md deleted file mode 100644 index 92e0309..0000000 --- a/web/solver-report/README.zh-CN.md +++ /dev/null @@ -1,72 +0,0 @@ -# 求解器验证书 Web - -这里版本化管理“求解器智能体团队验证书”的 Web 源码与构建工具。 - -本目录负责 Web 应用、交互行为、构建契约、静态验证器和部署验证脚本。场景包、运行回执和原始截图继续作为外部构建输入,避免让 DesireCore 核心 Agent 的 bootstrap 体积增加数百 MB。 - -## 图片查看器 - -截图预览支持: - -- 适应窗口与 1:1 原始像素模式; -- 有上下限的缩放按钮,以及 `+` / `-` / `0` / `1` 键盘快捷键(移动端也能达到截图的 1:1 原始像素); -- `Ctrl`/`Command` + 滚轮或触控板捏合,以指针位置为中心缩放; -- 双击缩放、鼠标/单指拖动平移; -- 双指触控捏合缩放; -- 原图下载入口和可访问的实时缩放状态。 - -每次打开新图片都从“适应窗口”开始;关闭后重新打开不会保留旧的缩放或平移状态。 - -## 人 + Agent 场景决策树 - -每个场景都会从 Scenario Pack、真实 Run 和独立验证回执派生一棵结构化决策树,展示: - -- 人类确认业务事实、硬规则和目标优先级; -- Agent 补问、建模以及调用兼容引擎求解或诊断; -- 独立验证者重新检查变量域、约束、目标和证据; -- 信息不足、规则未映射或校核失败时的 fail-closed 阻断分支; -- 实际运行最终到达的交付、冲突诊断、拒绝或恢复结果。 - -树数据遵循 [`decision-tree.schema.json`](./decision-tree.schema.json),并绑定 Scenario Pack、消息、session、工具回执、结果 payload、OptimizationSpec 和验证报告哈希。实际路径只由已接受 Run 中的 settled summary 与受治理回执决定,oracle 仅用于发现预期不一致;求解、仅验证和恢复场景使用不同拓扑,不为未发生的建模或求解步骤造假。页面可切换“完整分支/实际路径”,也可下载当前场景的 SVG 快照;视图代码只负责通用渲染,不包含具体场景业务树。 - -## 构建 - -构建输入目录需要包含 `scenario-packs/`、`evidence/`,产物默认写到 `report-web/dist/`。Markdown、公式和图片处理依赖从 DesireCore 应用 checkout 解析。 - -公网构建还必须提供 `evidence/public-release-attestation.json`。它按 [Draft-07 Schema](./public-release-attestation.schema.json) 绑定全部公开消息与 174 张截图的内容根哈希,并明确记录审查方法、证据归属和公开发布结论。消息或截图发生任何变化,旧证明都会失效,构建直接失败。 - -站点只消费 `evidence/screenshots/public-screenshot-plan.json`。该派生计划通过每张图的 `sourceSha256` 将公开脱敏图绑定到不可变原始证据;含本地路径的原始截图绝不能直接作为公网原图。 - -```bash -export SOLVER_REPORT_INPUT_ROOT=/absolute/path/to/scenario-book -export DESIRECORE_APP_ROOT=/absolute/path/to/desirecore -node scripts/build-report-web.mjs -node scripts/validate-report-web.mjs -``` - -可选环境变量: - -- `SOLVER_REPORT_OUTPUT_ROOT`:覆盖生成路径;为避免递归清理误伤,它必须位于 `/report-web/` 的子目录内,且不得与 Agent 源码或 DesireCore 应用目录重叠。 -- `LATEST_PLATFORM_REGRESSION_FILE`:覆盖最新回归摘要路径。 -- `SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE`:覆盖公开发布证明路径。 -- `SOLVER_REPORT_VALIDATION_FILE`:覆盖静态验证结果路径。 -- `REPORT_BASE_URL`:`validate-deployed-report.mjs` 使用的公网地址。 -- `SOLVER_REPORT_DEPLOYMENT_VALIDATION_FILE`:覆盖部署验证结果路径。 - -源码契约测试:`node --test tests/*.test.mjs`。 - -签署公开发布证明前,必须以可读分辨率逐张检查全部原始截图。联系表只用于核对场景 ID、截图哈希和覆盖范围,不能替代小字凭据审查。在 macOS 上还要运行原图级 Apple Vision OCR 门禁,作为第二条独立检查路径: - -```bash -export SOLVER_REPORT_SCREENSHOT_OCR_FILE=/private/tmp/solver-report-ocr.json -export SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE=/private/tmp/solver-report-ocr-validation.json -npm run privacy:ocr -``` - -验证器要求 174 张计划内原图按顺序全部出现,重新计算每张源图哈希,并对识别文字复用 fail-closed 敏感信息策略。OCR 与全分辨率人工视觉审查必须同时通过后,才能更新公开发布证明;审查产物不会进入站点或 Agent bootstrap。 - -## 部署边界 - -构建器只允许公开 provenance 白名单字段,并会拒绝本机路径、邮箱、私有 URL、常见密钥和具名凭据。部署验证同时要求 `build.json#gates.publicReleasePrivacy=pass` 且证明哈希与 manifest 一致。 - -每次生成新的不可变 release,切换前逐项验证 `integrity.json`,然后原子更新服务端 `current` 软链接。禁止覆盖既有 release,也禁止把历史证据重新标记为新版平台产物。 diff --git a/web/solver-report/decision-tree.schema.json b/web/solver-report/decision-tree.schema.json deleted file mode 100644 index 14cef80..0000000 --- a/web/solver-report/decision-tree.schema.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://desirecore.com/schemas/solver-report-decision-tree.schema.json", - "title": "Solver report human-Agent decision tree", - "description": "A scenario-specific, evidence-bound decision tree that explains which facts humans confirm, which reasoning Agents perform, which gates can block delivery, and which path was actually taken.", - "type": "object", - "additionalProperties": false, - "required": ["schemaVersion", "rootNodeId", "nodes", "edges", "observed", "evidenceBindings"], - "properties": { - "schemaVersion": { - "const": "solver.human-agent-decision-tree/v1", - "description": "Version of the stable decision-tree interchange contract." - }, - "rootNodeId": { - "type": "string", - "minLength": 1, - "description": "ID of the first node on the scenario decision path." - }, - "nodes": { - "type": "array", - "minItems": 1, - "description": "Decision, gate, action, and outcome nodes. Business wording is derived from scenario evidence rather than UI code.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["id", "kind", "owner", "title", "detail", "status", "evidenceRef"], - "properties": { - "id": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "Stable node identifier within one tree." }, - "kind": { - "enum": ["context", "question", "action", "check", "outcome"], - "description": "Visual and semantic role of the node." - }, - "owner": { - "enum": ["human", "agent", "shared", "validator", "platform"], - "description": "Party responsible for confirming or performing this step." - }, - "title": { "type": "string", "minLength": 1, "description": "Plain-language step or question shown to non-algorithm users." }, - "detail": { "type": "string", "minLength": 1, "description": "Scenario-specific explanation of the step, rule, or outcome." }, - "status": { - "enum": ["pass", "guard", "blocked", "neutral"], - "description": "Observed state in this evidence run; guard and blocked nodes explain non-delivery paths." - }, - "evidenceRef": { - "type": "string", - "pattern": "^#[a-z0-9-]+$", - "description": "In-page anchor where a reviewer can inspect the supporting evidence." - } - } - } - }, - "edges": { - "type": "array", - "minItems": 1, - "description": "Directed branches between nodes. Exactly one selected path records what happened in the accepted run.", - "items": { - "type": "object", - "additionalProperties": false, - "required": ["from", "to", "label", "tone", "selected"], - "properties": { - "from": { "type": "string", "description": "Source node ID." }, - "to": { "type": "string", "description": "Destination node ID." }, - "label": { "type": "string", "minLength": 1, "description": "Human-readable branch condition or observed answer." }, - "tone": { "enum": ["continue", "guard", "success"], "description": "Branch appearance and governance meaning." }, - "selected": { "type": "boolean", "description": "Whether this branch belongs to the observed evidence path." } - } - } - }, - "observed": { - "type": "object", - "additionalProperties": false, - "required": ["workflowKind", "runStatus", "solveStatus", "validationVerdict"], - "description": "Observed workflow and verdicts parsed from the accepted Run, settled tool summaries, and receipts; oracle expectations never substitute for these values.", - "properties": { - "workflowKind": { "enum": ["optimization", "validation", "recovery"], "description": "Actual tool path used by this run." }, - "runStatus": { "type": "string", "minLength": 1, "description": "Observed case-run acceptance status." }, - "solveStatus": { "type": ["string", "null"], "description": "Observed settled solver status, or null when no solve was performed." }, - "validationVerdict": { "type": ["string", "null"], "enum": ["pass", "fail", null], "description": "Observed independent validation verdict, or null when the workflow has no validation step." } - } - }, - "evidenceBindings": { - "type": "object", - "additionalProperties": false, - "required": ["scenarioPackSha256", "runId", "sourceMessagesSha256", "sourceSessionSha256", "sourceToolInvocationsSha256", "optimizationSpecSha256", "resultPayloadSha256", "validationReportSha256"], - "description": "Immutable bindings that prevent a generic or stale tree from being presented as scenario evidence.", - "properties": { - "scenarioPackSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of the source Scenario Pack." }, - "runId": { "type": "string", "minLength": 1, "description": "Accepted run that supplied observed outcomes." }, - "sourceMessagesSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of the source conversation log." }, - "sourceSessionSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of the session log containing settled solve, validation, or recovery-ledger summaries." }, - "sourceToolInvocationsSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "SHA-256 of the governed tool receipt log that proves which workflow actually ran." }, - "optimizationSpecSha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$", "description": "OptimizationSpec hash when a solver path exists." }, - "resultPayloadSha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$", "description": "Settled solver-result hash or recovery EvidenceLedger payload hash; null only for validation-only candidate workflows." }, - "validationReportSha256": { "type": ["string", "null"], "pattern": "^[a-f0-9]{64}$", "description": "Independent validation report hash when recorded." } - } - } - } -} diff --git a/web/solver-report/package.json b/web/solver-report/package.json deleted file mode 100644 index d347dba..0000000 --- a/web/solver-report/package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "@desirecore/solver-report-web", - "version": "1.0.0", - "private": true, - "type": "module", - "scripts": { - "test": "node --test tests/*.test.mjs", - "build": "node scripts/build-report-web.mjs", - "privacy:contact-sheets": "node scripts/create-privacy-contact-sheets.mjs", - "privacy:ocr": "swift scripts/audit-screenshot-privacy.swift \"$SOLVER_REPORT_INPUT_ROOT\" \"${SOLVER_REPORT_SCREENSHOT_PLAN_FILE:-$SOLVER_REPORT_INPUT_ROOT/evidence/screenshots/screenshot-plan.json}\" \"$SOLVER_REPORT_SCREENSHOT_OCR_FILE\" && node scripts/validate-screenshot-privacy-ocr.mjs", - "privacy:redact": "node scripts/create-public-screenshots.mjs", - "validate": "node scripts/validate-report-web.mjs", - "validate:deployed": "node scripts/validate-deployed-report.mjs" - } -} diff --git a/web/solver-report/public-release-attestation.schema.json b/web/solver-report/public-release-attestation.schema.json deleted file mode 100644 index 7aca3ce..0000000 --- a/web/solver-report/public-release-attestation.schema.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "$id": "https://desirecore.dev/schemas/solver-public-release-attestation-v1.json", - "title": "Solver report public release attestation", - "description": "Binds a public-release privacy review to the exact transcript and screenshot evidence roots consumed by the report builder.", - "type": "object", - "additionalProperties": false, - "required": ["schemaVersion", "audience", "review", "evidence", "metadata"], - "properties": { - "schemaVersion": { "const": "solver.public-release-attestation/v1", "description": "Attestation contract version." }, - "audience": { "const": "public", "description": "Confirms that the reviewed output is approved for a public audience." }, - "review": { - "type": "object", - "additionalProperties": false, - "required": ["status", "reviewedAt", "reviewer", "methods", "assertions"], - "properties": { - "status": { "const": "passed", "description": "Fail-closed public privacy review result." }, - "reviewedAt": { "type": "string", "format": "date-time", "description": "Time at which the bound evidence was reviewed." }, - "reviewer": { "type": "string", "minLength": 1, "description": "Accountable reviewer or release process identity." }, - "methods": { "type": "array", "minItems": 1, "items": { "type": "string" }, "description": "Review methods used for transcripts and screenshots." }, - "assertions": { - "type": "object", - "additionalProperties": false, - "required": ["transcriptsContainNoSensitiveData", "screenshotsContainNoSensitiveData", "customerIdentifiersExcluded"], - "properties": { - "transcriptsContainNoSensitiveData": { "const": true, "description": "All published transcript text was reviewed for secrets and personal data." }, - "screenshotsContainNoSensitiveData": { "const": true, "description": "Every screenshot hash in the bound root was visually reviewed for secrets and personal data." }, - "customerIdentifiersExcluded": { "const": true, "description": "Customer names, codenames, and private identifiers are excluded." } - } - } - } - }, - "evidence": { - "type": "object", - "additionalProperties": false, - "required": ["publicTranscriptContentRootSha256", "publicScreenshotContentRootSha256", "screenshotPlanSha256", "screenshotValidationSha256", "screenshotPrivacyValidationSha256"], - "properties": { - "publicTranscriptContentRootSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Canonical root hash of every public user and Agent message." }, - "publicScreenshotContentRootSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Canonical root hash binding every reviewed screenshot path, digest, and dimension." }, - "screenshotPlanSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Canonical screenshot plan digest." }, - "screenshotValidationSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Canonical screenshot validation digest." }, - "screenshotPrivacyValidationSha256": { "type": "string", "pattern": "^[a-f0-9]{64}$", "description": "Canonical digest of the full-resolution OCR privacy validation bound to every public screenshot." } - } - }, - "metadata": { - "type": "object", - "additionalProperties": false, - "required": ["platformCommit", "agentSkillVersion", "representativeStabilityPasses"], - "properties": { - "platformCommit": { "type": "string", "minLength": 7, "description": "Platform commit that owns the historical evidence." }, - "agentSkillVersion": { "type": "string", "minLength": 1, "description": "Solver Agent Skill version that produced the historical evidence." }, - "representativeStabilityPasses": { "type": "integer", "minimum": 0, "description": "Number of representative stability passes supported by the evidence." } - } - } - } -} diff --git a/web/solver-report/scripts/audit-screenshot-privacy.swift b/web/solver-report/scripts/audit-screenshot-privacy.swift deleted file mode 100644 index 1d864aa..0000000 --- a/web/solver-report/scripts/audit-screenshot-privacy.swift +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env swift -import AppKit -import Foundation -import Vision - -enum AuditError: Error, CustomStringConvertible { - case usage - case malformedPlan(String) - case unsafePath(String) - case unreadableImage(String) - - var description: String { - switch self { - case .usage: - return "usage: audit-screenshot-privacy.swift " - case .malformedPlan(let detail): - return "malformed screenshot plan: \(detail)" - case .unsafePath(let path): - return "screenshot path escapes input root: \(path)" - case .unreadableImage(let path): - return "cannot decode screenshot: \(path)" - } - } -} - -func requiredString(_ object: [String: Any], _ key: String) throws -> String { - guard let value = object[key] as? String, !value.isEmpty else { - throw AuditError.malformedPlan("missing \(key)") - } - return value -} - -do { - guard CommandLine.arguments.count == 4 else { throw AuditError.usage } - let inputRoot = URL(fileURLWithPath: CommandLine.arguments[1]).standardizedFileURL - let planURL = URL(fileURLWithPath: CommandLine.arguments[2]).standardizedFileURL - let outputURL = URL(fileURLWithPath: CommandLine.arguments[3]).standardizedFileURL - let planData = try Data(contentsOf: planURL) - guard - let plan = try JSONSerialization.jsonObject(with: planData) as? [String: Any], - let scenarios = plan["scenarios"] as? [[String: Any]] - else { - throw AuditError.malformedPlan("scenarios must be an array") - } - - var entries: [[String: Any]] = [] - for scenario in scenarios { - let scenarioId = try requiredString(scenario, "scenarioId") - guard let screenshots = scenario["screenshots"] as? [[String: Any]] else { - throw AuditError.malformedPlan("\(scenarioId).screenshots must be an array") - } - for screenshot in screenshots { - let relativePath = try requiredString(screenshot, "file") - let imageURL = inputRoot.appendingPathComponent(relativePath).standardizedFileURL - let rootPrefix = inputRoot.path.hasSuffix("/") ? inputRoot.path : inputRoot.path + "/" - guard imageURL.path.hasPrefix(rootPrefix) else { throw AuditError.unsafePath(relativePath) } - - let recognizedLines: [[String: Any]] = try autoreleasepool { - guard - let image = NSImage(contentsOf: imageURL), - let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) - else { - throw AuditError.unreadableImage(relativePath) - } - let request = VNRecognizeTextRequest() - request.recognitionLevel = .accurate - request.recognitionLanguages = ["zh-Hans", "en-US"] - request.usesLanguageCorrection = true - try VNImageRequestHandler(cgImage: cgImage, options: [:]).perform([request]) - return (request.results ?? []).compactMap { observation in - guard let text = observation.topCandidates(1).first?.string else { return nil } - let box = observation.boundingBox - return [ - "text": text, - "boundingBox": [box.origin.x, box.origin.y, box.size.width, box.size.height], - ] - } - } - entries.append([ - "scenarioId": scenarioId, - "file": relativePath, - "lines": recognizedLines, - ]) - FileHandle.standardError.write(Data("OCR \(entries.count): \(relativePath)\n".utf8)) - } - } - - let output: [String: Any] = [ - "schemaVersion": "solver.screenshot-privacy-ocr/v1", - "generatedAt": ISO8601DateFormatter().string(from: Date()), - "engine": "Apple Vision VNRecognizeTextRequest accurate zh-Hans+en-US", - "entries": entries, - ] - let outputData = try JSONSerialization.data(withJSONObject: output, options: [.prettyPrinted, .sortedKeys]) - try outputData.write(to: outputURL, options: .atomic) -} catch { - FileHandle.standardError.write(Data("\(error)\n".utf8)) - exit(1) -} diff --git a/web/solver-report/scripts/build-report-web.mjs b/web/solver-report/scripts/build-report-web.mjs deleted file mode 100644 index fe96787..0000000 --- a/web/solver-report/scripts/build-report-web.mjs +++ /dev/null @@ -1,882 +0,0 @@ -import { createRequire } from 'node:module' -import { cpSync, existsSync, globSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs' -import { basename, dirname, extname, join, relative, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { - assertPublicText, - canonical, - computePublicEvidenceRoots, - loadPublicReleaseAttestation, - projectPublicProvenance, - resolveSafeOutputRoot, - sha256, -} from './public-release-policy.mjs' - -const codeRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.env.BUILDER_ROOT ?? process.cwd()) -const configuredAppRoot = process.env.DESIRECORE_APP_ROOT -if (!configuredAppRoot) throw new Error('DESIRECORE_APP_ROOT is required') -const appRoot = resolve(configuredAppRoot) -const requireFromApp = createRequire(join(appRoot, 'package.json')) -const { marked } = requireFromApp('marked') -const katex = requireFromApp('katex') -const sharp = requireFromApp('sharp') -const Ajv = requireFromApp('ajv').default -const addFormats = requireFromApp('ajv-formats').default -const ajv = new Ajv({ allErrors: true, strict: true }) -addFormats(ajv) -const validatePublicReleaseAttestationSchema = ajv.compile( - JSON.parse(readFileSync(join(codeRoot, 'public-release-attestation.schema.json'), 'utf8')) -) -const validateDecisionTreeSchema = ajv.compile( - JSON.parse(readFileSync(join(codeRoot, 'decision-tree.schema.json'), 'utf8')) -) - -const outputRoot = resolveSafeOutputRoot({ - inputRoot, - codeRoot, - appRoot, - requestedOutputRoot: process.env.SOLVER_REPORT_OUTPUT_ROOT ?? join(inputRoot, 'report-web', 'dist'), -}) -const sourceRoot = join(codeRoot, 'source') -const sourceScreenshotPlan = JSON.parse( - readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json'), 'utf8') -) -const publicScreenshotPlanFile = - process.env.SOLVER_REPORT_PUBLIC_SCREENSHOT_PLAN_FILE ?? - join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json') -const screenshotPlan = JSON.parse(readFileSync(publicScreenshotPlanFile, 'utf8')) -const screenshotValidation = JSON.parse( - readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-validation.json'), 'utf8') -) -const screenshotPrivacyValidation = JSON.parse( - readFileSync( - process.env.SOLVER_REPORT_SCREENSHOT_PRIVACY_VALIDATION_FILE ?? - join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-privacy-validation.json'), - 'utf8' - ) -) -const historicalBuildManifest = JSON.parse(readFileSync(join(inputRoot, 'build-manifest.json'), 'utf8')) -const stabilityMatrix = JSON.parse(readFileSync(join(inputRoot, 'evidence', 'stability', 'stability-matrix.json'), 'utf8')) -if (!screenshotValidation.passed || !stabilityMatrix.passed) - throw new Error('M8 evidence gates must pass before Web build') -if ( - screenshotPlan.schemaVersion !== 'solver.public-screenshot-plan/v1' || - screenshotPlan.sourcePlanSha256 !== sha256(canonical(sourceScreenshotPlan)) || - screenshotPlan.scenarios.length !== sourceScreenshotPlan.scenarios.length -) { - throw new Error('public screenshot plan is missing or differs from the source evidence plan') -} -for (const sourceScenario of sourceScreenshotPlan.scenarios) { - const publicScenario = screenshotPlan.scenarios.find((candidate) => candidate.scenarioId === sourceScenario.scenarioId) - if ( - !publicScenario || - publicScenario.runId !== sourceScenario.runId || - publicScenario.conversationId !== sourceScenario.conversationId || - publicScenario.screenshots.length !== sourceScenario.screenshots.length || - publicScenario.screenshots.some( - (screenshot, index) => screenshot.sourceSha256 !== sourceScenario.screenshots[index].sha256 - ) - ) { - throw new Error(`${sourceScenario.scenarioId}: public screenshot derivation differs from source evidence`) - } -} -const latestRegressionFile = - process.env.LATEST_PLATFORM_REGRESSION_FILE ?? - join(inputRoot, 'evidence', 'latest-platform-regression-20260830', 'summary.json') -const latestPlatformRegression = JSON.parse(readFileSync(latestRegressionFile, 'utf8')) -if ( - latestPlatformRegression.schemaVersion !== 'solver.latest-platform-regression/v1' || - latestPlatformRegression.passed !== true -) { - throw new Error('latest platform regression summary is missing, unsupported, or not passed') -} -const historicalPlatformCommit = historicalBuildManifest.platform?.commit -const historicalAgentSkillVersion = historicalBuildManifest.skill?.version -const representativeStabilityPasses = stabilityMatrix.groups?.reduce( - (total, group) => total + 1 + (group.freshAndPerturbedRuns?.length ?? 0), - 0 -) -if ( - historicalBuildManifest.schema_version !== 'solver-agent-team-build/v1' || - typeof historicalPlatformCommit !== 'string' || - typeof historicalAgentSkillVersion !== 'string' || - !Number.isInteger(representativeStabilityPasses) || - representativeStabilityPasses < 1 -) { - throw new Error('historical build manifest cannot derive public evidence ownership') -} -if ( - !historicalPlatformCommit.startsWith(latestPlatformRegression.historicalEvidence?.platformCommit ?? '') || - latestPlatformRegression.historicalEvidence?.representativeStabilityPasses !== representativeStabilityPasses -) { - throw new Error('latest regression historical attribution differs from the source evidence') -} - -const scenarioOrder = [ - 'rw.territory-assignment', - 'rw.resource-allocation', - 'rw.performance-target', - 'rw.customer-task-scheduling', - 'rw.workforce-shift', - 'rw.insufficient-capacity-iis', - 'of.api-baseline', - 'of.diet', - 'of.facility-location', - 'of.weekly-workforce', - 'of.soap-production', - 'of.ad-traffic', - 'of.min-cost-flow', - 'of.max-flow', - 'of.portfolio', - 'of.iis-diagnosis', - 'of.seven-day-shift', - 'boundary.qp-unsupported', - 'qcp.risk-budget', - 'cp.single-machine-detection', - 'exploratory.two-person-shift', - 'validation.independent-pass', - 'validation.independent-fail', - 'recovery.cold-start', -] - -const sectionIds = [ - 'scenario-and-decision', - 'input-and-provenance', - 'data-quality', - 'model-definition', - 'train-validation-test', - 'solve-process', - 'solve-result', - 'independent-validation', - 'assumptions-risks-boundary', - 'reproduction-and-conclusion', -] - -const readJsonLines = (file) => - readFileSync(file, 'utf8') - .split('\n') - .filter(Boolean) - .map((line) => JSON.parse(line)) -const escapeHtml = (value) => - String(value ?? '') - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", ''') - -let activeFormulaStats = null -const renderer = new marked.Renderer() -renderer.html = ({ text }) => escapeHtml(text) -marked.use({ - extensions: [ - { - name: 'blockMath', - level: 'block', - start: (source) => source.indexOf('\\['), - tokenizer(source) { - const match = /^\\\[([\s\S]*?)\\\](?:\n|$)/.exec(source) - return match ? { type: 'blockMath', raw: match[0], text: match[1].trim() } : undefined - }, - renderer(token) { - if (activeFormulaStats) { - activeFormulaStats.source += 1 - activeFormulaStats.display += 1 - } - const html = katex.renderToString(token.text, { - throwOnError: true, - strict: 'ignore', - trust: false, - output: 'htmlAndMathml', - displayMode: true, - }) - if (activeFormulaStats) activeFormulaStats.rendered += 1 - return `${html}\n` - }, - }, - { - name: 'inlineMath', - level: 'inline', - start: (source) => source.indexOf('\\('), - tokenizer(source) { - const match = /^\\\(([^\n]+?)\\\)/.exec(source) - return match ? { type: 'inlineMath', raw: match[0], text: match[1].trim() } : undefined - }, - renderer(token) { - if (activeFormulaStats) activeFormulaStats.source += 1 - const html = katex.renderToString(token.text, { - throwOnError: true, - strict: 'ignore', - trust: false, - output: 'htmlAndMathml', - displayMode: false, - }) - if (activeFormulaStats) activeFormulaStats.rendered += 1 - return html - }, - }, - ], -}) - -const renderMarkdown = (markdown) => { - const html = marked.parse(markdown, { gfm: true, breaks: true, renderer }) - if (typeof html !== 'string') throw new Error('marked returned non-string output') - return html -} - -const splitFinalReport = (markdown, scenarioId) => { - const matches = [...markdown.matchAll(/^##\s+(10|[1-9])\.\s+(.+)\s*$/gm)] - if (matches.length !== 10 || matches.some((match, index) => Number(match[1]) !== index + 1)) { - throw new Error(`${scenarioId}: final report is not the required ten-part structure`) - } - return matches.map((match, index) => { - const start = match.index + match[0].length - const end = matches[index + 1]?.index ?? markdown.length - const source = markdown.slice(start, end).trim() - return { - id: sectionIds[index], - number: index + 1, - title: match[2].trim(), - markdown: source, - markdownSha256: sha256(source), - html: renderMarkdown(source), - } - }) -} - -const packById = new Map( - globSync(join(inputRoot, 'scenario-packs', '*', '2.0.0', 'scenario-pack.json')).map((file) => { - const pack = JSON.parse(readFileSync(file, 'utf8')) - return [pack.manifest.scenarioId, { file, pack }] - }) -) -const caseRunById = new Map( - globSync(join(inputRoot, 'evidence', '**', 'case-run.json')).map((file) => { - const data = JSON.parse(readFileSync(file, 'utf8')) - return [data.runId, { file, data, runDir: join(dirname(file), 'run') }] - }) -) - -const parseSummaryJson = (event) => { - try { - return JSON.parse(event.data?.summary ?? '') - } catch { - return null - } -} - -const compactList = (items, limit = 3) => { - const visible = items.slice(0, limit) - return `${visible.join(';')}${items.length > limit ? `;另有 ${items.length - limit} 项` : ''}` -} -const observedStatusLabel = (status) => - ({ optimal: '最优解', infeasible: '不可行', pass: '通过', fail: '拒绝' })[status] ?? String(status) - -const buildDecisionTree = ({ pack, packSha256, evidence, workflowKind, observed, problemFamily, engineId }) => { - const hardChecks = pack.oracle?.hardChecks ?? [] - const intent = { - id: 'intent', - kind: 'context', - owner: 'shared', - title: pack.businessContract.decision, - detail: pack.businessContract.plainLanguageStory, - status: 'neutral', - evidenceRef: '#scenario-and-decision', - } - let nodes - let edges - if (workflowKind === 'optimization') { - const isInfeasible = observed.solveStatus === 'infeasible' - const isRejected = observed.validationVerdict === 'fail' - nodes = [ - intent, - { - id: 'facts', kind: 'question', owner: 'human', title: '业务事实、硬规则与优先级是否已确认?', - detail: `${compactList(pack.businessContract.objectives)};${compactList(pack.businessContract.constraints)}`, - status: 'pass', evidenceRef: '#input-and-provenance', - }, - { - id: 'clarify', kind: 'action', owner: 'agent', title: '信息不足:向人类补问', - detail: '缺少决定性事实时保持阻断,不猜测参数、不静默补值。', status: 'guard', - evidenceRef: '#input-and-provenance', - }, - { - id: 'model', kind: 'check', owner: 'agent', - title: `规则是否完整映射为 ${String(problemFamily).toUpperCase()} 决策模型?`, - detail: hardChecks.length ? `交付必须覆盖 ${compactList(hardChecks, 4)}` : pack.businessContract.professionalSummary, - status: 'pass', evidenceRef: '#model-definition', - }, - { - id: 'mapping-block', kind: 'outcome', owner: 'shared', title: '模型映射不完整:停止求解', - detail: '由人类补充或修正规则,Agent 不得以近似模型冒充原问题。', status: 'blocked', - evidenceRef: '#model-definition', - }, - { - id: 'candidate', kind: 'action', owner: 'agent', - title: isInfeasible ? `使用 ${engineId} 做可行性诊断` : `使用 ${engineId} 生成候选方案`, - detail: `真实求解回执:${observedStatusLabel(observed.solveStatus)}(${observed.solveStatus});${pack.businessContract.baseline}`, - status: isInfeasible ? 'guard' : 'pass', evidenceRef: '#solve-process', - }, - { - id: 'verify', kind: 'question', owner: 'validator', - title: isInfeasible ? '不可行结论与冲突集合是否可追溯?' : '独立校核是否接受当前候选?', - detail: hardChecks.length ? `重新检查 ${compactList(hardChecks, 4)}` : '重新计算变量域、硬约束、目标与证据哈希。', - status: isRejected ? 'guard' : 'pass', evidenceRef: '#independent-validation', - }, - { - id: 'verification-guard', kind: 'outcome', owner: 'validator', title: '校核证据不足:拒绝交付', - detail: '不采信求解器自报;保留候选、失败检查和哈希后返回 Agent 修正。', status: 'blocked', - evidenceRef: '#independent-validation', - }, - { - id: 'outcome', kind: 'outcome', owner: 'human', - title: isRejected ? '阻断违规方案,不进入执行' : isInfeasible ? '交付冲突诊断,等待人类调整规则' : '建议方案交由业务负责人确认', - detail: isRejected ? '独立校核拒绝该候选;保留失败证据供人复核。' : isInfeasible ? '不偷偷删除任务、放宽容量或伪造可行解。' : `${pack.businessContract.objectives.join(';')};执行前仍由人类确认适用边界。`, - status: isRejected ? 'blocked' : isInfeasible ? 'guard' : 'pass', evidenceRef: '#reproduction-and-conclusion', - }, - ] - edges = [ - { from: 'intent', to: 'facts', label: '进入事实确认', tone: 'continue', selected: true }, - { from: 'facts', to: 'clarify', label: '否:缺少决定性信息', tone: 'guard', selected: false }, - { from: 'facts', to: 'model', label: '是:事实门关闭', tone: 'continue', selected: true }, - { from: 'model', to: 'mapping-block', label: '否:存在未映射规则', tone: 'guard', selected: false }, - { from: 'model', to: 'candidate', label: '是:模型可执行', tone: 'continue', selected: true }, - { from: 'candidate', to: 'verify', label: `求解回执:${observedStatusLabel(observed.solveStatus)}`, tone: isInfeasible ? 'guard' : 'continue', selected: true }, - { from: 'verify', to: 'verification-guard', label: '证据不足或对账失败', tone: 'guard', selected: false }, - { from: 'verify', to: 'outcome', label: isRejected ? '校核回执:拒绝' : isInfeasible ? '校核回执:通过,冲突可追溯' : '校核回执:通过', tone: isRejected ? 'guard' : 'success', selected: true }, - ] - } else if (workflowKind === 'validation') { - const isRejected = observed.validationVerdict === 'fail' - nodes = [ - intent, - { - id: 'candidate', kind: 'question', owner: 'human', title: '候选变量、目标声明与适用边界是否齐全?', - detail: `${compactList(pack.businessContract.constraints)};${pack.businessContract.baseline}`, - status: 'pass', evidenceRef: '#input-and-provenance', - }, - { - id: 'clarify', kind: 'action', owner: 'agent', title: '候选信息不全:请求补充', - detail: '缺少变量、约束、目标口径或来源时不启动校核。', status: 'guard', evidenceRef: '#input-and-provenance', - }, - { - id: 'verify', kind: 'check', owner: 'validator', title: '不重新求解,独立复算候选', - detail: hardChecks.length ? `核对 ${compactList(hardChecks, 4)}` : '核对变量域、硬约束、目标与基线。', - status: isRejected ? 'guard' : 'pass', evidenceRef: '#independent-validation', - }, - { - id: 'verification-guard', kind: 'outcome', owner: 'validator', title: '校核证据不足:拒绝下结论', - detail: '验证报告或工具回执缺失时保持阻断。', status: 'blocked', evidenceRef: '#independent-validation', - }, - { - id: 'outcome', kind: 'outcome', owner: 'human', - title: isRejected ? '阻断违规方案,不进入执行' : '候选通过校核,交由人类决定是否采用', - detail: isRejected ? '高目标值不能覆盖硬约束违规;本次独立校核回执为拒绝(fail)。' : '本次独立校核回执为通过(pass);验证通过不等于自动批准执行。', - status: isRejected ? 'blocked' : 'pass', evidenceRef: '#reproduction-and-conclusion', - }, - ] - edges = [ - { from: 'intent', to: 'candidate', label: '进入候选校核', tone: 'continue', selected: true }, - { from: 'candidate', to: 'clarify', label: '否:候选信息不完整', tone: 'guard', selected: false }, - { from: 'candidate', to: 'verify', label: '是:只校核、不重算方案', tone: 'continue', selected: true }, - { from: 'verify', to: 'verification-guard', label: '验证证据缺失', tone: 'guard', selected: false }, - { from: 'verify', to: 'outcome', label: `校核回执:${observedStatusLabel(observed.validationVerdict)}`, tone: isRejected ? 'guard' : 'success', selected: true }, - ] - } else { - nodes = [ - intent, - { - id: 'recovery-source', kind: 'question', owner: 'platform', title: '正常关闭标记与恢复前基线是否可读取?', - detail: pack.businessContract.baseline, status: 'pass', evidenceRef: '#input-and-provenance', - }, - { - id: 'missing-state', kind: 'outcome', owner: 'platform', title: '恢复来源不完整:停止声称恢复成功', - detail: '缺少关闭标记、团队快照或账本时,必须进入人工排查。', status: 'blocked', evidenceRef: '#input-and-provenance', - }, - { - id: 'restore', kind: 'action', owner: 'platform', title: '恢复团队、Skill、工具与既有证据', - detail: '该路径不建立、不编译、不求解优化模型。', status: 'pass', evidenceRef: '#solve-process', - }, - { - id: 'ledger-check', kind: 'check', owner: 'validator', title: '恢复前后 EvidenceLedger 是否保持幂等?', - detail: compactList(pack.businessContract.constraints, 4), status: 'pass', evidenceRef: '#independent-validation', - }, - { - id: 'ledger-guard', kind: 'outcome', owner: 'validator', title: '账本计数或证据变化:拒绝恢复结论', - detail: '发现重复已结算调用或身份漂移时保持阻断。', status: 'blocked', evidenceRef: '#independent-validation', - }, - { - id: 'outcome', kind: 'outcome', owner: 'human', title: '恢复完成,不重复已结算求解', - detail: '本次 Run 已通过,且 EvidenceLedger 回执已绑定到这棵树。', status: 'pass', evidenceRef: '#reproduction-and-conclusion', - }, - ] - edges = [ - { from: 'intent', to: 'recovery-source', label: '进入恢复核对', tone: 'continue', selected: true }, - { from: 'recovery-source', to: 'missing-state', label: '否:恢复来源缺失', tone: 'guard', selected: false }, - { from: 'recovery-source', to: 'restore', label: '是:加载既有状态', tone: 'continue', selected: true }, - { from: 'restore', to: 'ledger-check', label: '读取 EvidenceLedger', tone: 'continue', selected: true }, - { from: 'ledger-check', to: 'ledger-guard', label: '否:计数或身份漂移', tone: 'guard', selected: false }, - { from: 'ledger-check', to: 'outcome', label: '是:无重复求解', tone: 'success', selected: true }, - ] - } - const tree = { - schemaVersion: 'solver.human-agent-decision-tree/v1', rootNodeId: 'intent', nodes, edges, observed, - evidenceBindings: { - scenarioPackSha256: packSha256, - runId: evidence.runId, - sourceMessagesSha256: evidence.sourceMessagesSha256, - sourceSessionSha256: evidence.sourceSessionSha256, - sourceToolInvocationsSha256: evidence.sourceToolInvocationsSha256, - optimizationSpecSha256: evidence.optimizationSpecSha256, - resultPayloadSha256: evidence.resultPayloadSha256, - validationReportSha256: evidence.validationReportSha256, - }, - } - if (!validateDecisionTreeSchema(tree)) { - throw new Error(`decision tree schema validation failed: ${ajv.errorsText(validateDecisionTreeSchema.errors)}`) - } - const nodeIds = new Set(tree.nodes.map((node) => node.id)) - if (nodeIds.size !== tree.nodes.length || !nodeIds.has(tree.rootNodeId)) throw new Error('decision tree node IDs invalid') - if (tree.edges.some((edge) => !nodeIds.has(edge.from) || !nodeIds.has(edge.to))) { - throw new Error('decision tree edge refers to an unknown node') - } - const selectedEdges = tree.edges.filter((edge) => edge.selected) - const selectedByFrom = new Map() - for (const edge of selectedEdges) selectedByFrom.set(edge.from, [...(selectedByFrom.get(edge.from) ?? []), edge]) - if ([...selectedByFrom.values()].some((outgoing) => outgoing.length !== 1)) { - throw new Error('decision tree path must have exactly one selected outgoing edge per path node') - } - if (tree.nodes.some((node) => node.id !== tree.rootNodeId && !tree.edges.some((edge) => edge.to === node.id))) { - throw new Error('decision tree contains an orphan node') - } - let cursor = tree.rootNodeId - const visited = new Set() - while (selectedByFrom.has(cursor) && !visited.has(cursor)) { - visited.add(cursor) - cursor = selectedByFrom.get(cursor)[0].to - } - if (cursor !== 'outcome' || visited.size !== selectedEdges.length || selectedByFrom.has('outcome')) { - throw new Error('decision tree selected path must be acyclic, complete, and terminate at outcome') - } - return tree -} - -const buildChapter = async (planItem, order) => { - const source = caseRunById.get(planItem.runId) - if (!source) throw new Error(`${planItem.scenarioId}: accepted Run evidence missing: ${planItem.runId}`) - const packEntry = packById.get(planItem.scenarioId) - if (!packEntry) throw new Error(`${planItem.scenarioId}: Scenario Pack missing`) - const { pack } = packEntry - const messagesFile = join(source.runDir, 'messages.jsonl') - const sessionFile = join(source.runDir, 'sessions', 'session.jsonl') - const invocationsFile = join(source.runDir, 'receipts', 'tool-invocations.jsonl') - const messages = readJsonLines(messagesFile) - const session = readJsonLines(sessionFile) - const invocations = readJsonLines(invocationsFile) - const conversationMessages = messages.filter((item) => ['user', 'assistant'].includes(item.role)) - const finalAssistant = conversationMessages.filter((item) => item.role === 'assistant' && item.content?.trim()).at(-1) - if (!finalAssistant) throw new Error(`${planItem.scenarioId}: final assistant message missing`) - - activeFormulaStats = { source: 0, rendered: 0, display: 0 } - const renderedMessages = conversationMessages.map((message, index) => ({ - kind: 'message', - sequence: index + 1, - role: message.role, - timestamp: message.timestamp, - markdown: message.content ?? '', - markdownSha256: sha256(message.content ?? ''), - html: message.content?.trim() ? renderMarkdown(message.content) : '', - empty: !message.content?.trim(), - })) - const toolEvents = invocations - .filter((item) => item.phase === 'finished') - .map((item, index) => ({ - kind: 'tool', - sequence: index + 1, - timestamp: Date.parse(item.timestamp), - tool: item.tool_name, - status: item.status, - durationMs: item.duration_ms ?? null, - provenance: projectPublicProvenance(item.result_provenance), - })) - const sections = splitFinalReport(finalAssistant.content, planItem.scenarioId) - const timeline = [...renderedMessages, ...toolEvents].sort( - (left, right) => Number(left.timestamp ?? 0) - Number(right.timestamp ?? 0) || left.kind.localeCompare(right.kind) - ) - const parsedToolSummaries = session - .filter((event) => event.type === 'tool_use_summary') - .map((event) => ({ event, summary: parseSummaryJson(event) })) - .filter((entry) => entry.summary && typeof entry.summary === 'object' && !Array.isArray(entry.summary)) - const finished = invocations.filter((item) => item.phase === 'finished' && item.status === 'success') - const solve = finished.find((item) => item.tool_name === 'OptimizationSolve') - const validate = [...finished].reverse().find((item) => item.tool_name === 'OptimizationValidate') - const ledger = [...finished].reverse().find((item) => item.tool_name === 'OptimizationEvidenceLedger') - const compile = finished.find((item) => item.tool_name === 'OptimizationCompile') - const solveSummary = solve - ? typeof solve.result_provenance?.result_payload_hash === 'string' - ? parsedToolSummaries.find( - ({ summary }) => - typeof summary.status === 'string' && - summary.result_payload_hash === solve.result_provenance?.result_payload_hash - )?.summary ?? null - : null - : null - const validationEntry = validate - ? typeof validate.result_provenance?.validation_report_hash === 'string' - ? [...parsedToolSummaries] - .reverse() - .find( - ({ event, summary }) => - ['pass', 'fail'].includes(summary.verdict) && - event.data?.metadata?.validation_report_hash === validate.result_provenance?.validation_report_hash - ) ?? null - : null - : null - const validation = validationEntry?.summary ?? null - const ledgerSummary = ledger - ? [...parsedToolSummaries] - .reverse() - .find(({ summary }) => summary.schema_version === 'solver.optimization-evidence-ledger/v1')?.summary ?? null - : null - if (source.data.status !== 'passed') throw new Error(`${planItem.scenarioId}: decision tree requires an accepted Run`) - const workflowKind = solve ? 'optimization' : validate ? 'validation' : ledger ? 'recovery' : null - if (!workflowKind) throw new Error(`${planItem.scenarioId}: no observed solve, validation, or recovery workflow`) - if (workflowKind !== 'optimization' && compile) { - throw new Error(`${planItem.scenarioId}: ${workflowKind} workflow cannot hide an observed OptimizationCompile step`) - } - if (workflowKind === 'optimization' && (!solveSummary || !validation)) { - throw new Error(`${planItem.scenarioId}: settled solve or validation summary missing`) - } - if ( - workflowKind === 'optimization' && - (!solve.result_provenance?.optimization_spec_hash || - !solve.result_provenance?.result_payload_hash || - !validate.result_provenance?.validation_report_hash) - ) { - throw new Error(`${planItem.scenarioId}: optimization workflow provenance hashes missing`) - } - if (workflowKind === 'validation' && !validation) { - throw new Error(`${planItem.scenarioId}: observed validation summary missing`) - } - if (workflowKind === 'validation' && !validate.result_provenance?.validation_report_hash) { - throw new Error(`${planItem.scenarioId}: validation workflow report hash missing`) - } - if (workflowKind === 'recovery' && !ledgerSummary) { - throw new Error(`${planItem.scenarioId}: recovery EvidenceLedger summary missing`) - } - if (workflowKind === 'recovery' && !ledger.result_provenance?.result_payload_hash) { - throw new Error(`${planItem.scenarioId}: recovery EvidenceLedger result hash missing`) - } - const problemFamily = - solve?.result_provenance?.problem_family ?? - validate?.result_provenance?.problem_family ?? - pack.oracle?.expectedProblemFamily ?? - pack.manifest.tags.find((tag) => ['lp', 'milp', 'qp', 'qcp', 'cp'].includes(tag)) ?? - 'validation' - const stability = stabilityMatrix.groups.find((group) => group.scenarioId === planItem.scenarioId) ?? null - const resultStatus = - workflowKind === 'optimization' - ? solveSummary.status - : workflowKind === 'validation' - ? validation.verdict - : 'recovered' - const validationVerdict = validation?.verdict ?? null - if (pack.oracle.expectedStatuses?.length && !pack.oracle.expectedStatuses.includes(resultStatus)) { - throw new Error(`${planItem.scenarioId}: observed result ${resultStatus} differs from Scenario Pack oracle`) - } - if ( - source.data.diagnostics?.observedValidationVerdict != null && - source.data.diagnostics.observedValidationVerdict !== validationVerdict - ) { - throw new Error(`${planItem.scenarioId}: validation summary differs from case-run diagnostics`) - } - const sourceMessagesSha256 = sha256(readFileSync(messagesFile)) - const evidence = { - runId: planItem.runId, - conversationId: planItem.conversationId, - sourceMessagesSha256, - sourceSessionSha256: sha256(readFileSync(sessionFile)), - sourceToolInvocationsSha256: sha256(readFileSync(invocationsFile)), - optimizationSpecSha256: solve?.result_provenance?.optimization_spec_hash ?? null, - compiledRequestSha256: solve?.result_provenance?.compiled_request_hash ?? null, - resultPayloadSha256: - solve?.result_provenance?.result_payload_hash ?? ledger?.result_provenance?.result_payload_hash ?? null, - validationReportSha256: validate?.result_provenance?.validation_report_hash ?? null, - } - const decisionTree = buildDecisionTree({ - pack, - packSha256: sha256(readFileSync(packEntry.file)), - evidence, - workflowKind, - observed: { - workflowKind, - runStatus: source.data.status, - solveStatus: solveSummary?.status ?? null, - validationVerdict, - }, - problemFamily, - engineId: solve?.result_provenance?.engine_id ?? null, - }) - - const media = [] - for (const screenshot of planItem.screenshots) { - const sourceFile = join(inputRoot, screenshot.file) - const name = basename(sourceFile) - const originalRel = `media/original/${planItem.scenarioId}/${name}` - const thumb756Rel = `media/thumb-756/${planItem.scenarioId}/${name.replace(/\.png$/, '.webp')}` - const thumb1512Rel = `media/thumb-1512/${planItem.scenarioId}/${name.replace(/\.png$/, '.webp')}` - const originalTarget = join(outputRoot, originalRel) - const thumb756Target = join(outputRoot, thumb756Rel) - const thumb1512Target = join(outputRoot, thumb1512Rel) - mkdirSync(dirname(originalTarget), { recursive: true }) - mkdirSync(dirname(thumb756Target), { recursive: true }) - mkdirSync(dirname(thumb1512Target), { recursive: true }) - cpSync(sourceFile, originalTarget) - await sharp(sourceFile) - .resize({ width: 756, withoutEnlargement: true }) - .webp({ lossless: true, effort: 4 }) - .toFile(thumb756Target) - await sharp(sourceFile) - .resize({ width: 1512, withoutEnlargement: true }) - .webp({ lossless: true, effort: 4 }) - .toFile(thumb1512Target) - media.push({ - stage: - media.length === 0 - ? 'conversation-start' - : media.length === planItem.screenshots.length - 1 - ? 'delivery-conclusion' - : 'conversation-process', - original: originalRel, - thumb756: thumb756Rel, - thumb1512: thumb1512Rel, - width: screenshot.width, - height: screenshot.height, - sha256: screenshot.sha256, - alt: `${pack.manifest.title}真实多轮会话第 ${media.length + 1} 段,展示${media.length === 0 ? '用户请求与智能体补问' : media.length === planItem.screenshots.length - 1 ? '验证结论与复现证据' : '工具过程或报告正文'}`, - }) - } - - const formulaStats = { ...activeFormulaStats } - activeFormulaStats = null - const chapter = { - schemaVersion: 'solver.web-report-chapter/v1', - scenarioId: planItem.scenarioId, - scenarioVersion: pack.manifest.version, - scenarioOrder: order, - title: pack.manifest.title, - scenarioFamily: pack.manifest.scenarioFamily, - claimBoundary: pack.manifest.claimBoundary, - tags: pack.manifest.tags, - story: pack.businessContract.plainLanguageStory, - decision: pack.businessContract.decision, - professionalSummary: pack.businessContract.professionalSummary, - objectives: pack.businessContract.objectives, - constraints: pack.businessContract.constraints, - baseline: pack.businessContract.baseline, - outOfScope: pack.businessContract.outOfScope, - problemFamily, - resultStatus, - validationVerdict, - engineId: solve?.result_provenance?.engine_id ?? null, - engineRequirement: solve?.result_provenance - ? { - problemFamily: solve.result_provenance.problem_family, - irFamily: solve.result_provenance.ir_family, - irVersion: solve.result_provenance.ir_version, - capabilitySha256: solve.result_provenance.capability_hash, - } - : null, - evidence, - decisionTree, - counts: { - rawConversationMessages: conversationMessages.length, - visibleConversationMessages: renderedMessages.filter((item) => !item.empty).length, - toolEvents: toolEvents.length, - screenshots: media.length, - }, - formulaStats, - timeline, - sections, - validation, - stability: stability - ? { - policy: '3 fresh + 1 natural-language perturbation', - effectivePasses: 4, - semanticConvergence: stability.assertions.semanticConvergence, - singleSolvePerRun: stability.assertions.singleSolvePerRun, - semanticSha256: stability.freshAndPerturbedRuns[0].optimizationSemanticHash, - engineRequirementSha256: stability.freshAndPerturbedRuns[0].engineRequirementHash, - } - : { policy: 'standard real Run accepted; representative families use 3+1 gate', effectivePasses: 1 }, - media, - } - assertPublicText(chapter, `${planItem.scenarioId} chapter`) - chapter.contentSha256 = sha256(canonical(chapter)) - return chapter -} - -const publicEvidenceRoots = computePublicEvidenceRoots({ - inputRoot, - screenshotPlan, - screenshotValidation, - screenshotPrivacyValidation, - caseRunById, -}) -const publicAttestationFile = - process.env.SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE ?? - join(inputRoot, 'evidence', 'public-release-attestation.json') -const { attestation: publicReleaseAttestation, sha256: publicReleaseAttestationSha256 } = - loadPublicReleaseAttestation(publicAttestationFile, publicEvidenceRoots) -if (!validatePublicReleaseAttestationSchema(publicReleaseAttestation)) { - throw new Error(`public release attestation schema validation failed: ${ajv.errorsText(validatePublicReleaseAttestationSchema.errors)}`) -} -if ( - !historicalPlatformCommit.startsWith(publicReleaseAttestation.metadata.platformCommit) || - publicReleaseAttestation.metadata.agentSkillVersion !== historicalAgentSkillVersion || - publicReleaseAttestation.metadata.representativeStabilityPasses !== representativeStabilityPasses -) { - throw new Error('public release attestation metadata differs from derived source evidence') -} -assertPublicText(latestPlatformRegression, 'latest platform regression') - -rmSync(outputRoot, { recursive: true, force: true }) -mkdirSync(join(outputRoot, 'assets'), { recursive: true }) -mkdirSync(join(outputRoot, 'data', 'chapters'), { recursive: true }) -for (const name of ['index.html', 'report.css', 'report.js']) - cpSync(join(sourceRoot, name), join(outputRoot, name === 'index.html' ? name : `assets/${name}`)) -cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'katex.min.css'), join(outputRoot, 'assets', 'katex.min.css')) -cpSync(join(appRoot, 'node_modules', 'katex', 'dist', 'fonts'), join(outputRoot, 'assets', 'fonts'), { - recursive: true, -}) -cpSync(join(codeRoot, 'decision-tree.schema.json'), join(outputRoot, 'data', 'decision-tree.schema.json')) - -const chapters = [] -for (const [index, scenarioId] of scenarioOrder.entries()) { - const item = screenshotPlan.scenarios.find((candidate) => candidate.scenarioId === scenarioId) - if (!item) throw new Error(`screenshot plan missing ${scenarioId}`) - const chapter = await buildChapter(item, index + 1) - const fileName = `${String(index + 1).padStart(2, '0')}-${scenarioId}.json` - const jsName = fileName.replace(/\.json$/, '.js') - writeFileSync(join(outputRoot, 'data', 'chapters', fileName), `${JSON.stringify(chapter)}\n`) - writeFileSync( - join(outputRoot, 'data', 'chapters', jsName), - `window.__REPORT_CHAPTERS__=window.__REPORT_CHAPTERS__||{};window.__REPORT_CHAPTERS__[${JSON.stringify(scenarioId)}]=${JSON.stringify(chapter)};\n` - ) - chapters.push({ - scenarioId, - order: index + 1, - title: chapter.title, - story: chapter.story, - problemFamily: chapter.problemFamily, - resultStatus: chapter.resultStatus, - validationVerdict: chapter.validationVerdict, - engineId: chapter.engineId, - json: `data/chapters/${fileName}`, - script: `data/chapters/${jsName}`, - sha256: chapter.contentSha256, - bytes: statSync(join(outputRoot, 'data', 'chapters', fileName)).size, - screenshots: chapter.media.length, - }) -} - -const historicalEvidenceContentRootSha256 = sha256( - canonical(chapters.map((item) => ({ scenarioId: item.scenarioId, sha256: item.sha256 }))) -) -const latestPlatformRegressionSha256 = sha256(canonical(latestPlatformRegression)) -const contentRootSha256 = sha256(canonical({ historicalEvidenceContentRootSha256, latestPlatformRegressionSha256 })) -const manifest = { - schemaVersion: 'solver.web-report-manifest/v1', - buildId: `${new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15)}Z-${contentRootSha256.slice(0, 12)}`, - title: '通用求解器智能体团队 · 真实场景验证书', - generatedAt: new Date().toISOString(), - contentRootSha256, - reportVersion: '3.4.0', - platformCommit: latestPlatformRegression.platform.commit, - agentSkillVersion: latestPlatformRegression.platform.agentSkillVersion, - evidencePlatformCommit: historicalPlatformCommit, - publicReleaseAttestationSha256, - publicEvidenceRoots, - historicalEvidenceContentRootSha256, - latestPlatformRegressionSha256, - latestPlatformRegression, - counts: { - chapters: chapters.length, - decisionTrees: chapters.length, - screenshots: screenshotPlan.scenarios.reduce((total, item) => total + item.screenshots.length, 0), - representativeStabilityPasses, - }, - chapters, - rendering: { - markdown: { engine: 'marked', version: requireFromApp('marked/package.json').version }, - math: { engine: 'katex', version: requireFromApp('katex/package.json').version, output: 'htmlAndMathml' }, - }, -} -writeFileSync(join(outputRoot, 'data', 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`) -writeFileSync(join(outputRoot, 'data', 'manifest.js'), `window.__REPORT_MANIFEST__=${JSON.stringify(manifest)};\n`) -writeFileSync( - join(outputRoot, 'data', 'latest-platform-regression.json'), - `${JSON.stringify(latestPlatformRegression, null, 2)}\n` -) - -const mime = (file) => - ({ - '.html': 'text/html', - '.css': 'text/css', - '.js': 'text/javascript', - '.json': 'application/json', - '.png': 'image/png', - '.webp': 'image/webp', - '.woff2': 'font/woff2', - })[extname(file)] ?? 'application/octet-stream' -const integrityEntriesBeforeBuild = globSync(join(outputRoot, '**', '*')).filter((file) => statSync(file).isFile()).length -const expectedIntegrityEntries = integrityEntriesBeforeBuild + 1 -writeFileSync( - join(outputRoot, 'build.json'), - `${JSON.stringify( - { - schemaVersion: 'solver.web-report-build/v1', - buildId: manifest.buildId, - contentRootSha256, - counts: manifest.counts, - integrityEntries: expectedIntegrityEntries, - gates: { - scenarioPacks: 'pass', - decisionTrees: 'pass', - stability: 'pass', - screenshots: 'pass', - publicReleasePrivacy: 'pass', - markdown: 'pass', - math: 'pass', - }, - publicReleaseAttestationSha256, - publicEvidenceRoots, - }, - null, - 2 - )}\n` -) -const integrityEntries = globSync(join(outputRoot, '**', '*')) - .filter((file) => statSync(file).isFile()) - .map((file) => { - const bytes = readFileSync(file) - return { path: relative(outputRoot, file), mime: mime(file), bytes: bytes.length, sha256: sha256(bytes) } - }) - .sort((left, right) => left.path.localeCompare(right.path)) -const integrity = { schemaVersion: 'solver.web-report-integrity/v1', contentRootSha256, entries: integrityEntries } -writeFileSync(join(outputRoot, 'integrity.json'), `${JSON.stringify(integrity, null, 2)}\n`) -if (integrityEntries.length !== expectedIntegrityEntries) throw new Error('integrity entry count changed unexpectedly') - -console.log( - JSON.stringify( - { - outputRoot, - buildId: manifest.buildId, - contentRootSha256, - counts: manifest.counts, - integrityEntries: integrityEntries.length, - }, - null, - 2 - ) -) diff --git a/web/solver-report/scripts/create-privacy-contact-sheets.mjs b/web/solver-report/scripts/create-privacy-contact-sheets.mjs deleted file mode 100644 index 9910922..0000000 --- a/web/solver-report/scripts/create-privacy-contact-sheets.mjs +++ /dev/null @@ -1,75 +0,0 @@ -import { createRequire } from 'node:module' -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' - -const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd()) -const appRoot = process.env.DESIRECORE_APP_ROOT -if (!appRoot) throw new Error('DESIRECORE_APP_ROOT is required') -const reviewRoot = process.env.SOLVER_REPORT_PRIVACY_REVIEW_ROOT -if (!reviewRoot) throw new Error('SOLVER_REPORT_PRIVACY_REVIEW_ROOT is required') -const outputRoot = resolve(reviewRoot) -const requireFromApp = createRequire(join(resolve(appRoot), 'package.json')) -const sharp = requireFromApp('sharp') -const plan = JSON.parse(readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json'), 'utf8')) - -const entries = plan.scenarios.flatMap((scenario) => - scenario.screenshots.map((screenshot, index) => ({ - scenarioId: scenario.scenarioId, - index: index + 1, - file: screenshot.file, - sha256: screenshot.sha256, - })) -) -const columns = 5 -const rows = 6 -const tileWidth = 240 -const tileHeight = 175 -const imageHeight = 145 -const perSheet = columns * rows -const escapeXml = (value) => - String(value).replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char]) - -mkdirSync(outputRoot, { recursive: true }) -const sheets = [] -for (let offset = 0; offset < entries.length; offset += perSheet) { - const page = entries.slice(offset, offset + perSheet) - const tiles = [] - for (const entry of page) { - const preview = await sharp(join(inputRoot, entry.file)) - .resize({ width: tileWidth, height: imageHeight, fit: 'contain', background: '#0b1120' }) - .png() - .toBuffer() - const label = `${entry.scenarioId} · ${entry.index} · ${entry.sha256.slice(0, 10)}` - const tile = await sharp({ - create: { width: tileWidth, height: tileHeight, channels: 4, background: '#111a2d' }, - }) - .composite([ - { input: preview, left: 0, top: 0 }, - { - input: Buffer.from( - `${escapeXml(label)}` - ), - left: 0, - top: imageHeight, - }, - ]) - .png() - .toBuffer() - const position = tiles.length - tiles.push({ input: tile, left: (position % columns) * tileWidth, top: Math.floor(position / columns) * tileHeight }) - } - const file = `contact-sheet-${String(sheets.length + 1).padStart(2, '0')}.png` - await sharp({ - create: { width: columns * tileWidth, height: rows * tileHeight, channels: 4, background: '#080d18' }, - }) - .composite(tiles) - .png() - .toFile(join(outputRoot, file)) - sheets.push({ file, entries: page }) -} - -writeFileSync( - join(outputRoot, 'review-index.json'), - `${JSON.stringify({ schemaVersion: 'solver.privacy-contact-sheets/v1', screenshots: entries.length, sheets }, null, 2)}\n` -) -console.log(JSON.stringify({ outputRoot, screenshots: entries.length, sheets: sheets.length }, null, 2)) diff --git a/web/solver-report/scripts/create-public-screenshots.mjs b/web/solver-report/scripts/create-public-screenshots.mjs deleted file mode 100644 index be6f1ba..0000000 --- a/web/solver-report/scripts/create-public-screenshots.mjs +++ /dev/null @@ -1,143 +0,0 @@ -import { createRequire } from 'node:module' -import { cpSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' -import { basename, dirname, join, relative, resolve } from 'node:path' -import { canonical, publicTextViolations, sha256 } from './public-release-policy.mjs' - -const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd()) -const appRoot = process.env.DESIRECORE_APP_ROOT -const ocrFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_FILE -if (!appRoot) throw new Error('DESIRECORE_APP_ROOT is required') -if (!ocrFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_FILE is required') -const requireFromApp = createRequire(join(resolve(appRoot), 'package.json')) -const sharp = requireFromApp('sharp') -const planFile = join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json') -const outputPlanFile = join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json') -const outputRoot = join(inputRoot, 'evidence', 'screenshots', 'public-redacted') -const sourcePlan = JSON.parse(readFileSync(planFile, 'utf8')) -const ocr = JSON.parse(readFileSync(ocrFile, 'utf8')) -const sourceScreenshots = sourcePlan.scenarios.flatMap((scenario) => - scenario.screenshots.map((screenshot) => ({ scenarioId: scenario.scenarioId, ...screenshot })) -) -if (ocr.schemaVersion !== 'solver.screenshot-privacy-ocr/v1' || ocr.entries.length !== sourceScreenshots.length) { - throw new Error('OCR result does not cover the source screenshot plan') -} - -const clamp = (value, minimum, maximum) => Math.max(minimum, Math.min(maximum, value)) -const overlaps = (left, right) => - left.left <= right.left + right.width && - right.left <= left.left + left.width && - left.top <= right.top + right.height && - right.top <= left.top + left.height -const mergeRegions = (regions) => { - const merged = [] - for (const region of regions) { - const existing = merged.find((candidate) => overlaps(candidate, region)) - if (!existing) { - merged.push({ ...region }) - continue - } - const right = Math.max(existing.left + existing.width, region.left + region.width) - const bottom = Math.max(existing.top + existing.height, region.top + region.height) - existing.left = Math.min(existing.left, region.left) - existing.top = Math.min(existing.top, region.top) - existing.width = right - existing.left - existing.height = bottom - existing.top - } - return merged -} - -let redactedScreenshots = 0 -let redactionRegions = 0 -const publicByFile = new Map() -for (const [index, source] of sourceScreenshots.entries()) { - const ocrEntry = ocr.entries[index] - const sourceFile = join(inputRoot, source.file) - if ( - ocrEntry?.scenarioId !== source.scenarioId || - ocrEntry?.file !== source.file || - sha256(readFileSync(sourceFile)) !== source.sha256 - ) { - throw new Error(`${source.file}: OCR source identity differs from screenshot plan`) - } - const metadata = await sharp(sourceFile).metadata() - if (metadata.width !== source.width || metadata.height !== source.height) { - throw new Error(`${source.file}: source dimensions differ from screenshot plan`) - } - const proposedRegions = ocrEntry.lines - .filter((line) => publicTextViolations(line.text).length > 0) - .map((line) => { - const [x, y, width, height] = line.boundingBox - const left = clamp(Math.floor(x * source.width) - 12, 0, source.width - 1) - const top = clamp(Math.floor((1 - y - height) * source.height) - 8, 0, source.height - 1) - const right = clamp(Math.ceil((x + width) * source.width) + 12, left + 1, source.width) - const bottom = clamp(Math.ceil((1 - y) * source.height) + Math.ceil(height * source.height * 1.2) + 8, top + 1, source.height) - return { left, top, width: right - left, height: bottom - top, reason: 'private-path' } - }) - const regions = mergeRegions(proposedRegions) - const targetRelative = `evidence/screenshots/public-redacted/${source.scenarioId}/${basename(source.file)}` - const targetFile = join(inputRoot, targetRelative) - if (relative(outputRoot, targetFile).startsWith('..')) throw new Error(`${targetRelative}: unsafe public screenshot target`) - mkdirSync(dirname(targetFile), { recursive: true }) - if (regions.length === 0) cpSync(sourceFile, targetFile) - else { - await sharp(sourceFile) - .composite( - regions.map((region) => ({ - input: { - create: { - width: region.width, - height: region.height, - channels: 4, - background: { r: 32, g: 34, b: 39, alpha: 1 }, - }, - }, - left: region.left, - top: region.top, - })) - ) - .png() - .toFile(targetFile) - redactedScreenshots += 1 - redactionRegions += regions.length - } - publicByFile.set(source.file, { - file: targetRelative, - sha256: sha256(readFileSync(targetFile)), - width: source.width, - height: source.height, - sourceSha256: source.sha256, - redactions: regions, - }) -} - -const publicPlan = { - schemaVersion: 'solver.public-screenshot-plan/v1', - generatedAt: new Date().toISOString(), - sourcePlanSha256: sha256(canonical(sourcePlan)), - derivation: { - method: 'Apple Vision exact-line private-path masking', - redactedScreenshots, - redactionRegions, - }, - scenarios: sourcePlan.scenarios.map((scenario) => ({ - ...scenario, - screenshots: scenario.screenshots.map((screenshot) => publicByFile.get(screenshot.file)), - })), -} -writeFileSync(outputPlanFile, `${JSON.stringify(publicPlan, null, 2)}\n`) -console.log( - JSON.stringify( - { - outputPlanFile, - screenshots: sourceScreenshots.length, - redactedScreenshots, - redactionRegions, - bytes: sourceScreenshots.reduce( - (total, screenshot) => total + statSync(join(inputRoot, publicByFile.get(screenshot.file).file)).size, - 0 - ), - }, - null, - 2 - ) -) diff --git a/web/solver-report/scripts/public-release-policy.mjs b/web/solver-report/scripts/public-release-policy.mjs deleted file mode 100644 index 6df1d51..0000000 --- a/web/solver-report/scripts/public-release-policy.mjs +++ /dev/null @@ -1,257 +0,0 @@ -import { createHash } from 'node:crypto' -import { existsSync, readFileSync, realpathSync } from 'node:fs' -import { dirname, isAbsolute, join, posix, relative, resolve } from 'node:path' - -export const sha256 = (value) => createHash('sha256').update(value).digest('hex') - -export const canonical = (value) => { - if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]` - if (value && typeof value === 'object') { - return `{${Object.keys(value) - .sort() - .map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`) - .join(',')}}` - } - return JSON.stringify(value) -} - -const isDescendant = (parent, candidate) => { - const path = relative(parent, candidate) - return path !== '' && !path.startsWith('..') && !isAbsolute(path) -} - -const overlaps = (left, right) => - left === right || isDescendant(left, right) || isDescendant(right, left) - -const resolvePhysicalPath = (candidate) => { - let ancestor = resolve(candidate) - while (!existsSync(ancestor)) { - const parent = dirname(ancestor) - if (parent === ancestor) throw new Error(`cannot resolve an existing ancestor for ${candidate}`) - ancestor = parent - } - return resolve(realpathSync(ancestor), relative(ancestor, resolve(candidate))) -} - -export function resolveSafeOutputRoot({ inputRoot, codeRoot, appRoot, requestedOutputRoot }) { - const roots = { - inputRoot: resolvePhysicalPath(inputRoot), - codeRoot: resolvePhysicalPath(codeRoot), - appRoot: resolvePhysicalPath(appRoot), - outputRoot: resolvePhysicalPath(requestedOutputRoot), - } - const lexicalInputRoot = resolve(inputRoot) - const lexicalOutputRoot = resolve(requestedOutputRoot) - const lexicalManagedParent = join(lexicalInputRoot, 'report-web') - const physicalManagedParent = resolvePhysicalPath(lexicalManagedParent) - if ( - !isDescendant(lexicalManagedParent, lexicalOutputRoot) || - !isDescendant(roots.inputRoot, physicalManagedParent) || - !isDescendant(physicalManagedParent, roots.outputRoot) - ) { - throw new Error('SOLVER_REPORT_OUTPUT_ROOT must be a child of /report-web') - } - if (roots.outputRoot === roots.inputRoot || isDescendant(roots.outputRoot, roots.inputRoot)) { - throw new Error('report output must not equal or contain the evidence input root') - } - for (const [name, protectedRoot] of [ - ['Agent source', roots.codeRoot], - ['DesireCore application', roots.appRoot], - ]) { - if (overlaps(roots.outputRoot, protectedRoot)) { - throw new Error(`report output must not overlap ${name} root`) - } - } - return roots.outputRoot -} - -export function resolveSafeRemoteEntryUrl(baseUrl, entryPath) { - if ( - typeof entryPath !== 'string' || - entryPath.length === 0 || - entryPath.startsWith('/') || - entryPath.includes('\\') || - entryPath.includes('%') || - entryPath.includes('?') || - entryPath.includes('#') || - entryPath.split('/').some((segment) => segment === '' || segment === '.' || segment === '..') || - posix.normalize(entryPath) !== entryPath || - /^[A-Za-z][A-Za-z0-9+.-]*:/.test(entryPath) - ) { - throw new Error(`unsafe integrity entry path: ${String(entryPath)}`) - } - const base = new URL(baseUrl) - if (!['http:', 'https:'].includes(base.protocol)) throw new Error('report base URL must use HTTP or HTTPS') - const basePath = base.pathname.endsWith('/') ? base.pathname : `${base.pathname}/` - const resolved = new URL(entryPath, base) - if (resolved.origin !== base.origin || !resolved.pathname.startsWith(basePath)) { - throw new Error(`integrity entry escapes report base URL: ${entryPath}`) - } - return resolved -} - -const PUBLIC_PROVENANCE_KEYS = new Set([ - 'adapter_id', - 'capability_checked_at', - 'capability_hash', - 'compiled_request_hash', - 'compiler_id', - 'compiler_version', - 'connector_id', - 'decision_grade', - 'engine_id', - 'governance_mode', - 'ir_family', - 'ir_version', - 'job_id', - 'optimization_spec_hash', - 'problem_family', - 'request_id', - 'result_payload_hash', - 'validation_input_hash', - 'validation_report_hash', -]) - -export function projectPublicProvenance(provenance) { - if (!provenance || typeof provenance !== 'object' || Array.isArray(provenance)) return null - return Object.fromEntries( - Object.entries(provenance).filter( - ([key, value]) => - PUBLIC_PROVENANCE_KEYS.has(key) && - (value === null || ['string', 'number', 'boolean'].includes(typeof value)) - ) - ) -} - -const PUBLIC_TEXT_RULES = [ - ['private-path', /(?:\/Users\/|\/Volumes\/|[A-Za-z]:\\Users\\)/i], - ['customer-codename', /(?:贝壳|beike|bei-ke)/i], - ['email-address', /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i], - ['private-key', /-----BEGIN (?:RSA |OPENSSH |EC )?PRIVATE KEY-----/i], - ['provider-secret', /\b(?:sk-[A-Za-z0-9_-]{12,}|gh[pousr]_[A-Za-z0-9_]{12,}|xox[baprs]-[A-Za-z0-9-]{12,})\b/], - ['bearer-token', /\bBearer\s+[A-Za-z0-9._~+/-]{12,}={0,2}\b/i], - [ - 'named-secret', - /\b(?:password|passwd|authorization|api[_ -]?key|access[_ -]?token|refresh[_ -]?token|client[_ -]?secret)\b\s*[:=]\s*["']?[^\s"',}]{6,}/i, - ], - [ - 'private-url', - /https?:\/\/(?:localhost|127(?:\.\d{1,3}){3}|10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2}|[^/\s"']+\.internal)\b/i, - ], -] - -export function publicTextViolations(value) { - const text = (typeof value === 'string' ? value : JSON.stringify(value)).replace( - /\b\d+@2x\.(?:png|webp)\b/gi, - '[reviewed-retina-image]' - ) - return PUBLIC_TEXT_RULES.filter(([, pattern]) => pattern.test(text)).map(([rule]) => rule) -} - -export function assertPublicText(value, label) { - const violations = publicTextViolations(value) - if (violations.length > 0) { - throw new Error(`${label}: public-release text policy rejected ${violations.join(',')}`) - } -} - -const readJsonLines = (file) => - readFileSync(file, 'utf8') - .split('\n') - .filter(Boolean) - .map((line) => JSON.parse(line)) - -export function computePublicEvidenceRoots({ - inputRoot, - screenshotPlan, - screenshotValidation, - screenshotPrivacyValidation, - caseRunById, -}) { - const transcriptEntries = screenshotPlan.scenarios.map((item) => { - const source = caseRunById.get(item.runId) - if (!source) throw new Error(`${item.scenarioId}: source Run missing for public attestation`) - const messages = readJsonLines(join(source.runDir, 'messages.jsonl')) - .filter((message) => ['user', 'assistant'].includes(message.role)) - .map((message) => ({ role: message.role, content: message.content ?? '', timestamp: message.timestamp ?? null })) - return { scenarioId: item.scenarioId, runId: item.runId, messages } - }) - const screenshotEntries = screenshotPlan.scenarios.flatMap((item) => - item.screenshots.map((screenshot) => { - const file = join(inputRoot, screenshot.file) - if (!existsSync(file)) throw new Error(`${item.scenarioId}: screenshot missing: ${screenshot.file}`) - const actualSha256 = sha256(readFileSync(file)) - if (actualSha256 !== screenshot.sha256) { - throw new Error(`${item.scenarioId}: screenshot hash differs from reviewed plan: ${screenshot.file}`) - } - return { - scenarioId: item.scenarioId, - file: screenshot.file, - sha256: screenshot.sha256, - width: screenshot.width, - height: screenshot.height, - } - }) - ) - const screenshotPlanSha256 = sha256(canonical(screenshotPlan)) - const publicScreenshotContentRootSha256 = sha256(canonical(screenshotEntries)) - if ( - screenshotPrivacyValidation?.schemaVersion !== 'solver.screenshot-privacy-validation/v1' || - screenshotPrivacyValidation.passed !== true || - screenshotPrivacyValidation.screenshots !== screenshotEntries.length || - screenshotPrivacyValidation.screenshotPlanSha256 !== screenshotPlanSha256 || - screenshotPrivacyValidation.screenshotContentRootSha256 !== publicScreenshotContentRootSha256 || - screenshotPrivacyValidation.ocr?.screenshotsWithText !== screenshotEntries.length || - screenshotPrivacyValidation.ocr?.sensitiveMatches !== 0 - ) { - throw new Error('public screenshot privacy validation is missing or differs from current public images') - } - return { - publicTranscriptContentRootSha256: sha256(canonical(transcriptEntries)), - publicScreenshotContentRootSha256, - screenshotPlanSha256, - screenshotValidationSha256: sha256(canonical(screenshotValidation)), - screenshotPrivacyValidationSha256: sha256(canonical(screenshotPrivacyValidation)), - } -} - -const HASH_PATTERN = /^[a-f0-9]{64}$/ - -export function validatePublicReleaseAttestation(attestation, evidenceRoots) { - if (attestation?.schemaVersion !== 'solver.public-release-attestation/v1') { - throw new Error('public release attestation is missing or has an unsupported schema') - } - if (attestation.audience !== 'public' || attestation.review?.status !== 'passed') { - throw new Error('public release attestation must explicitly pass for a public audience') - } - const assertions = attestation.review?.assertions ?? {} - for (const key of ['transcriptsContainNoSensitiveData', 'screenshotsContainNoSensitiveData', 'customerIdentifiersExcluded']) { - if (assertions[key] !== true) throw new Error(`public release attestation assertion is not true: ${key}`) - } - if (!attestation.review?.reviewedAt || !attestation.review?.reviewer || !Array.isArray(attestation.review?.methods)) { - throw new Error('public release attestation review identity, time, and methods are required') - } - for (const [key, expected] of Object.entries(evidenceRoots)) { - const actual = attestation.evidence?.[key] - if (!HASH_PATTERN.test(actual ?? '') || actual !== expected) { - throw new Error(`public release attestation does not match current evidence: ${key}`) - } - } - const metadata = attestation.metadata ?? {} - if ( - typeof metadata.platformCommit !== 'string' || - typeof metadata.agentSkillVersion !== 'string' || - !Number.isInteger(metadata.representativeStabilityPasses) || - metadata.representativeStabilityPasses < 0 - ) { - throw new Error('public release attestation metadata is incomplete') - } - return attestation -} - -export function loadPublicReleaseAttestation(file, evidenceRoots) { - if (!existsSync(file)) throw new Error(`public release attestation is required: ${file}`) - const bytes = readFileSync(file) - const attestation = validatePublicReleaseAttestation(JSON.parse(bytes), evidenceRoots) - return { attestation, sha256: sha256(bytes) } -} diff --git a/web/solver-report/scripts/validate-deployed-report.mjs b/web/solver-report/scripts/validate-deployed-report.mjs deleted file mode 100644 index 1335b35..0000000 --- a/web/solver-report/scripts/validate-deployed-report.mjs +++ /dev/null @@ -1,100 +0,0 @@ -import { createHash } from 'node:crypto' -import { writeFileSync } from 'node:fs' -import { join } from 'node:path' -import { canonical, resolveSafeRemoteEntryUrl } from './public-release-policy.mjs' - -const baseUrl = (process.env.REPORT_BASE_URL ?? 'https://build.desirecore.cc/solver-agent-team/').replace(/\/?$/, '/') -const inputRoot = process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd() -const validationFile = - process.env.SOLVER_REPORT_DEPLOYMENT_VALIDATION_FILE ?? join(inputRoot, 'report-web', 'deployment-validation.json') - -async function fetchWithoutRedirect(url, init = {}) { - const response = await fetch(url, { ...init, redirect: 'manual' }) - if (response.status >= 300 && response.status < 400) { - throw new Error(`redirect-rejected:${response.status}`) - } - return response -} - -const integrityResponse = await fetchWithoutRedirect(new URL('integrity.json', baseUrl)) -if (!integrityResponse.ok) throw new Error(`integrity.json: ${integrityResponse.status}`) -const integrity = await integrityResponse.json() -const failures = [] -let checked = 0 -let checkedBytes = 0 -let cursor = 0 - -async function worker() { - while (cursor < integrity.entries.length) { - const index = cursor++ - const entry = integrity.entries[index] - try { - const entryUrl = resolveSafeRemoteEntryUrl(baseUrl, entry.path) - const response = await fetchWithoutRedirect(entryUrl) - if (!response.ok) { - failures.push({ path: entry.path, issue: `http-${response.status}` }) - continue - } - const bytes = Buffer.from(await response.arrayBuffer()) - const digest = createHash('sha256').update(bytes).digest('hex') - if (bytes.length !== entry.bytes || digest !== entry.sha256) { - failures.push({ path: entry.path, issue: 'integrity-mismatch' }) - } - checked += 1 - checkedBytes += bytes.length - } catch (error) { - failures.push({ path: entry.path, issue: error instanceof Error ? error.message : String(error) }) - } - } -} -await Promise.all(Array.from({ length: 12 }, () => worker())) - -const rootResponse = await fetchWithoutRedirect(baseUrl) -const mediaResponse = await fetchWithoutRedirect(new URL('media/original/boundary.qp-unsupported/01@2x.png', baseUrl), { - method: 'HEAD', -}) -const [buildResponse, manifestResponse] = await Promise.all([ - fetchWithoutRedirect(new URL('build.json', baseUrl)), - fetchWithoutRedirect(new URL('data/manifest.json', baseUrl)), -]) -const build = buildResponse.ok ? await buildResponse.json() : null -const manifest = manifestResponse.ok ? await manifestResponse.json() : null -if (build?.gates?.publicReleasePrivacy !== 'pass') failures.push({ path: 'build.json', issue: 'public-release-privacy-gate' }) -if ( - !build?.publicReleaseAttestationSha256 || - build.publicReleaseAttestationSha256 !== manifest?.publicReleaseAttestationSha256 -) - failures.push({ path: 'build.json', issue: 'public-release-attestation-mismatch' }) -if ( - !build || - !manifest || - build.buildId !== manifest.buildId || - build.contentRootSha256 !== manifest.contentRootSha256 || - integrity.contentRootSha256 !== manifest.contentRootSha256 || - canonical(build.publicEvidenceRoots) !== canonical(manifest.publicEvidenceRoots) || - canonical(build.counts) !== canonical(manifest.counts) || - build.integrityEntries !== integrity.entries.length || - !integrity.entries.some((entry) => entry.path === 'build.json') -) - failures.push({ path: 'build.json', issue: 'build-manifest-integrity-projection' }) -const report = { - schemaVersion: 'solver.web-deployment-validation/v1', - testedAt: new Date().toISOString(), baseUrl, - contentRootSha256: integrity.contentRootSha256, - checkedEntries: checked, checkedBytes, - rootStatus: rootResponse.status, - csp: rootResponse.headers.get('content-security-policy'), - mediaStatus: mediaResponse.status, - mediaCacheControl: mediaResponse.headers.get('cache-control'), - failures, - passed: - failures.length === 0 && - checked === integrity.entries.length && - rootResponse.ok && - mediaResponse.ok && - buildResponse.ok && - manifestResponse.ok, -} -writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`) -console.log(JSON.stringify(report, null, 2)) -if (!report.passed) process.exitCode = 1 diff --git a/web/solver-report/scripts/validate-report-web.mjs b/web/solver-report/scripts/validate-report-web.mjs deleted file mode 100644 index 62b3f73..0000000 --- a/web/solver-report/scripts/validate-report-web.mjs +++ /dev/null @@ -1,388 +0,0 @@ -import { existsSync, globSync, readFileSync, statSync, writeFileSync } from 'node:fs' -import { createRequire } from 'node:module' -import { dirname, join, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' -import { - assertPublicText, - canonical, - computePublicEvidenceRoots, - loadPublicReleaseAttestation, - sha256, -} from './public-release-policy.mjs' - -const codeRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..') -const require = createRequire(import.meta.url) -const Ajv = require('ajv').default -const ajv = new Ajv({ allErrors: true, strict: true }) -const validateDecisionTreeSchema = ajv.compile( - JSON.parse(readFileSync(join(codeRoot, 'decision-tree.schema.json'), 'utf8')) -) -const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.env.BUILDER_ROOT ?? process.cwd()) -const dist = process.env.SOLVER_REPORT_OUTPUT_ROOT ?? join(inputRoot, 'report-web', 'dist') -const validationFile = - process.env.SOLVER_REPORT_VALIDATION_FILE ?? join(inputRoot, 'report-web', 'validation.json') -const manifest = JSON.parse(readFileSync(join(dist, 'data', 'manifest.json'), 'utf8')) -const integrity = JSON.parse(readFileSync(join(dist, 'integrity.json'), 'utf8')) -const build = JSON.parse(readFileSync(join(dist, 'build.json'), 'utf8')) -const latestRegression = JSON.parse(readFileSync(join(dist, 'data', 'latest-platform-regression.json'), 'utf8')) -const failures = [] -const readJsonLines = (file) => - readFileSync(file, 'utf8') - .split('\n') - .filter(Boolean) - .map((line) => JSON.parse(line)) - -const caseRunById = new Map( - globSync(join(inputRoot, 'evidence', '**', 'case-run.json')).map((file) => { - const data = JSON.parse(readFileSync(file, 'utf8')) - return [data.runId, { file, data, runDir: join(dirname(file), 'run') }] - }) -) -const packById = new Map( - globSync(join(inputRoot, 'scenario-packs', '*', '2.0.0', 'scenario-pack.json')).map((file) => { - const pack = JSON.parse(readFileSync(file, 'utf8')) - return [pack.manifest.scenarioId, { file, pack }] - }) -) -const screenshotPlan = JSON.parse( - readFileSync( - process.env.SOLVER_REPORT_PUBLIC_SCREENSHOT_PLAN_FILE ?? - join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-plan.json'), - 'utf8' - ) -) -const screenshotValidation = JSON.parse( - readFileSync(join(inputRoot, 'evidence', 'screenshots', 'screenshot-validation.json'), 'utf8') -) -const screenshotPrivacyValidation = JSON.parse( - readFileSync( - process.env.SOLVER_REPORT_SCREENSHOT_PRIVACY_VALIDATION_FILE ?? - join(inputRoot, 'evidence', 'screenshots', 'public-screenshot-privacy-validation.json'), - 'utf8' - ) -) -const publicEvidenceRoots = computePublicEvidenceRoots({ - inputRoot, - screenshotPlan, - screenshotValidation, - screenshotPrivacyValidation, - caseRunById, -}) -const publicAttestationFile = - process.env.SOLVER_REPORT_PUBLIC_RELEASE_ATTESTATION_FILE ?? - join(inputRoot, 'evidence', 'public-release-attestation.json') -const { attestation: publicReleaseAttestation, sha256: publicReleaseAttestationSha256 } = - loadPublicReleaseAttestation(publicAttestationFile, publicEvidenceRoots) - -if (manifest.chapters.length !== 24) failures.push({ issue: 'chapter-count', actual: manifest.chapters.length }) -if (manifest.counts.decisionTrees !== 24) - failures.push({ issue: 'decision-tree-count', actual: manifest.counts.decisionTrees }) -if (manifest.counts.screenshots !== 174) - failures.push({ issue: 'screenshot-count', actual: manifest.counts.screenshots }) -const historicalEvidenceContentRootSha256 = sha256( - canonical(manifest.chapters.map((item) => ({ scenarioId: item.scenarioId, sha256: item.sha256 }))) -) -const latestPlatformRegressionSha256 = sha256(canonical(latestRegression)) -const expectedContentRootSha256 = sha256( - canonical({ historicalEvidenceContentRootSha256, latestPlatformRegressionSha256 }) -) -if (manifest.historicalEvidenceContentRootSha256 !== historicalEvidenceContentRootSha256) - failures.push({ issue: 'historical-evidence-root' }) -if (manifest.latestPlatformRegressionSha256 !== latestPlatformRegressionSha256) - failures.push({ issue: 'latest-regression-hash' }) -if ( - manifest.contentRootSha256 !== expectedContentRootSha256 || - integrity.contentRootSha256 !== expectedContentRootSha256 -) - failures.push({ issue: 'report-content-root' }) -if (latestRegression.schemaVersion !== 'solver.latest-platform-regression/v1' || latestRegression.passed !== true) - failures.push({ issue: 'latest-regression-status' }) -if (manifest.latestPlatformRegression?.platform?.commit !== latestRegression.platform?.commit) - failures.push({ issue: 'latest-regression-manifest-projection' }) -if ( - !manifest.evidencePlatformCommit?.startsWith(publicReleaseAttestation.metadata.platformCommit) || - !manifest.evidencePlatformCommit?.startsWith(latestRegression.historicalEvidence?.platformCommit) -) - failures.push({ issue: 'historical-platform-attribution' }) -if ( - manifest.platformCommit !== latestRegression.platform?.commit || - manifest.agentSkillVersion !== latestRegression.platform?.agentSkillVersion || - manifest.counts.representativeStabilityPasses !== publicReleaseAttestation.metadata.representativeStabilityPasses -) - failures.push({ issue: 'derived-release-metadata' }) -if ( - manifest.publicReleaseAttestationSha256 !== publicReleaseAttestationSha256 || - canonical(manifest.publicEvidenceRoots) !== canonical(publicEvidenceRoots) -) - failures.push({ issue: 'public-release-attestation-projection' }) -if ( - build.buildId !== manifest.buildId || - build.contentRootSha256 !== manifest.contentRootSha256 || - build.publicReleaseAttestationSha256 !== manifest.publicReleaseAttestationSha256 || - canonical(build.publicEvidenceRoots) !== canonical(manifest.publicEvidenceRoots) || - canonical(build.counts) !== canonical(manifest.counts) || - build.integrityEntries !== integrity.entries.length || - build.gates?.decisionTrees !== 'pass' || - build.gates?.publicReleasePrivacy !== 'pass' || - !integrity.entries.some((entry) => entry.path === 'build.json') -) - failures.push({ issue: 'build-manifest-integrity-projection' }) -try { - assertPublicText(latestRegression, 'latest platform regression') -} catch (error) { - failures.push({ issue: 'latest-regression-public-text-policy', detail: error.message }) -} - -let formulas = 0 -let screenshotCount = 0 -let decisionTreeCount = 0 -for (const index of manifest.chapters) { - const file = join(dist, index.json) - const script = join(dist, index.script) - if (!existsSync(file) || !existsSync(script)) { - failures.push({ scenarioId: index.scenarioId, issue: 'chapter-file-missing' }) - continue - } - const chapter = JSON.parse(readFileSync(file, 'utf8')) - const withoutHash = structuredClone(chapter) - delete withoutHash.contentSha256 - if (sha256(canonical(withoutHash)) !== chapter.contentSha256 || chapter.contentSha256 !== index.sha256) { - failures.push({ scenarioId: index.scenarioId, issue: 'chapter-hash' }) - } - if (chapter.sections.length !== 10 || chapter.sections.some((section, i) => section.number !== i + 1)) { - failures.push({ scenarioId: index.scenarioId, issue: 'ten-part-report' }) - } - if (chapter.formulaStats.source !== chapter.formulaStats.rendered) { - failures.push({ scenarioId: index.scenarioId, issue: 'formula-count', stats: chapter.formulaStats }) - } - const renderedHtml = [ - ...chapter.timeline.filter((item) => item.kind === 'message').map((item) => item.html), - ...chapter.sections.map((item) => item.html), - ].join('\n') - const mathmlCount = (renderedHtml.match(//g, '').replace(//g, '') - if (/\\\(|\\\)|\\\[|\\\]/.test(renderedWithoutCode)) { - failures.push({ scenarioId: index.scenarioId, issue: 'unrendered-latex-delimiter' }) - } - formulas += chapter.formulaStats.rendered - screenshotCount += chapter.media.length - const source = caseRunById.get(chapter.evidence.runId) - const sourceMessagesFile = source ? join(source.runDir, 'messages.jsonl') : null - const sourceSessionFile = source ? join(source.runDir, 'sessions', 'session.jsonl') : null - const sourceInvocationsFile = source ? join(source.runDir, 'receipts', 'tool-invocations.jsonl') : null - const sourceInvocations = source ? readJsonLines(sourceInvocationsFile) : [] - const sourceSession = source ? readJsonLines(sourceSessionFile) : [] - const finishedReceipts = sourceInvocations.filter((item) => item.phase === 'finished' && item.status === 'success') - const finishedToolNames = finishedReceipts.map((item) => item.tool_name) - const parsedSummaries = sourceSession - .filter((event) => event.type === 'tool_use_summary') - .map((event) => { - try { - return { event, summary: JSON.parse(event.data?.summary ?? '') } - } catch { - return null - } - }) - .filter((entry) => entry?.summary && typeof entry.summary === 'object' && !Array.isArray(entry.summary)) - const solveReceipt = finishedReceipts.find((item) => item.tool_name === 'OptimizationSolve') - const validationReceipt = [...finishedReceipts].reverse().find((item) => item.tool_name === 'OptimizationValidate') - const ledgerReceipt = [...finishedReceipts].reverse().find((item) => item.tool_name === 'OptimizationEvidenceLedger') - const observedSolveSummary = solveReceipt - ? typeof solveReceipt.result_provenance?.result_payload_hash === 'string' - ? parsedSummaries.find( - ({ summary }) => summary.result_payload_hash === solveReceipt.result_provenance?.result_payload_hash - )?.summary ?? null - : null - : null - const observedValidationSummary = validationReceipt - ? typeof validationReceipt.result_provenance?.validation_report_hash === 'string' - ? [...parsedSummaries] - .reverse() - .find( - ({ event, summary }) => - ['pass', 'fail'].includes(summary.verdict) && - event.data?.metadata?.validation_report_hash === validationReceipt.result_provenance?.validation_report_hash - )?.summary ?? null - : null - : null - const observedLedgerSummary = ledgerReceipt - ? [...parsedSummaries] - .reverse() - .find(({ summary }) => summary.schema_version === 'solver.optimization-evidence-ledger/v1')?.summary ?? null - : null - const tree = chapter.decisionTree - const treeNodes = new Map(tree?.nodes?.map((node) => [node.id, node]) ?? []) - const packEntry = packById.get(index.scenarioId) - if ( - !validateDecisionTreeSchema(tree) || - tree?.schemaVersion !== 'solver.human-agent-decision-tree/v1' || - treeNodes.size !== tree?.nodes?.length || - !treeNodes.has(tree?.rootNodeId) || - tree?.edges?.some((edge) => !treeNodes.has(edge.from) || !treeNodes.has(edge.to)) || - !packEntry || - tree?.evidenceBindings?.scenarioPackSha256 !== sha256(readFileSync(packEntry.file)) || - tree?.evidenceBindings?.runId !== chapter.evidence.runId || - tree?.evidenceBindings?.sourceMessagesSha256 !== chapter.evidence.sourceMessagesSha256 || - tree?.evidenceBindings?.sourceSessionSha256 !== chapter.evidence.sourceSessionSha256 || - tree?.evidenceBindings?.sourceToolInvocationsSha256 !== chapter.evidence.sourceToolInvocationsSha256 || - tree?.evidenceBindings?.optimizationSpecSha256 !== chapter.evidence.optimizationSpecSha256 || - tree?.evidenceBindings?.resultPayloadSha256 !== chapter.evidence.resultPayloadSha256 || - tree?.evidenceBindings?.validationReportSha256 !== chapter.evidence.validationReportSha256 || - !source || - tree?.evidenceBindings?.sourceMessagesSha256 !== sha256(readFileSync(sourceMessagesFile)) || - tree?.evidenceBindings?.sourceSessionSha256 !== sha256(readFileSync(sourceSessionFile)) || - tree?.evidenceBindings?.sourceToolInvocationsSha256 !== sha256(readFileSync(sourceInvocationsFile)) || - source.data.status !== 'passed' || - tree?.observed?.runStatus !== source.data.status || - !['optimization', 'validation', 'recovery'].includes(tree?.observed?.workflowKind) || - (tree?.observed?.workflowKind === 'optimization' && - (!finishedToolNames.includes('OptimizationSolve') || - !finishedToolNames.includes('OptimizationValidate') || - !observedSolveSummary || - !observedValidationSummary || - tree.observed.solveStatus !== observedSolveSummary.status || - tree.observed.validationVerdict !== observedValidationSummary.verdict || - !solveReceipt?.result_provenance?.optimization_spec_hash || - tree.evidenceBindings.optimizationSpecSha256 !== solveReceipt.result_provenance.optimization_spec_hash || - tree.evidenceBindings.resultPayloadSha256 !== solveReceipt?.result_provenance?.result_payload_hash || - !validationReceipt?.result_provenance?.validation_report_hash || - tree.evidenceBindings.validationReportSha256 !== validationReceipt?.result_provenance?.validation_report_hash || - !tree.evidenceBindings.resultPayloadSha256 || - chapter.resultStatus !== tree.observed.solveStatus || - chapter.validationVerdict !== tree.observed.validationVerdict)) || - (tree?.observed?.workflowKind === 'validation' && - (finishedToolNames.includes('OptimizationCompile') || - finishedToolNames.includes('OptimizationSolve') || - tree.observed.solveStatus !== null || - !finishedToolNames.includes('OptimizationValidate') || - !observedValidationSummary || - tree.observed.validationVerdict !== observedValidationSummary.verdict || - !validationReceipt?.result_provenance?.validation_report_hash || - tree.evidenceBindings.validationReportSha256 !== validationReceipt?.result_provenance?.validation_report_hash || - treeNodes.has('model') || - chapter.resultStatus !== tree.observed.validationVerdict)) || - (tree?.observed?.workflowKind === 'recovery' && - (finishedToolNames.includes('OptimizationCompile') || - finishedToolNames.includes('OptimizationSolve') || - finishedToolNames.includes('OptimizationValidate') || - tree.observed.solveStatus !== null || - tree.observed.validationVerdict !== null || - !finishedToolNames.includes('OptimizationEvidenceLedger') || - !observedLedgerSummary || - !ledgerReceipt?.result_provenance?.result_payload_hash || - tree.evidenceBindings.resultPayloadSha256 !== ledgerReceipt?.result_provenance?.result_payload_hash || - treeNodes.has('model') || - !tree.evidenceBindings.resultPayloadSha256 || - chapter.resultStatus !== 'recovered')) - ) { - failures.push({ scenarioId: index.scenarioId, issue: 'decision-tree-contract-or-evidence-binding' }) - } else { - const selectedEdges = tree.edges.filter((edge) => edge.selected) - const selectedByFrom = new Map() - for (const edge of selectedEdges) selectedByFrom.set(edge.from, [...(selectedByFrom.get(edge.from) ?? []), edge]) - let cursor = tree.rootNodeId - const visited = new Set() - while (selectedByFrom.get(cursor)?.length === 1 && !visited.has(cursor)) { - visited.add(cursor) - cursor = selectedByFrom.get(cursor)[0].to - } - if ( - cursor !== 'outcome' || - visited.size !== selectedEdges.length || - [...selectedByFrom.values()].some((outgoing) => outgoing.length !== 1) || - tree.nodes.some((node) => node.id !== tree.rootNodeId && !tree.edges.some((edge) => edge.to === node.id)) - ) - failures.push({ scenarioId: index.scenarioId, issue: 'decision-tree-selected-path' }) - else decisionTreeCount += 1 - } - for (const image of chapter.media) { - const original = join(dist, image.original) - if (!existsSync(original) || sha256(readFileSync(original)) !== image.sha256) { - failures.push({ scenarioId: index.scenarioId, issue: 'original-image', path: image.original }) - } - for (const thumb of [image.thumb756, image.thumb1512]) { - if (!existsSync(join(dist, thumb))) - failures.push({ scenarioId: index.scenarioId, issue: 'thumbnail-missing', path: thumb }) - } - } - if (!source) failures.push({ scenarioId: index.scenarioId, issue: 'source-run-missing' }) - else { - const raw = readJsonLines(sourceMessagesFile).filter((item) => - ['user', 'assistant'].includes(item.role) - ) - const projected = chapter.timeline.filter((item) => item.kind === 'message') - if ( - raw.length !== projected.length || - raw.some((message, i) => message.role !== projected[i].role || (message.content ?? '') !== projected[i].markdown) - ) { - failures.push({ scenarioId: index.scenarioId, issue: 'transcript-not-1-to-1' }) - } - } - const serialized = JSON.stringify(chapter) - try { - assertPublicText(serialized, `${index.scenarioId} chapter`) - } catch (error) { - failures.push({ scenarioId: index.scenarioId, issue: 'public-text-policy', detail: error.message }) - } - if ( - ['qp', 'qcp', 'cp'].includes(String(chapter.problemFamily).toLowerCase()) && - /当前只支持\s*LP\/?MILP/i.test(serialized) - ) { - failures.push({ scenarioId: index.scenarioId, issue: 'stale-capability-summary' }) - } -} - -if (screenshotCount !== 174) failures.push({ issue: 'chapter-screenshot-sum', actual: screenshotCount }) -if (decisionTreeCount !== 24) failures.push({ issue: 'chapter-decision-tree-sum', actual: decisionTreeCount }) -for (const entry of integrity.entries) { - const file = join(dist, entry.path) - if (!existsSync(file)) failures.push({ issue: 'integrity-file-missing', path: entry.path }) - else { - const bytes = readFileSync(file) - if (bytes.length !== entry.bytes || sha256(bytes) !== entry.sha256) - failures.push({ issue: 'integrity-mismatch', path: entry.path }) - } -} -for (const required of [ - 'index.html', - 'assets/report.css', - 'assets/report.js', - 'assets/katex.min.css', - 'data/manifest.js', - 'data/decision-tree.schema.json', - 'data/latest-platform-regression.json', -]) { - if (!existsSync(join(dist, required))) failures.push({ issue: 'required-asset', path: required }) -} - -const report = { - schemaVersion: 'solver.web-report-validation/v1', - generatedAt: new Date().toISOString(), - buildId: manifest.buildId, - chapters: manifest.chapters.length, - screenshots: screenshotCount, - decisionTrees: decisionTreeCount, - formulas, - integrityEntries: integrity.entries.length, - bytes: globSync(join(dist, '**', '*')) - .filter((file) => statSync(file).isFile()) - .reduce((sum, file) => sum + statSync(file).size, 0), - latestPlatformRegression: { - commit: latestRegression.platform.commit, - scenarios: latestRegression.runtime.scenarios.length, - decisionWorkspace: latestRegression.runtime.decisionWorkspace.status, - }, - passed: failures.length === 0, - failures, -} -writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`) -console.log(JSON.stringify(report, null, 2)) -if (!report.passed) process.exitCode = 1 diff --git a/web/solver-report/scripts/validate-screenshot-privacy-ocr.mjs b/web/solver-report/scripts/validate-screenshot-privacy-ocr.mjs deleted file mode 100644 index 138cc8c..0000000 --- a/web/solver-report/scripts/validate-screenshot-privacy-ocr.mjs +++ /dev/null @@ -1,80 +0,0 @@ -import { existsSync, readFileSync, writeFileSync } from 'node:fs' -import { join, resolve } from 'node:path' -import { assertPublicText, canonical, sha256 } from './public-release-policy.mjs' - -const inputRoot = resolve(process.env.SOLVER_REPORT_INPUT_ROOT ?? process.cwd()) -const planFile = - process.env.SOLVER_REPORT_SCREENSHOT_PLAN_FILE ?? - join(inputRoot, 'evidence', 'screenshots', 'screenshot-plan.json') -const ocrFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_FILE -if (!ocrFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_FILE is required') -const validationFile = process.env.SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE -if (!validationFile) throw new Error('SOLVER_REPORT_SCREENSHOT_OCR_VALIDATION_FILE is required') - -const plan = JSON.parse(readFileSync(planFile, 'utf8')) -const ocr = JSON.parse(readFileSync(ocrFile, 'utf8')) -if (ocr.schemaVersion !== 'solver.screenshot-privacy-ocr/v1' || !Array.isArray(ocr.entries)) { - throw new Error('unsupported screenshot OCR result') -} - -const planned = plan.scenarios.flatMap((scenario) => - scenario.screenshots.map((screenshot) => ({ scenarioId: scenario.scenarioId, ...screenshot })) -) -if (planned.length !== 174 || ocr.entries.length !== planned.length) { - throw new Error(`screenshot OCR coverage mismatch: expected ${planned.length}, received ${ocr.entries.length}`) -} - -const failures = [] -const screenshotRootEntries = [] -let recognizedLines = 0 -let screenshotsWithText = 0 -for (const [index, expected] of planned.entries()) { - const actual = ocr.entries[index] - const imageFile = join(inputRoot, expected.file) - if (!existsSync(imageFile)) failures.push({ file: expected.file, issue: 'missing-image' }) - const actualSha256 = existsSync(imageFile) ? sha256(readFileSync(imageFile)) : null - if ( - actual?.scenarioId !== expected.scenarioId || - actual?.file !== expected.file || - actualSha256 !== expected.sha256 || - !Array.isArray(actual?.lines) || - actual.lines.some((line) => typeof line?.text !== 'string' || !Array.isArray(line?.boundingBox)) - ) { - failures.push({ file: expected.file, issue: 'plan-or-hash-mismatch' }) - continue - } - const text = actual.lines.map((line) => line.text).join('\n') - recognizedLines += actual.lines.length - if (actual.lines.length > 0) screenshotsWithText += 1 - try { - assertPublicText(text, `${expected.scenarioId} screenshot OCR`) - } catch (error) { - failures.push({ file: expected.file, issue: 'sensitive-text-policy', detail: error.message }) - } - screenshotRootEntries.push({ - scenarioId: expected.scenarioId, - file: expected.file, - sha256: expected.sha256, - width: expected.width, - height: expected.height, - }) -} - -const report = { - schemaVersion: 'solver.screenshot-privacy-validation/v1', - auditedAt: new Date().toISOString(), - screenshotPlanSha256: sha256(canonical(plan)), - screenshotContentRootSha256: sha256(canonical(screenshotRootEntries)), - screenshots: planned.length, - ocr: { - engine: ocr.engine, - screenshotsWithText, - recognizedLines, - sensitiveMatches: failures.filter((failure) => failure.issue === 'sensitive-text-policy').length, - }, - passed: failures.length === 0 && screenshotsWithText === planned.length, - failures, -} -writeFileSync(validationFile, `${JSON.stringify(report, null, 2)}\n`) -console.log(JSON.stringify(report, null, 2)) -if (!report.passed) process.exitCode = 1 diff --git a/web/solver-report/scripts/verify-static-release.py b/web/solver-report/scripts/verify-static-release.py deleted file mode 100644 index 8f33ad3..0000000 --- a/web/solver-report/scripts/verify-static-release.py +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env python3 -import hashlib -import json -import pathlib -import sys - -root = pathlib.Path(sys.argv[1]).resolve() -integrity = json.loads((root / "integrity.json").read_text(encoding="utf-8")) -failures = [] -for entry in integrity["entries"]: - raw_path = entry.get("path") - if ( - not isinstance(raw_path, str) - or not raw_path - or "//" in raw_path - or "\\" in raw_path - or "%" in raw_path - or "?" in raw_path - or "#" in raw_path - ): - failures.append({"path": str(raw_path), "issue": "unsafe-path"}) - continue - relative_path = pathlib.PurePosixPath(raw_path) - if relative_path.is_absolute() or any(part in ("", ".", "..") for part in relative_path.parts): - failures.append({"path": raw_path, "issue": "unsafe-path"}) - continue - path = (root / pathlib.Path(*relative_path.parts)).resolve() - if path == root or root not in path.parents: - failures.append({"path": raw_path, "issue": "path-escape"}) - continue - if not path.is_file(): - failures.append({"path": raw_path, "issue": "missing"}) - continue - data = path.read_bytes() - digest = hashlib.sha256(data).hexdigest() - if len(data) != entry["bytes"] or digest != entry["sha256"]: - failures.append({"path": raw_path, "issue": "mismatch"}) - -manifest = json.loads((root / "data" / "manifest.json").read_text(encoding="utf-8")) -build = json.loads((root / "build.json").read_text(encoding="utf-8")) -if build.get("gates", {}).get("publicReleasePrivacy") != "pass": - failures.append({"path": "build.json", "issue": "public-release-privacy-gate"}) -if build.get("gates", {}).get("decisionTrees") != "pass": - failures.append({"path": "build.json", "issue": "decision-tree-gate"}) -if build.get("publicReleaseAttestationSha256") != manifest.get("publicReleaseAttestationSha256"): - failures.append({"path": "build.json", "issue": "public-release-attestation-mismatch"}) -if ( - build.get("buildId") != manifest.get("buildId") - or build.get("contentRootSha256") != manifest.get("contentRootSha256") - or integrity.get("contentRootSha256") != manifest.get("contentRootSha256") - or build.get("publicEvidenceRoots") != manifest.get("publicEvidenceRoots") - or build.get("counts") != manifest.get("counts") - or build.get("integrityEntries") != len(integrity.get("entries", [])) - or not any(entry.get("path") == "build.json" for entry in integrity.get("entries", [])) -): - failures.append({"path": "build.json", "issue": "build-manifest-integrity-projection"}) -result = { - "buildId": manifest["buildId"], - "contentRootSha256": manifest["contentRootSha256"], - "chapters": len(manifest["chapters"]), - "integrityEntries": len(integrity["entries"]), - "failures": failures, - "passed": not failures, -} -print(json.dumps(result, ensure_ascii=False)) -raise SystemExit(0 if result["passed"] else 1) diff --git a/web/solver-report/source/index.html b/web/solver-report/source/index.html deleted file mode 100644 index 2946cfa..0000000 --- a/web/solver-report/source/index.html +++ /dev/null @@ -1,107 +0,0 @@ - - - - - - - - - 通用求解器智能体团队 · 真实场景验证书 - - - - - -
- -
-
- -
- 载入证据… -
-
-
- -
-
-
-
- -
-
-
- - - - - - - -

证据完整性

-
-
- - -
-
- - - - - diff --git a/web/solver-report/source/report.css b/web/solver-report/source/report.css deleted file mode 100644 index eccfd1f..0000000 --- a/web/solver-report/source/report.css +++ /dev/null @@ -1,1430 +0,0 @@ -:root { - --ink: #172238; - --muted: #637089; - --line: #dce3ed; - --surface: #fff; - --soft: #f5f7fb; - --navy: #101b35; - --navy2: #182a50; - --cyan: #23b8b2; - --green: #35b779; - --amber: #e29b38; - --red: #d95c63; - --violet: #7767d9; - --shadow: 0 18px 50px rgba(24, 42, 80, 0.1); - font-family: Inter, 'SF Pro Text', 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif; - color: var(--ink); - background: #eef2f7; -} -* { - box-sizing: border-box; -} -html { - scroll-behavior: smooth; -} -body { - margin: 0; - background: radial-gradient(circle at 85% 0, #e3f4f2 0, transparent 30%), #eef2f7; -} -.skip-link { - position: fixed; - left: 16px; - top: -60px; - z-index: 99; - background: #fff; - padding: 10px 14px; - border-radius: 10px; -} -.skip-link:focus { - top: 16px; -} -.app-shell { - display: grid; - grid-template-columns: 330px minmax(0, 1fr); - min-height: 100vh; -} -.rail { - position: fixed; - inset: 0 auto 0 0; - width: 330px; - background: linear-gradient(160deg, var(--navy), #122346 55%, #163752); - color: #fff; - padding: 24px 20px; - display: flex; - flex-direction: column; - z-index: 20; - box-shadow: 12px 0 40px rgba(16, 27, 53, 0.16); -} -.brand { - display: flex; - gap: 12px; - align-items: center; - padding: 2px 4px 18px; -} -.brand-mark { - display: grid; - place-items: center; - width: 42px; - height: 42px; - border-radius: 13px; - background: linear-gradient(135deg, var(--cyan), #6ce0ca); - color: #0e2941; - font-size: 24px; - font-weight: 900; - box-shadow: 0 10px 22px rgba(35, 184, 178, 0.28); -} -.brand strong, -.brand small { - display: block; -} -.brand strong { - font-size: 17px; - letter-spacing: 0.02em; -} -.brand small { - margin-top: 3px; - color: #b7c4da; - font-size: 12px; -} -.rail-summary { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 6px; - margin-bottom: 14px; -} -.rail-summary span { - background: rgba(255, 255, 255, 0.07); - border: 1px solid rgba(255, 255, 255, 0.09); - padding: 9px 6px; - border-radius: 10px; - text-align: center; - font-size: 11px; - color: #bfcce0; -} -.rail-summary b { - display: block; - color: #fff; - font-size: 16px; - margin-bottom: 2px; -} -.search { - display: block; - font-size: 11px; - color: #b8c5d9; - margin-bottom: 10px; -} -.search span { - position: absolute; - clip: rect(0 0 0 0); -} -.search input { - width: 100%; - border: 1px solid rgba(255, 255, 255, 0.12); - background: rgba(0, 0, 0, 0.14); - color: #fff; - border-radius: 11px; - padding: 11px 12px; - outline: none; -} -.search input:focus { - border-color: var(--cyan); - box-shadow: 0 0 0 3px rgba(35, 184, 178, 0.16); -} -.filters { - display: flex; - flex-wrap: wrap; - gap: 6px; - margin-bottom: 12px; -} -.filter { - border: 1px solid rgba(255, 255, 255, 0.12); - background: transparent; - color: #c8d3e4; - border-radius: 999px; - padding: 5px 9px; - font-size: 10px; - cursor: pointer; -} -.filter.active { - background: var(--cyan); - color: #0d2940; - border-color: transparent; -} -.chapter-nav { - overflow: auto; - min-height: 0; - overscroll-behavior: contain; - padding-right: 5px; -} -.nav-item { - display: grid; - grid-template-columns: 28px 1fr auto; - gap: 8px; - align-items: start; - width: 100%; - text-align: left; - color: #e8edf6; - background: transparent; - border: 0; - border-radius: 11px; - padding: 9px 8px; - cursor: pointer; -} -.nav-item:hover { - background: rgba(255, 255, 255, 0.07); -} -.nav-item.active { - background: linear-gradient(90deg, rgba(35, 184, 178, 0.25), rgba(119, 103, 217, 0.18)); - box-shadow: inset 3px 0 var(--cyan); -} -.nav-index { - display: grid; - place-items: center; - width: 25px; - height: 25px; - border-radius: 8px; - background: rgba(255, 255, 255, 0.08); - font-size: 10px; -} -.nav-copy strong, -.nav-copy small { - display: block; -} -.nav-copy strong { - font-size: 12px; - line-height: 1.35; -} -.nav-copy small { - margin-top: 3px; - color: #aebdd2; - font-size: 10px; -} -.nav-status { - width: 8px; - height: 8px; - border-radius: 50%; - margin-top: 8px; - background: var(--green); -} -.nav-status.guard { - background: var(--amber); -} -.rail-foot { - display: flex; - justify-content: space-between; - color: #8698b4; - font-size: 9px; - padding: 12px 4px 0; -} -.main { - grid-column: 2; - min-width: 0; -} -.topbar { - position: sticky; - top: 0; - z-index: 12; - height: 66px; - background: rgba(245, 247, 251, 0.88); - backdrop-filter: blur(18px); - display: flex; - align-items: center; - gap: 18px; - padding: 0 34px; - border-bottom: 1px solid rgba(220, 227, 237, 0.8); -} -.icon-button { - display: none; -} -.progress { - flex: 1; - display: flex; - align-items: center; - gap: 14px; - font-size: 11px; - color: var(--muted); -} -.progress > div { - height: 5px; - flex: 1; - max-width: 280px; - border-radius: 4px; - background: #dce3ed; - overflow: hidden; -} -.progress i { - display: block; - height: 100%; - background: linear-gradient(90deg, var(--cyan), var(--violet)); - width: 0; - transition: width 0.4s; -} -.top-actions { - display: flex; - gap: 8px; -} -.top-actions button, -.pager button, -.lightbox button, -.lightbox a, -.integrity-dialog button { - border: 1px solid var(--line); - background: #fff; - color: var(--ink); - border-radius: 10px; - padding: 8px 12px; - cursor: pointer; - text-decoration: none; -} -.chapter { - max-width: 1180px; - margin: 0 auto; - padding: 36px 42px 64px; - outline: none; -} -.chapter-hero { - position: relative; - overflow: hidden; - background: linear-gradient(135deg, var(--navy), var(--navy2) 62%, #18545c); - color: #fff; - border-radius: 24px; - padding: 42px; - box-shadow: var(--shadow); -} -.chapter-hero:after { - content: ''; - position: absolute; - width: 360px; - height: 360px; - border-radius: 50%; - right: -130px; - top: -170px; - background: radial-gradient(circle, rgba(82, 226, 207, 0.28), transparent 67%); -} -.eyebrow { - display: flex; - gap: 8px; - align-items: center; - font-size: 11px; - color: #b8c9df; - text-transform: uppercase; - letter-spacing: 0.1em; -} -.badge { - display: inline-flex; - align-items: center; - border-radius: 999px; - padding: 5px 9px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.12); - font-size: 10px; - color: #dce7f4; -} -.chapter-hero h1 { - font-size: 36px; - line-height: 1.2; - margin: 20px 0 12px; - max-width: 780px; -} -.chapter-hero .story { - font-size: 17px; - line-height: 1.75; - color: #e6edf7; - max-width: 860px; -} -.hero-grid { - display: grid; - grid-template-columns: 2fr 1fr; - gap: 16px; - margin-top: 26px; -} -.hero-card { - background: rgba(255, 255, 255, 0.08); - border: 1px solid rgba(255, 255, 255, 0.11); - border-radius: 15px; - padding: 16px; -} -.hero-card small { - display: block; - color: #a9bdd5; - margin-bottom: 6px; -} -.hero-card strong { - font-size: 14px; - line-height: 1.55; -} -.kpis { - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 10px; - margin: 20px 0; -} -.kpi { - background: #fff; - border: 1px solid var(--line); - border-radius: 14px; - padding: 15px; - box-shadow: 0 8px 24px rgba(24, 42, 80, 0.05); -} -.kpi small { - color: var(--muted); - font-size: 10px; -} -.kpi strong { - display: block; - margin-top: 7px; - font-size: 14px; - overflow-wrap: anywhere; -} -.section-shell { - background: #fff; - border: 1px solid var(--line); - border-radius: 20px; - padding: 28px; - margin-top: 18px; - box-shadow: 0 9px 28px rgba(24, 42, 80, 0.05); -} -.section-head { - display: flex; - align-items: end; - justify-content: space-between; - gap: 20px; - margin-bottom: 20px; -} -.section-head h2 { - margin: 0; - font-size: 23px; -} -.section-head p { - margin: 5px 0 0; - color: var(--muted); - font-size: 12px; -} -.evidence-tag { - font-family: ui-monospace, monospace; - font-size: 9px; - color: #477070; - background: #edf8f6; - border-radius: 7px; - padding: 6px 8px; -} -.timeline { - display: flex; - flex-direction: column; - gap: 13px; -} -.message { - max-width: 86%; - border-radius: 16px; - padding: 16px 18px; - position: relative; -} -.message.user { - align-self: flex-end; - background: linear-gradient(135deg, #dcf8ee, #e9f7ff); - border: 1px solid #c9eae4; -} -.message.assistant { - align-self: flex-start; - background: var(--soft); - border: 1px solid #e5e9f1; -} -.message-meta { - display: flex; - justify-content: space-between; - gap: 10px; - color: var(--muted); - font-size: 9px; - margin-bottom: 9px; -} -.message-body { - font-size: 13px; - line-height: 1.7; -} -.message-body > :first-child { - margin-top: 0; -} -.message-body > :last-child { - margin-bottom: 0; -} -.tool-event { - display: flex; - gap: 12px; - align-items: center; - border-left: 3px solid var(--violet); - background: #f8f7ff; - border-radius: 0 12px 12px 0; - padding: 10px 13px; - font-size: 11px; -} -.tool-event strong { - font-family: ui-monospace, monospace; -} -.tool-event .status { - margin-left: auto; -} -.status.pass { - color: #16855a; -} -.status.guard { - color: #ae6a16; -} -.report-grid { - display: grid; - gap: 14px; -} -.report-part { - display: grid; - grid-template-columns: 58px minmax(0, 1fr); - border: 1px solid var(--line); - border-radius: 16px; - overflow: hidden; -} -.part-number { - display: grid; - place-items: start center; - padding-top: 18px; - background: linear-gradient(#f1f5fa, #eaf3f3); - font-size: 20px; - font-weight: 800; - color: #426477; -} -.part-body { - padding: 18px 22px; - min-width: 0; -} -.part-body h3 { - margin: 0 0 13px; - font-size: 18px; -} -.markdown { - font-size: 13px; - line-height: 1.72; - overflow-wrap: anywhere; -} -.markdown h3, -.markdown h4 { - margin: 1.1em 0 0.45em; -} -.markdown table { - border-collapse: collapse; - width: 100%; - display: block; - overflow: auto; -} -.markdown th, -.markdown td { - border: 1px solid var(--line); - padding: 7px 9px; - text-align: left; -} -.markdown th { - background: #f1f4f8; -} -.markdown code { - background: #edf2f5; - color: #24775b; - padding: 2px 5px; - border-radius: 5px; -} -.markdown pre { - background: #1a2130; - color: #e8edf5; - border-radius: 12px; - padding: 15px; - overflow: auto; -} -.markdown pre code { - background: transparent; - color: inherit; - padding: 0; -} -.katex-display { - overflow-x: auto; - overflow-y: hidden; - padding: 8px 0; -} -.validation-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 12px; -} -.validation-card { - border: 1px solid var(--line); - background: var(--soft); - border-radius: 14px; - padding: 16px; -} -.validation-card small { - color: var(--muted); -} -.validation-card strong { - display: block; - margin-top: 7px; - overflow-wrap: anywhere; -} -.decision-tree-section { - overflow: hidden; - scroll-margin-top: 82px; - background: - radial-gradient(circle at 10% 0, rgba(35, 184, 178, 0.1), transparent 30%), - #fff; -} -.tree-toolbar, -.tree-proof { - display: flex; - align-items: center; - justify-content: space-between; - gap: 14px; -} -.tree-toolbar { - margin-bottom: 16px; -} -.tree-legend, -.tree-actions { - display: flex; - flex-wrap: wrap; - gap: 7px; -} -.tree-legend span { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 5px 8px; - border: 1px solid var(--line); - border-radius: 999px; - background: #fff; - color: var(--muted); - font-size: 9px; -} -.tree-legend span::before { - content: ''; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--cyan); -} -.tree-legend .human::before { - background: var(--violet); -} -.tree-legend .agent::before { - background: var(--cyan); -} -.tree-legend .validator::before { - background: var(--green); -} -.tree-legend .blocked::before { - background: var(--red); -} -.tree-actions button { - min-height: 36px; - border: 1px solid var(--line); - border-radius: 10px; - padding: 8px 11px; - background: #fff; - color: var(--ink); - font: inherit; - font-size: 10px; - cursor: pointer; -} -.tree-actions button:hover, -.tree-actions button:focus-visible { - border-color: #7ecac1; - outline: none; - box-shadow: 0 0 0 3px rgba(35, 184, 178, 0.12); -} -.tree-scroll { - overflow-x: auto; - overscroll-behavior-inline: contain; - padding: 8px 4px 18px; - scrollbar-color: #aab8ca #edf1f6; - outline: none; -} -.tree-scroll:focus-visible { - border-radius: 12px; - box-shadow: inset 0 0 0 3px rgba(35, 184, 178, 0.24); -} -.tree-flow { - display: grid; - grid-auto-flow: column; - grid-auto-columns: 200px; - align-items: start; - gap: 52px; - width: max-content; - min-width: 100%; -} -.tree-stage { - position: relative; - min-width: 0; -} -.tree-node { - position: relative; - min-height: 224px; - padding: 15px; - border: 1px solid var(--line); - border-top: 5px solid var(--cyan); - border-radius: 15px; - background: #fff; - box-shadow: 0 10px 28px rgba(24, 42, 80, 0.08); -} -.tree-node.neutral { - border-top-color: var(--violet); -} -.tree-node.guard { - border-top-color: var(--amber); -} -.tree-node.blocked { - border-top-color: var(--red); - background: #fff9f8; -} -.tree-node-top { - display: flex; - justify-content: space-between; - align-items: center; - gap: 7px; -} -.tree-owner, -.tree-kind { - border-radius: 999px; - padding: 4px 7px; - font-size: 8px; - line-height: 1; - white-space: nowrap; -} -.tree-owner { - background: #e9f8f5; - color: #177369; - font-weight: 800; -} -.tree-owner.human { - background: #f0edff; - color: #6251bd; -} -.tree-owner.validator { - background: #eaf8f0; - color: #247b53; -} -.tree-owner.platform { - background: #eef2f8; - color: #4d627e; -} -.tree-kind { - background: var(--soft); - color: var(--muted); - text-transform: uppercase; -} -.tree-node h3 { - margin: 14px 0 9px; - font-size: 14px; - line-height: 1.42; -} -.tree-node p { - margin: 0 0 14px; - color: var(--muted); - font-size: 10px; - line-height: 1.65; -} -.tree-node a { - position: absolute; - left: 15px; - bottom: 14px; - color: #167f75; - font-size: 9px; - text-decoration: none; -} -.tree-connector { - position: absolute; - z-index: 2; - left: calc(100% + 5px); - top: 74px; - width: 42px; - color: #2a9289; - text-align: center; -} -.tree-connector span { - display: block; - width: 88px; - margin-left: -23px; - margin-bottom: 7px; - color: var(--muted); - font-size: 8px; - line-height: 1.25; -} -.tree-connector i { - display: block; - font-size: 25px; - font-style: normal; - line-height: 1; -} -.tree-connector.guard { - color: var(--amber); -} -.tree-connector.success { - color: var(--green); -} -.tree-alternatives { - position: relative; - margin-top: 46px; -} -.tree-alternatives::before { - content: ''; - position: absolute; - left: 24px; - bottom: 100%; - width: 1px; - height: 32px; - background: repeating-linear-gradient(to bottom, var(--red) 0 4px, transparent 4px 8px); -} -.tree-alternative > span { - display: inline-block; - margin: 0 0 7px 10px; - border-radius: 999px; - padding: 4px 7px; - background: #fff1ec; - color: #a34f43; - font-size: 8px; -} -.tree-node.branch { - min-height: 190px; - border-style: dashed; - box-shadow: none; -} -.decision-tree.path-only .tree-alternatives { - display: none; -} -.tree-proof { - align-items: flex-start; - border-top: 1px solid var(--line); - padding-top: 12px; - color: var(--muted); - font-size: 9px; - line-height: 1.5; -} -.tree-proof code { - color: #477070; - overflow-wrap: anywhere; - text-align: right; -} -.gallery { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 14px; -} -.shot { - position: relative; - border: 1px solid var(--line); - background: #fff; - border-radius: 15px; - padding: 0; - overflow: hidden; - cursor: zoom-in; - text-align: left; - font: inherit; - color: inherit; - transition: - transform 0.2s ease, - border-color 0.2s ease, - box-shadow 0.2s ease; -} -.shot:hover, -.shot:focus-visible { - transform: translateY(-3px); - border-color: #9bcfc9; - box-shadow: 0 16px 34px rgba(24, 42, 80, 0.13); - outline: none; -} -.shot picture, -.shot img { - display: block; - width: 100%; -} -.shot img { - aspect-ratio: 3024/1824; - object-fit: cover; - background: #eef2f7; -} -.shot-caption { - padding: 10px 12px; - font-size: 10px; - color: var(--muted); - display: flex; - justify-content: space-between; -} -.pager { - max-width: 1180px; - margin: 0 auto 50px; - padding: 0 42px; - display: flex; - align-items: center; - justify-content: space-between; - color: var(--muted); - font-size: 11px; -} -.lightbox { - width: min(96vw, 1680px); - height: 94vh; - max-height: 94vh; - border: 0; - border-radius: 18px; - padding: 0; - background: #0b1120; - color: #fff; - overflow: hidden; - grid-template-rows: auto minmax(0, 1fr) auto; -} -.lightbox[open] { - display: grid; -} -.lightbox::backdrop { - background: rgba(6, 11, 22, 0.86); - backdrop-filter: blur(4px); -} -.lightbox-bar, -.lightbox-controls { - display: flex; - justify-content: space-between; - align-items: center; - padding: 12px 16px; -} -.lightbox-bar { - min-width: 0; - gap: 16px; - border-bottom: 1px solid rgba(255, 255, 255, 0.09); - background: rgba(11, 17, 32, 0.96); -} -.lightbox-heading { - min-width: 0; -} -.lightbox-heading strong, -.lightbox-heading small { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} -.lightbox-bar small { - display: block; - color: #aebad0; - margin-top: 3px; -} -.lightbox-viewport { - position: relative; - min-height: 0; - overflow: hidden; -} -.lightbox-stage { - width: 100%; - height: 100%; - overflow: auto; - background: #080d18; - overscroll-behavior: contain; - scrollbar-color: #42516d #121a2a; - cursor: default; - touch-action: none; - outline: none; -} -.lightbox-stage:focus-visible { - box-shadow: inset 0 0 0 3px rgba(88, 212, 199, 0.82); -} -.lightbox-stage.is-pannable { - cursor: grab; -} -.lightbox-stage.is-dragging { - cursor: grabbing; - user-select: none; -} -.lightbox-canvas { - width: max-content; - min-width: 100%; - min-height: 100%; - display: grid; - place-items: center; - padding: 18px; -} -.lightbox-stage img { - display: block; - max-width: none; - width: 1px; - height: 1px; - object-fit: contain; - user-select: none; - -webkit-user-drag: none; - box-shadow: 0 20px 70px rgba(0, 0, 0, 0.38); - will-change: width, height; -} -.lightbox-zoom-hint { - position: absolute; - left: 16px; - bottom: 14px; - display: inline-flex; - width: max-content; - max-width: calc(100% - 32px); - z-index: 1; - padding: 7px 10px; - border: 1px solid rgba(255, 255, 255, 0.12); - border-radius: 999px; - background: rgba(8, 13, 24, 0.76); - color: #aebad0; - font-size: 10px; - pointer-events: none; - backdrop-filter: blur(10px); -} -.lightbox-zoom-hint-mobile { - display: none; -} -.lightbox-controls { - justify-content: center; - gap: 8px; - border-top: 1px solid rgba(255, 255, 255, 0.09); - background: rgba(11, 17, 32, 0.98); -} -.lightbox-controls > button, -.lightbox-controls > a, -.lightbox-zoom-controls button { - min-height: 36px; - border-color: rgba(255, 255, 255, 0.16); - background: #151f34; - color: #edf3ff; -} -.lightbox-controls > button:hover, -.lightbox-controls > a:hover, -.lightbox-zoom-controls button:hover { - border-color: #58d4c7; - background: #1b2d42; -} -.lightbox-zoom-controls { - display: grid; - grid-template-columns: 38px minmax(86px, auto) 38px; - align-items: center; - overflow: hidden; - border: 1px solid rgba(255, 255, 255, 0.16); - border-radius: 10px; -} -.lightbox-zoom-controls button { - border: 0; - border-radius: 0; - padding: 7px 10px; - font-size: 17px; -} -.lightbox-zoom-controls output { - align-self: stretch; - display: grid; - place-items: center; - padding: 0 10px; - border-inline: 1px solid rgba(255, 255, 255, 0.12); - background: #0f182a; - color: #b8c8de; - font: 11px ui-monospace, monospace; - white-space: nowrap; -} -.integrity-dialog { - width: min(92vw, 680px); - box-sizing: border-box; - max-width: 680px; - border: 0; - border-radius: 18px; - padding: 28px; - box-shadow: var(--shadow); -} -.integrity-dialog::backdrop { - background: rgba(16, 27, 53, 0.55); -} -.dialog-close { - float: right; -} -.integrity-list { - display: grid; - gap: 9px; -} -.integrity-row { - display: grid; - grid-template-columns: 170px 1fr; - gap: 12px; - border-bottom: 1px solid var(--line); - padding-bottom: 8px; - font-size: 12px; -} -.integrity-row code { - overflow-wrap: anywhere; -} -.regression-banner { - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto; - align-items: center; - gap: 14px; - margin: 0 0 18px; - padding: 15px 17px; - border: 1px solid #a9ddd5; - border-radius: 18px; - background: linear-gradient(120deg, #e9faf6, #f3f4ff); - box-shadow: 0 8px 24px rgba(24, 42, 80, 0.05); -} -.regression-mark { - display: grid; - place-items: center; - width: 46px; - height: 46px; - border-radius: 14px; - background: linear-gradient(135deg, var(--cyan), #6ce0ca); - color: #0d2940; - font-size: 11px; - font-weight: 900; - letter-spacing: 0.08em; -} -.regression-copy { - display: grid; - gap: 3px; - min-width: 0; -} -.regression-copy small { - font-size: 10px; - color: #39706e; -} -.regression-copy strong { - font-size: 13px; -} -.regression-copy span { - font-size: 10px; - color: var(--muted); -} -.regression-banner button { - border: 0; - border-radius: 10px; - padding: 9px 11px; - background: var(--navy); - color: #fff; - font-size: 11px; - cursor: pointer; - white-space: nowrap; -} -.latest-dialog { - width: min(92vw, 860px); - max-height: 88vh; - border: 0; - border-radius: 22px; - padding: 28px; - box-shadow: var(--shadow); - overflow: auto; -} -.latest-dialog::backdrop { - background: rgba(16, 27, 53, 0.55); -} -.latest-dialog .dialog-close { - border: 1px solid var(--line); - background: #fff; - border-radius: 10px; - padding: 8px 12px; - cursor: pointer; -} -.regression-dialog-head { - padding: 2px 0 18px; - border-bottom: 1px solid var(--line); -} -.regression-dialog-head p { - margin: 10px 0 3px; - color: var(--muted); - font-size: 11px; -} -.regression-dialog-head h2 { - margin: 0 0 5px; - font-size: 24px; -} -.regression-dialog-head small { - color: var(--muted); -} -.regression-cases { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px; - margin: 18px 0; -} -.regression-case { - border: 1px solid var(--line); - border-radius: 15px; - background: var(--soft); - padding: 16px; - min-width: 0; -} -.regression-case > div { - display: flex; - justify-content: space-between; - gap: 8px; - align-items: center; -} -.regression-case small { - font-size: 9px; - color: var(--muted); - overflow-wrap: anywhere; -} -.regression-case h3 { - font-size: 14px; - margin: 12px 0 7px; -} -.regression-case p { - font-size: 11px; - line-height: 1.6; - color: var(--muted); -} -.regression-case code { - font-size: 9px; - overflow-wrap: anywhere; -} -.regression-boundary { - border-top: 1px solid var(--line); - padding-top: 16px; -} -.regression-boundary h3 { - margin: 0 0 8px; -} -.regression-boundary ul { - padding-left: 20px; - color: var(--muted); - font-size: 11px; - line-height: 1.7; -} -.regression-boundary p { - font-size: 11px; - line-height: 1.65; - background: #fff8e8; - border: 1px solid #eed9a8; - border-radius: 12px; - padding: 12px; -} -.noscript { - margin: 40px; -} -.empty { - color: var(--muted); - font-style: italic; -} -@media (max-width: 920px) { - .app-shell { - display: block; - } - .rail { - transform: translateX(-105%); - transition: transform 0.25s; - width: min(88vw, 340px); - } - .rail.open { - transform: none; - } - .main { - grid-column: auto; - } - .icon-button { - display: block; - } - .chapter { - padding: 24px 16px 50px; - } - .topbar { - padding: 0 16px; - } - .chapter-hero { - padding: 28px 22px; - } - .chapter-hero h1 { - font-size: 28px; - } - .hero-grid, - .validation-grid { - grid-template-columns: 1fr; - } - .decision-tree-section { - padding: 22px 16px; - } - .tree-toolbar, - .tree-proof { - align-items: stretch; - flex-direction: column; - } - .tree-actions { - display: grid; - grid-template-columns: 1fr 1fr; - } - .tree-actions button { - min-height: 44px; - } - .tree-scroll { - overflow-x: hidden; - padding-inline: 1px; - } - .tree-flow { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; - gap: 0; - } - .tree-stage { - width: 100%; - } - .tree-node { - min-height: 190px; - } - .tree-connector { - position: static; - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - padding: 9px 0; - } - .tree-connector span { - width: auto; - margin: 0 0 2px; - } - .tree-connector i { - transform: rotate(90deg); - } - .tree-alternatives { - margin: 30px 0 4px 24px; - } - .tree-node.branch { - min-height: 176px; - } - .tree-proof code { - text-align: left; - } - .kpis { - grid-template-columns: repeat(2, 1fr); - } - .gallery { - grid-template-columns: 1fr; - } - .report-part { - grid-template-columns: 44px 1fr; - } - .pager { - padding: 0 16px; - } - #print-button { - display: none; - } - .regression-banner { - grid-template-columns: auto 1fr; - } - .regression-banner button { - grid-column: 1/-1; - } - .regression-cases { - grid-template-columns: 1fr; - } - .lightbox { - width: 100vw; - max-width: 100vw; - height: 100dvh; - max-height: 100dvh; - margin: 0; - border-radius: 0; - } - .lightbox-bar { - padding: - calc(10px + env(safe-area-inset-top)) - calc(12px + env(safe-area-inset-right)) - 10px - calc(12px + env(safe-area-inset-left)); - } - .lightbox-heading strong { - font-size: 12px; - } - .lightbox-heading small { - font-size: 9px; - } - .lightbox-controls { - display: grid; - grid-template-columns: repeat(5, minmax(0, 1fr)); - padding: - 8px - calc(8px + env(safe-area-inset-right)) - calc(8px + env(safe-area-inset-bottom)) - calc(8px + env(safe-area-inset-left)); - } - .lightbox-controls > button, - .lightbox-controls > a { - display: grid; - place-items: center; - padding: 7px 8px; - font-size: 10px; - min-height: 44px; - } - .lightbox-bar button, - .lightbox-zoom-controls button { - min-width: 44px; - min-height: 44px; - } - .lightbox-zoom-controls { - grid-column: 1 / -1; - grid-row: 1; - grid-template-columns: 44px minmax(110px, 1fr) 44px; - } - .lightbox-canvas { - padding: 8px; - } - .lightbox-zoom-hint { - left: 8px; - bottom: 8px; - max-width: calc(100% - 16px); - font-size: 9px; - } - .lightbox-zoom-hint-desktop { - display: none; - } - .lightbox-zoom-hint-mobile { - display: inline; - } - .integrity-dialog { - width: calc(100vw - 32px); - max-width: calc(100vw - 32px); - padding: 20px; - } - .integrity-row { - grid-template-columns: 1fr; - gap: 4px; - } -} -@media print { - body { - background: #fff; - } - .rail, - .topbar, - .pager, - .top-actions, - .shot-caption { - display: none !important; - } - .main { - display: block; - } - .chapter { - max-width: none; - padding: 0; - } - .chapter-hero { - box-shadow: none; - background: #fff; - color: #111; - border: 1px solid #bbb; - break-after: avoid; - } - .chapter-hero .story, - .eyebrow { - color: #333; - } - .section-shell, - .report-part { - box-shadow: none; - break-inside: avoid; - } - .gallery { - grid-template-columns: 1fr 1fr; - } - .shot { - break-inside: avoid; - } - .chapter > * { - break-before: auto; - } -} diff --git a/web/solver-report/source/report.js b/web/solver-report/source/report.js deleted file mode 100644 index 8decdbe..0000000 --- a/web/solver-report/source/report.js +++ /dev/null @@ -1,583 +0,0 @@ -;(() => { - 'use strict' - const manifest = window.__REPORT_MANIFEST__ - if (!manifest) throw new Error('report manifest missing') - window.__REPORT_CHAPTERS__ = window.__REPORT_CHAPTERS__ || {} - - const $ = (selector) => document.querySelector(selector) - const esc = (value) => - String(value ?? '').replace( - /[&<>"']/g, - (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[char] - ) - const statusTone = (status) => - ['infeasible', 'fail', 'blocked_engine_unavailable'].includes(status) ? 'guard' : 'pass' - const statusLabel = (status) => - ({ - optimal: '最优解', - infeasible: '不可行 · 已诊断', - pass: '验证通过', - fail: '验证失败 · 已阻断', - recovered: '恢复通过', - idle: '已完成', - })[status] || - status || - '已验证' - const familyLabel = (family) => - ({ lp: 'LP', milp: 'MILP', qp: 'QP', miqp: 'MIQP', qcp: 'QCP', miqcp: 'MIQCP', cp: 'CP' })[ - String(family).toLowerCase() - ] || String(family).toUpperCase() - const shortHash = (value) => (value ? `${value.slice(0, 10)}…${value.slice(-8)}` : '不适用') - const formatTime = (value) => (value ? new Date(value).toLocaleString('zh-CN', { hour12: false }) : '未记录') - const state = { current: 0, filter: 'ALL', query: '', chapter: null, lightboxIndex: 0, visible: manifest.chapters } - const viewer = { - zoom: 1, - fitScale: 1, - mode: 'fit', - pointers: new Map(), - drag: null, - pinch: null, - resizeFrame: 0, - } - const MIN_VIEWER_ZOOM = 0.5 - const MAX_VIEWER_ZOOM = 12 - const clamp = (value, min, max) => Math.min(max, Math.max(min, value)) - - const loadScript = (src) => - new Promise((resolve, reject) => { - const script = document.createElement('script') - script.src = src - script.onload = resolve - script.onerror = reject - document.head.appendChild(script) - }) - async function loadChapter(index) { - const item = manifest.chapters[index] - if (!item) return - if (!window.__REPORT_CHAPTERS__[item.scenarioId]) { - if (location.protocol === 'file:') await loadScript(item.script) - else { - const response = await fetch(item.json) - if (!response.ok) throw new Error(`chapter load failed: ${response.status}`) - window.__REPORT_CHAPTERS__[item.scenarioId] = await response.json() - } - } - state.current = index - state.chapter = window.__REPORT_CHAPTERS__[item.scenarioId] - renderChapter() - renderNav() - updatePager() - updateHash() - $('#chapter').focus({ preventScroll: true }) - window.scrollTo({ top: 0, behavior: 'instant' }) - } - - function updateHash() { - const desired = `#case-${state.current + 1}` - if (location.hash !== desired) history.replaceState(null, '', desired) - } - function hashIndex() { - const caseMatch = /^#case-(\d+)$/.exec(location.hash) - if (caseMatch) return Math.max(0, Math.min(manifest.chapters.length - 1, Number(caseMatch[1]) - 1)) - const id = decodeURIComponent(location.hash.slice(1)) - const index = manifest.chapters.findIndex((item) => item.scenarioId === id) - return index >= 0 ? index : 0 - } - - function renderShell() { - $('#rail-summary').innerHTML = - `${manifest.counts.chapters}场景${manifest.counts.decisionTrees}决策树${manifest.counts.screenshots}真机图${manifest.counts.representativeStabilityPasses}稳定运行` - $('#build-id').textContent = manifest.buildId - const families = ['ALL', ...new Set(manifest.chapters.map((item) => familyLabel(item.problemFamily)))] - $('#filters').innerHTML = families - .map( - (family) => - `` - ) - .join('') - $('#filters').addEventListener('click', (event) => { - const button = event.target.closest('[data-family]') - if (!button) return - state.filter = button.dataset.family - document.querySelectorAll('.filter').forEach((item) => item.classList.toggle('active', item === button)) - applyNavFilter() - }) - $('#chapter-search').addEventListener('input', (event) => { - state.query = event.target.value.trim().toLowerCase() - applyNavFilter() - }) - $('#print-button').addEventListener('click', () => window.print()) - $('#menu-button').addEventListener('click', () => $('.rail').classList.toggle('open')) - $('#prev-button').addEventListener('click', () => - loadChapter((state.current - 1 + manifest.chapters.length) % manifest.chapters.length) - ) - $('#next-button').addEventListener('click', () => loadChapter((state.current + 1) % manifest.chapters.length)) - $('#latest-button').addEventListener('click', openLatestRegression) - $('#integrity-button').addEventListener('click', openIntegrity) - $('#integrity-dialog .dialog-close').addEventListener('click', () => $('#integrity-dialog').close()) - $('#latest-dialog .dialog-close').addEventListener('click', () => $('#latest-dialog').close()) - renderNav() - } - - function renderLatestRegressionBanner() { - const regression = manifest.latestPlatformRegression - if (!regression) return '' - const scenarios = regression.runtime?.scenarios ?? [] - const passed = scenarios.filter((item) => item.status === 'passed').length - return `
NEW
最新平台定向回归 · ${esc(formatTime(regression.testedAt))}${esc(regression.platform.commit.slice(0, 10))} 已通过 ${passed}/${scenarios.length} 个真实场景与决策工作台真机检查下方 24 章仍保持原始证据提交 ${esc(manifest.evidencePlatformCommit)} 的归属,不把历史截图冒充为新版重跑。
` - } - - function applyNavFilter() { - state.visible = manifest.chapters.filter((item) => { - const familyOk = state.filter === 'ALL' || familyLabel(item.problemFamily) === state.filter - const haystack = `${item.title} ${item.story} ${item.problemFamily} ${item.engineId}`.toLowerCase() - return familyOk && (!state.query || haystack.includes(state.query)) - }) - renderNav() - } - - function renderNav() { - $('#chapter-nav').innerHTML = - state.visible - .map((item) => { - const actualIndex = manifest.chapters.indexOf(item) - return `` - }) - .join('') || '

没有匹配的章节

' - $('#chapter-nav') - .querySelectorAll('[data-index]') - .forEach((button) => - button.addEventListener('click', () => { - $('.rail').classList.remove('open') - loadChapter(Number(button.dataset.index)) - }) - ) - $('#chapter-nav .active')?.scrollIntoView({ block: 'nearest' }) - } - - function renderTimeline(chapter) { - const visible = chapter.timeline.filter((item) => item.kind === 'tool' || !item.empty) - const finalMessageSequence = Math.max( - ...visible.filter((item) => item.kind === 'message' && item.role === 'assistant').map((item) => item.sequence) - ) - return visible - .map((item) => { - if (item.kind === 'tool') - return `
受治理工具${esc(item.tool)}${esc(item.status)}${item.durationMs == null ? '' : `${item.durationMs} ms`}
` - const isFinal = item.role === 'assistant' && item.sequence === finalMessageSequence - const body = isFinal - ? `
智能体最终完整报告(原始 Markdown 1:1 渲染)
${item.html}
` - : `
${item.html}
` - return `
${item.role === 'user' ? '用户' : '求解决策顾问'}消息 ${item.sequence} · SHA ${shortHash(item.markdownSha256)}
${body}
` - }) - .join('') - } - - const ownerLabel = (owner) => - ({ human: '人类确认', agent: 'Agent 推导', shared: '人 + Agent', validator: '独立校核', platform: '平台执行' })[ - owner - ] || owner - const kindLabel = (kind) => - ({ context: '决策背景', question: '判断门', action: '执行', check: '校核门', outcome: '结果' })[kind] || kind - - function decisionTreePath(tree) { - const nodes = new Map(tree.nodes.map((node) => [node.id, node])) - const selected = new Map(tree.edges.filter((edge) => edge.selected).map((edge) => [edge.from, edge])) - const path = [] - const visited = new Set() - let current = tree.rootNodeId - while (nodes.has(current) && !visited.has(current)) { - visited.add(current) - path.push(nodes.get(current)) - current = selected.get(current)?.to - } - return path - } - - function renderTreeNode(node, branch = false) { - return `
-
${esc(ownerLabel(node.owner))}${esc(kindLabel(node.kind))}
-

${esc(node.title)}

${esc(node.detail)}

- 查看关联证据 → -
` - } - - function renderDecisionTree(tree) { - const path = decisionTreePath(tree) - const nodes = new Map(tree.nodes.map((node) => [node.id, node])) - const selectedEdges = new Map(tree.edges.filter((edge) => edge.selected).map((edge) => [edge.from, edge])) - const stages = path - .map((node, index) => { - const nextEdge = selectedEdges.get(node.id) - const alternatives = tree.edges - .filter((edge) => edge.from === node.id && !edge.selected) - .map( - (edge) => - `
${esc(edge.label)}${renderTreeNode(nodes.get(edge.to), true)}
` - ) - .join('') - return `
${renderTreeNode(node)}${alternatives ? `
${alternatives}
` : ''}${index < path.length - 1 ? `
${esc(nextEdge.label)}
` : ''}
` - }) - .join('') - return `
-
人类确认Agent 推导独立校核阻断分支
-
${stages}
-
当前实际路径以实线连接;旁路展示系统何时必须补问或阻断。PACK ${esc(shortHash(tree.evidenceBindings.scenarioPackSha256))} · RUN ${esc(shortHash(tree.evidenceBindings.runId))}
-
` - } - - const svgTextLines = (value, max = 18, lines = 3) => { - const chars = [...String(value)] - const result = [] - while (chars.length && result.length < lines) result.push(chars.splice(0, max).join('')) - if (chars.length) result[result.length - 1] = `${result[result.length - 1].slice(0, -1)}…` - return result - } - - function downloadDecisionTreeSvg(c) { - const path = decisionTreePath(c.decisionTree) - const width = Math.max(1200, path.length * 230 + 100) - const height = 420 - const cards = path - .map((node, index) => { - const x = 50 + index * 230 - const title = svgTextLines(node.title) - .map( - (line, lineIndex) => - `${esc(line)}` - ) - .join('') - const detail = svgTextLines(node.detail, 22, 3) - .map( - (line, lineIndex) => - `${esc(line)}` - ) - .join('') - const connector = - index < path.length - 1 - ? `` - : '' - return `${esc(ownerLabel(node.owner))}${title}${detail}${connector}` - }) - .join('') - const svg = `${esc(c.title)} · 人 + Agent 决策树${cards}实际证据路径场景 ${esc(c.scenarioId)} · ${esc(c.decisionTree.evidenceBindings.scenarioPackSha256)}此 SVG 由验证书中的结构化决策树即时导出;完整旁路与证据链接请在 Web 页面审阅。` - const url = URL.createObjectURL(new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })) - const link = document.createElement('a') - link.href = url - link.download = `${c.scenarioId.replace(/[^a-z0-9._-]/gi, '-')}-decision-tree.svg` - link.click() - setTimeout(() => URL.revokeObjectURL(url), 0) - } - - function renderChapter() { - const c = state.chapter - const tone = statusTone(c.resultStatus) - const hard = c.validation?.satisfaction_report?.hard_constraints - const soft = c.validation?.satisfaction_report?.soft_constraints - $('#chapter').innerHTML = `${renderLatestRegressionBanner()} -
-
CHAPTER ${String(c.scenarioOrder).padStart(2, '0')}${familyLabel(c.problemFamily)}${statusLabel(c.resultStatus)}${esc(c.claimBoundary)}
-

${esc(c.title)}

${esc(c.story)}

-
决策问题${esc(c.decision)}
专业结论${esc(c.professionalSummary)}
-
-
-
问题族${familyLabel(c.problemFamily)}
-
实际引擎${esc(c.engineId || '独立校核')}
-
结果 / 校核${statusLabel(c.resultStatus)} / ${esc(c.validationVerdict || '不适用')}
-
人机决策树${c.decisionTree.nodes.length} 节点 · 证据绑定
-
真实消息 / 工具${c.counts.rawConversationMessages} / ${c.counts.toolEvents}
-
原生真机截图${c.counts.screenshots} 张 · 2x
-
-

场景故事与意义

为什么这个问题具有代表性,以及哪些业务规则决定可行性。

PACK ${esc(c.scenarioVersion)} · ${shortHash(c.contentSha256)}
-
目标${c.objectives.map(esc).join(';')}
基线${esc(c.baseline)}
-
硬规则${c.constraints.map(esc).join(';')}
-
-

人 + Agent 场景决策树

从业务确认到交付门禁,展示本次实际路径以及必须补问、拒绝或阻断的旁路。

EVIDENCE-BOUND · ${c.decisionTree.nodes.length} NODES
${renderDecisionTree(c.decisionTree)}
-

真实多轮对话

普通消息逐条来自 messages.jsonl;工具事件来自持久回执,未用离线脚本改写对话。

RUN ${shortHash(c.evidence.runId)}
${renderTimeline(c)}
-

智能体最终十部分报告

十个 section ID、数量和顺序固定;正文由最终消息 Markdown 原样构建。

${c.formulaStats.rendered} FORMULAS · HTML+MATHML
${c.sections.map((section) => `
${section.number}

${esc(section.title)}

${section.html}
`).join('')}
-

专业校核与稳定性

求解结果、独立验证、等价 formulation 和重复运行分别留证。

${esc(c.stability.policy)}
-
硬约束${hard ? `${hard.satisfied}/${hard.total} 满足,最大违反 ${hard.max_violation}` : '诊断/候选模式,详见第 8 节'}
软约束${soft ? `${soft.violated}/${soft.total} 违反,罚分 ${soft.weighted_penalty_sum}` : '无或不适用'}
稳定性${c.stability.effectivePasses} 次有效通过${c.stability.semanticConvergence ? ',语义收敛' : ''}
-
OptimizationSpec${shortHash(c.evidence.optimizationSpecSha256)}
Result payload${shortHash(c.evidence.resultPayloadSha256)}
Validation report${shortHash(c.evidence.validationReportSha256)}
-
-

真机完整过程

DesireCore 原生窗口 backing store,应用尺寸不变,点击可查看 3024×1824 原图。

DEVTOOLS CLOSED
` - $('#chapter') - .querySelectorAll('[data-shot]') - .forEach((item) => { - item.addEventListener('click', () => openLightbox(Number(item.dataset.shot))) - item.addEventListener('keydown', (event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault() - openLightbox(Number(item.dataset.shot)) - } - }) - }) - $('#chapter [data-open-regression]')?.addEventListener('click', openLatestRegression) - $('#chapter [data-tree-mode]')?.addEventListener('click', (event) => { - const tree = event.currentTarget.closest('[data-decision-tree]') - const pathOnly = tree.classList.toggle('path-only') - event.currentTarget.setAttribute('aria-pressed', String(pathOnly)) - event.currentTarget.textContent = pathOnly ? '显示完整分支' : '只看实际路径' - }) - $('#chapter [data-tree-export]')?.addEventListener('click', () => downloadDecisionTreeSvg(c)) - document.title = `${String(c.scenarioOrder).padStart(2, '0')} · ${c.title} — ${manifest.title}` - $('#app').setAttribute('aria-busy', 'false') - } - - function updatePager() { - $('#progress-label').textContent = `第 ${state.current + 1} / ${manifest.chapters.length} 章` - $('#progress-bar').style.width = `${((state.current + 1) / manifest.chapters.length) * 100}%` - $('#pager-label').textContent = state.chapter.title - } - - function currentShot() { - return state.chapter?.media?.[state.lightboxIndex] ?? null - } - - function calculateFitScale() { - const shot = currentShot() - const stage = $('#lightbox-stage') - if (!shot || !stage.clientWidth || !stage.clientHeight) return 1 - const inset = window.matchMedia('(max-width: 920px)').matches ? 16 : 36 - return Math.min( - Math.max(1, stage.clientWidth - inset) / shot.width, - Math.max(1, stage.clientHeight - inset) / shot.height, - 1 - ) - } - - function updateViewerStatus() { - const stage = $('#lightbox-stage') - const image = $('#lightbox-image') - const actualScale = viewer.fitScale * viewer.zoom - const percent = Math.max(1, Math.round(actualScale * 100)) - $('#lightbox-zoom').textContent = - viewer.mode === 'fit' ? `${percent}% · 适应` : viewer.mode === 'actual' ? '100% · 1:1' : `${percent}%` - $('#lightbox-zoom-out').disabled = viewer.zoom <= MIN_VIEWER_ZOOM + 0.001 - $('#lightbox-zoom-in').disabled = viewer.zoom >= MAX_VIEWER_ZOOM - 0.001 - $('#lightbox-fit').setAttribute('aria-pressed', String(viewer.mode === 'fit')) - $('#lightbox-actual').setAttribute('aria-pressed', String(viewer.mode === 'actual')) - const rect = image.getBoundingClientRect() - stage.classList.toggle('is-pannable', rect.width > stage.clientWidth + 1 || rect.height > stage.clientHeight + 1) - } - - function setViewerZoom(nextZoom, options = {}) { - const shot = currentShot() - const stage = $('#lightbox-stage') - const image = $('#lightbox-image') - if (!shot || !image.complete || !image.naturalWidth) return - - const before = image.getBoundingClientRect() - const stageRect = stage.getBoundingClientRect() - const focusClientX = options.clientX ?? stageRect.left + stage.clientWidth / 2 - const focusClientY = options.clientY ?? stageRect.top + stage.clientHeight / 2 - const imageRatioX = before.width ? clamp((focusClientX - before.left) / before.width, 0, 1) : 0.5 - const imageRatioY = before.height ? clamp((focusClientY - before.top) / before.height, 0, 1) : 0.5 - - viewer.zoom = clamp(nextZoom, MIN_VIEWER_ZOOM, MAX_VIEWER_ZOOM) - viewer.mode = options.mode ?? 'manual' - const actualScale = viewer.fitScale * viewer.zoom - image.style.width = `${Math.max(1, Math.round(shot.width * actualScale))}px` - image.style.height = `${Math.max(1, Math.round(shot.height * actualScale))}px` - - const after = image.getBoundingClientRect() - stage.scrollLeft += after.left + imageRatioX * after.width - focusClientX - stage.scrollTop += after.top + imageRatioY * after.height - focusClientY - updateViewerStatus() - } - - function resetViewer(mode = 'fit') { - const shot = currentShot() - const image = $('#lightbox-image') - const stage = $('#lightbox-stage') - if (!shot || !image.complete || !image.naturalWidth) return - viewer.fitScale = calculateFitScale() - const nextZoom = mode === 'actual' ? 1 / viewer.fitScale : 1 - viewer.zoom = clamp(nextZoom, MIN_VIEWER_ZOOM, MAX_VIEWER_ZOOM) - viewer.mode = mode === 'actual' && viewer.zoom !== nextZoom ? 'manual' : mode - const actualScale = viewer.fitScale * viewer.zoom - image.style.width = `${Math.max(1, Math.round(shot.width * actualScale))}px` - image.style.height = `${Math.max(1, Math.round(shot.height * actualScale))}px` - stage.scrollTo({ left: 0, top: 0 }) - updateViewerStatus() - } - - function zoomViewerBy(factor, event) { - setViewerZoom(viewer.zoom * factor, { - clientX: event?.clientX, - clientY: event?.clientY, - mode: 'manual', - }) - } - - function openLightbox(index) { - state.lightboxIndex = index - const shot = state.chapter.media[index] - $('#lightbox-title').textContent = `${state.chapter.title} · ${index + 1}/${state.chapter.media.length}` - $('#lightbox-meta').textContent = `${shot.width}×${shot.height} · SHA-256 ${shot.sha256}` - const image = $('#lightbox-image') - image.alt = shot.alt - image.onload = () => resetViewer('fit') - image.src = shot.original - $('#lightbox-original').href = shot.original - if (!$('#lightbox').open) $('#lightbox').showModal() - if (image.complete && image.naturalWidth) requestAnimationFrame(() => resetViewer('fit')) - } - $('#lightbox-close').addEventListener('click', () => $('#lightbox').close()) - $('#lightbox-prev').addEventListener('click', () => - openLightbox((state.lightboxIndex - 1 + state.chapter.media.length) % state.chapter.media.length) - ) - $('#lightbox-next').addEventListener('click', () => - openLightbox((state.lightboxIndex + 1) % state.chapter.media.length) - ) - $('#lightbox-zoom-out').addEventListener('click', () => zoomViewerBy(1 / 1.25)) - $('#lightbox-zoom-in').addEventListener('click', () => zoomViewerBy(1.25)) - $('#lightbox-fit').addEventListener('click', () => resetViewer('fit')) - $('#lightbox-actual').addEventListener('click', () => resetViewer('actual')) - $('#lightbox-stage').addEventListener( - 'wheel', - (event) => { - if (!event.ctrlKey && !event.metaKey) return - event.preventDefault() - zoomViewerBy(event.deltaY < 0 ? 1.18 : 1 / 1.18, event) - }, - { passive: false } - ) - $('#lightbox-stage').addEventListener('dblclick', (event) => { - if (viewer.mode === 'fit') setViewerZoom(2, { clientX: event.clientX, clientY: event.clientY }) - else resetViewer('fit') - }) - $('#lightbox-stage').addEventListener('pointerdown', (event) => { - if (event.pointerType === 'mouse' && event.button !== 0) return - const stage = $('#lightbox-stage') - if (viewer.pointers.size >= 2) return - viewer.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) - stage.setPointerCapture?.(event.pointerId) - if (viewer.pointers.size === 2) { - const [first, second] = [...viewer.pointers.values()] - viewer.pinch = { - distance: Math.hypot(second.x - first.x, second.y - first.y), - zoom: viewer.zoom, - } - viewer.drag = null - stage.classList.remove('is-dragging') - } else if (stage.classList.contains('is-pannable')) { - viewer.drag = { - pointerId: event.pointerId, - x: event.clientX, - y: event.clientY, - left: stage.scrollLeft, - top: stage.scrollTop, - } - stage.classList.add('is-dragging') - } - }) - $('#lightbox-stage').addEventListener('pointermove', (event) => { - if (!viewer.pointers.has(event.pointerId)) return - const stage = $('#lightbox-stage') - viewer.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY }) - if (viewer.pointers.size >= 2 && viewer.pinch) { - const [first, second] = [...viewer.pointers.values()] - const distance = Math.hypot(second.x - first.x, second.y - first.y) - setViewerZoom(viewer.pinch.zoom * (distance / Math.max(1, viewer.pinch.distance)), { - clientX: (first.x + second.x) / 2, - clientY: (first.y + second.y) / 2, - }) - return - } - if (viewer.drag?.pointerId === event.pointerId) { - stage.scrollLeft = viewer.drag.left - (event.clientX - viewer.drag.x) - stage.scrollTop = viewer.drag.top - (event.clientY - viewer.drag.y) - } - }) - const releaseViewerPointer = (event) => { - const stage = $('#lightbox-stage') - if (!viewer.pointers.has(event.pointerId)) return - viewer.pointers.delete(event.pointerId) - viewer.drag = null - viewer.pinch = null - stage.classList.remove('is-dragging') - if (viewer.pointers.size === 1 && stage.classList.contains('is-pannable')) { - const [[pointerId, point]] = [...viewer.pointers.entries()] - viewer.drag = { - pointerId, - x: point.x, - y: point.y, - left: stage.scrollLeft, - top: stage.scrollTop, - } - stage.classList.add('is-dragging') - } - } - $('#lightbox-stage').addEventListener('pointerup', releaseViewerPointer) - $('#lightbox-stage').addEventListener('pointercancel', releaseViewerPointer) - $('#lightbox-stage').addEventListener('lostpointercapture', releaseViewerPointer) - $('#lightbox').addEventListener('keydown', (event) => { - if (event.key === 'ArrowLeft') $('#lightbox-prev').click() - else if (event.key === 'ArrowRight') $('#lightbox-next').click() - else if (event.key === '+' || event.key === '=') zoomViewerBy(1.25) - else if (event.key === '-') zoomViewerBy(1 / 1.25) - else if (event.key === '0') resetViewer('fit') - else if (event.key === '1') resetViewer('actual') - else return - event.preventDefault() - }) - $('#lightbox').addEventListener('close', () => { - viewer.pointers.clear() - viewer.drag = null - viewer.pinch = null - $('#lightbox-stage').classList.remove('is-dragging', 'is-pannable') - }) - window.addEventListener('resize', () => { - if (!$('#lightbox').open) return - cancelAnimationFrame(viewer.resizeFrame) - viewer.resizeFrame = requestAnimationFrame(() => { - const previousActualScale = viewer.fitScale * viewer.zoom - viewer.fitScale = calculateFitScale() - const nextZoom = - viewer.mode === 'fit' ? 1 : viewer.mode === 'actual' ? 1 / viewer.fitScale : previousActualScale / viewer.fitScale - setViewerZoom(nextZoom, { mode: viewer.mode }) - }) - }) - - async function openIntegrity() { - let integrity - if (location.protocol === 'file:') - integrity = { - entries: [], - note: '离线模式下逐文件哈希见 integrity.json', - contentRootSha256: manifest.contentRootSha256, - } - else integrity = await (await fetch('integrity.json')).json() - $('#integrity-content').innerHTML = - `
报告内容根哈希${esc(manifest.contentRootSha256)}
历史证据根哈希${esc(manifest.historicalEvidenceContentRootSha256)}
最新版回归哈希${esc(manifest.latestPlatformRegressionSha256)}
章节${manifest.counts.chapters}
截图${manifest.counts.screenshots}
完整性条目${integrity.entries?.length || '见离线 integrity.json'}
历史证据平台 / Skill${esc(manifest.evidencePlatformCommit)} / ${esc(manifest.agentSkillVersion)}
最新回归平台${esc(manifest.latestPlatformRegression.platform.commit)}
` - $('#integrity-dialog').showModal() - } - - function openLatestRegression() { - const regression = manifest.latestPlatformRegression - const scenarios = regression.runtime.scenarios - .map((item) => { - const verdict = - item.expectedValidationVerdict === 'fail' && item.observedValidationVerdict === 'fail' - ? '违规候选按预期被阻断' - : `独立验证 ${item.observedValidationVerdict}` - return `
通过${esc(item.scenarioId)}

${esc(item.label)}

${esc(verdict)} · Solve ${item.settledSolveCalls} 次 · 十部分报告 ${item.tenPartReport ? '完整' : '不完整'}

${esc(shortHash(item.caseRunSha256))}
` - }) - .join('') - const workspace = regression.runtime.decisionWorkspace - $('#latest-content').innerHTML = - `
PASS

最新平台定向回归

${esc(regression.platform.commit.slice(0, 10))} · DesireCore ${esc(regression.platform.version)}

${esc(formatTime(regression.testedAt))} · ${esc(regression.platform.source)}
${scenarios}
通过Decision Workspace r${workspace.revision}

人 + Agent 共管决策工作台

业务、模型、证据三视图可用;定义通过,缺少模型映射时保持阻断;${workspace.viewport.join('×')} 无横向溢出。

review: ${esc(workspace.reviewState)}

证据边界

    ${regression.claimBoundary.map((item) => `
  • ${esc(item)}
  • `).join('')}

历史全量证据:${regression.historicalEvidence.chapters} 章、${regression.historicalEvidence.screenshots} 张截图、${regression.historicalEvidence.representativeStabilityPasses} 次代表性稳定运行,归属于平台 ${esc(regression.historicalEvidence.platformCommit)}。

` - $('#latest-dialog').showModal() - } - - window.addEventListener('hashchange', () => { - const index = hashIndex() - if (index !== state.current) loadChapter(index) - }) - renderShell() - loadChapter(hashIndex()).catch((error) => { - $('#chapter').innerHTML = - `

报告载入失败

${esc(error.stack || error.message)}
` - throw error - }) -})() diff --git a/web/solver-report/tests/public-release-policy.test.mjs b/web/solver-report/tests/public-release-policy.test.mjs deleted file mode 100644 index 3b2928d..0000000 --- a/web/solver-report/tests/public-release-policy.test.mjs +++ /dev/null @@ -1,132 +0,0 @@ -import assert from 'node:assert/strict' -import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync } from 'node:fs' -import { join } from 'node:path' -import { tmpdir } from 'node:os' -import test from 'node:test' -import { - assertPublicText, - projectPublicProvenance, - resolveSafeRemoteEntryUrl, - resolveSafeOutputRoot, - validatePublicReleaseAttestation, -} from '../scripts/public-release-policy.mjs' - -test('输出目录只能位于证据 report-web 的真实专用子目录', (context) => { - const sandbox = mkdtempSync(join(tmpdir(), 'solver-report-path-policy-')) - context.after(() => rmSync(sandbox, { recursive: true, force: true })) - const roots = { - inputRoot: join(sandbox, 'evidence'), - codeRoot: join(sandbox, 'agent-desirecore'), - appRoot: join(sandbox, 'desirecore-app'), - } - for (const path of [...Object.values(roots), join(roots.inputRoot, 'report-web')]) mkdirSync(path, { recursive: true }) - const safeOutput = join(roots.inputRoot, 'report-web', 'dist') - assert.equal( - resolveSafeOutputRoot({ ...roots, requestedOutputRoot: safeOutput }), - join(realpathSync(join(roots.inputRoot, 'report-web')), 'dist') - ) - for (const requestedOutputRoot of [ - '/', - sandbox, - roots.inputRoot, - join(roots.inputRoot, 'report-web'), - join(roots.codeRoot, 'web-output'), - join(sandbox, 'unmanaged-report-output'), - ]) { - assert.throws(() => resolveSafeOutputRoot({ ...roots, requestedOutputRoot: requestedOutputRoot })) - } - - const symlinkInputRoot = join(sandbox, 'symlink-evidence') - const externalTarget = join(sandbox, 'external-target') - mkdirSync(symlinkInputRoot) - mkdirSync(externalTarget) - symlinkSync(externalTarget, join(symlinkInputRoot, 'report-web')) - assert.throws(() => - resolveSafeOutputRoot({ - inputRoot: symlinkInputRoot, - codeRoot: roots.codeRoot, - appRoot: roots.appRoot, - requestedOutputRoot: join(symlinkInputRoot, 'report-web', 'dist'), - }) - ) -}) - -test('远端完整性条目不能改变 origin 或越过报告路径', () => { - assert.equal( - resolveSafeRemoteEntryUrl('https://build.example.com/solver-agent-team/', 'media/original/case/01@2x.png').href, - 'https://build.example.com/solver-agent-team/media/original/case/01@2x.png' - ) - for (const path of [ - 'https://127.0.0.1/private', - 'http://evil.example/private', - '../admin', - '/absolute', - 'media\\secret', - 'media/%2e%2e/secret', - 'media//secret', - 'media/file?redirect=http://127.0.0.1', - ]) { - assert.throws(() => resolveSafeRemoteEntryUrl('https://build.example.com/solver-agent-team/', path)) - } -}) - -test('provenance 使用递归数据不可穿透的字段白名单', () => { - assert.deepEqual( - projectPublicProvenance({ - engine_id: 'scip-build', - request_id: 'request-1', - endpoint: 'https://private.internal', - nested: { access_token: 'secret-value' }, - }), - { engine_id: 'scip-build', request_id: 'request-1' } - ) -}) - -test('公开文本策略拒绝身份、凭据、私有路径和私有网络地址', () => { - assert.doesNotThrow(() => assertPublicText('SCIP 10.0.2 · request 853ca9c7-440b', 'safe')) - for (const value of [ - 'admin@example.com', - '/Users/reviewer/private/report.json', - 'authorization: Bearer abcdefghijklmnop', - 'password=not-for-public', - 'http://192.168.1.10/private', - ]) { - assert.throws(() => assertPublicText(value, 'unsafe')) - } -}) - -test('公开发布证明必须绑定全部证据根并显式通过', () => { - const evidenceRoots = Object.fromEntries( - [ - 'publicTranscriptContentRootSha256', - 'publicScreenshotContentRootSha256', - 'screenshotPlanSha256', - 'screenshotValidationSha256', - 'screenshotPrivacyValidationSha256', - ].map((key, index) => [key, String(index + 1).repeat(64)]) - ) - const attestation = { - schemaVersion: 'solver.public-release-attestation/v1', - audience: 'public', - review: { - status: 'passed', - reviewedAt: '2026-08-30T09:00:00.000Z', - reviewer: 'release-review', - methods: ['text-scan', 'visual-review'], - assertions: { - transcriptsContainNoSensitiveData: true, - screenshotsContainNoSensitiveData: true, - customerIdentifiersExcluded: true, - }, - }, - evidence: evidenceRoots, - metadata: { platformCommit: 'be0cc2cce', agentSkillVersion: '4.4.4', representativeStabilityPasses: 20 }, - } - assert.equal(validatePublicReleaseAttestation(attestation, evidenceRoots), attestation) - assert.throws(() => - validatePublicReleaseAttestation( - { ...attestation, evidence: { ...evidenceRoots, screenshotPlanSha256: '0'.repeat(64) } }, - evidenceRoots - ) - ) -}) diff --git a/web/solver-report/tests/source-contract.test.mjs b/web/solver-report/tests/source-contract.test.mjs deleted file mode 100644 index 9356d7a..0000000 --- a/web/solver-report/tests/source-contract.test.mjs +++ /dev/null @@ -1,137 +0,0 @@ -import assert from 'node:assert/strict' -import { readFileSync } from 'node:fs' -import { join } from 'node:path' -import test from 'node:test' -import { fileURLToPath } from 'node:url' - -const root = join(fileURLToPath(new URL('..', import.meta.url))) -const html = readFileSync(join(root, 'source', 'index.html'), 'utf8') -const css = readFileSync(join(root, 'source', 'report.css'), 'utf8') -const javascript = readFileSync(join(root, 'source', 'report.js'), 'utf8') -const builder = readFileSync(join(root, 'scripts', 'build-report-web.mjs'), 'utf8') -const validator = readFileSync(join(root, 'scripts', 'validate-report-web.mjs'), 'utf8') -const ocrValidator = readFileSync(join(root, 'scripts', 'validate-screenshot-privacy-ocr.mjs'), 'utf8') -const deploymentValidator = readFileSync(join(root, 'scripts', 'validate-deployed-report.mjs'), 'utf8') -const attestationSchema = JSON.parse( - readFileSync(join(root, 'public-release-attestation.schema.json'), 'utf8') -) -const decisionTreeSchema = JSON.parse(readFileSync(join(root, 'decision-tree.schema.json'), 'utf8')) - -test('图片查看器暴露完整的可访问缩放控制', () => { - for (const id of [ - 'lightbox-stage', - 'lightbox-canvas', - 'lightbox-zoom-out', - 'lightbox-zoom', - 'lightbox-zoom-in', - 'lightbox-fit', - 'lightbox-actual', - ]) { - assert.match(html, new RegExp(`id="${id}"`)) - } - assert.match(html, /aria-live="polite"/) - assert.match(html, /aria-describedby="lightbox-zoom-hint"/) - assert.match(javascript, /