Skip to content

Knowledge.document_id FK + 孤儿清算 CLI(Phase 3 · stacked on #484) - #486

Merged
ThreeFish-AI merged 4 commits into
ThreeFish-AI/feat-corpus-delete-cascadefrom
ThreeFish-AI/feat-corpus-fk-cleanup-cli
May 9, 2026
Merged

Knowledge.document_id FK + 孤儿清算 CLI(Phase 3 · stacked on #484)#486
ThreeFish-AI merged 4 commits into
ThreeFish-AI/feat-corpus-delete-cascadefrom
ThreeFish-AI/feat-corpus-fk-cleanup-cli

Conversation

@ThreeFish-AI

Copy link
Copy Markdown
Owner

Stacked PR · 依赖 #484(Phase 2)+ #483(Phase 1):base 为 Phase 2 分支。Phase 1/2 merge 后 GitHub 会自动重新 base,最终 stack 解开后只看本 PR 的 diff(不含前序改动)。

背景

ISSUE-078 Phase 3 — DB Schema 防御层 + 历史脏数据一次性清算。三层防御正交可独立回滚:

Phase 层次 收益
1 (#483) SQL 计数口径 UI 立即显示正确数字
2 (#484) 应用层级联 未来删除路径同步 chunks
3(本 PR) DB Schema FK + CLI DB 层兜底 + 历史数据可清算

改动

1. alembic 迁移 0030

apps/negentropy/src/negentropy/db/migrations/versions/0030_knowledge_document_fk.py

  • ALTER TABLE knowledge ADD COLUMN document_id UUID NULL
  • FOREIGN KEY (document_id) REFERENCES knowledge_documents(id) ON DELETE CASCADE
  • 部分索引 CREATE INDEX ix_knowledge_document_id ON knowledge (document_id) WHERE document_id IS NOT NULL
  • 仅加列与约束、不做数据变更:避免大表 UPDATE 长锁;downgrade 仅 DROP COLUMN
  • 已在 dev DB 验证 upgrade → downgrade → upgrade 完整 round-trip

2. ORM 同步

apps/negentropy/src/negentropy/models/perception.pyKnowledge.document_id: Mapped[UUID | None] + FK ondelete=CASCADEnullable=True(KG 类直连知识无 doc 来源必须允许 NULL)。

3. KnowledgeChunk dataclass + 写入路径单点 stamp

  • apps/negentropy/src/negentropy/knowledge/types.py KnowledgeChunkdocument_id: UUID | None = None
  • service.py _ingest_text_with_trackerdocument_id 参数,在 _build_chunks 之后通过 dataclasses.replace 单点 stamp 到每个 chunk——避免大面积改造下游函数
  • 仅 2 个 ingest 入口(execute_ingest_url_document_pipeline / execute_ingest_file_pipeline)传入 document_id,其余 10 个入口(纯文本、URL、replace、rebuild、KG)保留 None
  • repository.py add_knowledgepg_insert.values() 加入 document_id 字段

4. 独立 CLI cleanup_orphan_knowledge

apps/negentropy/src/negentropy/scripts/cleanup_orphan_knowledge.py

```bash
uv run python -m negentropy.scripts.cleanup_orphan_knowledge \
--dry-run | --commit [--corpus-id ...] [--app-name ...] [--json]
```

三步流水线:

  1. 回填:基于 (corpus_id, app_name, source_uri ↔ d.gcs_uri OR d.metadata->>'origin_url') 关联(包含软删 doc)
  2. 报告:按 corpus 分组列出 total / linked / unlinked / would_delete 四档计数
  3. 清理:仅 --commit 模式真正 DELETE,且仅当 document_id IS NULL AND source_uri IS NOT NULL AND source_uri ~ '^(gs://|https?://)'(白名单形态防误删);KG 类(NULL source_uri)永不被清理

为什么独立 CLI 而非 alembic data migration:DELETE 不可逆、迁移强制执行不利于审计;CLI 提供 dry-run + JSON 报告 + scope 过滤更稳。

5. 观测函数 count_orphan_knowledge

CLI 模块同时导出 count_orphan_knowledge(corpus_id?, app_name?) 函数,与 CLI 共享同一过滤口径,cron 接入:每日扫描 → total_orphans 上报为 metric negentropy.knowledge.orphan_count{corpus_id}> 0 即告警。即便三层防御兜住,仍是入侵 / 异常 SQL 的 canary。

dev DB 实地验证(关键)

在 user 真实数据上跑:

```
$ uv run python -m negentropy.scripts.cleanup_orphan_knowledge --dry-run
{
"mode": "dry-run",
"backfilled": 849,
"would_delete_total": 0,
"deleted": 0,
"per_corpus": [{
"corpus_id": "43bacd7e-...",
"corpus_name": "Harness Engineering",
"total": 849,
"linked": 849,
"unlinked": 0,
"would_delete": 0
}]
}
```

结论:user 当时走的是软删(doc 还在 DB 里 status='deleted'),所以没有 FK 意义孤儿--commit 后 849 条 Knowledge.document_id 全部成功回填,与对应 KnowledgeDocument 建立 FK 关联。Phase 1 → Phase 3 三层防御已在用户实际数据上端到端闭环

影响面 & 兼容性

  • API 接口不变KnowledgeChunk 加可选字段,KnowledgeRepository.add_knowledge 签名不变
  • 既有 ingest 路径无回归:纯文本/URL/replace/rebuild 不传 document_id → chunk 写入 document_id=NULL → DB 接受
  • 既有数据无需迁移:迁移仅加列;CLI 独立运行回填,由 DBA 在 staging/prod 分别审计
  • 生产部署可直接执行:PG 14+ 的 ADD COLUMN ... NULL 是元数据级 O(1) 操作,FK 约束不会回扫(新列默认 NULL);CREATE INDEX 用 partial index 范围极小

集成测试

test_phase3_document_fk.py 8 场景覆盖:

场景 验证
orm_field_roundtrip ORM Knowledge.document_id 读写正确,nullable 接受 None
fk_cascade_db_level 直接 DELETE knowledge_documents 行触发 Knowledge 级联清理(bypass 应用层)
cli_dry_run_no_persist dry-run 后 DB 状态未变(事务回滚)
cli_commit_persists commit 后回填 + 删孤儿 都生效
kg_null_source_preserved KG 类(source_uri NULL)永不被清理
unknown_uri_form_preserved 非白名单 source_uri 形态保留(防内部脏数据误删)
count_orphan_observation 观测函数返回正确 per-corpus 计数
migration_round_trip_safe column / FK / index 实际存在于 DB

既有 67 项单测无回归。

Phase 化交付(本 PR 是 Phase 3 · 收官)

Test plan

  • pytest tests/integration_tests/knowledge/test_phase3_document_fk.py 8 场景全 pass
  • pytest tests/unit_tests/knowledge/test_api_corpus.py tests/unit_tests/knowledge/test_api_documents.py tests/unit_tests/knowledge/test_chunking.py 67 项无回归
  • alembic upgrade 0030 → downgrade 0029 → upgrade 0030 round-trip 在 dev DB 通过
  • CLI dry-run + commit 在 dev DB 用户真实数据(Harness Engineering 849 chunks)端到端验证
  • docs/issue.md ISSUE-078 追加 Phase 3 补充章节
  • Reviewer staging 部署步骤
    • alembic upgrade 0030
    • python -m negentropy.scripts.cleanup_orphan_knowledge --dry-run --json 审计报告
    • DBA backup
    • python -m negentropy.scripts.cleanup_orphan_knowledge --commit --json 实际清算
    • cron 接入 count_orphan_knowledge 观测函数

🤖 Generated with Claude Code

ISSUE-078 Phase 3 — DB Schema 防御层 + 历史脏数据一次性清算。Phase 1 修计数口径、
Phase 2 修应用层级联、Phase 3 在 DB 层加 FK ON DELETE CASCADE 兜底,三层防御
正交可独立回滚。

变更:
- alembic 0030: ADD COLUMN knowledge.document_id UUID NULL +
  FK fk_knowledge_document_id REFERENCES knowledge_documents(id) ON DELETE
  CASCADE + 部分索引 ix_knowledge_document_id WHERE document_id IS NOT NULL;
  仅加列与约束、不做数据变更(避免大表 UPDATE 长锁);downgrade 仅 DROP COLUMN
- ORM 同步: models/perception.py Knowledge.document_id Mapped[UUID|None]
- KnowledgeChunk dataclass: 加 document_id 字段;types.py
- 写入路径 stamp: _ingest_text_with_tracker 加 document_id 参数,单点
  dataclasses.replace stamp 到每个 chunk;repository.add_knowledge values 加
  document_id 字段。仅 2 个 ingest 入口(execute_ingest_url_document_pipeline /
  execute_ingest_file_pipeline)传入 document_id,其余 10 个入口(纯文本、URL、
  replace、rebuild、KG)保留 None
- 独立 CLI scripts/cleanup_orphan_knowledge.py:
    uv run python -m negentropy.scripts.cleanup_orphan_knowledge \
      --dry-run | --commit [--corpus-id ...] [--app-name ...] [--json]
  三步流水线: 回填 document_id → 按 corpus 分组报告四档计数 → --commit 才
  真正 DELETE(白名单 source_uri 形态防误删);不放进 alembic 迁移因为
  DELETE 不可逆需 dry-run + 审批
- 观测函数 count_orphan_knowledge: 与 CLI 共享过滤口径,cron 接入将
  total_orphans 上报为 metric negentropy.knowledge.orphan_count{corpus_id}

dev DB 实地验证:
- 在 user 真实数据上跑 --dry-run 报告 Harness Engineering corpus
  total=849, would_delete=0;--commit 后 849 条 Knowledge.document_id 全部
  回填,与对应 KnowledgeDocument 建立 FK 关联
- Phase 1 → Phase 3 三层防御端到端在用户实际数据上闭环

测试: test_phase3_document_fk.py 8 场景覆盖 ORM 字段读写、DB 层 FK CASCADE
(直接 DELETE knowledge_documents 行触发 Knowledge 级联清理)、CLI dry-run/
commit、KG NULL source_uri 保留、非白名单 URI 形态保留、观测函数、migration
round-trip;既有 67 项单测无回归;alembic upgrade/downgrade 已在 dev DB 验证

详见 docs/issue.md ISSUE-078 Phase 3 补充章节。

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
…iew #1);

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
- conftest: 补丁 storage.service / cleanup_orphan_knowledge 的模块级
  AsyncSessionLocal 引用(from-import 绑定问题),修复 asyncpg 事件循环边界
  RuntimeError
- _sync_document_chunk_stats: 包裹 try/except 降级为 warning,避免纯单元
  测试(FakeRepository 无真实 DB)触发 DB 连接崩溃
- _backfill_sql: 改回 UPDATE ... FROM + DISTINCT ON 子查询,仅更新有文档
  匹配的行(孤儿行自然排除),修复 backfilled 计数断言失败

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
- _backfill_sql: 子查询内 scope 条件引用外部别名 k → 改为 k_sub,
  修复 PG UndefinedTableError
- conftest: 补丁 knowledge.api 的模块级 AsyncSessionLocal 引用,
  修复 _scenario_soft_delete_archives 调用 list_corpora 时的事件循环边界错误

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
@ThreeFish-AI
ThreeFish-AI merged commit 769344a into ThreeFish-AI/feat-corpus-delete-cascade May 9, 2026
7 checks passed
@ThreeFish-AI
ThreeFish-AI deleted the ThreeFish-AI/feat-corpus-fk-cleanup-cli branch May 10, 2026 04:18
ThreeFish-AI added a commit that referenced this pull request Jul 5, 2026
…-2026-4372 处置) (#1057)

* fix(deps): ts-deepmerge 补丁地板至 8.0.0 修复 GHSA-87mf-gv2c-c62c (#481);

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>

* fix(negentropy): pydantic-settings 升 2.14.2 + joserfc>=1.6.7 补丁地板修复 #482/#488;

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>

* fix(cognizes): joserfc/msgpack/python-socketio/python-engineio 补丁地板修复 #483/#486/#487/#490/#491;

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>

* docs(perceives): ISSUE-092 增补 CVE-2026-4372 威胁模型;transformers 钉 4.x + dismiss #492(marker 5.x 不兼容实测);

🤖 Generated with [Claude Code](https://github.com/claude), [CodeX](https://openai.com), [Gemini](https://github.com/apps/gemini-code-assist)
Co-Authored-By: Aurelius Huang<threefish.ai@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant