Files
knowledge-governance/docs/P42_SOURCE_SCAN_ACCEPTANCE.md
T
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

251 lines
6.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P4.2 来源只读扫描与变化入账验收
## 范围
本切片将 PP 本地历史快照接入普通 Worker 的持久扫描链路:
```text
来源管理手工触发
→ 原子创建 source_scan + source.scan.pp Job
→ 普通 Worker 只读 ledger/manifest/制品元数据
→ 与 613 条治理 Source Item 对账
→ 原子保存聚合、变化记录与连接健康
→ API/Web 展示扫描历史和变化结果
```
本切片不上传、移动、重命名或删除 PP 文件,不调用 PP 网络写接口,不删除治理对象或 WeKnora Knowledge。`missing` 只记录为 `unavailable`,54 个 `over_limit` 项继续阻断,生产 Release Worker 保持关闭。
## 安全配置与装配
- `PP_SCAN_ENABLED` 默认 `false`;仅显式启用且 allowed root、ledger、manifest 三个路径完整时注册 `source.scan.pp` Handler。
- allowed root 必须是绝对规范路径;ledger 和 manifest 必须位于其内。
- Scanner 拒绝越界、软链接、非普通文件、缺少 `local_path`、打开期间文件身份变化和超过字节上限的元数据文件。
- `over_limit` 记录允许其根目录内的制品不存在,因为它从未被下载;若制品存在仍必须通过普通文件和非软链接校验。
- Compose 只给普通 Worker 挂载 PP 数据目录,挂载模式为只读;API、Web、Release Worker均没有该挂载。
- 实际容器探针:ledger/manifest 可读,向 manifest 追加写入被只读文件系统拒绝。
- 本地测试 WeKnora Key 仍只挂载给普通 Worker;本切片不调用 WeKnora 上传或删除 API。
## Migration 5
Migration 5 新增:
```text
source_scans
source_scan_changes
idx_source_scans_one_active
idx_source_scans_connection_created
idx_source_scan_changes_pending
```
关键约束:
- 每个来源连接最多一个 `pending/running` Scan;
- Scan 状态限定为 `pending/running/completed/failed`;
- Change 类型限定为 `added/updated/unavailable`;
- `snapshot_sha256` 只在内部审计,不进入 API;
- down migration 先删除依赖的 changes 表,再删除 scans 表。
一次性 PostgreSQL 执行了:
```text
up to 5 → down to 4 → up to 5
result: passed
```
持久库迁移前逻辑备份:
```text
tmp/backups/governance-pre-migration5-20260906T011223+0800.dump
bytes=242987
mode=0600
pg_restore catalog=valid
```
持久库应用后:
```text
schema_migrations=1,2,3,4,5
source_items=618
content_objects=535
source_item_versions=618
test_publications=535
```
旧业务数据计数未因 Migration 5 改变。
## API
```text
POST /api/v1/source-connections/{id}/scan
GET /api/v1/source-connections/{id}/scans?limit=20&offset=0
GET /api/v1/source-connections/{id}/scans/{scan_id}
```
触发语义:
- 成功返回 `202`;
- 同一连接已有活动扫描返回 `409`;
- 不存在或不可扫描的连接返回 `404`;
- 创建 Scan 和 Job 在同一条 PostgreSQL data-modifying CTE 中完成。
安全只读投影允许:
```text
Scan: id/state/聚合计数/固定错误码/创建完成时间
Change: id/source_item_id(可空)/change_type/observed_size_bytes(可空)/created_at
```
明确省略:
```text
snapshot_sha256
stable_external_id
previous_revision
observed_revision
allowed root、ledger/manifest/local_path
来源 URL、正文、base_url、secret_ref、Token/API Key/Cookie
```
跨连接读取 Scan 详情返回 `404`。前端再次使用 allowlist 重建对象,不透传服务端额外字段。
## 真实 PP 只读扫描
真实输入只读基线:
```text
ledger events=1093
manifest/source items=613
existing artifacts=559
historical over_limit without downloaded artifact=54
```
第一次真实扫描按安全规则失败:
```text
state=failed
error_code=pp_snapshot_invalid
```
根因是初版 Scanner 把 54 个历史 `over_limit` 项未下载的制品也要求为已存在普通文件。修复后规则为:`over_limit` 缺失制品是合法阻断状态,但路径仍必须声明且位于 allowed root 内;其他状态缺失仍 fail-closed。
修复后的多次真实扫描均一致:
```text
observed=613
unchanged=559
added=0
updated=0
over_limit=54
missing=0
planned_uploads=0
changes=0
```
PP ledger、manifest 和来源制品未被修改;没有生成上传计划。
## 调度、并发与恢复
### 单活动扫描
Worker 停止时连续触发同一连接:
```text
first=202
second=409
active_scans=1
```
恢复 Worker 后唯一活动扫描完成。
### pending 恢复
```text
Worker stopped
→ POST creates pending Scan/Job
→ Worker started
→ Scan completed, Job completed
```
### 过期 running 租约重领
真实持久队列构造“旧 Worker 已领取但完成前崩溃”的状态:
```text
before: scan=running job=running attempt=1 lease_expired=true
restart persistent Worker
after: scan=completed job=completed attempt=2
result: observed=613 unchanged=559 over_limit=54
```
Handler 对 `running` Scan 可恢复;若 Scan 已经 `completed` 而 Job 尚未完成,则重领为幂等 no-op,不重复扫描或写 Change。
最终状态:
```text
source_scans=6
completed=5
failed_fixed_code=1
active_scans=0
active_source_scan_jobs=0
```
## Web 与 Firefox
“来源管理”保留左侧导航和顶部标签,并新增:
- 来源连接的最近扫描聚合;
- “立即扫描”按钮及活动态禁用;
- 最近 20 条扫描历史;
- 单次扫描变化记录安全详情;
- 零变化时明确显示“本次扫描未发现新增、更新或不可用变化”。
Firefox headless 1440×1200 验收:
```text
history_rows=6
completed=5
failed=1
detail_selection=passed
empty_changes_message=visible
safe_projection_leaks=0
root_overflow=false
table_overflow=false
```
截图:`tmp/P42_SOURCE_SCAN_HISTORY_FIREFOX.png`
视觉复核确认:导航、顶部标签、5 条完成/1 条失败、观察数、上限阻断和变化面板均清晰;无横向裁切、遮挡或乱码。
## 自动化验证
```text
PP配置安全默认和路径校验:passed
Worker Handler装配测试:passed
File Scanner边界与over_limit回归:passed
Store扫描触发/完成/失败/恢复/安全读取:passed
HTTP触发/历史/详情/404/409安全投影:passed
前端allowlist解析、请求和页面渲染:passed
一次性PostgreSQL真实扫描与变化读取:passed
Migration 5 up/down/up:passed
Web tests:31/31 passed
TypeScript typecheck:passed
Vite production build:passed
Compose config:passed
Compose全镜像build:passed
Go全仓库test:passed
Go vet:passed
Go cmd build:passed
gofmt差异:0
Firefox真实页面:passed
```
## 运行边界
- 本地治理平台与本地测试 WeKnora 范围内完成;
- PP 来源只读,未调用来源写/删 API;
- 未删除 PostgreSQL 或 WeKnora 卷;
- 未连接或修改生产环境;
- `RELEASE_WORKER_ENABLED=false`;
- 54 个 `over_limit` 项继续阻断;
- 未 commit、未 push。