merge: fix/document-revision-quality-context
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
---
|
||||
title: "行动计划表状态列「待续」被质量门禁误判为敷衍"
|
||||
status: in_progress
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
severity: high
|
||||
---
|
||||
|
||||
# 行动计划表状态列「待续」被质量门禁误判为敷衍
|
||||
|
||||
## 现象
|
||||
|
||||
第三轮真实 UI(新用户 `mtzzliz5`,新项目,真实 MiniMax):
|
||||
|
||||
- UI-00~02 PASS;**UI-03** 两轮 auto-repair 后 **`quality_failed` 80/100**
|
||||
- workspace=`intake_1789314411395330821`
|
||||
- `manifest.prohibited_phrases` 仅 **「待续」**
|
||||
- `output.md` 第163–164 行与 `repair_1` / `repair_2` 对应行均为行动计划表状态单元格,而非省略正文:
|
||||
|
||||
```text
|
||||
| **待续** | 合规专员基于用户补充材料... | 合规专员 | D-01至D-07全部确认后 |
|
||||
| **待续** | 组织专员编制里程碑规划与检测排期方案... | 组织专员 | 合规评估完成后 |
|
||||
```
|
||||
|
||||
表头为 `| 阶段 | 行动项 | 负责方 | 触发条件 |`,节标题为「下一步行动建议」。泛化修复提示未能让模型改掉结构化状态词,造成真实假阳性。
|
||||
|
||||
## 根因分析
|
||||
|
||||
`containsProhibitedUsage` 对禁词「待续」做全文命中;既有豁免只覆盖自检/规则行的否定或引号引用。行动计划表把「待续」用作**独立阶段状态**(与「立即」并列),不是「后续章节待续」式正文省略。门禁无法区分结构,扣 20 分后触发无效 auto-repair。
|
||||
|
||||
审查补强:豁免必须按表头把命中列对齐到「阶段/状态/进度」;动作/负责方/触发条件按表头索引取值。负责方或触发条件单元格为「待续」不得豁免。
|
||||
|
||||
## 严重度
|
||||
|
||||
`high`
|
||||
|
||||
## 关联整改
|
||||
|
||||
- Short-term: [../ops/short-term/2026-09-13-document-revision-quality-context.md](../ops/short-term/2026-09-13-document-revision-quality-context.md)
|
||||
- 分支: `fix/document-revision-quality-context`
|
||||
- 前序自污染豁免不得回退:[./2026-09-13-document-revision-quality-gate-self-pollution.md](./2026-09-13-document-revision-quality-gate-self-pollution.md)
|
||||
|
||||
## 证据
|
||||
|
||||
- Evidence root(勿删、勿提交本轮):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui03-quality-status-false-positive`
|
||||
- 只读 workspace:`data/dev/workspaces/bizdev/intake_1789314411395330821`
|
||||
|
||||
## 开放问题
|
||||
|
||||
- 占槽后真实 UI-03 重跑;本轮不操作容器、不改 E2E
|
||||
+5
-3
@@ -1,11 +1,11 @@
|
||||
# 当前焦点
|
||||
|
||||
**更新**: 2026-09-13
|
||||
**production_ready**: no(DOCREV:schema **30**;当前受控文档指示语路由在 `fix/document-revision-active-doc-routing`;不操作容器)
|
||||
**production_ready**: no(DOCREV:schema **30**;行动表状态「待续」假阳性在 `fix/document-revision-quality-context`;不操作容器)
|
||||
|
||||
## 焦点
|
||||
|
||||
1. **DOCREV-RT**: [当前受控文档指示语路由](short-term/2026-09-13-document-revision-active-doc-routing.md)(`fix/document-revision-active-doc-routing`:UI-05 不得被法规关键词改路由;不 push / 不操作容器)
|
||||
1. **DOCREV-RT**: [行动计划表状态「待续」窄豁免](short-term/2026-09-13-document-revision-quality-context.md)(`fix/document-revision-quality-context`:不删禁词、不放宽正文;不 push / 不操作容器)
|
||||
2. **CHAT**: [刷新恢复整改已实施](short-term/2026-09-13-chat-history-refresh-remediation.md)(待合 dev + PI E2E 补录)
|
||||
3. **CI**: `TEST_DATABASE_URL` + 真实 PI E2E 实录;INTENT 发布 blocker 见 conversation-intent short-term
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
| Cursor | document-revision-wave1-6-implementation | feature/document-revision-management | - | docgov | done |
|
||||
| Cursor-Auto | document-revision-runtime-ui03 | fix/document-revision-runtime-ui03 | - | docgov | done |
|
||||
| Cursor-Auto | document-revision-sse-idempotency | fix/document-revision-sse-idempotency | - | docgov | done |
|
||||
| Cursor-Auto | document-revision-active-doc-routing | fix/document-revision-active-doc-routing | - | docgov | in_progress |
|
||||
| Cursor-Auto | document-revision-active-doc-routing | fix/document-revision-active-doc-routing | - | docgov | done |
|
||||
| Cursor-Auto | document-revision-quality-context | fix/document-revision-quality-context | - | docgov | in_progress |
|
||||
|
||||
> 新任务开始前登记。`branch` 必须为 `feature/<slug>` 或 `fix/<slug>`,**禁止** `dev` / `test` / `main`。同一 `mutex` 同时仅一个 `in_progress`。
|
||||
|
||||
@@ -54,3 +55,4 @@
|
||||
| SSE/幂等审计 | [../audit/2026-09-13-document-revision-sse-idempotency.md](../audit/2026-09-13-document-revision-sse-idempotency.md) |
|
||||
| 质量门禁自污染审计 | [../audit/2026-09-13-document-revision-quality-gate-self-pollution.md](../audit/2026-09-13-document-revision-quality-gate-self-pollution.md) |
|
||||
| 当前文档路由审计 | [../audit/2026-09-13-document-revision-active-doc-routing.md](../audit/2026-09-13-document-revision-active-doc-routing.md) |
|
||||
| 行动表状态假阳性审计 | [../audit/2026-09-13-document-revision-quality-status-false-positive.md](../audit/2026-09-13-document-revision-quality-status-false-positive.md) |
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "质量门禁:行动计划表状态「待续」窄豁免"
|
||||
status: in_progress
|
||||
created: 2026-09-13
|
||||
updated: 2026-09-13
|
||||
owner: Cursor-Auto
|
||||
branch: fix/document-revision-quality-context
|
||||
worktree: "-"
|
||||
mutex: docgov
|
||||
---
|
||||
|
||||
# 质量门禁:行动计划表状态「待续」窄豁免
|
||||
|
||||
## 目标
|
||||
|
||||
结构化行动计划表中独立状态单元格「待续」不再误报;正文/标题/列表/普通表中的「待续」仍 FAIL。禁词表不得删除「待续」。
|
||||
|
||||
## 范围
|
||||
|
||||
- `internal/squad/quality_gate.go` 窄 Markdown 表结构识别
|
||||
- 敌意单测 + 既有自检豁免 / 投影 / repair 隔离不回归
|
||||
- 文档:audit / gap-matrix / CURRENT / FEATURE-MATRIX
|
||||
|
||||
## 非目标
|
||||
|
||||
- 不删除「待续」禁词;不放宽普通正文/任意同行表格
|
||||
- 不触碰 assets / E2E;不操作容器
|
||||
|
||||
## 步骤
|
||||
|
||||
1. [x] 认领 CURRENT;写 audit/short-term
|
||||
2. [x] 仅豁免行动计划表「阶段/状态/进度」列独立单元格(去粗体精确等于「待续」+ 表头列索引对齐 + 实质行动 + 负责/触发完整)
|
||||
3. [x] 敌意用例:正文「后续章节待续」、`| 内容 | 待续 |`、待续说明、残行/空动作、负责方/触发=待续仍 FAIL;列重排仅状态列可 PASS
|
||||
4. [x] 门禁 + 原子提交
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [x] 现场同构行动表 PASS,且不触发 repair
|
||||
- [x] 敌意上下文仍记录 `待续`
|
||||
- [x] `ProhibitedPhrases` 仍含「待续」
|
||||
- [x] targeted / `go vet` / `go test -short` / `go test -race ./internal/taskrun` / web test+build / `git diff --check`
|
||||
|
||||
## 相关链接
|
||||
|
||||
- Audit: [../../audit/2026-09-13-document-revision-quality-status-false-positive.md](../../audit/2026-09-13-document-revision-quality-status-false-positive.md)
|
||||
- Evidence: [../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui03-quality-status-false-positive](../../test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui03-quality-status-false-positive)
|
||||
@@ -65,6 +65,7 @@
|
||||
| DOCREV-SSE-IDEM-BUSY | SSE WriteTimeout 断线后同 turn 再 POST 误 409 DOC_BUSY + 重复聊天 | **FIX-LANDED**(工程) | [audit](../audit/2026-09-13-document-revision-sse-idempotency.md);证据 [assets/2026-09-13-document-revision-runtime-ui](../test-reports/assets/2026-09-13-document-revision-runtime-ui/);`fix/document-revision-sse-idempotency` | 占槽真实 UI-03 重跑;不删证据 |
|
||||
| DOCREV-QUALITY-SELF-POLLUTE | 自检表引用禁词致 score=40 quality_failed;auto_repair 未启用;UI 映射 completed | **FIX-LANDED**(工程) | [audit](../audit/2026-09-13-document-revision-quality-gate-self-pollution.md);workspace `intake_1789308825666278807` output.md:204;同分支 | 占槽真实重跑;不删证据 |
|
||||
| DOCREV-ACTIVE-DOC-ROUTING | UI-05「当前受控文档」被法规关键词改路由到 `feasibility_reg`,intake 无 pending | **FIX-LANDED**(工程) | [audit](../audit/2026-09-13-document-revision-active-doc-routing.md);证据 `schema30-ui05-active-doc-routing-failure`;`fix/document-revision-active-doc-routing` | 占槽真实 UI-05 重跑;**不改弱 E2E**;不删证据 |
|
||||
| DOCREV-QUALITY-STATUS-FP | 行动计划表状态列「待续」被门禁当敷衍,两轮 repair 后 quality_failed 80 | **FIX-LANDED**(工程) | [audit](../audit/2026-09-13-document-revision-quality-status-false-positive.md);证据 `schema30-ui03-quality-status-false-positive`;`fix/document-revision-quality-context` | 占槽真实 UI-03 重跑;**不删禁词、不放宽正文**;不删证据 |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
| 工作流治理 | 三正式工作流 + 阶段门禁 | done | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md) | Phase2 设计器 partial | `workflow` |
|
||||
| IAM / 鉴权 | 项目级 API 鉴权 + RBAC | partial | [audit/authorization](../audit/authorization-matrix-v2.md);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | 历史读取端点补统一会话 owner + 阶段授权;旧 API 410 标注(BUG-11 partial) | `authz` |
|
||||
| 文档治理 | 法定文档生命周期 + 租约 | done | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md) | — | `docgov` |
|
||||
| 文档版本与修改认知 | 智能体版本边界、人工修订标记、任意版本比较、变更报告与负责人通知 | partial | [test 2026-09-13](../test-reports/2026-09-13-document-revision-management-test-report.md);[运行时审计](../audit/2026-09-13-document-revision-runtime-404.md);[当前文档路由](../audit/2026-09-13-document-revision-active-doc-routing.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **运行时 blocker**:PG 未接线→404/重启丢失;overlay/freeform 保存缺口;PI 叙事报告待集成;见 [runtime remediation](../remediation/2026-09-13-document-revision-runtime-remediation.md)。指示语钉住 active `doc_proj` 工程已落地,待占槽重跑 UI-05 | `docgov` |
|
||||
| 文档版本与修改认知 | 智能体版本边界、人工修订标记、任意版本比较、变更报告与负责人通知 | partial | [test 2026-09-13](../test-reports/2026-09-13-document-revision-management-test-report.md);[运行时审计](../audit/2026-09-13-document-revision-runtime-404.md);[当前文档路由](../audit/2026-09-13-document-revision-active-doc-routing.md);[行动表状态假阳性](../audit/2026-09-13-document-revision-quality-status-false-positive.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **运行时 blocker**:PG 未接线→404/重启丢失;overlay/freeform 保存缺口;PI 叙事报告待集成;见 [runtime remediation](../remediation/2026-09-13-document-revision-runtime-remediation.md)。行动表状态「待续」窄豁免已落地,待占槽重跑 UI-03 | `docgov` |
|
||||
| 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 错误卡片/任务进度/文档 busy 状态刷新恢复与人工抽检待完成 | `web` |
|
||||
| PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` |
|
||||
| 三环境运维 | dev/test/main + 共享 KB | done | [PROJECT.md](../../PROJECT.md) §4;四环境 healthy | — | `infra` |
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"chat-agent-system/internal/agent"
|
||||
"chat-agent-system/internal/stream"
|
||||
@@ -180,10 +182,12 @@ func containsProhibitedUsage(content, phrase string) bool {
|
||||
return false
|
||||
}
|
||||
abs := searchFrom + rel
|
||||
if !isNegatedOrMetaProhibitedCitation(content, abs, phrase) {
|
||||
return true
|
||||
if isNegatedOrMetaProhibitedCitation(content, abs, phrase) ||
|
||||
isActionPlanStatusCell(content, abs, phrase) {
|
||||
searchFrom = abs + len(phrase)
|
||||
continue
|
||||
}
|
||||
searchFrom = abs + len(phrase)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,6 +245,213 @@ func phraseQuotedOnLine(line, phrase string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
var (
|
||||
actionPlanStatusHeaders = []string{"阶段", "状态", "进度"}
|
||||
actionPlanActionHeaders = []string{"行动项", "行动", "动作", "后续动作", "后续行动", "工作内容"}
|
||||
actionPlanOwnerHeaders = []string{"负责方", "责任人", "负责人"}
|
||||
actionPlanTriggerHeaders = []string{"触发条件", "触发", "完成条件", "前置条件"}
|
||||
)
|
||||
|
||||
// isActionPlanStatusCell 仅豁免行动计划/后续动作表中「阶段/状态/进度」列的独立单元格「待续」。
|
||||
// 命中列必须与表头状态列索引一致;动作/负责/触发按表头取值。不得凭同行任意列或邻近词放宽。
|
||||
// 只接受同一连续表格:表头后须紧跟合法 separator,且数据行列数与表头相等。
|
||||
func isActionPlanStatusCell(content string, idx int, phrase string) bool {
|
||||
if phrase != "待续" || idx < 0 || idx >= len(content) {
|
||||
return false
|
||||
}
|
||||
lineStart := strings.LastIndex(content[:idx], "\n") + 1
|
||||
lineEnd := strings.Index(content[idx:], "\n")
|
||||
if lineEnd < 0 {
|
||||
lineEnd = len(content)
|
||||
} else {
|
||||
lineEnd = idx + lineEnd
|
||||
}
|
||||
line := content[lineStart:lineEnd]
|
||||
cells := markdownTableCells(line)
|
||||
if len(cells) < 4 || isMarkdownTableSeparator(cells) {
|
||||
return false
|
||||
}
|
||||
header := findPrecedingTableHeader(content, lineStart)
|
||||
if len(header) == 0 || len(cells) != len(header) {
|
||||
return false
|
||||
}
|
||||
cols, ok := parseActionPlanColumns(header)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
cell, hitIdx, ok := tableCellAt(line, idx-lineStart)
|
||||
if !ok || hitIdx != cols.status || stripMarkdownEmphasis(cell) != "待续" {
|
||||
return false
|
||||
}
|
||||
if cols.action >= len(cells) || cols.owner >= len(cells) || cols.trigger >= len(cells) || cols.status >= len(cells) {
|
||||
return false
|
||||
}
|
||||
if strings.TrimSpace(stripMarkdownEmphasis(cells[cols.owner])) == "" ||
|
||||
strings.TrimSpace(stripMarkdownEmphasis(cells[cols.trigger])) == "" {
|
||||
return false
|
||||
}
|
||||
return hasSubstantialActionText(stripMarkdownEmphasis(cells[cols.action]))
|
||||
}
|
||||
|
||||
func markdownTableCells(line string) []string {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if !strings.HasPrefix(trimmed, "|") {
|
||||
return nil
|
||||
}
|
||||
parts := strings.Split(trimmed, "|")
|
||||
if len(parts) > 0 && parts[0] == "" {
|
||||
parts = parts[1:]
|
||||
}
|
||||
if len(parts) > 0 && strings.TrimSpace(parts[len(parts)-1]) == "" {
|
||||
parts = parts[:len(parts)-1]
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return nil
|
||||
}
|
||||
return parts
|
||||
}
|
||||
|
||||
func tableCellAt(line string, localIdx int) (string, int, bool) {
|
||||
if localIdx < 0 || localIdx >= len(line) {
|
||||
return "", -1, false
|
||||
}
|
||||
var pipes []int
|
||||
for i := 0; i < len(line); i++ {
|
||||
if line[i] == '|' {
|
||||
pipes = append(pipes, i)
|
||||
}
|
||||
}
|
||||
if len(pipes) < 2 {
|
||||
return "", -1, false
|
||||
}
|
||||
for i := 0; i < len(pipes)-1; i++ {
|
||||
start, end := pipes[i], pipes[i+1]
|
||||
if localIdx > start && localIdx < end {
|
||||
return line[start+1 : end], i, true
|
||||
}
|
||||
}
|
||||
return "", -1, false
|
||||
}
|
||||
|
||||
func isMarkdownTableSeparator(cells []string) bool {
|
||||
if len(cells) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, c := range cells {
|
||||
s := strings.Trim(strings.TrimSpace(c), ":")
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" || strings.Trim(s, "-") != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stripMarkdownEmphasis(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
s = strings.ReplaceAll(s, "**", "")
|
||||
s = strings.ReplaceAll(s, "__", "")
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) >= 2 && strings.HasPrefix(s, "*") && strings.HasSuffix(s, "*") {
|
||||
s = strings.TrimSpace(s[1 : len(s)-1])
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func hasSubstantialActionText(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" || strings.Contains(s, "待续") {
|
||||
return false
|
||||
}
|
||||
if utf8.RuneCountInString(s) < 12 {
|
||||
return false
|
||||
}
|
||||
letters := 0
|
||||
for _, r := range s {
|
||||
if unicode.Is(unicode.Han, r) || unicode.IsLetter(r) {
|
||||
letters++
|
||||
}
|
||||
}
|
||||
return letters >= 10
|
||||
}
|
||||
|
||||
func findPrecedingTableHeader(content string, lineStart int) []string {
|
||||
var above []string
|
||||
prevEnd := lineStart - 1
|
||||
for prevEnd >= 0 {
|
||||
prevStart := 0
|
||||
if prevEnd > 0 {
|
||||
if i := strings.LastIndex(content[:prevEnd], "\n"); i >= 0 {
|
||||
prevStart = i + 1
|
||||
}
|
||||
}
|
||||
line := strings.TrimSpace(content[prevStart:prevEnd])
|
||||
if line == "" {
|
||||
break
|
||||
}
|
||||
cells := markdownTableCells(line)
|
||||
if cells == nil {
|
||||
break
|
||||
}
|
||||
above = append(above, line)
|
||||
prevEnd = prevStart - 1
|
||||
}
|
||||
if len(above) < 2 {
|
||||
return nil
|
||||
}
|
||||
header := markdownTableCells(above[len(above)-1])
|
||||
sep := markdownTableCells(above[len(above)-2])
|
||||
if len(header) == 0 || isMarkdownTableSeparator(header) {
|
||||
return nil
|
||||
}
|
||||
if !isMarkdownTableSeparator(sep) || len(sep) != len(header) {
|
||||
return nil
|
||||
}
|
||||
return header
|
||||
}
|
||||
|
||||
type actionPlanColumns struct {
|
||||
status int
|
||||
action int
|
||||
owner int
|
||||
trigger int
|
||||
}
|
||||
|
||||
func headerColumnIndex(header []string, names []string) int {
|
||||
want := make(map[string]struct{}, len(names))
|
||||
for _, n := range names {
|
||||
want[n] = struct{}{}
|
||||
}
|
||||
for i, c := range header {
|
||||
if _, ok := want[stripMarkdownEmphasis(c)]; ok {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func parseActionPlanColumns(header []string) (actionPlanColumns, bool) {
|
||||
cols := actionPlanColumns{
|
||||
status: headerColumnIndex(header, actionPlanStatusHeaders),
|
||||
action: headerColumnIndex(header, actionPlanActionHeaders),
|
||||
owner: headerColumnIndex(header, actionPlanOwnerHeaders),
|
||||
trigger: headerColumnIndex(header, actionPlanTriggerHeaders),
|
||||
}
|
||||
if cols.status < 0 || cols.action < 0 || cols.owner < 0 || cols.trigger < 0 {
|
||||
return actionPlanColumns{}, false
|
||||
}
|
||||
seen := map[int]struct{}{cols.status: {}, cols.action: {}, cols.owner: {}, cols.trigger: {}}
|
||||
if len(seen) != 4 {
|
||||
return actionPlanColumns{}, false
|
||||
}
|
||||
return cols, true
|
||||
}
|
||||
|
||||
func isActionPlanTableHeader(cells []string) bool {
|
||||
_, ok := parseActionPlanColumns(cells)
|
||||
return ok
|
||||
}
|
||||
|
||||
func negationAdjacentToPhrase(line string, phraseIdx int) bool {
|
||||
if phraseIdx <= 0 || phraseIdx > len(line) {
|
||||
return false
|
||||
|
||||
@@ -288,3 +288,195 @@ func TestQualityGate_NegatedSelfCheckCitation_NoFalsePositive(t *testing.T) {
|
||||
t.Fatalf("meta heading plus real hedging must fail; passed=%v phrases=%v", mo.Passed, mo.ProhibitedPhrases)
|
||||
}
|
||||
}
|
||||
|
||||
func intakeTemplate(t *testing.T) (workDir string, gate *squad.QualityGate) {
|
||||
t.Helper()
|
||||
engine := squad.NewWorkspaceEngine()
|
||||
ws, err := engine.PrepareWorkspace(squad.WorkspacePrepareRequest{
|
||||
WorkDir: t.TempDir(),
|
||||
TaskKey: "intake",
|
||||
StageTitle: "用户初始需求收集表",
|
||||
ProjectName: "医疗设备",
|
||||
TemplateContent: "# 用户初始需求收集表\n\n## 1. 客户与项目画像\n\n## 2. 预期用途\n\n## 3. 待确认清单\n",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PrepareWorkspace: %v", err)
|
||||
}
|
||||
return ws.WorkDir, squad.NewQualityGate()
|
||||
}
|
||||
|
||||
func wrapIntakeBody(body string) string {
|
||||
return `# 用户初始需求收集表
|
||||
|
||||
## 1. 客户与项目画像
|
||||
本项目客户为专业医疗机构。四段式展开客户背景、项目边界与采集依据。
|
||||
|
||||
## 2. 预期用途
|
||||
用于临床辅助诊断与生理信号监测,覆盖病房与门诊场景。
|
||||
|
||||
## 3. 待确认清单
|
||||
- 供电模块认证周期待窗口确认。
|
||||
|
||||
` + body
|
||||
}
|
||||
|
||||
// 现场 intake_1789314411395330821 output.md:163-164 — 行动计划表独立状态「待续」不得误报。
|
||||
func TestQualityGate_ActionPlanStatusCell_PendingNotHedging(t *testing.T) {
|
||||
workDir, gate := intakeTemplate(t)
|
||||
|
||||
stillListed := false
|
||||
for _, p := range squad.ProhibitedPhrases {
|
||||
if p == "待续" {
|
||||
stillListed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !stillListed {
|
||||
t.Fatal("待续 must remain in ProhibitedPhrases")
|
||||
}
|
||||
|
||||
// 与 workspace 第163-164 行同构(下一步行动建议 / 阶段·行动项·负责方·触发条件)
|
||||
actionPlan := `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| **立即** | 用户补充D-01(注册分类)、D-02(预期用途)两项高优先级缺口,提供分类依据或产品技术要求初稿。 | 北京水木东方 | 收到用户反馈后 |
|
||||
| **待续** | 合规专员基于用户补充材料,评估GB 9706.1-2020符合性与软件合规要求,出具初步合规评估意见。 | 合规专员 | D-01至D-07全部确认后 |
|
||||
| **待续** | 组织专员编制里程碑规划与检测排期方案,明确各阶段时间节点、责任部门及交付物要求。 | 组织专员 | 合规评估完成后 |
|
||||
`
|
||||
report, err := gate.Validate(workDir, "intake", wrapIntakeBody(actionPlan))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
if !report.Passed {
|
||||
t.Fatalf("action-plan status cell must pass; violations=%v phrases=%v score=%.0f",
|
||||
report.Violations, report.ProhibitedPhrases, report.Score)
|
||||
}
|
||||
if len(report.ProhibitedPhrases) != 0 {
|
||||
t.Fatalf("expected no prohibited hits on action-plan status, got %v", report.ProhibitedPhrases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualityGate_ActionPlanStatus_ReorderedColumnsPass(t *testing.T) {
|
||||
workDir, gate := intakeTemplate(t)
|
||||
reordered := `### 下一步行动建议
|
||||
|
||||
| 行动项 | 负责方 | 触发条件 | 阶段 |
|
||||
|---|---|---|---|
|
||||
| 合规专员基于用户补充材料评估GB 9706.1符合性并出具初步意见。 | 合规专员 | D-01至D-07全部确认后 | **待续** |
|
||||
`
|
||||
report, err := gate.Validate(workDir, "intake", wrapIntakeBody(reordered))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
if !report.Passed || len(report.ProhibitedPhrases) != 0 {
|
||||
t.Fatalf("reordered status column must pass; violations=%v phrases=%v",
|
||||
report.Violations, report.ProhibitedPhrases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualityGate_PendingPhrase_HostileContextsStillFail(t *testing.T) {
|
||||
workDir, gate := intakeTemplate(t)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
body string
|
||||
}{
|
||||
{"body_hedging", "后续章节待续,本节仅列框架。\n"},
|
||||
{"heading", "## 后续章节待续\n正文已展开风险与电气安全条款适用性。\n"},
|
||||
{"list", "- 后续章节待续\n"},
|
||||
{"plain_two_col_table", "| 项目 | 内容 |\n|---|---|\n| 风险管理 | 待续 |\n"},
|
||||
{"four_col_non_action_table", "| 章节 | 摘要 | 作者 | 备注 |\n|---|---|---|---|\n| 风险 | 待续 | 张三 | 无 |\n"},
|
||||
{"labeled_pending_note", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| 待续说明 | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 合规专员 | D-01确认后 |
|
||||
`},
|
||||
{"incomplete_row", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| **待续** | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 合规专员 | |
|
||||
`},
|
||||
{"thin_action", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| **待续** | 再议 | 合规专员 | 完成后 |
|
||||
`},
|
||||
{"owner_cell_pending", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| 立即 | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 待续 | D-01确认后 |
|
||||
`},
|
||||
{"trigger_cell_pending", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| 立即 | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 合规专员 | 待续 |
|
||||
`},
|
||||
{"no_header_separator", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
| **待续** | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 合规专员 | D-01确认后 |
|
||||
`},
|
||||
{"header_4_data_5", `### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| **待续** | 合规专员基于用户补充材料评估符合性并出具初步意见。 | 合规专员 | D-01确认后 | 额外列 |
|
||||
`},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rep, err := gate.Validate(workDir, "intake", wrapIntakeBody(tc.body))
|
||||
if err != nil {
|
||||
t.Fatalf("Validate: %v", err)
|
||||
}
|
||||
if rep.Passed || len(rep.ProhibitedPhrases) == 0 {
|
||||
t.Fatalf("must still fail; passed=%v phrases=%v", rep.Passed, rep.ProhibitedPhrases)
|
||||
}
|
||||
hit := false
|
||||
for _, p := range rep.ProhibitedPhrases {
|
||||
if p == "待续" {
|
||||
hit = true
|
||||
}
|
||||
}
|
||||
if !hit {
|
||||
t.Fatalf("must record 待续, got %v", rep.ProhibitedPhrases)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQualityGate_ActionPlanStatus_DoesNotTriggerRepair(t *testing.T) {
|
||||
workDir, gate := intakeTemplate(t)
|
||||
content := wrapIntakeBody(`### 下一步行动建议
|
||||
|
||||
| 阶段 | 行动项 | 负责方 | 触发条件 |
|
||||
|---|---|---|---|
|
||||
| **待续** | 合规专员基于用户补充材料评估电气安全符合性并出具意见。 | 合规专员 | D-01至D-07全部确认后 |
|
||||
`)
|
||||
if err := os.WriteFile(filepath.Join(workDir, "output.md"), []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
driver := squad.NewWorkflowDriver(squad.NewWorkspaceEngine())
|
||||
report, err := gate.EnsureQualityOrRepair(context.Background(), workDir, "intake", driver, &agent.StubBackend{}, 1, squad.ExecuteOptions{
|
||||
Timeout: 5 * time.Second,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureQualityOrRepair: %v", err)
|
||||
}
|
||||
if report == nil || !report.Passed {
|
||||
t.Fatalf("action-plan pending must pass without repair; report=%+v", report)
|
||||
}
|
||||
wf, _ := driver.LoadWorkflow(workDir)
|
||||
for _, s := range wf.Steps {
|
||||
if strings.HasPrefix(s.ID, "step_repair_") {
|
||||
t.Fatalf("must not append repair for status-cell false positive, got %q", s.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user