diff --git a/README.md b/README.md index 17b2168..c97f16c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ | ----------------------------------------- | ---------------------------------------------------- | ----- | | [Token 任务炼金炉](./labs/token-forge/) | 把闲置 Token、仓库上下文和目标转换为可验收的开发任务 | Alpha | | [AI 故障侦探](./labs/incident-detective/) | 在合成事故中练习按证据排障,而不是让 AI 猜根因 | Alpha | -| [SMART / RMA 报告机](./labs/smart-rma/) | 本地解析硬盘信息,分离工具状态、关键指标与未知项 | Alpha | +| [SMART / RMA 报告机](./labs/smart-rma/) | 本地解析并遮蔽硬盘标识,分离关键指标与未知项 | Alpha | ## 产品原则 @@ -58,6 +58,7 @@ Web 端采用 **Astro + TypeScript + Preact Islands**,首期生成静态 HTML - Incident Detective 分享卡:遵循 [Score-only 隐私 SVG](./docs/incident-detective-share-card.md)。 - SMART / RMA 合成输入:遵循 [完全合成 Fixture 合同](./docs/smart-rma-synthetic-fixtures.md)。 - SMART / RMA 浏览器解析:遵循 [Parse Result v1 与失败边界](./docs/smart-rma-parser-v1.md)。 +- SMART / RMA 本地脱敏:遵循 [预览、Boundary Projection 与 sink 边界](./docs/smart-rma-redaction-v1.md)。 - 处理用户输入:先读 [隐私模型](./docs/privacy-model.md) 和 [脱敏包](./packages/redaction/README.md)。 - 发布站点:遵循 [Cloudflare Workers 部署](./docs/cloudflare-deployment.md)。 @@ -91,5 +92,6 @@ Attempt v1。P2-004 已加入五维 100 分确定性评分、逐条判定和改 Score-only 的确定性 SVG 下载,Incident Detective MVP 六个任务全部闭环。P3-001 已建立 7 份完全合成的 ATA、NVMe 与未知协议 `smartctl` 输入及版本化索引,覆盖健康、预警、危险、 缺失字段、厂商扩展、SMART 不可用和冲突信号。P3-002 已上线浏览器端只读解析工作台和 -Parse Result v1:原文只在当前页面内存中处理,输出排除原文与设备标识,且明确不做健康或 -保修判断;下一步是 P3-003 完整输入脱敏。 +Parse Result v1。P3-003 已增加序列号、WWN、主机名、IP 与常见 Secret 的本地脱敏预览, +以及不含自由文本的 Boundary Projection v1;URL、日志和 Analytics 进一步收窄为固定元数据, +AI 与导出尚未启用。下一步是 P3-004 确定性健康分类,现有结果仍不构成厂商保修判断。 diff --git a/TODO.md b/TODO.md index 7c6c998..00ce714 100644 --- a/TODO.md +++ b/TODO.md @@ -101,7 +101,12 @@ - 验收:结构化提取版本、协议、设备类别、SMART 支持、工具报告状态、ATA/NVMe 指标、稳定 信号和缺失字段;64 KiB 上限与错误失败关闭;不上传、持久化或回显原文和设备标识;不做 健康分类、AI 解释或保修判断。 -- [ ] **P3-003 [M] 实现序列号、WWN、主机名和 IP 脱敏** +- [x] **P3-003 [M] 实现序列号、WWN、主机名和 IP 脱敏** + - 产物:smartctl 标识格式扩展、本地脱敏预览、Boundary Projection v1 Schema、五类 sink + 最小投影和工作台隐私报告。 + - 验收:覆盖序列号/WWN/主机名/IPv4/IPv6 的重复、大小写与分隔符变体;原文与自由文本不 + 进入 URL、日志、Analytics、AI 请求或导出;Secret 本地遮蔽;错误不回显;无网络、存储 + 和日志副作用。 - [ ] **P3-004 [M] 实现规则化健康指标与未知状态** - [ ] **P3-005 [M] 实现 AI 通俗解释** - [ ] **P3-006 [M] 生成中文摘要与英文 RMA Markdown** @@ -125,7 +130,7 @@ ## 推荐下一项任务 -接下来做 **P3-003**:在浏览器内对用户粘贴的真实 SMART 文本实现序列号、WWN、主机名和 -IP 完整脱敏,并为后续导出与 AI 边界提供只含允许字段的结构化投影。必须覆盖大小上限、 -不同 smartctl 格式、重复标识、大小写与分隔符变体,以及 URL、日志、Analytics、导出和 AI -请求不含原文的失败测试。每次仍只处理一个任务;健康分类由 P3-004 单独实现。 +接下来做 **P3-004**:基于 Parse Result v1 和 Boundary Projection v1 实现确定性的健康、 +注意、危险和未知分类。规则必须处理总体 PASSED 与关键计数非零的冲突,未知厂商属性不得 +强行判故障,SMART 不可用或关键字段缺失必须显式为未知;每条结论需指向输入信号和规则, +不得调用 AI 或声称厂商必须保修。每次仍只处理一个任务。 diff --git a/apps/web/scripts/verify-static-output.mjs b/apps/web/scripts/verify-static-output.mjs index 68c0997..13c5ee0 100644 --- a/apps/web/scripts/verify-static-output.mjs +++ b/apps/web/scripts/verify-static-output.mjs @@ -295,7 +295,20 @@ const checks = [ "本阶段不调用", "不写入 URL", "Local Storage", - "不包含序列号、WWN、型号原文和原始文本", + "序列号、WWN、主机名和 IP", + "无自由文本的允许字段投影", + ].every((label) => smartRmaHtml.includes(label)), + ], + [ + "SMART / RMA local redaction preview", + [ + "本地解析并脱敏", + "本地脱敏预览", + "主机名 / 域名", + "查看脱敏后的本地文本", + "[REDACTED:SERIAL_NUMBER]", + "[REDACTED:WWN]", + "Boundary Projection v1", ].every((label) => smartRmaHtml.includes(label)), ], [ @@ -323,7 +336,8 @@ const checks = [ styles.includes(".smart-rma-form textarea") && styles.includes(".smart-rma-summary-grid") && styles.includes(".smart-rma-table-scroll") && - styles.includes(".smart-rma-nvme-grid"), + styles.includes(".smart-rma-nvme-grid") && + styles.includes(".smart-rma-redaction-counts"), ], ]; diff --git a/apps/web/src/components/SmartRmaWorkbench.tsx b/apps/web/src/components/SmartRmaWorkbench.tsx index 90c8b28..4d55211 100644 --- a/apps/web/src/components/SmartRmaWorkbench.tsx +++ b/apps/web/src/components/SmartRmaWorkbench.tsx @@ -1,5 +1,10 @@ import { useMemo, useState } from "preact/hooks"; +import { + type SmartRmaRedactionPreview, + SmartRmaRedactionError, + redactSmartctlText, +} from "../lib/smart-rma-redaction"; import { type SmartRmaDeviceKind, type SmartRmaMissingField, @@ -84,7 +89,7 @@ const initialStatus: WorkbenchStatus = { tone: "info", title: "合成样例已就绪", message: - "选择一个完全合成的 smartctl 样例,或粘贴文本后在当前浏览器标签页本地解析。", + "选择一个完全合成的 smartctl 样例,或粘贴文本后在当前浏览器标签页解析并脱敏。", }; const formatMetric = (value: number | null, suffix = ""): string => @@ -101,8 +106,13 @@ export default function SmartRmaWorkbench({ const [selectedSampleId, setSelectedSampleId] = useState(initialSample.id); const [rawText, setRawText] = useState(initialSample.raw); - const [result, setResult] = useState(() => - parseSmartctlText(initialSample.raw), + const [redactionPreview, setRedactionPreview] = + useState(() => + redactSmartctlText(initialSample.raw), + ); + const [result, setResult] = useState( + () => + redactionPreview?.parse_result ?? parseSmartctlText(initialSample.raw), ); const [status, setStatus] = useState(initialStatus); const selectedSample = useMemo( @@ -113,43 +123,60 @@ export default function SmartRmaWorkbench({ const loadSelectedSample = (): void => { setRawText(selectedSample.raw); setResult(null); + setRedactionPreview(null); setStatus({ tone: "info", title: "合成样例已载入", - message: `${selectedSample.label} 已放入输入框;点击“本地解析”查看结构化证据。`, + message: `${selectedSample.label} 已放入输入框;点击“本地解析并脱敏”查看结果。`, }); }; const parseLocally = (): void => { try { - const parsed = parseSmartctlText(rawText); - setResult(parsed); + const preview = redactSmartctlText(rawText); + setRedactionPreview(preview); + setResult(preview.parse_result); setStatus({ tone: "ready", - title: "本地解析完成", + title: "本地解析与脱敏完成", message: - "只展示结构化字段和解析信号;原始文本、型号原文、序列号与 WWN 都不进入结果。", + "序列号、WWN、主机名、IP 与其他常见敏感值已遮蔽;跨边界只使用无自由文本的结构化投影。", }); } catch (error) { setResult(null); + setRedactionPreview(null); setStatus({ tone: "error", - title: "无法解析", + title: "无法解析或脱敏", message: + error instanceof SmartRmaRedactionError || error instanceof SmartRmaParserError ? error.message - : "输入没有通过本地解析合同,请检查是否为 smartctl 文本。", + : "输入没有通过本地解析与脱敏合同,请检查是否为 smartctl 文本。", }); } }; const resetWorkbench = (): void => { + const preview = redactSmartctlText(initialSample.raw); setSelectedSampleId(initialSample.id); setRawText(initialSample.raw); - setResult(parseSmartctlText(initialSample.raw)); + setRedactionPreview(preview); + setResult(preview.parse_result); setStatus(initialStatus); }; + const updateRawText = (value: string): void => { + setRawText(value); + setResult(null); + setRedactionPreview(null); + setStatus({ + tone: "info", + title: "输入已改变", + message: "点击“本地解析并脱敏”生成与当前文本对应的新结果。", + }); + }; + return (
smartctl 文本 - 当前版本不导出原文;P3-003 - 完成前,请不要把真实文本复制到其他位置。 + 当前版本不导出原文;解析时会同时生成仅供本页查看的脱敏预览。