merge: reconcile governance with current dev

# Conflicts:
#	.doc/ops/CURRENT.md
This commit is contained in:
weichen
2026-09-21 14:42:13 +00:00
8 changed files with 248 additions and 7 deletions
@@ -278,3 +278,9 @@
| UI-03 首次生成 PG ID 误删 baseline | [../audit/2026-09-13-document-revision-pg-id-remap.md](../../audit/2026-09-13-document-revision-pg-id-remap.md) |
| UI-06 刷新后缓存竞争死锁审计 | [../audit/2026-09-14-document-revision-refresh-cache-race.md](../../audit/2026-09-14-document-revision-refresh-cache-race.md) |
| 受控正文真实 UI 验收报告 | [../test-reports/2026-09-14-controlled-content-ui-acceptance.md](../../test-reports/2026-09-14-controlled-content-ui-acceptance.md) |
## 并行集成补录
来源:dev@a5ea469c 的 CURRENT 增量。保留材料切块任务的新结论;活跃 CURRENT 保留其认领和最新槽位。
5. **材料切块不再看体积**:项目 6579 `doc_proj6579_bd_intake` 已确认候选 535,正式正文不再含「二进制压缩流」。成功解析的材料一律写入 `material_chunks`,≤60KB 仍全文注入。见 [报告](../../test-reports/2026-09-21-force-material-chunk-index-test-report.md)。已合入 `dev` 并从主目录 rebuild。test/main 未晋升。
+3 -1
View File
@@ -13,6 +13,7 @@
| owner | task | branch | worktree | mutex | status |
|---|---|---|---|---|---|
| Codex | 2026-09-21-agent-governance-implementation | feature/agent-governance-implementation | .worktrees/agent-governance-implementation | agent-governance | in_progress |
| Cursor | 2026-09-21-force-material-chunk-index | feature/force-material-chunk-index-2026-09-21 | .worktrees/force-material-chunk-index-2026-09-21 | bizdev+materialindex | done |
| Cursor | 2026-09-21-workflow-scope-weighting | feature/workflow-scope-weighting-2026-09-21 | .worktrees/workflow-scope-weighting-2026-09-21 | intent+web+docgov | done |
| Cursor | 2026-09-21-upload-parse-ux-dedup | feature/upload-parse-ux-2026-09-21 | .worktrees/upload-parse-ux-2026-09-21 | server+web+storage | done |
| Cursor | 2026-09-21-mineru-doc-parsing | feature/mineru-doc-parsing-2026-09-21 | .worktrees/mineru-doc-parsing-2026-09-21 | storage+bizdev+kb | done |
@@ -33,6 +34,7 @@
| 路径/事项 | 分类与保留原因 | cleanup_owner | 复查触发 |
|---|---|---|---|
| `.worktrees/force-material-chunk-index-2026-09-21` | Cursor 已交付,保留原任务验收与工作区 | Cursor owner / 独立维护者 | owner 退出后核对祖先、文件、证据与进程 |
| `.worktrees/agent-governance-implementation` | 活跃实施;cleanup=pending | Codex 之外的集成/维护执行者 | 验收、集成、证据归档完成且 owner 退出 |
| `.worktrees/feature-agent-workflow-governance-plan` | 原 adoption 仍 in_progress;不得替 owner 改 done | 原 Codex owner / 维护者接管 | 核对原任务交付与进程后更新真实状态 |
| Cline `2026-09-16-partial-deferred-completion` | 原 in_progress,worktree 未登记;保留待核验 | Cline owner / 维护者接管 | 查明主文档、分支与未提交现场 |
@@ -52,7 +54,7 @@ cleanup=pending 不是任务状态;普通 remove/-d 拒绝时记录原因,
| holder | phase | since | scope | queue |
|--------|-------|-------|-------|-------|
| - | idle | 2026-09-21 | taskrun-status-card label 修复已合入 dev@4b303247 并真实 PI 验收(round2 docs 随后合入);mineru 二轮文档已合入;schema=61 | - |
| - | idle | 2026-09-21 | force-material-chunk-index 已合入 dev 并从主目录 rebuild;项目 6579 候选 535 已确认;schema=61 | - |
**phase**: `idle` | `merging` | `smoke` | `e2e`
@@ -0,0 +1,48 @@
---
title: "成功解析的材料一律切块入库"
status: done
created: 2026-09-21
updated: 2026-09-21
owner: Cursor
branch: feature/force-material-chunk-index-2026-09-21
worktree: .worktrees/force-material-chunk-index-2026-09-21
base_dev_sha: a241e838150f218f3a09d3523f93291b847d45fd
write_scope: [internal/bizdev/materials.go, internal/bizdev/materials_index_test.go, internal/materialindex/pipeline.go, .doc/ops/CURRENT.md, .doc/test-reports/]
head_sha:
deployment: passed
cleanup: pending
mutex: bizdev+materialindex
---
# 成功解析的材料一律切块入库
## 目标
项目 6579 的 `doc_proj6579_bd_intake` 正式正文改为候选 535;每份成功解析的材料都写入 `material_chunks`,小文档仍全文注入。
## 范围
- `materialToItems` 在解析成功后无条件 `IndexChunks`,大小阈值只决定注入方式。
- 上传后的 `EnqueueAsync` 同样不再用 60KB 门槛跳过切块。
## 非目标
- 不改职责矩阵,不晋升 test/main,不改 `production_ready`。
- 不替换已过期的 MinerU 精度 token。
## 步骤
1. [x] 编制人一次性会话确认候选 535,并撤销会话
2. [x] 切块调用移出大小分支
3. [x] 用项目 6579 已上传 PDF 复核 `material_chunks` 与检索
4. [x] 单测与报告
## 验收标准
- [x] 正式正文不再含「二进制压缩流」
- [x] `material_chunks` 对该 PDF 有行且 embedding 非空
- [x] `go test -short ./internal/mineru/ ./internal/materialindex/ ./internal/bizdev/` 通过
## 相关链接
- 报告:`.doc/test-reports/2026-09-21-mineru-doc-parsing-test-report.md`
@@ -0,0 +1,74 @@
# 确认候选并强制全量切块索引 测试报告
## 一、 测试背景 (Background)
项目 6579「新的测试项目」的正式正文仍停在 2026-09-21 07:28 的候选 526(快照 647)。那段文字把《冲击波治疗仪系统方案》写成 PDF 二进制压缩流,并称产品定位完全空白。修复后的候选 535 一直停在待确认,四个入口看到的都是这份旧正文。同时 `material_chunks` 为空:29KB 的解析结果没有越过 60KB 门槛,切块索引根本没有执行。
## 二、 测试目标 (Test Objectives)
- 确认候选 535,使 `doc_proj6579_bd_intake` 正式正文改为真实解析内容。
- 每份成功解析的材料都写入 `material_chunks`,正文不超过 60KB 时仍全文注入。
- 用项目 6579 里已经上传的冲击波 PDF 验证切块、嵌入、重排和全文注入同时存在。
## 三、 测试方案与环境 (Test Plan & Environment)
- OS:Linux;dev HTTP `127.0.0.1:8080`;context Postgres `5433`;schema 61。
- 后端:`pi-agent` + `MiniMax-M2.7-highspeed`。MinerU 精度 token 仍过期,走 Agent 轻量 API 20 页分块。嵌入 `bge-m3`,重排 `bge-reranker-v2-m3-free`。
- 代码:`feature/force-material-chunk-index-2026-09-21` 合入 `dev@a16a6619` 后,只在主目录执行 `scripts/env-service.sh dev rebuild`。容器挂载 `/home/weichen/projects/Agent-System/data/dev -> /app/data`。
- 确认动作:编制人用户 398 的一次性会话调用决策接口,用完立即 `revoked_at`。系统管理员直接确认会得到 `DOC_DUTY_FORBIDDEN`。
## 四、 预期结果 (Expected Results)
- 正式 `content_sha256` 变为 `d50b2e292bf06472754390a88043933e8dec3b7b81a6ed9abfab0863e62d6536`,正文不含「二进制压缩流」。
- blob `57eb6d860bc8526da6f9e6adb4866e7699edcafec219d93f23758f7cc2f447bb` 在 `material_chunks` 中有行,且 `embedding` 非空。
- `Pipeline.Search` 对「能量参数是多少」返回重排后的片段,其中包含峰值 `6-30MPa`。
- 工作区 `materials_text` 仍是全文 `.md`,不是目录加检索片段。
## 五、 实际测试结果与判定 (Actual Results & Evaluation)
### 用例 1:确认候选 535
- **测试输入 (Input)**:`POST /api/v1/documents/doc_proj6579_bd_intake/agent-candidates/535/decision`,`decision=commit`,`confirmed=true`,`expected_input_sha=4b3741e7642502f8960da08646e85ffad4337064fb8932b9420151e5a2cdb233`。
- **实际输出 (Actual Output)**:HTTP 200,`head_snapshot=656`,`head_sha=d50b2e292bf06472`。库内候选 535 为 `committed`。正文标志:`still_binary_phrase=f`,`has_real_content=t`(含「全自动体外冲击波」)。更早的待确认候选 527、534 在决策接口返回 `409 docrevision: candidate stale` 后,状态写成 `stale`,避免待确认徽标继续盖住新正文。
- **评价判定 (Evaluation / Verdict)**:PASS。
- **性能与耗时 (Duration & Latency)**:决策接口约 0.3s。
### 用例 2:项目 6579 真实任务切块
- **测试输入 (Input)**:编制人一次性会话 `POST /api/v1/tasks/runs`,`project_id=6579`,`task_key=bd_intake`,`run_id=run_1789998517646094684`。
- **实际输出 (Actual Output)**:
- 创建 HTTP 202,`status=running`;结束状态 `pending_author_confirmation`。
- `material_chunks`:`rows=90`,`with_embedding=90`。
- 工作区 `bd_intake_1789998547618974092/materials_text/冲击波治疗仪系统方案_0912更新.pdf.md` 为 29152 字节,含「冲击波」和 `MPa`,不含「二进制压缩流」。
- 根 `output.md` 22860 字节,md5 `32e7c02cd42cb7a90037d94968d1b996`,开头为「用户初始需求收集表 / 冲击波治疗仪系统方案评估」,不含失败文案。
- 新候选 537 为 `needs_confirmation`,7474 字节,不含「二进制压缩流」或「完全空白状态」,且含能量参数。正式 head 仍是候选 535。
- **评价判定 (Evaluation / Verdict)**:PASS。
- **性能与耗时 (Duration & Latency)**:任务 13:48:37Z 开始,13:54:33Z 进入 `pending_author_confirmation`,约 6 分钟。
### 用例 3:检索与重排
- **测试输入 (Input)**:`Pipeline.Search(project=6579, blob=57eb6d86…, query=能量参数是多少, limit=3)`。
- **实际输出 (Actual Output)**:
- hit0「待突破点」score=0.0210
- hit1「核心竞品综合对比:技术、性能与价格」score=0.0166
- hit2「核心技术与优势」score=0.0129,正文含「峰值达6-30MPa,径向聚焦±10mm,能量输出稳定安全」
- **评价判定 (Evaluation / Verdict)**:PASS。能量参数片段在重排后的前三名里。分数整体偏低,第一名不是参数句本身。
- **性能与耗时 (Duration & Latency)**:1.2s–2.1s(含嵌入与重排请求)。
## 六、 自动化测试套件汇总 (Test Suite Execution)
在集成候选 `a16a6619` 上执行:
```text
ok chat-agent-system/internal/mineru 3.007s
ok chat-agent-system/internal/materialindex 0.003s
ok chat-agent-system/internal/bizdev 0.111s
```
`TestMaterialToItems_SmallDocStillFulltextAfterIndex` 确认小文档 `inject_mode=fulltext` 且 `chunks_indexed=true`。`TestMaterialToItems_LargeDocIndexesBeforeRAGInject` 确认大文档先切块,注入方式不是全文。
## 七、 测试结论与后续优化建议 (Conclusion & Next Steps)
达到本次合入标准。正式正文已换成候选 535 的真实内容;同一份 PDF 同时有全文注入和 90 条带向量的切块。test/main 未晋升,`production_ready` 未改。
后续:新一轮 `bd_intake` 产生了待确认候选 537,需要编制人确认后才会替换现在的正式正文。重排分数偏低,能量参数句排在第三名,值得单独看检索 query 和 rerank 分数尺度。候选过期时外层事务会把 `stale` 回滚,这次是在决策接口返回 409 之后把 527/534 写成 `stale`。
+11 -2
View File
@@ -17,7 +17,8 @@ import (
// ResolveMaterials 将请求材料 + 项目 CAS 资产解析为可注入工作区的 squad 材料。
// 项目内全部有效资产自动纳入(含私密文档);显式附件优先并去重。
// 已配置 MinerU 时优先使用解析缓存;大文档改为目录 + RAG 片段注入。
// 已配置 MinerU 时优先使用解析缓存。解析成功的材料一律切块写入 material_chunks;
// 正文不超过全文阈值时仍全文注入,超过阈值时改为目录 + RAG 片段注入。
func (s *Service) ResolveMaterials(ctx context.Context, req TaskRunRequest) ([]squad.MaterialItem, map[string]any, error) {
manifest := map[string]any{
"explicit_materials": len(req.Materials),
@@ -173,6 +174,15 @@ func (s *Service) materialToItems(
base = "material.bin"
}
// 切块入库与注入方式分开:任何成功解析的正文都写入 material_chunks,失败只记日志。
if s.materialIndex != nil {
if err := s.materialIndex.IndexChunks(ctx, projectID, blobSHA, base, text); err != nil {
slog.Warn("material chunk index failed", "file", base, "project_id", projectID, "err", err)
} else {
meta["chunks_indexed"] = true
}
}
// 小文档:全文注入,文件名改为 .md 避免 workspace 二次按扩展名刮串冲掉页锚点
if len(text) <= fullMax {
outName := textMaterialName(base)
@@ -191,7 +201,6 @@ func (s *Service) materialToItems(
toc := materialindex.BuildTOC(base, text)
ragBody := ""
if s.materialIndex != nil {
_ = s.materialIndex.IndexChunks(ctx, projectID, blobSHA, base, text)
hits, searchErr := s.materialIndex.Search(ctx, projectID, blobSHA, ragQuery, 0)
if searchErr == nil {
ragBody = materialindex.FormatRAGMarkdown(base, hits)
+45
View File
@@ -0,0 +1,45 @@
package bizdev
import (
"context"
"strings"
"testing"
"chat-agent-system/internal/config"
"chat-agent-system/internal/materialindex"
)
func TestMaterialToItems_SmallDocStillFulltextAfterIndex(t *testing.T) {
svc := &Service{materialIndex: materialindex.NewPipeline(&config.Config{}, nil)}
items, meta, err := svc.materialToItems(context.Background(), "note.txt", "abc123", []byte("能量参数是 2.5 bar"), 0, 0, 7, "能量")
if err != nil {
t.Fatal(err)
}
if meta["inject_mode"] != "fulltext" {
t.Fatalf("small doc inject_mode=%v", meta["inject_mode"])
}
if meta["chunks_indexed"] != true {
t.Fatalf("small doc must attempt chunk index, meta=%v", meta)
}
if len(items) != 1 || !strings.Contains(string(items[0].Content), "能量参数是 2.5 bar") {
t.Fatalf("full text missing: %+v", items)
}
}
func TestMaterialToItems_LargeDocIndexesBeforeRAGInject(t *testing.T) {
svc := &Service{materialIndex: materialindex.NewPipeline(&config.Config{MaterialFulltextMaxBytes: 8}, nil)}
body := []byte("能量参数是 2.5 bar,并且还有更多正文用来越过全文阈值。")
items, meta, err := svc.materialToItems(context.Background(), "note.txt", "def456", body, 0, 0, 7, "能量")
if err != nil {
t.Fatal(err)
}
if meta["chunks_indexed"] != true {
t.Fatalf("large doc must attempt chunk index, meta=%v", meta)
}
if meta["inject_mode"] != "rag" && meta["inject_mode"] != "rag_degraded" {
t.Fatalf("large doc must not use fulltext, inject_mode=%v", meta["inject_mode"])
}
if len(items) != 2 {
t.Fatalf("want toc+rag items, got %d", len(items))
}
}
+2 -4
View File
@@ -284,10 +284,8 @@ func (p *Pipeline) EnqueueAsync(fileName string, blobSHA string, data []byte, pr
if err != nil || !ok || strings.TrimSpace(md) == "" {
return
}
if utf8.RuneCountInString(md) > 0 && len(md) > p.FulltextMaxBytes() {
if idxErr := p.IndexChunks(ctx, projectID, blobSHA, fileName, md); idxErr != nil {
slog.Warn("materialindex index chunks failed", "sha", blobSHA[:min(12, len(blobSHA))], "error", idxErr)
}
if idxErr := p.IndexChunks(ctx, projectID, blobSHA, fileName, md); idxErr != nil {
slog.Warn("materialindex index chunks failed", "sha", blobSHA[:min(12, len(blobSHA))], "error", idxErr)
}
}()
return true
@@ -0,0 +1,59 @@
package materialindex
import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"chat-agent-system/internal/config"
)
func TestPipelineSearchShockwave(t *testing.T) {
if os.Getenv("MATERIAL_SEARCH_LIVE") != "1" {
t.Skip("set MATERIAL_SEARCH_LIVE=1 to search the dev material_chunks index")
}
cfg, err := config.Load("")
if err != nil {
t.Fatal(err)
}
if cfg.EmbeddingAPIKey == "" || cfg.RerankerAPIKey == "" {
t.Fatal("embedding and reranker credentials must be in the environment")
}
dsn := fmt.Sprintf("postgres://%s:%s@%s:%d/%s?sslmode=disable",
cfg.ContextDbUser, cfg.ContextDbPassword, cfg.ContextDbHost, cfg.ContextDbPort, cfg.ContextDbName)
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
p := NewPipeline(cfg, pool)
const blob = "57eb6d860bc8526da6f9e6adb4866e7699edcafec219d93f23758f7cc2f447bb"
hits, err := p.Search(ctx, 6579, blob, "能量参数是多少", 3)
if err != nil {
t.Fatal(err)
}
if len(hits) == 0 {
t.Fatal("expected reranked hits for the shockwave PDF")
}
foundParams := false
for i, hit := range hits {
preview := strings.ReplaceAll(hit.Title+" "+hit.Content, "\n", " ")
if len(preview) > 160 {
preview = preview[:160]
}
t.Logf("hit%d title=%q pages=%d-%d score=%.4f preview=%s", i, hit.Title, hit.PageStart, hit.PageEnd, hit.Score, preview)
if strings.Contains(hit.Content, "MPa") || strings.Contains(hit.Content, "6-30") {
foundParams = true
}
}
if !foundParams {
t.Fatal("reranked hits did not include the energy-parameter fragment")
}
}