浏览代码

docs: plan blood routine report prototype closure

WangKang 2 周之前
父节点
当前提交
435da009be

+ 995 - 0
docs/initiatives/FEAT-202606-001-unified-entry-client/dev-progress/患者服务台血常规闭环原型实施计划.md

@@ -0,0 +1,995 @@
+---
+doc_id: DEV-202607-002
+feature_id: FEAT-202606-001-unified-entry-client
+type: dev-progress
+title: 患者服务台血常规闭环原型实施计划
+status: draft
+owner: 医梦研发团队
+created_at: 2026-07-05
+updated_at: 2026-07-05
+reviewers:
+  - 产品负责人
+  - 终端负责人
+related_docs:
+  - DES-202607-001
+  - ODS-202606-002
+  - ADR-202606-001
+  - TST-202607-001
+related_modules:
+  - emoon-terminal-client
+tags:
+  - 患者服务台
+  - 血常规
+  - 报告闭环
+  - 移动端原型
+  - Open Design
+---
+
+# 患者服务台血常规闭环原型实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use
+> superpowers:subagent-driven-development (recommended) or
+> superpowers:executing-plans to implement this plan task-by-task.
+> Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 在现有 `patient-service-desk-v3` 中加入患者端血常规深闭环,同时保留
+独立的挂号、签到和候诊演示链。
+
+**Architecture:** 继续使用纯前端事件投影原型。状态模型保存报告来源快照、
+患者事项、授权、解释结果、反馈和失效状态;视图根据投影渲染报告中心、详情、
+授权、处理中、结果和降级页面。报告解读不创建挂号命令,检查报告只增加统一入口
+和扩展框架。
+
+**Tech Stack:** HTML5、CSS3、原生JavaScript ES Modules、Node.js内置
+`node:test`、esbuild经典浏览器包、Lucide图标、Open Design HTML Artifact。
+
+---
+
+## 1. 范围与文件
+
+### 1.1 修改文件
+
+| 文件 | 职责 |
+|---|---|
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs` | 报告来源、状态迁移、反馈和场景fixture |
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js` | 报告中心、详情、结果、降级和控制器渲染 |
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css` | 报告组件、状态提示和双视口布局 |
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs` | 状态、渲染、交互和安全边界测试 |
+| `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js` | 从JS入口重新生成的经典浏览器包 |
+| `docs/prototypes/patient-service-desk-v3/使用说明.md` | 场景、操作和验证方式 |
+| `docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md` | 报告闭环验收项 |
+| `docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md` | 最终执行记录 |
+
+### 1.2 不修改
+
+- 不修改FEAT-006生产接口和开发代码;
+- 不连接真实LIS、FastGPT或医院接口;
+- 不设计医护OCR审核工作台;
+- 不增加AI推荐科室、用药、治疗或具体复查周期;
+- 不把报告结果串入挂号TaskState;
+- 不修改历史v1/v2原型。
+
+## 2. 目标状态模型
+
+在初始状态中增加报告源投影和反馈集合:
+
+```js
+{
+  reportSourceProjections: {},
+  reportFeedback: [],
+  reportFilters: {
+    family: "ALL",
+    status: "ALL"
+  }
+}
+```
+
+血常规报告源fixture使用以下稳定结构:
+
+```js
+{
+  reportId: "report-blood-20260702",
+  family: "LAB",
+  reportType: "BLOOD_ROUTINE",
+  title: "血常规",
+  hospitalName: "空海医院",
+  departmentName: "检验科",
+  issuedAt: "2026-07-03 09:30",
+  sourceStatus: "FINAL",
+  interpretationStatus: "NOT_REQUESTED",
+  sourceVersion: 1,
+  sourceHash: "sha256:blood-v1",
+  metrics: [
+    {
+      metricId: "metric-wbc",
+      code: "WBC",
+      name: "白细胞计数",
+      value: "11.2",
+      unit: "10^9/L",
+      referenceRange: "3.5–9.5",
+      sourceFlag: "HIGH"
+    }
+  ]
+}
+```
+
+原型允许的患者端解释状态:
+
+```text
+NOT_REQUESTED
+CONSENT_REQUIRED
+GENERATING
+READY
+NEED_REVIEW
+CRITICAL_NOTICE
+AI_UNAVAILABLE
+INVALIDATED
+```
+
+## 3. 实施任务
+
+### Task 1:锁定场景分组与报告源契约
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+新增测试,要求控制器存在两个场景组,并锁定报告组八个场景:
+
+```js
+assert.deepEqual(demoScenarioGroups.REPORT_CLOSURE, [
+  "REPORT_ARRIVED",
+  "REPORT_DETAIL",
+  "REPORT_CONSENT",
+  "REPORT_GENERATING",
+  "REPORT_READY",
+  "REPORT_NEED_REVIEW",
+  "REPORT_CRITICAL",
+  "REPORT_INVALIDATED",
+]);
+assert.ok(demoScenarioGroups.CARE_CLOSURE.includes("REGISTRATION_QUERY"));
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="scenario groups" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,提示`demoScenarioGroups`尚未导出。
+
+- [ ] **Step 3:实现最小场景契约**
+
+在`patient-service-desk-v3.js`导出冻结分组,并让`demoScenarios`由分组扁平化生成:
+
+```js
+export const demoScenarioGroups = Object.freeze({
+  REPORT_CLOSURE: Object.freeze([
+    "REPORT_ARRIVED",
+    "REPORT_DETAIL",
+    "REPORT_CONSENT",
+    "REPORT_GENERATING",
+    "REPORT_READY",
+    "REPORT_NEED_REVIEW",
+    "REPORT_CRITICAL",
+    "REPORT_INVALIDATED",
+  ]),
+  CARE_CLOSURE: Object.freeze([
+    "EMPTY_HOME",
+    "REGISTRATION_QUERY",
+    "COMMAND_PREPARED",
+    "APPOINTMENT_CREATED",
+    "CHECKIN_AVAILABLE",
+    "QUEUE_UPDATED",
+  ]),
+});
+
+export const demoScenarios = Object.freeze(
+  Object.values(demoScenarioGroups).flat(),
+);
+```
+
+- [ ] **Step 4:运行测试确认通过**
+
+```bash
+node --test --test-name-pattern="scenario groups" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "test: define patient report closure scenarios"
+```
+
+### Task 2:建立报告源投影与筛选状态
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+覆盖报告出具、详情读取和筛选:
+
+```js
+const bloodRoutineReportFixture = {
+  reportId: "report-blood-20260702",
+  family: "LAB",
+  reportType: "BLOOD_ROUTINE",
+  title: "血常规",
+  hospitalName: "空海医院",
+  departmentName: "检验科",
+  issuedAt: "2026-07-03 09:30",
+  sourceStatus: "FINAL",
+  interpretationStatus: "NOT_REQUESTED",
+  verificationStatus: "VERIFIED",
+  sourceVersion: 1,
+  sourceHash: "sha256:blood-v1",
+  readStatus: "UNREAD",
+  metrics: [],
+};
+const arrived = applyServerProjectionFixture(createInitialState(), {
+  id: "report-issued-blood-v1",
+  type: "REPORT_ISSUED",
+  occurredAt: "2026-07-03T09:30:00+08:00",
+  payload: bloodRoutineReportFixture,
+});
+
+assert.equal(
+  arrived.reportSourceProjections["report-blood-20260702"].sourceStatus,
+  "FINAL",
+);
+assert.equal(arrived.patientWorkItems.length, 1);
+assert.equal(arrived.activeAgentTaskProjection, null);
+assert.deepEqual(
+  selectReports(arrived, { family: "LAB", status: "UNREAD" })
+    .map((report) => report.reportId),
+  ["report-blood-20260702"],
+);
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="report source|report filters" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,报告源集合或`selectReports`不存在。
+
+- [ ] **Step 3:实现报告源投影**
+
+在`createInitialState`增加:
+
+```js
+reportSourceProjections: {},
+reportFeedback: [],
+reportFilters: { family: "ALL", status: "ALL" },
+```
+
+`REPORT_ISSUED`只新增或版本化更新报告源、患者事项和消息,不创建活动Agent任务。
+导出纯函数:
+
+```js
+export function selectReports(
+  state,
+  { family = "ALL", status = "ALL" } = {},
+) {
+  return Object.values(state?.reportSourceProjections ?? {})
+    .filter((report) => family === "ALL" || report.family === family)
+    .filter((report) => status === "ALL" || report.readStatus === status)
+    .sort((a, b) => b.issuedAt.localeCompare(a.issuedAt));
+}
+```
+
+- [ ] **Step 4:运行报告状态测试**
+
+```bash
+node --test --test-name-pattern="report source|report filters" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: add patient report source projections"
+```
+
+### Task 3:实现报告中心与血常规详情
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+```js
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
+const centerHtml = renderReportCenter(reportState);
+assert.match(centerHtml, /全部/);
+assert.match(centerHtml, /检验报告/);
+assert.match(centerHtml, /检查报告/);
+assert.match(centerHtml, /血常规/);
+
+const detailHtml = renderReportDetail(
+  reportState.reportSourceProjections["report-blood-20260702"],
+);
+assert.match(detailHtml, /医院原始报告/);
+assert.match(detailHtml, /白细胞计数/);
+assert.match(detailHtml, /11\\.2/);
+assert.match(detailHtml, /10\\^9\\/L/);
+assert.match(detailHtml, /3\\.5–9\\.5/);
+assert.doesNotMatch(detailHtml, /建议挂.*科|建议服用|确诊/);
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="report center|blood routine detail" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,渲染接口或完整内容不存在。
+
+- [ ] **Step 3:实现页面与交互**
+
+新增并导出:
+
+```js
+export const renderReportCenter = (state) => {
+  const reports = selectReports(state, state?.reportFilters);
+  return `
+    <section class="report-center" aria-labelledby="reportCenterTitle">
+      <h1 id="reportCenterTitle">检验检查报告</h1>
+      <div class="report-filters" aria-label="报告类型筛选">
+        <button data-action="set-report-family-filter" data-filter="ALL">全部</button>
+        <button data-action="set-report-family-filter" data-filter="LAB">检验报告</button>
+        <button data-action="set-report-family-filter" data-filter="EXAM">检查报告</button>
+      </div>
+      <div class="report-list">
+        ${reports.map((report) => `
+          <button
+            class="report-list-item"
+            data-action="view-report"
+            data-report-id="${escapeHtml(report.reportId)}"
+          >
+            <strong>${escapeHtml(report.title)}</strong>
+            <span>${escapeHtml(report.hospitalName)} · ${escapeHtml(report.issuedAt)}</span>
+          </button>
+        `).join("")}
+      </div>
+    </section>
+  `;
+};
+
+export const renderReportDetail = (report, expanded = false) => {
+  const metrics = expanded ? report.metrics : report.metrics.slice(0, 3);
+  return `
+    <section class="report-detail" aria-labelledby="reportDetailTitle">
+      <h1 id="reportDetailTitle">${escapeHtml(report.title)}</h1>
+      <p>${escapeHtml(report.hospitalName)} · ${escapeHtml(report.departmentName)}</p>
+      <h2>医院原始报告</h2>
+      <dl>
+        ${metrics.map((metric) => `
+          <div>
+            <dt>${escapeHtml(metric.name)}</dt>
+            <dd>
+              ${escapeHtml(metric.value)} ${escapeHtml(metric.unit)}
+              · 参考 ${escapeHtml(metric.referenceRange)}
+            </dd>
+          </div>
+        `).join("")}
+      </dl>
+      <button data-action="interpret-report"
+        data-report-id="${escapeHtml(report.reportId)}">AI辅助解读</button>
+    </section>
+  `;
+};
+```
+
+增加操作:
+
+```text
+open-report-center
+set-report-family-filter
+set-report-status-filter
+view-report
+toggle-report-metrics
+```
+
+报告详情中的主按钮为“AI辅助解读”,不能出现“推荐复诊”或“预约相关科室”。
+
+- [ ] **Step 4:运行对应测试**
+
+```bash
+node --test --test-name-pattern="report center|blood routine detail" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: add blood routine report center and detail"
+```
+
+### Task 4:收紧授权、可信门禁与处理中状态
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+测试授权绑定和可信门禁:
+
+```js
+const reportId = "report-blood-20260702";
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
+const requested = requestReportInterpretation(reportState, reportId);
+assert.equal(requested.overlays.consentId, `consent-ai-${reportId}`);
+
+const granted = grantConsent(requested, `consent-ai-${reportId}`);
+assert.equal(granted.reportSourceProjections[reportId].interpretationStatus,
+  "GENERATING");
+
+for (const reason of [
+  "SOURCE_NOT_FINAL",
+  "OCR_REVIEW_REQUIRED",
+  "REFERENCE_CONFLICT",
+]) {
+  const blockedState = {
+    ...reportState,
+    reportSourceProjections: {
+      ...reportState.reportSourceProjections,
+      [reportId]: {
+        ...reportState.reportSourceProjections[reportId],
+        verificationStatus: "NEED_REVIEW",
+        verificationReason: reason,
+      },
+    },
+  };
+  const blocked = evaluateReportInterpretationGate(
+    blockedState,
+    reportId,
+  );
+  assert.equal(blocked.allowed, false);
+}
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="report consent boundary|interpretation gate" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,门禁函数或绑定结构不存在。
+
+- [ ] **Step 3:实现最小门禁**
+
+导出:
+
+```js
+export function evaluateReportInterpretationGate(state, reportId) {
+  const report = state?.reportSourceProjections?.[reportId];
+  if (!report) return { allowed: false, reason: "REPORT_NOT_FOUND" };
+  if (report.sourceStatus !== "FINAL") {
+    return { allowed: false, reason: "SOURCE_NOT_FINAL" };
+  }
+  if (report.verificationStatus !== "VERIFIED") {
+    return { allowed: false, reason: report.verificationReason };
+  }
+  if (report.criticalNotice === true) {
+    return { allowed: false, reason: "CRITICAL_NOTICE" };
+  }
+  return { allowed: true, reason: null };
+}
+```
+
+授权只绑定当前患者和当前报告。门禁失败时不创建结果,不启动解释计时器。
+同时将现有`grantConsent`改为接受动态`consent-ai-${reportId}`,并从当前overlay校验
+患者和报告绑定;不新增第二套授权函数。
+
+- [ ] **Step 4:运行授权与门禁测试**
+
+```bash
+node --test --test-name-pattern="report consent boundary|interpretation gate" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: enforce patient report interpretation gate"
+```
+
+### Task 5:实现安全解读结果与反馈
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+```js
+const reportId = "report-blood-20260702";
+const generating = loadScenario(createInitialState(), "REPORT_GENERATING");
+const ready = applyReportInterpretationProjectionFixture(generating, reportId);
+const html = renderReportInterpretationResult(
+  ready.resultSnapshotProjections["result-report-interpretation-001"],
+);
+
+for (const text of [
+  "结果概括",
+  "需要关注",
+  "指标解释",
+  "常见相关因素",
+  "可以怎么做",
+  "内容依据",
+]) {
+  assert.match(html, new RegExp(text));
+}
+assert.doesNotMatch(html, /推荐科室|复查周期|治疗方案|建议服用/);
+
+const feedback = submitReportFeedback(ready, {
+  reportId,
+  category: "HARD_TO_UNDERSTAND",
+});
+assert.equal(feedback.reportFeedback.at(-1).category, "HARD_TO_UNDERSTAND");
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="safe report result|report feedback" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,六层结果或反馈接口不存在。
+
+- [ ] **Step 3:实现结果与反馈**
+
+结果只提供以下操作:
+
+```text
+查看原始报告
+咨询开单医生
+在线咨询
+联系人工服务
+提交反馈
+撤回授权
+```
+
+导出纯函数:
+
+```js
+export function submitReportFeedback(state, { reportId, category }) {
+  const allowed = new Set([
+    "HELPFUL",
+    "METRIC_INACCURATE",
+    "HARD_TO_UNDERSTAND",
+    "DIFFERS_FROM_DOCTOR",
+    "OTHER",
+  ]);
+  if (!allowed.has(category)) return state;
+  return {
+    ...state,
+    reportFeedback: [
+      ...(state.reportFeedback ?? []),
+      { id: `feedback-${reportId}-${category}`, reportId, category },
+    ],
+  };
+}
+```
+
+- [ ] **Step 4:运行结果与反馈测试**
+
+```bash
+node --test --test-name-pattern="safe report result|report feedback" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: add safe blood report result and feedback"
+```
+
+### Task 6:实现患者端降级、危急值与失效
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+```js
+const reportId = "report-blood-20260702";
+const readyState = loadScenario(createInitialState(), "REPORT_READY");
+const needsReview = loadScenario(createInitialState(), "REPORT_NEED_REVIEW");
+assert.match(renderReportUnavailable({
+  reason: "OCR_REVIEW_REQUIRED",
+}), /暂不适合自动解读/);
+assert.doesNotMatch(renderReportUnavailable({
+  reason: "OCR_REVIEW_REQUIRED",
+}), /继续生成/);
+
+const critical = loadScenario(createInitialState(), "REPORT_CRITICAL");
+assert.match(renderReportUnavailable({
+  reason: "CRITICAL_NOTICE",
+}), /请按医院提示及时联系/);
+assert.doesNotMatch(renderReportUnavailable({
+  reason: "CRITICAL_NOTICE",
+}), /AI解读|推荐科室/);
+
+const invalidated = applyServerProjectionFixture(readyState, {
+  id: "report-corrected-blood-v2",
+  type: "REPORT_CORRECTED",
+  occurredAt: "2026-07-03T11:00:00+08:00",
+  payload: {
+    reportId,
+    sourceVersion: 2,
+    sourceHash: "sha256:blood-v2",
+  },
+});
+assert.equal(
+  invalidated.reportSourceProjections[reportId].interpretationStatus,
+  "INVALIDATED",
+);
+assert.equal(
+  invalidated.resultSnapshotProjections[
+    "result-report-interpretation-001"
+  ].valid,
+  false,
+);
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="report degradation|critical notice|report invalidation" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,降级页面或更正事件未实现。
+
+- [ ] **Step 3:实现状态和页面**
+
+统一降级组件接收原因码:
+
+```js
+const REPORT_UNAVAILABLE_PRESENTATION = Object.freeze({
+  OCR_REVIEW_REQUIRED: {
+    title: "当前报告暂不适合自动解读",
+    description: "报告内容需要人工核对,您仍可查看医院原始报告。",
+  },
+  CRITICAL_NOTICE: {
+    title: "请按医院提示及时联系",
+    description: "当前报告需要按医院既有流程处理。",
+  },
+  AI_UNAVAILABLE: {
+    title: "AI辅助解读暂不可用",
+    description: "医院原始报告仍可正常查看,请稍后重试。",
+  },
+  UNSUPPORTED: {
+    title: "暂不支持自动解读",
+    description: "您可以查看原始报告或联系人工服务。",
+  },
+});
+
+export const renderReportUnavailable = ({ reason }) => {
+  const presentation = REPORT_UNAVAILABLE_PRESENTATION[reason]
+    ?? REPORT_UNAVAILABLE_PRESENTATION.UNSUPPORTED;
+  return `
+    <section class="report-unavailable" role="status">
+      <h1>${escapeHtml(presentation.title)}</h1>
+      <p>${escapeHtml(presentation.description)}</p>
+      <button data-action="open-original-report">查看原始报告</button>
+      <button data-action="contact-human-service">联系人工服务</button>
+    </section>
+  `;
+};
+```
+
+`REPORT_CORRECTED`和`REPORT_WITHDRAWN`必须使旧结果失效;旧结果可保留在状态中用于
+演示审计,但不能由当前页面读取为有效结果。
+
+- [ ] **Step 4:运行降级和失效测试**
+
+```bash
+node --test --test-name-pattern="report degradation|critical notice|report invalidation" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: add patient report safety degradation states"
+```
+
+### Task 7:增加检查报告扩展框架和四入口联动
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写失败测试**
+
+```js
+const reportState = loadScenario(createInitialState(), "REPORT_ARRIVED");
+const imaging = {
+  reportId: "report-ct-20260702",
+  family: "EXAM",
+  reportType: "IMAGING",
+  title: "胸部CT",
+  bodyPart: "胸部",
+  findings: "双肺纹理清晰。",
+  conclusion: "本次检查未见明确异常。",
+  sourceStatus: "FINAL",
+};
+const html = renderExaminationReportDetail(imaging);
+assert.match(html, /检查所见/);
+assert.match(html, /报告结论/);
+assert.doesNotMatch(html, /参考范围|异常指标列表/);
+assert.match(html, /该类型暂未开放AI辅助解读/);
+
+assert.match(renderHome(reportState), /血常规报告已出/);
+assert.match(renderMessages(reportState), /血常规报告/);
+assert.match(renderRecords({
+  ...reportState,
+  activeTab: "records",
+}), /检验报告/);
+assert.match(renderAssistant({
+  ...reportState,
+  activeTab: "assistant",
+}), /帮我看报告/);
+```
+
+- [ ] **Step 2:运行测试确认失败**
+
+```bash
+node --test --test-name-pattern="examination report framework|report entry points" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:FAIL,检查报告详情或入口联动不足。
+
+- [ ] **Step 3:实现扩展框架**
+
+检查报告只渲染:
+
+```text
+检查名称和部位
+检查所见
+报告结论
+报告医生与审核状态
+图像或PDF入口
+暂未开放AI辅助解读
+```
+
+不得复用检验报告的指标、单位和参考区间组件。
+
+- [ ] **Step 4:运行测试**
+
+```bash
+node --test --test-name-pattern="examination report framework|report entry points" \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:PASS。
+
+- [ ] **Step 5:提交**
+
+```bash
+git add docs/prototypes/patient-service-desk-v3
+git commit -m "feat: add examination report extension shell"
+```
+
+### Task 8:完成控制器、可访问性、bundle和发布验证
+
+**Files:**
+
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css`
+- Modify: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js`
+- Modify: `docs/prototypes/patient-service-desk-v3/使用说明.md`
+- Modify: `docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md`
+- Modify: `docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md`
+- Test: `docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs`
+
+- [ ] **Step 1:写完整故事线失败测试**
+
+```js
+for (const scenario of demoScenarioGroups.REPORT_CLOSURE) {
+  const state = loadScenario(createInitialState(), scenario);
+  assert.equal(state.demoScenario, scenario);
+  assert.equal(state.activeAgentTaskProjection, null);
+}
+
+const reportStory = [
+  "REPORT_ARRIVED",
+  "REPORT_DETAIL",
+  "REPORT_CONSENT",
+  "REPORT_GENERATING",
+  "REPORT_READY",
+  "REPORT_INVALIDATED",
+];
+assert.deepEqual(
+  reportStory.map((scenario) => (
+    loadScenario(createInitialState(), scenario).demoScenario
+  )),
+  reportStory,
+);
+```
+
+同时断言:
+
+- 弹窗仍在手机画布内部;
+- 390×844和430×932无横向溢出;
+- 控件触控区不小于44px;
+- 对话框焦点进入、Tab循环和焦点恢复有效;
+- 患者视图不出现OCR、RAG、Prompt、Workflow、traceId和L0/L1等术语;
+- HTML继续通过经典bundle支持`file://`直接打开。
+
+- [ ] **Step 2:运行全部测试确认尚有失败**
+
+```bash
+node --test \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+```
+
+预期:新增完整故事线测试FAIL,既有测试不得出现无关失败。
+
+- [ ] **Step 3:完成控制器与样式**
+
+遥控器分组显示“报告闭环”和“就医闭环”,保持:
+
+```text
+上一步
+下一步
+重置
+390×844
+430×932
+隐藏控制器
+```
+
+所有报告弹窗挂载在`.phone-shell`内部,不得覆盖外部页面。
+
+- [ ] **Step 4:重新生成经典bundle**
+
+```bash
+/Users/destiny/node_modules/.bin/esbuild \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js \
+  --bundle \
+  --format=iife \
+  --platform=browser \
+  --target=es2022 \
+  --banner:js='/* Generated from patient-service-desk-v3.js for direct file:// opening. */' \
+  --outfile=docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js
+```
+
+预期:生成成功,无未解析import。
+
+- [ ] **Step 5:运行完整验证**
+
+```bash
+node --test \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.test.mjs
+node --check \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs
+node --check \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js
+node --check \
+  docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js
+git diff --check
+```
+
+预期:全部测试通过、语法检查通过、差异检查无输出。
+
+- [ ] **Step 6:同步Open Design发布副本**
+
+```bash
+OPEN_DESIGN="/Users/destiny/Library/Application Support/Open Design/namespaces/release-stable/data/projects/b7a08855-87bd-493c-92a4-31655a384570/web"
+
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.html \
+  "$OPEN_DESIGN/patient-service-desk-v3.html"
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.css \
+  "$OPEN_DESIGN/patient-service-desk-v3.css"
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.js \
+  "$OPEN_DESIGN/patient-service-desk-v3.js"
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.state.mjs \
+  "$OPEN_DESIGN/patient-service-desk-v3.state.mjs"
+cp docs/prototypes/patient-service-desk-v3/patient-service-desk-v3.bundle.js \
+  "$OPEN_DESIGN/patient-service-desk-v3.bundle.js"
+```
+
+逐个使用`cmp`确认发布副本与仓库源码一致。
+
+- [ ] **Step 7:更新文档**
+
+更新测试计划、测试结果和使用说明,记录:
+
+- 报告闭环场景;
+- 实际测试数量;
+- 真实视觉复核状态;
+- Open Design发布哈希;
+- 检查报告仅为扩展框架;
+- 不包含真实LIS、FastGPT和医护审核台。
+
+- [ ] **Step 8:提交**
+
+```bash
+git add \
+  docs/prototypes/patient-service-desk-v3 \
+  docs/initiatives/FEAT-202606-001-unified-entry-client/测试计划.md \
+  docs/initiatives/FEAT-202606-001-unified-entry-client/测试结果.md
+git commit -m "docs: complete patient blood report prototype delivery"
+```
+
+## 4. 最终验收
+
+完成后必须满足:
+
+1. 报告闭环和就医闭环在控制器中分组且可独立加载;
+2. `REPORT_ISSUED`只创建患者事项,不自动启动AI解读;
+3. 报告详情先展示医院原始事实;
+4. 授权绑定患者、报告、用途和有效期;
+5. 只有可信门禁通过才进入生成态;
+6. 患者结果不包含诊断、用药、治疗、AI推荐科室和具体复查周期;
+7. OCR待确认、来源冲突和危急值不会硬生成解读;
+8. 报告更正、撤回或作废使旧结果失效;
+9. 报告反馈结构化保存在医梦原型状态;
+10. 检查报告不复用检验指标组件;
+11. 报告闭环不创建挂号Command或活动挂号TaskState;
+12. 原挂号、签到和候诊测试继续通过;
+13. 双视口、弹窗、焦点、44px触控和直接打开能力保持有效;
+14. Open Design发布副本与仓库源码一致。
+
+## 5. 阶段记录
+
+| 日期 | 进展 | 风险 | 下一步 |
+|---|---|---|---|
+| 2026-07-05 | 完成功能策略、边界和逐页设计评审 | 原型仍是fixture,不代表生产接口 | 按Task 1开始TDD实现 |
+
+## 6. 阻塞项
+
+- 内置浏览器安全策略可能继续拒绝本地`file://`地址;自动测试和静态契约不能替代
+  产品负责人在可访问环境中的真实双视口视觉复核。
+- Open Design属于本地发布目录,不纳入Git;每次修改必须显式同步并使用`cmp`验证。

+ 2 - 1
docs/initiatives/FEAT-202606-001-unified-entry-client/专题索引.md

@@ -1,7 +1,7 @@
 ---
 feature_id: FEAT-202606-001-unified-entry-client
 title: 统一入口客户端
-status: implemented
+status: reviewing
 owner: 医梦研发团队
 created_at: 2026-06-17
 updated_at: 2026-07-05
@@ -35,6 +35,7 @@ tags:
 | 开发进度 | `DEV-202606-002` | [Terminal Web Demo Implementation Plan](dev-progress/终端Web演示实施计划.md) | implemented |
 | 开发进度 | `DEV-202606-003` | [Unified Entry Registration Demo Implementation Plan](dev-progress/统一入口挂号演示实施计划.md) | implemented |
 | 开发进度 | `DEV-202607-001` | [患者服务台移动端原型实施计划](dev-progress/患者服务台移动端原型实施计划.md) | implemented |
+| 开发进度 | `DEV-202607-002` | [患者服务台血常规闭环原型实施计划](dev-progress/患者服务台血常规闭环原型实施计划.md) | draft |
 | 测试计划 | `TST-202607-001` | [患者服务台移动端原型测试计划](测试计划.md) | approved |
 | 测试结果 | `TRS-202607-001` | [患者服务台移动端原型测试结果](测试结果.md) | reviewing |
 | 使用说明 | `RUN-202607-001` | [患者服务台移动端原型使用说明](../../prototypes/patient-service-desk-v3/使用说明.md) | implemented |