diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..c851e3ab --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,36 @@ +## Summary + + + +--- + +## 自检验收清单(合并前须满足) + +> **Ink 轨**:高敏书面复检见 `docs/harness/reviews/` 或 `docs/tasks/reinspect_results/`;**本清单不替代** `human_gate_check`。 + +- [ ] 本地:`pytest tests -m "not intent_eval and not intent_benchmark"` 通过 +- [ ] PR 上 **pytest** + **tech-graph** + **tech-graph-contract**(若触达)Required 全绿 +- [ ] 若改 `api/` / SSE / 契约:已更新 task **§行为变更 Delta** + `_manifest` / 契约 fixture(**同 PR**) +- [ ] 若改结构图:已更新 `.ai.md` 且 `graph_export --check` 通过 +- [ ] 高敏变更(`api/`、`test_strategy: required`):**独立复检** 已落盘或本 PR 链 `reinspect_results/` + +### Blocking 提示(须人判断 · 非 PR 关键词过闸) + +| 类型 | 例 | 须 | +| --- | --- | --- | +| **对外契约** | 改响应字段、SSE 事件名、路由 | task Delta + 50 + 契约 CI | +| **运行锚点** | 改 env 名、端点、部署前提 | manifest + 任务单声明 | +| **主依赖** | 主框架大版本、copyleft 新依赖 | 人读 changelog/LICENSE + 扩大回归 | + +--- + +## Test plan + +- [ ] (作者填写:关键手动场景或 pytest 路径) + +--- + +## 关联 + +- Task:`docs/tasks/active/` 或 `done/` 路径 +- 审查:`docs/harness/reviews/`(如有) diff --git a/.github/workflows/tech-graph.yml b/.github/workflows/tech-graph.yml index b8831f85..fa1e9221 100644 --- a/.github/workflows/tech-graph.yml +++ b/.github/workflows/tech-graph.yml @@ -41,3 +41,58 @@ jobs: - name: Tech graph token estimate (Gate A appendix) run: python tools/tech_graph_token_estimate.py --json + + task_validate: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.11" + + - name: Detect task file changes + id: tasks + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="${{ github.base_ref }}" + git fetch origin "$BASE" --depth=1 2>/dev/null || true + CHANGED="$(git diff --name-only "origin/${BASE}...HEAD" -- 'docs/tasks/' || true)" + else + CHANGED="$(git diff --name-only HEAD~1 HEAD -- 'docs/tasks/' || true)" + fi + if [ -n "$CHANGED" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Task paths changed:" + echo "$CHANGED" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No docs/tasks/ changes; skipping task_validate." + fi + + - name: Harness task validate (changed active tasks) + if: steps.tasks.outputs.changed == 'true' + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "pull_request" ]; then + BASE="${{ github.base_ref }}" + FILES="$(git diff --name-only "origin/${BASE}...HEAD" -- 'docs/tasks/active/*.md' || true)" + else + FILES="$(git diff --name-only HEAD~1 HEAD -- 'docs/tasks/active/*.md' || true)" + fi + if [ -z "$FILES" ]; then + echo "No active task .md changes." + exit 0 + fi + for f in $FILES; do + if [[ "$f" == *_AGENT_PROMPT* ]]; then + continue + fi + echo "Validating $f" + python tools/harness_task_validate.py "$f" + done diff --git a/docs/harness/README.md b/docs/harness/README.md index 57b46f62..7ed33b66 100644 --- a/docs/harness/README.md +++ b/docs/harness/README.md @@ -17,6 +17,9 @@ | commit / 关账 | `HANDOFF_AUTO_COMMIT`、`HANDOFF_CLOSE_TRACE` | | task 字段 | `HARNESS_V2_PLAN.md` §5 | | 流程 | `SDD_HAT_FLOW.md` | +| **FAQ 改进 · 09 PLAN** | [`prompts/PROMPT_FAQ改进_09PLAN_理解_v1_zh.md`](prompts/PROMPT_FAQ改进_09PLAN_理解_v1_zh.md) | +| **冷/温/热 术语** | [`guides/GUIDE_冷温热层_对内术语_v1_zh.md`](guides/GUIDE_冷温热层_对内术语_v1_zh.md) | +| **manifest/contract CI 红** | [`guides/RUNBOOK_graph_contract_ci_red_v1.md`](guides/RUNBOOK_graph_contract_ci_red_v1.md) | | 新 invoke | `invokes/` | | **Harness 裁决共识(已接受)** | [`../diary/2026-05-22-harness-evaluation-improvement-response.md`](../diary/2026-05-22-harness-evaluation-improvement-response.md) **§九** | diff --git "a/docs/harness/guides/GUIDE_\345\206\267\346\270\251\347\203\255\345\261\202_\345\257\271\345\206\205\346\234\257\350\257\255_v1_zh.md" "b/docs/harness/guides/GUIDE_\345\206\267\346\270\251\347\203\255\345\261\202_\345\257\271\345\206\205\346\234\257\350\257\255_v1_zh.md" new file mode 100644 index 00000000..62f4a8b1 --- /dev/null +++ "b/docs/harness/guides/GUIDE_\345\206\267\346\270\251\347\203\255\345\261\202_\345\257\271\345\206\205\346\234\257\350\257\255_v1_zh.md" @@ -0,0 +1,45 @@ +# 冷 / 温 / 热 · 对内术语(FAQ 纠偏) + +| 项 | 内容 | +| --- | --- | +| **用途** | 22/10 帽、task 编写、Agent 对话 — **纠正公众稿读者常见误读** | +| **公众真值** | 卷三 §11.2.1(已发表);本文件 **不** 另起定义 | +| **来源** | FAQ F1 + [`SUMMARY`](../../../../ai_coding_governance/narrative/reviews/SUMMARY_三卷读者FAQ_完整结论_20260530_v1_zh.md) §2 | + +--- + +## 1. 对照表(复制进 review 时可摘一行) + +| 层 | ✅ 正文真义 | ❌ 常见误读(勿用) | +| --- | --- | --- | +| **冷层** | **结构地图** = 卷二技术图谱;task **图谱入口** | 「框架、数据库、基础设施层」 | +| **温层** | **协作轨迹** = 任务单 + 书面签收 + 关账摘要 | 「API 签名、模块边界、契约层」 | +| **热层** | **运行时事件记忆**(远期;日常不必做) | 「函数内部、UI 样式、热代码」 | + +**一句**:冷 = **改哪里(地图)**;温 = **谁审、怎么关账(轨迹)**;热 = **线上事件网(远期)** — **不是** 按变更频率给模块贴标签。 + +--- + +## 2. 与后端落地的关系 + +| 读者误读导致的错误做法 | Ink 实际 | +| --- | --- | +| 在 graph 节点上标 cold/warm/hot | **不做**;图谱管流程与模块边界 | +| 温层 = 函数签名 CI | **契约** 靠 `_manifest` + `contract_check` + pytest | +| 冷层 = README/env alone | env/端点 是 **manifest 锚点**,属于 **Verify**;冷层叙事仍指 **结构地图** | + +--- + +## 3. Agent 自检(开工前答三问) + +1. 本轮 **图谱入口** 指向哪张 **子图/主图**?(冷层落点) +2. 本轮 **任务单 + 审查落盘** 是否齐全?(温层落点) +3. 是否误把「改 API 签名」说成「改温层」?→ 应说 **对外契约变更** + Delta + 50。 + +--- + +## 修订记录 + +| 版本 | 日期 | 说明 | +| --- | --- | --- | +| v1.0 | 2026-05-30 | IMP-B-20 · FAQ 纠偏 | diff --git a/docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md b/docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md new file mode 100644 index 00000000..5a60b2ad --- /dev/null +++ b/docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md @@ -0,0 +1,67 @@ +# Runbook · 图谱 manifest / contract CI 红字 + +> **用途**:开发者或 22 帽审查时,对照 CI stderr 三段式(位置 · 文档声明 · 当前代码 · 下一步)快速修复。 +> **关联**:IMP-B-01 · FAQ F20 卷四主失败分支 · `tech-graph.yml` / `tech-graph-contract.yml` + +--- + +## 1. 何时打开本文 + +| CI job | 命令 | 典型触发 | +|--------|------|----------| +| `manifest_check` | `python tools/tech_graph_manifest_check.py` | 改 `api/`、`supabase/sql/` 未同步 `_manifest.json` | +| `contract_check` | `python tools/tech_graph_contract_check.py` | 改 SSE 事件/字段未同步 `_contract_manifest.json` | +| `graph export --check` | `python tools/tech_graph_graph_export.py --check` | 改 `.ai.md` 未 export `graph.json` | + +stderr 首行以 `❌` 开头,每条 drift 含 **位置 / 文档声明 / 当前代码**。 + +--- + +## 2. 修复路径(路径 A · 推荐) + +1. 在 **active task** 的 `§行为变更(Delta)` 写明 ADDED/MODIFIED(触达 `api/` 时 `test_strategy: required` + 50 落盘)。 +2. **同 PR** 更新: + - `_manifest.json`(端点 / RPC / 表 / env / anchors) + - `_contract_manifest.json`(SSE 契约,若涉 Unified Chat) + - 受影响 `docs/_tech_graph/*.ai.md` → `python tools/tech_graph_graph_export.py` +3. 本地跑与 CI 相同命令(见 stderr 末尾)。 +4. `pytest tests -m "not intent_eval and not intent_benchmark"` 绿后再 push。 + +**禁止**:merge 后再开单独 PR 只改图谱(FAQ 已拒)。 + +--- + +## 3. 修复路径(路径 B · 误改) + +```bash +git checkout -- api/ # 或 git revert 指定 commit +python tools/tech_graph_manifest_check.py +python tools/tech_graph_contract_check.py +``` + +确认 stderr 无 `❌` 后再 push。 + +--- + +## 4. 常见 drift 对照 + +| stderr 位置前缀 | 文档侧 | 代码侧 | 下一步 | +|-----------------|--------|--------|--------| +| `manifest.endpoints` | `_manifest.json` endpoints | `api/index.py` 路由装饰器 | 补/删 manifest 条目或回滚路由 | +| `manifest.supabase.tables` | manifest tables | `.table("…")` / SQL CREATE | 同步表名清单 | +| `contract.sse.*` | `_contract_manifest.json` | `api/unified_chat.py` 等 | 同步 allowed_events / payload keys | +| `manifest.anchors` | anchors path+symbol | 源文件 def/class | 修正锚点或符号名 | + +--- + +## 5. 22 帽审查粘贴 + +CI 日志中 `--- 问题 N/M ---` 至 `Runbook:` 行可直接贴入 `reviews/` R1,无需重写。 + +--- + +## 修订记录 + +| 版本 | 日期 | 说明 | +| --- | --- | --- | +| v1.0 | 2026-05-31 | IMP-B-01 初版;链 tech_graph_ci_stderr | diff --git "a/docs/harness/prompts/PROMPT_FAQ\346\224\271\350\277\233_09PLAN_\347\220\206\350\247\243_v1_zh.md" "b/docs/harness/prompts/PROMPT_FAQ\346\224\271\350\277\233_09PLAN_\347\220\206\350\247\243_v1_zh.md" new file mode 100644 index 00000000..7cf003f9 --- /dev/null +++ "b/docs/harness/prompts/PROMPT_FAQ\346\224\271\350\277\233_09PLAN_\347\220\206\350\247\243_v1_zh.md" @@ -0,0 +1,97 @@ +# Prompt · 理解 FAQ 改进方案(09 PLAN)并执行 Batch A + +> **用途**:后端 Agent **开工前** 读本文 + 链内文档,理解「三卷发表后读者 FAQ → Ink 后端改进项」全貌,再动 `api/` / CI / Harness。 +> **真值规划**:治理仓 [`ai_coding_governance/09_PLAN_Ink后端改进方案_可推广_v1_zh.md`](../../../../ai_coding_governance/09_PLAN_Ink后端改进方案_可推广_v1_zh.md) +> **合成结论**:[`SUMMARY_三卷读者FAQ_完整结论_20260530_v1_zh.md`](../../../../ai_coding_governance/narrative/reviews/SUMMARY_三卷读者FAQ_完整结论_20260530_v1_zh.md) + +--- + +## 你的角色 + +你是 **Ink 后端(ai-ink-brain-api-python)** 执行 Agent。当前任务类型:**FAQ 驱动的工程补齐**(非重写方法论)。 +**Open Folder**:本仓根。跨仓只读治理仓 `ai_coding_governance/narrative/reviews/` 与 `09_PLAN`,**禁止**改工作区 `Projects/docs/harness/`。 + +--- + +## 必须先建立的结论(30 秒) + +1. **方法论不 redesign**:Harness 三支柱、`.ai.md`→export 双轨图谱、manifest/contract CI、`human_gate` + 50 — **保持不变**。 +2. **读者误读要纠**:冷/温/热 ≠ 架构/契约/实现 → 见本仓 [`../guides/GUIDE_冷温热层_对内术语_v1_zh.md`](../guides/GUIDE_冷温热层_对内术语_v1_zh.md)。 +3. **本批要做**:降低合并摩擦 + CI 红字可读 + 模板可复制 — **IMP-B Batch A**(见下)。 +4. **明确不做**:`graph.auto.json` 全仓扫描、PR `/approve` 唯一合闸、维护成本归零、merge 模型 KPI。 + +--- + +## 阅读顺序(按序打开) + +| 序 | 文档 | 目的 | +|----|------|------| +| 1 | 本仓 [`AGENTS.md`](../../../AGENTS.md) | 地图与禁止项 | +| 2 | [`docs/meta/PROJECT_CONFIG_AI_INK_BRAIN_API_PYTHON.md`](../../meta/PROJECT_CONFIG_AI_INK_BRAIN_API_PYTHON.md) | 契约/目录/安全 | +| 3 | 治理仓 **SUMMARY** §2、§5、§10 | FAQ 合成结论与五条原则 | +| 4 | 治理仓 **09_PLAN** §1~§3 | 缺口、IMP-B 全表、批次 A/B/C | +| 5 | 本仓 **active task** | [`docs/tasks/active/task_backend_improve_batch_a_p0_v1.md`](../../tasks/active/task_backend_improve_batch_a_p0_v1.md)(**draft** · Batch A) | +| 6 | 动 CI/契约时:[`GUIDE_续卷编写_Ink后端真值对照_v1_zh.md`](../../../../ai_coding_governance/narrative/GUIDE_续卷编写_Ink后端真值对照_v1_zh.md) §3 | workflow/命令真值 | + +**不必读**:`docs/diary/` 全文、`invokes/` 扁平扫描、Public 公众稿粘贴版。 + +--- + +## FAQ → 后端改进项(IMP-B 全表 · 状态以 task 为准) + +### Batch A · P0(优先) + +| ID | 做什么 | 交付物 | +|----|--------|--------| +| IMP-B-01 | manifest/contract CI **三段式 stderr** + Runbook | `tools/tech_graph_*_check.py` · `docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md` | +| IMP-B-02 | 改 task 时 CI 跑 `task_validate` | `verify-fast.yml` 或 `tech-graph.yml` | +| IMP-B-10 | Ink 轨 **PR 模板** | `.github/pull_request_template.md` | +| IMP-B-11 | **22 帽 Blocking 表** | `prompts/hats/22-task-audit.md` §Blocking | +| IMP-B-20 | **冷/温/热术语卡** | `docs/harness/guides/GUIDE_冷温热层_对内术语_v1_zh.md` | + +### Batch B · P1 + +IMP-B-03 L2 manifest SPEC 关账 · IMP-B-04 领域 Linter · IMP-B-12 Test plan 分层 · IMP-B-13 存量 task 抽样 · IMP-B-21 合并入口图 · IMP-B-30/31 failure-cases + 复盘模板 + +### Batch C · P2 + +IMP-B-05 增量 manifest · IMP-B-14 Delta→spec · IMP-B-22 卷四对内样例 PR + +--- + +## 执行纪律(FAQ 约束) + +| 约束 | 要求 | +|------|------| +| 成本 | 新流程 **不** 使稳态额外耗时 >15%;不加专职岗 | +| 高敏 | 动 `api/` → `test_strategy: required` + **50 落盘**;小团队可角色兼任,**不可删步骤** | +| 合并 | **同 PR** 提交代码 + manifest/`.ai.md`(若触达);禁止 merge 后 bot 单独改图 | +| 签收 | 闸在 **`reviews/` / `reinspect_results/` + human_gate**;PR 模板 **辅助**,不替代 | +| 测试 | 合并前:`pytest tests -m "not intent_eval and not intent_benchmark"` | + +--- + +## 开工检查清单 + +- [ ] 已读 active task 的 **非范围** 与 **验收** +- [ ] 改动范围 **仅** task 所列 IMP-ID +- [ ] 未引入 FAQ **已拒绝** 方案(见 SUMMARY §3 拒绝列) +- [ ] 若改 CI:本地或 PR 上 **故意红一次** 验证 stderr/Runbook +- [ ] 关账:22 review 落盘 + pytest 绿 + task → done 流程 + +--- + +## 输出要求 + +1. **变更摘要**:按 IMP-ID 列出文件与行为。 +2. **未做项**:Batch A 中本轮 **刻意不做** 的 ID 及原因。 +3. **公众稿**:**不** 把本 Prompt 或 governance reviews 整段复制进 PR/对外仓库。 +4. **图谱**:若动 `api/` 或锚点,同步 `_manifest` / 契约 fixture / `.ai.md`(同 PR)。 + +--- + +## 修订记录 + +| 版本 | 日期 | 说明 | +| --- | --- | --- | +| v1.0 | 2026-05-30 | FAQ Batch A kickoff;链 09_PLAN + SUMMARY | diff --git a/docs/harness/prompts/hats/22-task-audit.md b/docs/harness/prompts/hats/22-task-audit.md index 9c9db433..f6709ee2 100644 --- a/docs/harness/prompts/hats/22-task-audit.md +++ b/docs/harness/prompts/hats/22-task-audit.md @@ -40,6 +40,20 @@ | 1 | 验收含:`PR 上 pytest workflow 全绿` + 本地等价命令 | ☐ | | 2 | 40 自检 / PR 链接可核对(终轮 22 不得无证明签收) | ☐ | +### §Blocking · 高敏须人判断(FAQ F8 · IMP-B-11) + +> **非** PR `/approve` 过闸;**是** 22/50 审查时对照。小团队可 **角色兼任**,**不可省略步骤**。 + +| 类型 | 例 | 22 须确认 | +| --- | --- | --- | +| **对外契约** | 改 `api/` 响应/SSE/路由 | task **Delta** + **50 落盘**(`required`) | +| **运行锚点** | 改 `_manifest` env/端点/部署前提 | 与 **manifest_check** 一致;task 已声明 | +| **主依赖** | FastAPI 等大版本、GPL 类新依赖 | changelog/LICENSE **人读**;回归范围足够 | + +| # | 检查项 | 通过 | +|---|--------|------| +| 1 | 若触达 Blocking 任一行 → 上表已核对,缺项 **阻塞** | ☐ | + ### §3.3 独立复检(50)触发 | 变更类型 | `test_strategy` | 50 | diff --git a/docs/tasks/active/task_backend_improve_batch_a_p0_v1.md b/docs/tasks/active/task_backend_improve_batch_a_p0_v1.md new file mode 100644 index 00000000..b504ae01 --- /dev/null +++ b/docs/tasks/active/task_backend_improve_batch_a_p0_v1.md @@ -0,0 +1,159 @@ +# Task:FAQ 改进 · Batch A(P0)工程补齐 + +> **状态**:`draft`(**草稿 · 非最终**;执行 Agent 可按实际裁剪范围、改写验收与 IMP 勾选) +> **关联规划**:治理仓 [`09_PLAN_Ink后端改进方案_可推广_v1_zh.md`](../../../../ai_coding_governance/09_PLAN_Ink后端改进方案_可推广_v1_zh.md) · [`SUMMARY_三卷读者FAQ_完整结论`](../../../../ai_coding_governance/narrative/reviews/SUMMARY_三卷读者FAQ_完整结论_20260530_v1_zh.md) +> **Agent Prompt**:[`docs/harness/prompts/PROMPT_FAQ改进_09PLAN_理解_v1_zh.md`](../harness/prompts/PROMPT_FAQ改进_09PLAN_理解_v1_zh.md) +> **关联 Issue/PR**:(待开 PR 后填) +> **前端依赖**:无 + +> **落盘规则**:验收后 `git mv` → `docs/tasks/done/` 并更新 `_views/`。 +> **方法论**:**不** redesign Harness/图谱;仅 FAQ 驱动的 DX + CI 可读性 + 模板。 + +--- + +## Harness 元信息(执行 Agent 必读) + +| 字段 | 值 | +|------|-----| +| **test_strategy** | `not_applicable` | +| **test_strategy_note** | 本 task 以 docs / CI workflow / 工具 stderr 为主;**无** `api/` 行为变更。若 IMP-B-01/02 需补 pytest,在 **实现备忘** 回填后 Agent 可改为 `recommended` 并补测。 | +| **freeze_id** | `FAQ-IMPROVE-BATCH-A@2026-05-30` | +| **semi_auto** | `true` | +| **audit_profile** | `post_close` | +| **git_branch** | `task/backend-improve-batch-a-p0` | + +### 人工闸 `human_gate` + +| human_gate_id | status | blocks_hats | 说明 | +|---------------|--------|-------------|------| +| HG-TASK-DRAFT | pending | 22-R1,30 | 草稿 task 人扫后可改 `approved` | +| HG-AUDIT-R1 | pending | 30 | 22 R1 落盘 `docs/harness/reviews/` 后人签 | +| HG-AUDIT-CLOSE | pending | done | PR 合并 + 关账 22 | + +--- + +## 背景与目标 + +三卷公众稿发表后,读者 FAQ 与 Ink 后端真值对照结论:**方法论方向正确**,缺口在 **合并入口、CI 红字可读、Blocking 模板、冷温热术语**。 +本 task 交付 **Batch A(P0)** 工程项,使后端成为 FAQ 改进的 **样板仓**(Batch B/C 另 task)。 + +**完成态(草案)** + +- 新开 PR 自带 **Ink 轨** 自检模板;22 帽可对照 **Blocking** 表。 +- 开发者遇 **manifest/contract CI 红** 时,Runbook + stderr **能指导下一步**。 +- 改 task 文件时 CI **可选/默认** 跑 `harness_task_validate`。 +- 对内 **冷/温/热** 术语卡可被 22/10 引用。 + +--- + +## 范围(IMP-B · Batch A) + +> **首包已落盘(2026-05-30)** — Agent 验收时勾选,不必重做除非需修订。 + +| ID | 项 | 状态(草稿) | 交付物 | +| --- | --- | --- | --- | +| IMP-B-10 | Ink 轨 PR 模板 | ✅ 初稿 | `.github/pull_request_template.md` | +| IMP-B-11 | 22 Blocking 表 | ✅ 初稿 | `docs/harness/prompts/hats/22-task-audit.md` §Blocking | +| IMP-B-20 | 冷/温/热术语卡 | ✅ 初稿 | `docs/harness/guides/GUIDE_冷温热层_对内术语_v1_zh.md` | +| — | Agent Prompt | ✅ 初稿 | `docs/harness/prompts/PROMPT_FAQ改进_09PLAN_理解_v1_zh.md` | +| IMP-B-01 | CI 红字 + Runbook | ✅ | `tools/tech_graph_*_check.py` stderr · `docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md` | +| IMP-B-02 | task 路径 `task_validate` | ✅ | `.github/workflows/tech-graph.yml` job `task_validate` | + +**Agent 可调**:若 IMP-B-01/02 过大,可拆子 PR 或降 scope(须在 review 说明);**不可** 引入 FAQ 已拒项(`graph.auto.json`、PR `/approve` 唯一闸等,见 PROMPT)。 + +--- + +## 非范围 + +- OpenSpec **O7** Delta→spec 关账(Batch C · IMP-B-14) +- L2 manifest SPEC 全量关账(Batch B · IMP-B-03) +- failure-cases 目录(Batch B · IMP-B-30) +- 改 `api/` 业务逻辑、ChatBI 功能 +- 治理仓 `narrative/` 公众稿正文(本 task 仅后端工程) +- 工作区 `Projects/docs/harness/` 任何写入 + +--- + +## 行为变更(Delta) + +**无**(对外 HTTP/SSE/DB 行为不变)。 + +--- + +## 依赖与引用 + +| 依赖项 | 路径 | +|--------|------| +| 09 PLAN | `ai_coding_governance/09_PLAN_Ink后端改进方案_可推广_v1_zh.md` | +| FAQ SUMMARY | `ai_coding_governance/narrative/reviews/SUMMARY_…` | +| PROJECT_CONFIG | `docs/meta/PROJECT_CONFIG_AI_INK_BRAIN_API_PYTHON.md` | +| pr-post-ci | `docs/spec/governance/SPEC-Governance-PR-Post-CI-v1.md` | +| task_validate | `tools/harness_task_validate.py` | +| 图谱入口 | `docs/_tech_graph/00_main.ai.md`(一般 **不必改**) | + +--- + +## 失败路径 + +| # | Scenario ID | 触发条件 | 系统行为 | 可重试 | 用户可见 | +|---|-------------|----------|----------|--------|----------| +| F1 | `fp-task-validate-fail` | PR 改 task 缺 Delta/Scenario | CI `task_validate` exit 非 0 | 是 | PR checks 红 + stderr | +| F2 | `fp-manifest-check-fail` | manifest 与代码不一致 | `manifest_check` exit 非 0 | 是 | 见 IMP-B-01 三段式 stderr(待做) | + +--- + +## 验收标准 + +- [ ] **IMP-B-01**:故意制造 manifest/contract 不一致 PR,stderr 含 **位置 / 期望 vs 实际 / 下一步**;Runbook 链自 `docs/harness/README.md` +- [ ] **IMP-B-02**:仅改 `docs/tasks/active/*.md` 且缺字段的 PR,CI **失败**;补齐后 **通过** +- [ ] **IMP-B-10/11/20** 与 PROMPT:README 可发现;22 审查可对照 Blocking(首包已存在则 **抽检** 即可) +- [ ] `docs/harness/README.md` 已链 PROMPT + guides(首包已做则勾选) +- [ ] PR 上 `pytest` workflow 全绿(本地等价:`pytest tests -m "not intent_eval and not intent_benchmark"`) +- [ ] 22 R1 审查落盘:`docs/harness/reviews/by-task/backend-improve-batch-a-p0/`(路径 Agent 可微调) +- [ ] 关账:`git mv` 本 task → `done/`;可选 50(本 task `not_applicable` 时可省略,由 22 CLOSE 说明) + +--- + +## 实施清单(Agent 可增删) + +- [ ] 1.1 读 PROMPT + 本 task;确认 **首包** 四文件是否需润色 +- [x] 1.2 **IMP-B-01**:改 check 脚本 stderr + 写 Runbook;本地/PR 故意红一次 +- [x] 1.3 **IMP-B-02**:workflow `paths: docs/tasks/**` + `harness_task_validate.py` +- [x] 1.4 更新 `docs/harness/README.md`(Runbook 链) +- [ ] 1.5 22 R1 → 30 实现 → 40 自检 → 22 CLOSE +- [ ] 1.6 更新治理仓 `09_PLAN` §2 IMP 状态列(可选 · 跨仓只读后人工或另 commit) + +--- + +## 实现备忘(由 Agent 回填) + +| 项 | 内容 | +|----|------| +| 涉及文件 | `tools/tech_graph_ci_stderr.py` · `tools/tech_graph_manifest_check.py` · `tools/tech_graph_contract_check.py` · `docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md` · `docs/harness/README.md` · `.github/workflows/tech-graph.yml` | +| workflow 变更 | `tech-graph.yml` 增 job `task_validate`(`docs/tasks/active/*.md` 变更时跑 `harness_task_validate.py`) | +| 故意红 PR / commit | 本地:`python tools/tech_graph_manifest_check.py` 改 `_manifest.json` 后应见三段式 stderr;task:删 test_strategy 后 `harness_task_validate.py` 应 FAIL | +| 图谱变更点 | 无(预期) | + +--- + +## 自检结论(40 帽 · 待填) + +| 项 | 结果 | +|----|------| +| 命令 | | +| 结论 | | +| 要点 | | + +--- + +## 给 Cursor + +`task_backend_improve_batch_a_p0`、`FAQ-IMPROVE-BATCH-A`、`IMP-B-01`、`IMP-B-02`、`09_PLAN`、`PROMPT_FAQ改进_09PLAN`、`Harness`、`not_applicable`、`post_close` + +--- + +## 修订记录(task 本体) + +| 版本 | 日期 | 说明 | +| --- | --- | --- | +| draft-v0 | 2026-05-30 | 草稿;首包 IMP-B-10/11/20 + PROMPT 已落盘 | diff --git a/tools/tech_graph_ci_stderr.py b/tools/tech_graph_ci_stderr.py new file mode 100644 index 00000000..7a0c7217 --- /dev/null +++ b/tools/tech_graph_ci_stderr.py @@ -0,0 +1,40 @@ +"""图谱 manifest / contract CI 失败时的三段式 stderr(IMP-B-01)。""" +from __future__ import annotations + +from dataclasses import dataclass + +RUNBOOK_REL = "docs/harness/guides/RUNBOOK_graph_contract_ci_red_v1.md" + + +@dataclass(frozen=True) +class CiIssue: + """单条 drift:位置 · 文档声明 · 当前代码。""" + + location: str + declared: str + actual: str + + +def print_ci_failure( + *, + title: str, + check_name: str, + local_command: str, + issues: list[CiIssue], +) -> None: + """向 stderr 打印 F20 对齐的三段式失败摘要。""" + print(f"❌ {title}") + print(f"检查: {check_name}") + print() + for i, issue in enumerate(issues, start=1): + print(f"--- 问题 {i}/{len(issues)} ---") + print(f"位置: {issue.location}") + print(f"文档声明: {issue.declared}") + print(f"当前代码: {issue.actual}") + print() + print("你可以:") + print(" 1. 若确属契约变更:在任务单 §行为变更 写明 → 同 PR 更新锚点/manifest/contract 与 .ai.md → 补 pytest;") + print(" 2. 若为误改:回滚 api/ 或相关 hunks;") + print(f" 3. 本地运行与 CI 相同命令,确认变绿后再 push:") + print(f" {local_command}") + print(f"Runbook: {RUNBOOK_REL}") diff --git a/tools/tech_graph_contract_check.py b/tools/tech_graph_contract_check.py index eb6717ea..76b66630 100644 --- a/tools/tech_graph_contract_check.py +++ b/tools/tech_graph_contract_check.py @@ -2,12 +2,17 @@ import json import re +import sys from dataclasses import dataclass from pathlib import Path from typing import Any - REPO_ROOT = Path(__file__).resolve().parent.parent +_TOOLS_DIR = Path(__file__).resolve().parent +if str(_TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(_TOOLS_DIR)) + +from tech_graph_ci_stderr import CiIssue, print_ci_failure CONTRACT_PATH = REPO_ROOT / "docs" / "_tech_graph" / "_contract_manifest.json" # vNext:`agent.llm.*` 等在 `api/agent.py` emit;须与 manifest 同 PR 纳入静态真值 BACKEND_CONTRACT_SOURCES = [ @@ -41,17 +46,31 @@ def _diff_set(*, truth: set[str], declared: set[str]) -> Diff: return Diff(missing=missing, extra=extra) -def _fmt_diff(title: str, d: Diff) -> str: - lines: list[str] = [] - if d.missing: - lines.append(f"{title}: MISSING (contract -> truth)") - for x in d.missing[:80]: - lines.append(f" - {x}") - if d.extra: - lines.append(f"{title}: EXTRA (truth -> contract)") - for x in d.extra[:80]: - lines.append(f" - {x}") - return "\n".join(lines).strip() +def _summarize_items(items: list[str], *, limit: int = 8) -> str: + if not items: + return "(无)" + head = ", ".join(items[:limit]) + if len(items) > limit: + return f"{head} … 共 {len(items)} 项" + return head + + +def _diff_to_issue( + *, + location: str, + d: Diff, + missing_label: str, +) -> list[CiIssue]: + """Backend 契约:仅 contract ⊆ code truth(missing);extra 不失败。""" + if not d.missing: + return [] + return [ + CiIssue( + location=location, + declared=f"contract 声明但代码未覆盖: {_summarize_items(d.missing)}", + actual=missing_label, + ) + ] def _extract_string_keys_from_dict_literal(text: str) -> set[str]: @@ -274,10 +293,22 @@ def main() -> int: missing_contract.append("contract.sse.done.data_keys must include: ok, mode, run_id, session_id, request_id") if not must_types.issubset(set(pkbt.keys())): missing_contract.append("contract.sse.chain.payload_min_keys_by_type must include: rag.sources, sql.result") + if missing_contract: - print("FAIL: contract manifest incomplete.\n") - for x in missing_contract: - print(f"- {x}") + issues: list[CiIssue] = [ + CiIssue( + location="contract.sse (manifest 自检)", + declared=x, + actual="_contract_manifest.json 结构不完整", + ) + for x in missing_contract + ] + print_ci_failure( + title="合并被阻塞:contract manifest 自身不完整", + check_name="tech_graph_contract_check", + local_command="python tools/tech_graph_contract_check.py", + issues=issues, + ) return 1 # Load backend truth(多文件合并,避免 chain.type 仅落在 agent.py 时漏检) @@ -290,20 +321,28 @@ def main() -> int: backend_text = "\n\n".join(_read_text(p) for p in BACKEND_CONTRACT_SOURCES) bt = _backend_truth_from_unified_chat(backend_text) - problems: list[str] = [] + issues = [] - # backend_truth ⊇ contract d_ev = _diff_set(truth=set(bt["backend_events"]), declared=allowed_events) - if d_ev.missing: - problems.append(_fmt_diff("Backend SSE allowed events", d_ev)) + issues += _diff_to_issue( + location="contract.sse.allowed_events ↔ api/unified_chat.py", + d=d_ev, + missing_label="后端 _sse() 未 emit 上述事件", + ) d_types = _diff_set(truth=set(bt["chain_types"]), declared=chain_type_values) - if d_types.missing: - problems.append(_fmt_diff("Backend chain.type values", d_types)) + issues += _diff_to_issue( + location="contract.sse.chain.type_values ↔ api/", + d=d_types, + missing_label="后端未 emit 上述 chain.type", + ) d_done = _diff_set(truth=set(bt["done_keys"]), declared=done_data_keys) - if d_done.missing: - problems.append(_fmt_diff("Backend done.data keys", d_done)) + issues += _diff_to_issue( + location="contract.sse.done.data_keys ↔ api/unified_chat.py", + d=d_done, + missing_label="done 事件 payload 缺上述 key", + ) # payload keys: only check what we can statically extract payload_keys_by_type: dict[str, set[str]] = bt["payload_keys_by_type"] @@ -312,8 +351,11 @@ def main() -> int: if isinstance(required, list): req = set([x for x in required if isinstance(x, str)]) d_meta = _diff_set(truth=set(bt["meta_payload_keys"]), declared=req) - if d_meta.missing: - problems.append(_fmt_diff("Backend payload keys for meta", d_meta)) + issues += _diff_to_issue( + location="contract.sse.chain.payload_min_keys_by_type.meta", + d=d_meta, + missing_label="meta payload 缺 contract 要求的 key", + ) continue if typ == "rag.sources": if not isinstance(required, dict): @@ -323,14 +365,23 @@ def main() -> int: req_ret_keys = set(required.get("retrieval_keys") or []) d_rag_payload = _diff_set(truth=set(bt["rag_sources_payload_keys"]), declared=req_payload_keys) - if d_rag_payload.missing: - problems.append(_fmt_diff("Backend rag.sources payload keys", d_rag_payload)) + issues += _diff_to_issue( + location="contract.sse.chain.payload_min_keys_by_type.rag.sources (payload)", + d=d_rag_payload, + missing_label="rag.sources payload 缺 key", + ) d_rag_items = _diff_set(truth=set(bt["rag_sources_item_keys"]), declared=req_item_keys) - if d_rag_items.missing: - problems.append(_fmt_diff("Backend rag.sources.source item keys", d_rag_items)) + issues += _diff_to_issue( + location="contract.sse.chain.payload_min_keys_by_type.rag.sources (items)", + d=d_rag_items, + missing_label="source item 缺 key", + ) d_rag_ret = _diff_set(truth=set(bt["rag_sources_retrieval_keys"]), declared=req_ret_keys) - if d_rag_ret.missing: - problems.append(_fmt_diff("Backend rag.sources.retrieval keys", d_rag_ret)) + issues += _diff_to_issue( + location="contract.sse.chain.payload_min_keys_by_type.rag.sources (retrieval)", + d=d_rag_ret, + missing_label="retrieval 嵌套缺 key", + ) continue if isinstance(required, list): @@ -339,11 +390,20 @@ def main() -> int: continue truth = payload_keys_by_type.get(typ) if not truth: - problems.append(f"Backend payload keys for type {typ!r}: MISSING (cannot find dict-literal payload)") + issues.append( + CiIssue( + location=f"contract.sse.chain.payload_min_keys_by_type.{typ}", + declared=f"contract 要求 payload keys: {_summarize_items(sorted(req))}", + actual=f"静态分析未在 api/ 找到 {typ!r} 的 payload dict-literal", + ) + ) continue d = _diff_set(truth=set(truth), declared=req) - if d.missing: - problems.append(_fmt_diff(f"Backend payload keys for {typ}", d)) + issues += _diff_to_issue( + location=f"contract.sse.chain.payload_min_keys_by_type.{typ}", + d=d, + missing_label=f"{typ} payload 缺 contract 要求的 key", + ) # frontend_expect ⊆ contract fa = contract.get("frontend_anchors") @@ -370,8 +430,11 @@ def main() -> int: # Required handled events must be subset of allowed_events d_fe_events = _diff_set(truth=allowed_events, declared=set(fe["handled_events_required"])) - if d_fe_events.missing: - problems.append(_fmt_diff("Frontend handled events (required)", d_fe_events)) + issues += _diff_to_issue( + location="contract.frontend_anchors.sse_consumer_files (required events)", + d=d_fe_events, + missing_label="前端 SSE consumer 处理了 contract 未允许的事件", + ) # Keys used by frontend must be subset of contract (very lightweight) contract_key_union: set[str] = set() @@ -424,15 +487,21 @@ def main() -> int: forbidden = sorted([x for x in used_union if x not in contract_key_union]) if forbidden: - problems.append("Frontend forbidden keys (expect -> not in contract):\n" + "\n".join([f" - {x}" for x in forbidden[:120]])) - - if problems: - print("FAIL: cross-repo contract drift detected.\n") - for msg in problems: - if msg.strip(): - print(msg) - print() - # Optional info + issues.append( + CiIssue( + location="contract.frontend_anchors.sse_consumer_files (field reads)", + declared=f"contract 未声明字段: {_summarize_items(forbidden, limit=12)}", + actual="前端 TS 读取了上述 obj/payload 字段", + ) + ) + + if issues: + print_ci_failure( + title="合并被阻塞:跨端 contract 与代码不一致", + check_name="tech_graph_contract_check", + local_command="python tools/tech_graph_contract_check.py", + issues=issues, + ) opt = sorted(list(fe["handled_events_optional"])) if opt: print("INFO: frontend also handles optional event names (not enforced):") diff --git a/tools/tech_graph_manifest_check.py b/tools/tech_graph_manifest_check.py index e44f9206..6e1d26e9 100644 --- a/tools/tech_graph_manifest_check.py +++ b/tools/tech_graph_manifest_check.py @@ -9,6 +9,12 @@ from typing import Any, Iterable REPO_ROOT = Path(__file__).resolve().parent.parent +_TOOLS_DIR = Path(__file__).resolve().parent +if str(_TOOLS_DIR) not in sys.path: + sys.path.insert(0, str(_TOOLS_DIR)) + +from tech_graph_ci_stderr import CiIssue, print_ci_failure + API_DIR = REPO_ROOT / "api" SQL_DIR = REPO_ROOT / "supabase" / "sql" DEFAULT_BACKEND_MANIFEST = REPO_ROOT / "docs" / "_tech_graph" / "_manifest.json" @@ -200,70 +206,105 @@ def _set_diff(*, truth: set[str], declared: set[str]) -> dict[str, list[str]]: return {"missing": missing, "extra": extra} -def _format_diff(title: str, d: dict[str, list[str]]) -> str: - parts: list[str] = [] +def _summarize_items(items: list[str], *, limit: int = 8) -> str: + if not items: + return "(无)" + head = ", ".join(items[:limit]) + if len(items) > limit: + return f"{head} … 共 {len(items)} 项" + return head + + +def _set_diff_issues(*, location: str, d: dict[str, list[str]]) -> list[CiIssue]: + issues: list[CiIssue] = [] if d["missing"]: - parts.append(" 缺失(truth->manifest):") - for x in d["missing"][:60]: - parts.append(f" - {x}") - if len(d["missing"]) > 60: - parts.append(f" ... and {len(d['missing']) - 60} more") + issues.append( + CiIssue( + location=location, + declared=f"manifest 未声明: {_summarize_items(d['missing'])}", + actual=f"代码/SQL truth 存在: {_summarize_items(d['missing'])}", + ) + ) if d["extra"]: - parts.append(" 多余(manifest->truth):") - for x in d["extra"][:60]: - parts.append(f" - {x}") - if len(d["extra"]) > 60: - parts.append(f" ... and {len(d['extra']) - 60} more") - if not parts: - return f"{title}: OK" - return "\n".join([f"{title}: FAIL"] + parts) + issues.append( + CiIssue( + location=location, + declared=f"manifest 多余声明: {_summarize_items(d['extra'])}", + actual=f"代码/SQL truth 不存在上述项", + ) + ) + return issues def _endpoint_key(method: str, path: str) -> str: return f"{method.upper()} {path}" -def _endpoint_diff(truth: list[EndpointTruth], declared: list[dict[str, Any]]) -> list[str]: +def _endpoint_diff(truth: list[EndpointTruth], declared: list[dict[str, Any]]) -> list[CiIssue]: truth_map = {_endpoint_key(e.method, e.path): e for e in truth} declared_map: dict[str, dict[str, Any]] = {} for e in declared: k = _endpoint_key(str(e.get("method", "")).upper(), str(e.get("path", ""))) declared_map[k] = e - missing = sorted([k for k in truth_map.keys() if k not in declared_map]) - extra = sorted([k for k in declared_map.keys() if k not in truth_map]) - changed: list[str] = [] + issues: list[CiIssue] = [] + for k in sorted([k for k in truth_map.keys() if k not in declared_map]): + te = truth_map[k] + issues.append( + CiIssue( + location=f"manifest.endpoints · {k}", + declared="manifest 未声明此 HTTP 路由", + actual=f"api/index.py handler={te.handler!r} @ L{te.line}", + ) + ) + for k in sorted([k for k in declared_map.keys() if k not in truth_map]): + handler = declared_map[k].get("handler", "?") + issues.append( + CiIssue( + location=f"manifest.endpoints · {k}", + declared=f"manifest handler={handler!r}", + actual="api/index.py 无对应路由装饰器", + ) + ) for k, te in truth_map.items(): de = declared_map.get(k) if not de: continue handler = de.get("handler") if isinstance(handler, str) and handler != te.handler: - changed.append(f"{k}: handler truth={te.handler!r} manifest={handler!r}") - - msgs: list[str] = [] - if missing: - msgs.append("Routes 缺失(truth->manifest):\n" + "\n".join([f" - {x}" for x in missing])) - if extra: - msgs.append("Routes 多余(manifest->truth):\n" + "\n".join([f" - {x}" for x in extra])) - if changed: - msgs.append("Routes 不一致(同 method+path):\n" + "\n".join([f" - {x}" for x in changed])) - return msgs + issues.append( + CiIssue( + location=f"manifest.endpoints · {k}", + declared=f"manifest handler={handler!r}", + actual=f"api/index.py handler={te.handler!r} @ L{te.line}", + ) + ) + return issues -def _route_diff(truth: list[RouteTruth], declared: list[dict[str, Any]]) -> list[str]: +def _route_diff(truth: list[RouteTruth], declared: list[dict[str, Any]]) -> list[CiIssue]: truth_keys = {_endpoint_key(r.method, r.path) for r in truth} declared_keys = { _endpoint_key(str(r.get("method", "")).upper(), str(r.get("path", ""))) for r in declared } - missing = sorted([k for k in truth_keys if k not in declared_keys]) - extra = sorted([k for k in declared_keys if k not in truth_keys]) - msgs: list[str] = [] - if missing: - msgs.append("Routes 缺失(truth->manifest):\n" + "\n".join([f" - {x}" for x in missing])) - if extra: - msgs.append("Routes 多余(manifest->truth):\n" + "\n".join([f" - {x}" for x in extra])) - return msgs + issues: list[CiIssue] = [] + for k in sorted([k for k in truth_keys if k not in declared_keys]): + issues.append( + CiIssue( + location=f"manifest.routes · {k}", + declared="manifest 未声明此 Next route", + actual="app/ 存在对应 route.ts", + ) + ) + for k in sorted([k for k in declared_keys if k not in truth_keys]): + issues.append( + CiIssue( + location=f"manifest.routes · {k}", + declared="manifest 声明了此 route", + actual="app/ 无对应 route.ts", + ) + ) + return issues def _is_route_group_segment(segment: str) -> bool: @@ -375,48 +416,71 @@ def _run_backend_check(*, manifest_path: Path) -> int: tables_truth = set(sorted(table_truth | sql_tables)) rpc_truth2 = set(sorted(rpc_truth | sql_funcs)) - problems: list[str] = [] - problems += _endpoint_diff(endpoint_truth, manifest_endpoints) + issues: list[CiIssue] = [] + issues += _endpoint_diff(endpoint_truth, manifest_endpoints) d_tables = _set_diff(truth=tables_truth, declared=manifest_tables_set) - if d_tables["missing"] or d_tables["extra"]: - problems.append(_format_diff("Supabase tables", d_tables)) + issues += _set_diff_issues(location="manifest.supabase.tables", d=d_tables) d_rpc = _set_diff(truth=rpc_truth2, declared=manifest_rpc_set) - if d_rpc["missing"] or d_rpc["extra"]: - problems.append(_format_diff("Supabase RPC (public functions)", d_rpc)) + issues += _set_diff_issues(location="manifest.supabase.rpc", d=d_rpc) d_env = _set_diff(truth=env_truth, declared=manifest_env) - if d_env["missing"] or d_env["extra"]: - problems.append(_format_diff("Key env vars", d_env)) + issues += _set_diff_issues(location="manifest.env", d=d_env) anchors = manifest.get("anchors") if not isinstance(anchors, list) or not all(isinstance(x, dict) for x in anchors): - problems.append("Anchors: FAIL\n manifest.anchors must be list[object]") + issues.append( + CiIssue( + location="manifest.anchors", + declared="anchors 须为 list[object]", + actual=f"当前类型: {type(anchors).__name__}", + ) + ) else: for i, a in enumerate(anchors): p = a.get("path") sym = a.get("symbol") if not isinstance(p, str) or not isinstance(sym, str) or not p or not sym: - problems.append(f"Anchors: FAIL\n anchors[{i}] requires path/symbol string") + issues.append( + CiIssue( + location=f"manifest.anchors[{i}]", + declared="path + symbol 字符串", + actual=f"path={p!r} symbol={sym!r}", + ) + ) break abs_p = (REPO_ROOT / p).resolve() if not abs_p.exists(): - problems.append(f"Anchors: FAIL\n anchors[{i}] path not found: {p}") + issues.append( + CiIssue( + location=f"manifest.anchors[{i}] · {p}::{sym}", + declared=f"锚点文件 {p}", + actual="文件不存在", + ) + ) break try: ln = _find_def_line(_read_text(abs_p), sym) except Exception: # noqa: BLE001 ln = None if ln is None: - problems.append(f"Anchors: FAIL\n anchors[{i}] symbol not found: {p}::{sym}") + issues.append( + CiIssue( + location=f"manifest.anchors[{i}] · {p}::{sym}", + declared=f"symbol {sym!r}", + actual=f"{p} 中未找到 def/class {sym!r}", + ) + ) break - if problems: - print("FAIL: manifest drift detected.\n") - for msg in problems: - print(msg) - print() + if issues: + print_ci_failure( + title="合并被阻塞:manifest 锚点与代码不一致", + check_name="tech_graph_manifest_check", + local_command="python tools/tech_graph_manifest_check.py", + issues=issues, + ) return 1 ai_main = REPO_ROOT / "docs" / "_tech_graph" / "00_main.ai.md" @@ -458,23 +522,26 @@ def _run_frontend_check(*, repo_root: Path, manifest_path: Path) -> int: routes_truth = _extract_frontend_routes(app_dir) env_truth = _extract_frontend_env_names(repo_root) - problems: list[str] = [] + issues: list[CiIssue] = [] d_pages = _set_diff(truth=pages_truth, declared=manifest_pages) - if d_pages["missing"] or d_pages["extra"]: - problems.append(_format_diff("Pages", d_pages)) + issues += _set_diff_issues(location="manifest.pages", d=d_pages) - problems += _route_diff(routes_truth, manifest_routes) + issues += _route_diff(routes_truth, manifest_routes) d_env = _set_diff(truth=env_truth, declared=manifest_env) - if d_env["missing"] or d_env["extra"]: - problems.append(_format_diff("Key env vars", d_env)) - - if problems: - print("FAIL: manifest drift detected.\n") - for msg in problems: - print(msg) - print() + issues += _set_diff_issues(location="manifest.env", d=d_env) + + if issues: + print_ci_failure( + title="合并被阻塞:frontend manifest 与代码不一致", + check_name="tech_graph_manifest_check --repo frontend", + local_command=( + "python tools/tech_graph_manifest_check.py " + "--repo frontend --repo-root " + ), + issues=issues, + ) return 1 print(