fix(report): name final-parse guards and scrub blocked wording
Five-theme assemble cleared the chart and actionNotes caps, then failed three anonymous guards. Log field path plus kind only, and strip deterministic phrases from sections the producer already marked blocked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
## 2026-09-04 — 五主题完整报告不再因分盘张数上限装不出来
|
## 2026-09-04 — 五主题完整报告不再因分盘张数上限装不出来
|
||||||
|
|
||||||
默认五章(含健康)会带上全部正式分盘。以前文档最多只许 6 张图,五章写完后整份报告仍会被判失败。现在张数上限跟着分盘枚举走;各章行动条超过文档上限时按上限收下,不再整份作废。Skill 版本未变。
|
默认五章(含健康)会带上全部正式分盘。以前文档最多只许 6 张图,五章写完后整份报告仍会被判失败。现在张数上限跟着分盘枚举走;各章行动条超过文档上限时按上限收下;被降级的段落会去掉「一定会」这类说法,不再整份作废。Skill 版本未变。
|
||||||
|
|
||||||
## 2026-09-04 — 开场打招呼不再被 set-focus 清掉
|
## 2026-09-04 — 开场打招呼不再被 set-focus 清掉
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -8270,8 +8270,8 @@
|
|||||||
- 用户现象:报告中心显示生成失败。错误码 `report_schema_invalid`,日志 `innerReason=final_parse_rejected`。五章其实都已写完。
|
- 用户现象:报告中心显示生成失败。错误码 `report_schema_invalid`,日志 `innerReason=final_parse_rejected`。五章其实都已写完。
|
||||||
- 触发条件:accepted 生时、默认五主题、staging @ `e27d5dc5`。干净 attempt 1 与此前被租约打断后恢复的 attempt 都复现。
|
- 触发条件:accepted 生时、默认五主题、staging @ `e27d5dc5`。干净 attempt 1 与此前被租约打断后恢复的 attempt 都复现。
|
||||||
- 根因:`c2f23131` 把 `CHART_IDS` 扩到 9 个(加 D6/D8/D30),文档 v2 仍 `charts.max(6)`。分盘提取修好后,五主题装配 9 张图,终稿 parse 拒绝。四主题一直 ≤6 所以从未踩中。
|
- 根因:`c2f23131` 把 `CHART_IDS` 扩到 9 个(加 D6/D8/D30),文档 v2 仍 `charts.max(6)`。分盘提取修好后,五主题装配 9 张图,终稿 parse 拒绝。四主题一直 ≤6 所以从未踩中。
|
||||||
- 修复:v2 `charts.max(CHART_IDS.length)`;JSON Schema / Python 合同同步;装配允许集改为 `CHART_IDS`。`final_parse_rejected` 日志增加 `parsePaths`(仅 path + code)。装配把 `actionNotes` 截到合同上限 24,不放宽 schema。
|
- 修复:v2 `charts.max(CHART_IDS.length)`;JSON Schema / Python 合同同步;装配允许集改为 `CHART_IDS`。`final_parse_rejected` 日志增加 `parsePaths`(仅 path + code)。装配把 `actionNotes` 截到合同上限 24,不放宽 schema。guard 失败改为字段路径 + 种类。blocked 段去掉确定性用语。
|
||||||
- 验证:合同测试 9 图过 parse;五主题分章夹具 READY;每章 6 条行动仍 READY 且 notes=24。staging @ `d4627a50` request `31025c49`:五章 ready 后 `parsePaths=actionNotes/too_big`(charts 已不是原因)。截断修复待再部署。
|
- 验证:合同测试 9 图过 parse;五主题分章夹具 READY;每章 6 条行动仍 READY 且 notes=24。staging request `31025c49`:`actionNotes/too_big`。`a75929c1` request `f10daf6d`:三条匿名 guard。路径代号与 blocked 清洗待再部署。
|
||||||
- 防复发:上限必须绑定 `CHART_IDS.length`,不得再写裸 6/9。扩枚举必须同时改 Zod / JSON Schema enum / Python `CHART_IDS`。终稿失败必须带 parse path。
|
- 防复发:上限必须绑定 `CHART_IDS.length`,不得再写裸 6/9。扩枚举必须同时改 Zod / JSON Schema enum / Python `CHART_IDS`。终稿失败必须带 parse path。
|
||||||
- 相关记录:BUG-526、BUG-534;引入半拉子改动的提交 `c2f23131`
|
- 相关记录:BUG-526、BUG-534;引入半拉子改动的提交 `c2f23131`
|
||||||
- 复发自:无
|
- 复发自:无
|
||||||
|
|||||||
@@ -10,14 +10,15 @@
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| 1 上限绑定 + 消费点核对 + 回归 | 完成 | BUG-535 |
|
| 1 上限绑定 + 消费点核对 + 回归 | 完成 | BUG-535 |
|
||||||
| 2 终稿 parse path 日志 | 完成 | BUG-535 |
|
| 2 终稿 parse path 日志 | 完成 | BUG-535 |
|
||||||
| 3 部署后真实 personal_full | 进行中:第一次仍失败;parsePaths 指出 `actionNotes.too_big`,装配已按合同上限截断,待再部署 | BUG-535 |
|
| 3 部署后真实 personal_full | 进行中:第二次 charts/actionNotes 已过;3 条匿名 guard。已补 guard 路径代号,并在降级段去掉确定性用语,待再部署 | BUG-535 |
|
||||||
|
|
||||||
## 实现要点
|
## 实现要点
|
||||||
|
|
||||||
- v2 文档 `charts.max(CHART_IDS.length)`,单一真源,不写裸数字 9。
|
- v2 文档 `charts.max(CHART_IDS.length)`,单一真源,不写裸数字 9。
|
||||||
- 装配侧 `assembleReportDocumentV2` 的允许分盘集合改为 `new Set(CHART_IDS)`;`DOCUMENT_VARGA_CHART_IDS` 从 `CHART_IDS` 去掉 `D1` 派生。
|
- 装配侧 `assembleReportDocumentV2` 的允许分盘集合改为 `new Set(CHART_IDS)`;`DOCUMENT_VARGA_CHART_IDS` 从 `CHART_IDS` 去掉 `D1` 派生。
|
||||||
- 装配把 `actionNotes` 截到 `REPORT_DOCUMENT_V2_ACTION_NOTES_MAX`(24),**不放宽** schema。staging 第一次五章写完后又 `final_parse_rejected`,`parsePaths` 只有 `actionNotes`/`too_big`。
|
- 装配把 `actionNotes` 截到 `REPORT_DOCUMENT_V2_ACTION_NOTES_MAX`(24),**不放宽** schema。staging 第一次五章写完后又 `final_parse_rejected`,`parsePaths` 只有 `actionNotes`/`too_big`。
|
||||||
- 终稿 `safeParseServerReportDocument` 失败时,`generation_failed` 增加 `parsePaths: [{path, code}]`。只记路径与 zod/guard 代号,不记 `message`、不记正文或 hash 值。
|
- 终稿 `safeParseServerReportDocument` 失败时,`generation_failed` 增加 `parsePaths: [{path, code}]`。只记路径与 zod/guard 代号,不记 `message`、不记正文或 hash 值。合同 guard 不再一律写成 `(guard)/guard`,而是字段路径 + 种类。
|
||||||
|
- 装配后的 `applyReportGuard` 把章节标成 blocked 时,会去掉「一定会」这类确定性用语,并让 `currentPhase` 跟 timing 章一致。**不放宽** parse 侧字段校验。
|
||||||
- JSON Schema `maxItems` / chart `enum`、Python `CHART_IDS` 与 `len(CHART_IDS)` 与 TS 对齐(合同三份真源)。冻结 skill 包 `6.9.14` / `6.9.15` 未动。
|
- JSON Schema `maxItems` / chart `enum`、Python `CHART_IDS` 与 `len(CHART_IDS)` 与 TS 对齐(合同三份真源)。冻结 skill 包 `6.9.14` / `6.9.15` 未动。
|
||||||
|
|
||||||
## 消费点核对(红线 3)
|
## 消费点核对(红线 3)
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
- 五主题(含 health)分章管线 READY,文档 charts 正好 9 张、blocked 为空
|
- 五主题(含 health)分章管线 READY,文档 charts 正好 9 张、blocked 为空
|
||||||
- 五主题每章 6 条行动仍 READY,`actionNotes.length === 24`
|
- 五主题每章 6 条行动仍 READY,`actionNotes.length === 24`
|
||||||
- `final_parse_rejected` 日志含 `thematicNarrative.0.title` / `too_big`,不含超长标题正文
|
- `final_parse_rejected` 日志含 `thematicNarrative.0.title` / `too_big`,不含超长标题正文
|
||||||
|
- guard 失败日志含字段路径与种类(如 `requestedThemes`/`theme_coverage`、`actionNotes[0].note`/`forbidden_medical_diagnosis`),不含违规正文
|
||||||
- Python `test_v2_accepts_full_chart_ids_set`
|
- Python `test_v2_accepts_full_chart_ids_set`
|
||||||
|
|
||||||
## 门禁输出
|
## 门禁输出
|
||||||
@@ -53,7 +55,7 @@
|
|||||||
- `npm run lint`:0 error / 74 warning(既有,未动)
|
- `npm run lint`:0 error / 74 warning(既有,未动)
|
||||||
- 定向:`personal-report-contract` + `personal-report-generation-v2` + `personal-report-api`:**110 pass / 0 fail**(含新增 9 图 parse、五主题 READY、超限 too_big、parsePaths 日志)
|
- 定向:`personal-report-contract` + `personal-report-generation-v2` + `personal-report-api`:**110 pass / 0 fail**(含新增 9 图 parse、五主题 READY、超限 too_big、parsePaths 日志)
|
||||||
- `.venv/bin/python -m pytest tests/test_personal_report_contract.py`:**40 pass**(含 `test_v2_accepts_full_chart_ids_set`)
|
- `.venv/bin/python -m pytest tests/test_personal_report_contract.py`:**40 pass**(含 `test_v2_accepts_full_chart_ids_set`)
|
||||||
- 定向补跑 generation-v2 + contract:**56 pass**(含 actionNotes 截断)
|
- 定向补跑 contract + generation + generation-v2:**96 pass**(含 guard 路径代号、blocked 段去掉「一定会」)
|
||||||
- 全量 `npm test`:Docker 库测争用超时(`database-*` 与部分会起 Postgres 的套件),与既有环境缺口同类;与本单无关
|
- 全量 `npm test`:Docker 库测争用超时(`database-*` 与部分会起 Postgres 的套件),与既有环境缺口同类;与本单无关
|
||||||
- `next build`:本单未改 UI/路由,未跑
|
- `next build`:本单未改 UI/路由,未跑
|
||||||
|
|
||||||
@@ -61,6 +63,10 @@
|
|||||||
|
|
||||||
request `31025c49`:5× `consultation_workflow` 200;五章 ready;摘要 `stop`。终稿仍 `final_parse_rejected`。日志 `parsePaths: [{path:"actionNotes", code:"too_big"}]`——charts 上限已不是这条失败原因。未放宽 `actionNotes.max(24)`,装配截断到该上限后再验。
|
request `31025c49`:5× `consultation_workflow` 200;五章 ready;摘要 `stop`。终稿仍 `final_parse_rejected`。日志 `parsePaths: [{path:"actionNotes", code:"too_big"}]`——charts 上限已不是这条失败原因。未放宽 `actionNotes.max(24)`,装配截断到该上限后再验。
|
||||||
|
|
||||||
|
### 任务 3 第二次(health `a75929c1`)
|
||||||
|
|
||||||
|
request `f10daf6d`:五章 ready。终稿 `final_parse_rejected`,`parsePaths` 三条都是 `(guard)/guard`。charts 与 actionNotes 已过。日限默认 5 当天用满,staging 进程临时读 8(下次正式部署前 env 文件置回 8,验收后再恢复 5)。未放宽 schema。
|
||||||
|
|
||||||
## 让步
|
## 让步
|
||||||
|
|
||||||
无。JSON Schema 的 charts `maxItems` 只能写数字,用合同测试把它锁到 `CHART_IDS.length`。`actionNotes` 合同上限未放宽。
|
无。JSON Schema 的 charts `maxItems` 只能写数字,用合同测试把它锁到 `CHART_IDS.length`。`actionNotes` 合同上限未放宽。
|
||||||
|
|||||||
@@ -314,6 +314,63 @@ export type ReportDocumentParseError = Readonly<{
|
|||||||
code: string;
|
code: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map a guard message to a field path + kind. The message itself is kept on
|
||||||
|
* the typed error for tests; logs must use only path and code (no values).
|
||||||
|
*/
|
||||||
|
export function classifyReportDocumentGuardError(message: string): ReportDocumentParseError {
|
||||||
|
const forbidden = /^(.*?): forbidden content ([a-z_,]+)$/.exec(message);
|
||||||
|
if (forbidden) {
|
||||||
|
const kinds = forbidden[2].split(",").filter((kind) => /^[a-z_]+$/.test(kind));
|
||||||
|
return {
|
||||||
|
path: forbidden[1] || "(guard)",
|
||||||
|
message,
|
||||||
|
code: kinds[0] ? `forbidden_${kinds[0]}` : "forbidden_content",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const dateClaim = /^(.*?): date claim requires evidenceRefs$/.exec(message);
|
||||||
|
if (dateClaim) {
|
||||||
|
return { path: dateClaim[1] || "(guard)", message, code: "unsupported_date" };
|
||||||
|
}
|
||||||
|
const blocked = /^(.*?): blocked section contains deterministic prediction$/.exec(message);
|
||||||
|
if (blocked) {
|
||||||
|
return { path: blocked[1] || "(guard)", message, code: "blocked_deterministic" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
message.startsWith("requestedThemes must contain ")
|
||||||
|
|| message.startsWith("requested theme ")
|
||||||
|
|| message.startsWith("thematic section covers unrequested theme ")
|
||||||
|
|| message.startsWith("blocked disclosure covers unrequested theme ")
|
||||||
|
) {
|
||||||
|
return { path: "requestedThemes", message, code: "theme_coverage" };
|
||||||
|
}
|
||||||
|
if (message.includes(" requires structured ") && message.includes(" chart data")) {
|
||||||
|
return { path: "thematicNarrative", message, code: "missing_required_chart" };
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
message.startsWith("charts must ")
|
||||||
|
|| message.startsWith("duplicate chart id ")
|
||||||
|
|| message.startsWith("D1 chart ")
|
||||||
|
|| / chart contains duplicate house numbers$/.test(message)
|
||||||
|
) {
|
||||||
|
return { path: "charts", message, code: "chart_set" };
|
||||||
|
}
|
||||||
|
if (message.startsWith("evidence id ")) {
|
||||||
|
return { path: "evidenceAppendix", message, code: "duplicate_evidence_id" };
|
||||||
|
}
|
||||||
|
const dangling = /^evidenceRefs: unknown evidence id (.+):([^:]+)$/.exec(message);
|
||||||
|
if (dangling) {
|
||||||
|
return { path: dangling[1], message, code: "dangling_ref" };
|
||||||
|
}
|
||||||
|
if (message.startsWith("evidenceRefs:")) {
|
||||||
|
return { path: "evidenceRefs", message, code: "dangling_ref" };
|
||||||
|
}
|
||||||
|
if (message.startsWith("serialized document")) {
|
||||||
|
return { path: "(root)", message, code: "document_too_large" };
|
||||||
|
}
|
||||||
|
return { path: "(guard)", message, code: "guard" };
|
||||||
|
}
|
||||||
|
|
||||||
export class ReportDocumentValidationError extends Error {
|
export class ReportDocumentValidationError extends Error {
|
||||||
readonly errors: readonly ReportDocumentParseError[];
|
readonly errors: readonly ReportDocumentParseError[];
|
||||||
|
|
||||||
@@ -603,7 +660,7 @@ export function safeParseReportDocument(input: unknown): ReportDocumentParseResu
|
|||||||
if (guardErrors.length > 0) {
|
if (guardErrors.length > 0) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
errors: guardErrors.map((message) => ({ path: "(guard)", message, code: "guard" })),
|
errors: guardErrors.map((message) => classifyReportDocumentGuardError(message)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return { ok: true, document };
|
return { ok: true, document };
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
safeParseServerReportDocument,
|
safeParseServerReportDocument,
|
||||||
} from "./personal-report-contract.server-core.ts";
|
} from "./personal-report-contract.server-core.ts";
|
||||||
import {
|
import {
|
||||||
|
BLOCKED_DETERMINISTIC_PHRASES,
|
||||||
CHART_IDS,
|
CHART_IDS,
|
||||||
REPORT_DOCUMENT_V2_ACTION_NOTES_MAX,
|
REPORT_DOCUMENT_V2_ACTION_NOTES_MAX,
|
||||||
REQUIRED_THEME_CHARTS,
|
REQUIRED_THEME_CHARTS,
|
||||||
@@ -2922,6 +2923,28 @@ export function redactDeterministicSentences(
|
|||||||
|
|
||||||
const BLOCKED_SECTION_CAVEAT = "该部分证据受限,已按确定性边界降级,仅保留方向性描述。";
|
const BLOCKED_SECTION_CAVEAT = "该部分证据受限,已按确定性边界降级,仅保留方向性描述。";
|
||||||
|
|
||||||
|
const BLOCKED_DETERMINISTIC_PATTERNS = BLOCKED_DETERMINISTIC_PHRASES.map(
|
||||||
|
(phrase) => new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i"),
|
||||||
|
);
|
||||||
|
|
||||||
|
function scrubBlockedDeterministicStrings(target: JsonRecord, keys: readonly string[]): void {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = target[key];
|
||||||
|
if (typeof value === "string") {
|
||||||
|
const redacted = redactDeterministicSentences(value, BLOCKED_DETERMINISTIC_PATTERNS);
|
||||||
|
target[key] = redacted.text.length > 0 ? redacted.text : BLOCKED_SECTION_CAVEAT;
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
target[key] = value
|
||||||
|
.map((item) => (
|
||||||
|
typeof item === "string"
|
||||||
|
? redactDeterministicSentences(item, BLOCKED_DETERMINISTIC_PATTERNS).text
|
||||||
|
: item
|
||||||
|
))
|
||||||
|
.filter((item) => typeof item !== "string" || item.length > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type GuardSection = {
|
type GuardSection = {
|
||||||
id: string;
|
id: string;
|
||||||
narrative: string;
|
narrative: string;
|
||||||
@@ -3106,6 +3129,7 @@ export function applyReportGuard<D>(
|
|||||||
const caveats = stringArray(target.caveats);
|
const caveats = stringArray(target.caveats);
|
||||||
if (!caveats.includes(BLOCKED_SECTION_CAVEAT)) caveats.push(BLOCKED_SECTION_CAVEAT);
|
if (!caveats.includes(BLOCKED_SECTION_CAVEAT)) caveats.push(BLOCKED_SECTION_CAVEAT);
|
||||||
target.caveats = caveats;
|
target.caveats = caveats;
|
||||||
|
scrubBlockedDeterministicStrings(target, ["narrative", "actions", "caveats", "title"]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const summaryRedacted = redactDeterministicSentences(
|
const summaryRedacted = redactDeterministicSentences(
|
||||||
@@ -3116,6 +3140,7 @@ export function applyReportGuard<D>(
|
|||||||
if (summaryRedacted.removedCount > 0) {
|
if (summaryRedacted.removedCount > 0) {
|
||||||
summary.summary = summaryRedacted.text;
|
summary.summary = summaryRedacted.text;
|
||||||
summary.overallClaimStatus = "blocked";
|
summary.overallClaimStatus = "blocked";
|
||||||
|
scrubBlockedDeterministicStrings(summary, ["headline", "summary", "priorities"]);
|
||||||
}
|
}
|
||||||
if (Array.isArray(summary.priorities)) {
|
if (Array.isArray(summary.priorities)) {
|
||||||
const keptPriorities = summary.priorities
|
const keptPriorities = summary.priorities
|
||||||
@@ -3148,6 +3173,7 @@ export function applyReportGuard<D>(
|
|||||||
if (target.claimStatus === "blocked") blockedSectionCount += 1;
|
if (target.claimStatus === "blocked") blockedSectionCount += 1;
|
||||||
const finalStatus = target.claimStatus as string;
|
const finalStatus = target.claimStatus as string;
|
||||||
if (finalStatus === "blocked") {
|
if (finalStatus === "blocked") {
|
||||||
|
scrubBlockedDeterministicStrings(target, ["narrative", "actions", "caveats", "title"]);
|
||||||
const narrativeText = typeof target.narrative === "string" ? target.narrative : "";
|
const narrativeText = typeof target.narrative === "string" ? target.narrative : "";
|
||||||
const blockedClaims = findForbiddenDeterministicClaims(narrativeText);
|
const blockedClaims = findForbiddenDeterministicClaims(narrativeText);
|
||||||
const hardDomain = blockedClaims.find((claim) => claim.domain !== "timing");
|
const hardDomain = blockedClaims.find((claim) => claim.domain !== "timing");
|
||||||
@@ -3166,6 +3192,24 @@ export function applyReportGuard<D>(
|
|||||||
}
|
}
|
||||||
if (summary && (readModel.sections.length === 0 || blockedSectionCount === readModel.sections.length)) {
|
if (summary && (readModel.sections.length === 0 || blockedSectionCount === readModel.sections.length)) {
|
||||||
summary.overallClaimStatus = "blocked";
|
summary.overallClaimStatus = "blocked";
|
||||||
|
scrubBlockedDeterministicStrings(summary, ["headline", "summary", "priorities"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentPhase = record(next.currentPhase);
|
||||||
|
if (currentPhase) {
|
||||||
|
const timing = narrative.find((item) => {
|
||||||
|
const row = record(item);
|
||||||
|
return text(row?.theme) === "timing" || text(row?.id) === "theme-timing";
|
||||||
|
});
|
||||||
|
const timingRow = record(timing);
|
||||||
|
if (timingRow) {
|
||||||
|
if (typeof timingRow.narrative === "string") currentPhase.narrative = timingRow.narrative;
|
||||||
|
if (typeof timingRow.claimStatus === "string") currentPhase.claimStatus = timingRow.claimStatus;
|
||||||
|
if (Array.isArray(timingRow.caveats)) currentPhase.caveats = [...timingRow.caveats];
|
||||||
|
if (currentPhase.claimStatus === "blocked") {
|
||||||
|
scrubBlockedDeterministicStrings(currentPhase, ["narrative", "timingNotes", "caveats", "title", "phaseLabel"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ok: true, document: next as D };
|
return { ok: true, document: next as D };
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
findChartSetViolations,
|
findChartSetViolations,
|
||||||
findDanglingEvidenceRefs,
|
findDanglingEvidenceRefs,
|
||||||
findDuplicateEvidenceIds,
|
findDuplicateEvidenceIds,
|
||||||
|
classifyReportDocumentGuardError,
|
||||||
CURRENT_REPORT_DOCUMENT_SCHEMA_VERSION,
|
CURRENT_REPORT_DOCUMENT_SCHEMA_VERSION,
|
||||||
LEGACY_REPORT_DOCUMENT_SCHEMA_VERSION,
|
LEGACY_REPORT_DOCUMENT_SCHEMA_VERSION,
|
||||||
REPORT_DOCUMENT_SCHEMA_VERSION,
|
REPORT_DOCUMENT_SCHEMA_VERSION,
|
||||||
@@ -404,3 +405,28 @@ test("v2 guard rejects HTML and CSS while allowing ordinary Chinese prose", () =
|
|||||||
assert.equal(findForbiddenContent(ordinary.actionNotes[0].note).length, 0);
|
assert.equal(findForbiddenContent(ordinary.actionNotes[0].note).length, 0);
|
||||||
assert.equal(safeParseReportDocument(ordinary).ok, true);
|
assert.equal(safeParseReportDocument(ordinary).ok, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("guard parse errors expose field path and kind without the offending value", () => {
|
||||||
|
const missing = cloneV2();
|
||||||
|
missing.blockedConflictDisclosure = missing.blockedConflictDisclosure.filter(
|
||||||
|
(section) => section.theme !== "career",
|
||||||
|
);
|
||||||
|
const coverage = safeParseReportDocument(missing);
|
||||||
|
assert.equal(coverage.ok, false);
|
||||||
|
assert.ok(coverage.errors.some((error) => (
|
||||||
|
error.path === "requestedThemes" && error.code === "theme_coverage"
|
||||||
|
)));
|
||||||
|
|
||||||
|
const medical = cloneV2();
|
||||||
|
medical.actionNotes[0].note = "你已经患有糖尿病。";
|
||||||
|
const forbidden = safeParseReportDocument(medical);
|
||||||
|
assert.equal(forbidden.ok, false);
|
||||||
|
assert.ok(forbidden.errors.some((error) => (
|
||||||
|
error.path === "actionNotes[0].note" && error.code === "forbidden_medical_diagnosis"
|
||||||
|
)));
|
||||||
|
assert.ok(forbidden.errors.every((error) => !error.path.includes("糖尿病")));
|
||||||
|
assert.equal(
|
||||||
|
classifyReportDocumentGuardError("thematicNarrative[1]: blocked section contains deterministic prediction").code,
|
||||||
|
"blocked_deterministic",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -572,7 +572,7 @@ test("guard redacts precise timing and downgrades the section when timing is blo
|
|||||||
id: "career",
|
id: "career",
|
||||||
theme: "career",
|
theme: "career",
|
||||||
title: "事业",
|
title: "事业",
|
||||||
narrative: "方向稳定。2027年3月将迎来事业转折,届时务必把握机会。",
|
narrative: "方向稳定。2027年3月将迎来事业转折,届时务必把握机会。命主一定会升职。",
|
||||||
actions: ["2027年3月跳槽"],
|
actions: ["2027年3月跳槽"],
|
||||||
caveats: [],
|
caveats: [],
|
||||||
claimStatus: "single_system_inference",
|
claimStatus: "single_system_inference",
|
||||||
@@ -587,6 +587,7 @@ test("guard redacts precise timing and downgrades the section when timing is blo
|
|||||||
const section = (guarded.document as unknown as { thematicNarrative: { narrative: string; claimStatus: string; caveats: string[] }[] })
|
const section = (guarded.document as unknown as { thematicNarrative: { narrative: string; claimStatus: string; caveats: string[] }[] })
|
||||||
.thematicNarrative[0];
|
.thematicNarrative[0];
|
||||||
assert.doesNotMatch(section.narrative, /2027年3月/);
|
assert.doesNotMatch(section.narrative, /2027年3月/);
|
||||||
|
assert.doesNotMatch(section.narrative, /一定会/);
|
||||||
assert.equal(section.claimStatus, "blocked");
|
assert.equal(section.claimStatus, "blocked");
|
||||||
assert.ok(section.caveats.some((caveat) => caveat.includes("确定性边界")));
|
assert.ok(section.caveats.some((caveat) => caveat.includes("确定性边界")));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user