diff --git a/README.md b/README.md index 56c5edb..213b1da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ English | [简体中文](README.zh-CN.md) -> Current protocol version: `0.1.22` (in development); workflow version: `0.1.3` +> Current protocol version: `0.1.23` (in development); workflow version: `0.1.3` Polaris is a repo-native engineering workflow for coding agent hosts. It stores requirements, plans, implementation results, independent reviews, validation evidence, and task state in Git, then uses deterministic gates to prevent requirement drift, stale evidence, and agents declaring their own work complete. @@ -91,13 +91,13 @@ polaris code-intelligence add codegraph --repo . Run these commands from the target repository as appropriate. `codegraph init` creates the `.codegraph/` marker; without it Polaris uses source and Git directly and creates no stage record. Vendoring registers the project-scoped `polaris-codegraph` proxy in `.codex/config.toml` and `.mcp.json` without replacing unrelated settings. The host may require project trust or first-use approval; that approval remains the user's decision. -Polaris stages call only `polaris_codegraph_explore`. The proxy checks status and automatically runs at most one bounded incremental `codegraph sync` when pending changes exist, then runs one explore, rechecks status, and returns a freshness envelope before graph content. There is no separate stage status/sync MCP call. `CURRENT` means `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` mean `NAVIGATION_ONLY` and require the exact source/Git fallback before a conclusion is used; `UNAVAILABLE` means no graph. A current named file uses `READ_SOURCE`, a deleted file uses `INSPECT_GIT_DIFF`, and an index-wide or unsafe result uses `SEARCH_SOURCE`. Validation remains graph-free and relies on source, Git, builds, tests, static checks, and Human Checks. +Polaris stages call only `polaris_codegraph_explore`. After verifying repository identity, the proxy attempts exactly one bounded incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, then runs one explore, rechecks status, and returns a freshness envelope before graph content. Zero pending changes do not prove that the index reflects clean HEAD or the current branch. There is no separate stage status/sync MCP call. CodeGraph is never a source of truth: `CURRENT` means only `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` mean `NAVIGATION_ONLY` and require the exact source/Git fallback before a conclusion is used; `UNAVAILABLE` means no graph. A current named file uses `READ_SOURCE`, a deleted file uses `INSPECT_GIT_DIFF`, and an index-wide or unsafe result uses `SEARCH_SOURCE`. Validation remains graph-free and relies on source, Git, builds, tests, static checks, and Human Checks. When status cannot be verified but the project has a safe repository identity, the proxy still calls `polaris_codegraph_explore` and returns `UNKNOWN`/`TREAT_AS_STALE`. The graph remains navigation-only: use the exact source/Git fallback before any conclusion. The repository owner, not Polaris, owns CodeGraph installation, initialization, configuration, raw MCP registration, watcher, daemon, and every full `codegraph index` rebuild. Polaris never starts, configures, reconfigures, waits for, or manages them. Raw `codegraph_explore` or `codegraph explore` remains available out-of-band but cannot back `CURRENT` Polaris evidence. New records are v3 projections of the retained proxy bundle and completed fallbacks; v1/v2 are historical only. CodeGraph remains optional and never becomes a workflow gate. -Protocol `0.1.22` keeps Workflow at `0.1.3` and adds an explicit version-only migration from `0.1.21` that neither inventories nor rewrites Code Intelligence record v3 evidence. Protocol `0.1.21` introduced the project-scoped Polaris CodeGraph proxy, host adapter v3 registration, and auditable record v3; record v1 and v2 remain immutable historical evidence only. +Protocol `0.1.23` keeps Workflow at `0.1.3`, makes one incremental sync mandatory before each safe proxy query, publishes runtime bundle v3, and adds an explicit version-only migration from `0.1.22` that neither inventories nor rewrites Code Intelligence record v3 evidence. Runtime bundle v1/v2 and durable record v1/v2 remain readable as immutable historical evidence. ## v0.1 scope diff --git a/README.zh-CN.md b/README.zh-CN.md index 0585080..49421c9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ [English](README.md) | 简体中文 -> 当前协议版本:`0.1.22`(开发中);Workflow 版本:`0.1.3` +> 当前协议版本:`0.1.23`(开发中);Workflow 版本:`0.1.3` Polaris 是运行在 Coding Agent 宿主上的仓库原生工程工作流。它把需求、计划、实现、独立审查、验证和任务状态保存在 Git 仓库中,并通过确定性门禁防止需求漂移、证据过期和 Agent 自行宣布完成。 @@ -91,13 +91,13 @@ polaris code-intelligence add codegraph --repo . `codegraph init` 创建 `.codegraph/` marker;没有 marker 时 Polaris 直接使用源码和 Git,不生成阶段 record。Vendoring 会在 `.codex/config.toml` 与 `.mcp.json` 中非破坏地注册项目级 `polaris-codegraph` 代理,并保留其他设置。宿主可能要求信任项目或首次使用确认;是否批准仍由用户决定。 -Polaris 阶段只调用 `polaris_codegraph_explore`。代理先检查 status,存在 pending changes 时自动且至多执行一次有界增量 `codegraph sync`,再执行一次 explore、复查 status,并保证 freshness envelope 位于图内容之前;阶段没有独立的 status/sync MCP 调用。`CURRENT` 表示 `NON_AUTHORITATIVE_CONTEXT`;`STALE` 与 `UNKNOWN`/`TREAT_AS_STALE` 表示 `NAVIGATION_ONLY`,在使用任何结论前必须完成 envelope 指定的精确源码/Git 回退;`UNAVAILABLE` 表示没有图内容。当前具名文件使用 `READ_SOURCE`,已删除文件使用 `INSPECT_GIT_DIFF`,索引级或不安全结果使用 `SEARCH_SOURCE`。Validation 不调用 CodeGraph,仍以源码、Git、构建、测试、静态检查和 Human Check 为准。 +Polaris 阶段只调用 `polaris_codegraph_explore`。代理确认仓库身份安全后,会在每次代理查询前尝试并至多执行一次有界增量 `codegraph sync`,即使 status 报告零 pending changes 也不跳过;随后执行一次 explore、复查 status,并保证 freshness envelope 位于图内容之前。零 pending changes 不能证明索引已经对应 clean HEAD 或当前分支;阶段没有独立的 status/sync MCP 调用。CodeGraph 永远不是 source of truth:`CURRENT` 只表示 `NON_AUTHORITATIVE_CONTEXT`;`STALE` 与 `UNKNOWN`/`TREAT_AS_STALE` 表示 `NAVIGATION_ONLY`,在使用任何结论前必须完成 envelope 指定的精确源码/Git 回退;`UNAVAILABLE` 表示没有图内容。当前具名文件使用 `READ_SOURCE`,已删除文件使用 `INSPECT_GIT_DIFF`,索引级或不安全结果使用 `SEARCH_SOURCE`。Validation 不调用 CodeGraph,仍以源码、Git、构建、测试、静态检查和 Human Check 为准。 当 status 无法验证但仓库身份安全时,代理仍执行 `polaris_codegraph_explore`,并返回 `UNKNOWN`/`TREAT_AS_STALE`。图只用于导航;在使用任何结论前,必须完成精确源码/Git 回退。 CodeGraph 的安装、初始化、配置、raw MCP 注册、watcher、daemon 与每次全量 `codegraph index` 重建都归仓库所有者,而不是 Polaris;全量重建始终由用户主动执行。Polaris 绝不启动、配置、重新配置、等待或管理这些能力。raw `codegraph_explore` 或 `codegraph explore` 仍可作为带外工具使用,但不能支持 Polaris 的 `CURRENT` 证据。新 record 必须由保留的代理 bundle 与已完成回退投影为 v3;v1/v2 仅供历史读取。CodeGraph 始终可选,永远不是 Workflow 门禁。 -协议 `0.1.22` 保持 Workflow `0.1.3`,并新增从 `0.1.21` 出发的显式纯版本迁移;该迁移不清点也不重写 Code Intelligence record v3 证据。协议 `0.1.21` 引入项目级 Polaris CodeGraph 代理、Host Adapter v3 注册和可审计的 record v3;record v1/v2 仍仅作为不可变历史证据读取。 +协议 `0.1.23` 保持 Workflow `0.1.3`,要求每次安全的代理查询前执行一次增量同步尝试,发布 runtime bundle v3,并新增从 `0.1.22` 出发的显式纯版本迁移;该迁移不清点也不重写 Code Intelligence record v3 证据。runtime bundle v1/v2 与 durable record v1/v2 仍仅作为不可变历史证据读取。 ## v0.1 边界 diff --git a/VERSION b/VERSION index 7e72641..001d752 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.22 +0.1.23 diff --git a/docs/USAGE.md b/docs/USAGE.md index 3659232..2aae6c0 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -2,7 +2,7 @@ 本文面向希望在受支持 Coding Agent 宿主中使用 Polaris 管理软件工程任务的项目成员。当前内置 Codex 与 Claude Code 适配器;本文从首次接入讲到日常提出需求、独立 Implementation、进度查询、Review、验证、恢复与升级。 -> 当前协议版本:v0.1.22;Workflow 版本:v0.1.3。Polaris v0.1 是仓库原生的 Skills、宿主 worker 定义与 Python 脚本集合,并提供一个只分发到这些脚本的 `polaris` CLI;不提供后台服务或图形界面。 +> 当前协议版本:v0.1.23;Workflow 版本:v0.1.3。Polaris v0.1 是仓库原生的 Skills、宿主 worker 定义与 Python 脚本集合,并提供一个只分发到这些脚本的 `polaris` CLI;不提供后台服务或图形界面。 ## 1. 先理解 Polaris 保存什么 @@ -219,11 +219,11 @@ Python CLI 无法直接查看 Codex 或 Claude Code 当前会话中的 MCP 工 } ``` -CodeGraph watcher 与连接时 reconciliation 是常规实时更新机制。Polaris 阶段只调用 `polaris_codegraph_explore`:代理在同一有界窗口内检查 status,存在 pending changes 时自动且至多运行一次增量 `codegraph sync`,执行一次 explore,再复查 status。阶段没有独立的 status/sync MCP 工具,也不会等待 watcher、轮询、重试、启动 daemon 或改写用户的 raw MCP 配置。Documentation Sync 仅在 supported source 变化时执行一次查询,把 query 限制到 changed source paths 与 documented symbols;automatic incremental sync 仅由代理负责。 +CodeGraph watcher 与连接时 reconciliation 是常规实时更新机制。Polaris 阶段只调用 `polaris_codegraph_explore`:代理确认仓库身份安全后,在每次代理查询前尝试且至多尝试一次增量 `codegraph sync`,即使 status 报告零 pending changes 也不跳过,然后执行一次 explore 并复查 status。零 pending changes 不能证明索引已经对应 clean HEAD 或当前分支。阶段没有独立的 status/sync MCP 工具,也不会等待 watcher、轮询、重试、启动 daemon 或改写用户的 raw MCP 配置。Documentation Sync 仅在 supported source 变化时执行一次查询,把 query 限制到 changed source paths 与 documented symbols;automatic incremental sync 仅由代理负责。 当 status 无法验证但仓库身份安全时,代理仍执行 `polaris_codegraph_explore`,并返回 `UNKNOWN`/`TREAT_AS_STALE`。图只用于导航;在使用任何结论前,必须完成精确源码/Git fallback。 -代理结果的第一个内容块总是 freshness envelope。`CURRENT / NON_AUTHORITATIVE_CONTEXT` 表示图可作为非权威上下文;`STALE / NAVIGATION_ONLY` 表示已知失效;`UNKNOWN / TREAT_AS_STALE / NAVIGATION_ONLY` 表示无法证明新鲜度;`UNAVAILABLE / NO_GRAPH` 表示没有图输出。任何状态都不宣称与 Git commit 严格一致,`UNKNOWN` 必须按 `TREAT_AS_STALE` 处理,绝不能当作 current。raw `codegraph_explore` 或 `codegraph explore` 仍可由用户带外调用,但不能支持 Polaris 的 `CURRENT` 证据。 +代理结果的第一个内容块总是 freshness envelope,并明确写出 `source_of_truth: false`。CodeGraph 永远不是 source of truth:`CURRENT / NON_AUTHORITATIVE_CONTEXT` 只表示图可作为非权威上下文;`STALE / NAVIGATION_ONLY` 表示已知失效;`UNKNOWN / TREAT_AS_STALE / NAVIGATION_ONLY` 表示无法证明新鲜度;`UNAVAILABLE / NO_GRAPH` 表示没有图输出。任何状态都不宣称与 Git commit 严格一致,`UNKNOWN` 必须按 `TREAT_AS_STALE` 处理,绝不能当作 current。raw `codegraph_explore` 或 `codegraph explore` 仍可由用户带外调用,但对 Polaris 始终是 unverified,不能支持 `CURRENT` 证据。 `STALE` 或 `UNKNOWN`/`TREAT_AS_STALE` 必须先完成 envelope 指定的精确源码/Git fallback。当前具名普通文件直接读取并记录 `READ_SOURCE` 与当前 SHA-256;安全但已删除的路径检查注册 subject 的 Git diff,记录 `INSPECT_GIT_DIFF`、null observed SHA-256 与 base/head/diff hashes;不安全路径或索引级失效执行 `SEARCH_SOURCE`,记录有限、受限的当前文件路径与 SHA-256。图不能扩大冻结 scope、替代源码或决定 Review verdict,Validation 完全不调用 CodeGraph。 @@ -614,8 +614,9 @@ polaris migrate --repo . 3. `0.1.19 → 0.1.20` 使用 `replace_version_and_workflow` 与 `append_mapped_workflow_event`:冻结 workflow 更新到 `0.1.3`,旧 `IMPLEMENTED` / `DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`;旧 R0/R1 `VERIFIED` 也映射回 `VALIDATING`,以便通过 `PASS_AND_CLOSE` 重新提交关闭产物,R2 `VERIFIED` 保持不变。迁移事件记录源/目标状态及旧版本;旧 `events.jsonl` 行不可修改。 4. `0.1.20 → 0.1.21` 只替换协议版本,Workflow 保持 `0.1.3`。迁移会校验并清点 canonical v1/v2 Code Intelligence 历史记录的路径与 SHA-256,保持原字节不变;中断恢复前会重算清单,任何变化都会拒绝继续。v1/v2 此后仅可作为历史证据读取。 5. `0.1.21 → 0.1.22` 只替换协议版本,Workflow 保持 `0.1.3`。迁移记录中的 `retired_code_intelligence_records` 固定为空列表,不重新清点或重写任何 record v3 历史证据。 -6. `.polaris/migrations/MIG--to-.json` 先写为 `IN_PROGRESS`,全部投影更新后改为 `COMPLETED`。迁移锁会记录迁移/任务身份、主机名和 PID;若进程在中间终止,同一主机重新执行命令会接管已死亡的同迁移锁、验证并复用已经追加的事件,不会重复迁移。活跃进程、其他迁移或来源不明的锁不会被自动删除。 -7. 迁移完成后脚本自动运行项目校验;`validate_project.py` 会拒绝未完成记录、缺失/伪造的任务迁移事件或版本不一致。 +6. `0.1.22 → 0.1.23` 同样只替换协议版本并保持 Workflow `0.1.3`;不清点或重写任何 record v3,retirement inventory 固定为空。 +7. `.polaris/migrations/MIG--to-.json` 先写为 `IN_PROGRESS`,全部投影更新后改为 `COMPLETED`。迁移锁会记录迁移/任务身份、主机名和 PID;若进程在中间终止,同一主机重新执行命令会接管已死亡的同迁移锁、验证并复用已经追加的事件,不会重复迁移。活跃进程、其他迁移或来源不明的锁不会被自动删除。 +8. 迁移完成后脚本自动运行项目校验;`validate_project.py` 会拒绝未完成记录、缺失/伪造的任务迁移事件或版本不一致。 没有注册路径时不要手改版本号。应先取得包含所需相邻步骤的 Polaris 版本,逐级完成并分别提交;任何失败都先保留 `.polaris/migrations/` 和事件现场,修复原因后重跑同一迁移命令。 @@ -643,6 +644,8 @@ v0.1.21 新增项目级 Polaris CodeGraph MCP 代理、Host Adapter v3 注册与 v0.1.22 新增 `0.1.21 → 0.1.22` 显式相邻迁移;Workflow 仍为 v0.1.3。该迁移不重新清点或重写 record v3,迁移记录中的 retirement inventory 固定为空。 +v0.1.23 在每次安全的代理查询前强制尝试一次增量 `codegraph sync`,用 runtime bundle v3 固化新策略,并显式标明 CodeGraph 不是 source of truth;Workflow 仍为 v0.1.3。`0.1.22 → 0.1.23` 是不清点、不重写 record v3 的纯版本迁移。 + ## 13. 失败探索与卡点 如果一个技术方向被证据否定,不要让结论只留在聊天中。记录任务内探索: diff --git a/docs/superpowers/plans/2026-08-20-codegraph-clean-head-freshness.md b/docs/superpowers/plans/2026-08-20-codegraph-clean-head-freshness.md new file mode 100644 index 0000000..0a95845 --- /dev/null +++ b/docs/superpowers/plans/2026-08-20-codegraph-clean-head-freshness.md @@ -0,0 +1,290 @@ +# CodeGraph Clean-HEAD Freshness Hardening Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Prevent a clean committed change or clean branch switch from being delivered as current CodeGraph context by attempting one incremental sync before every Polaris proxy query. + +**Architecture:** Keep status, sync, explore, response classification, and post-query status inside the existing project-scoped proxy. Introduce runtime bundle v3 for the stronger refresh policy, retain v1/v2 read compatibility, and keep durable record v3 and Workflow 0.1.3 unchanged. + +**Tech Stack:** Python 3.10+ standard library, `unittest`, JSON/JSON Schema, Git, CodeGraph CLI 1.5.x optional real-CLI coverage. + +**Spec:** `docs/superpowers/specs/2026-08-20-codegraph-clean-head-freshness-design.md` + +## Global Constraints + +- Treat `plan.md` as the v0.1 product and implementation authority. +- Keep the runtime dependency-free beyond the Python standard library. +- Run at most one incremental `codegraph sync` per proxy query. +- Never run `codegraph index`, watcher, daemon, polling, or retries. +- Keep CodeGraph optional and non-gating. +- Keep Workflow version exactly `0.1.3`. +- Preserve all committed Code Intelligence v1/v2/v3 records and runtime bundle v1/v2 evidence. +- Do not modify `/Users/zero/Documents/work/ai/codegraph`. + +--- + +### Task 1: Specify mandatory clean-status synchronization + +**Files:** +- Modify: `tests/test_codegraph.py` +- Modify: `scripts/internal/codegraph_adapter.py:818-877` + +**Interfaces:** +- Consumes: `inspect_status(repo, descriptor, ...) -> dict[str, Any]`. +- Produces: `synchronize_observed_status(repo, descriptor, initial, ..., force_attempt=False) -> dict[str, Any]`; `force_attempt=True` executes one sync even when `initial["needs_sync"]` is false. + +- [ ] **Step 1: Add a failing adapter test for a clean initial status** + + Add a test that supplies clean status, sync success, and clean recheck; call + `synchronize_observed_status(..., force_attempt=True)` and assert the command + sequence is `sync`, `status`, the sync status is `SUCCESS`, and the returned + freshness basis includes `SYNC_ACKNOWLEDGED`. + +- [ ] **Step 2: Run the focused test and verify RED** + + Run: + + python3 -m unittest tests.test_codegraph.CodeGraphTests.test_clean_status_can_force_one_sync_and_recheck -v + + Expected: ERROR because `force_attempt` is not accepted. + +- [ ] **Step 3: Implement the minimal adapter option** + + Extend the function signature with keyword-only `force_attempt: bool = False` + and replace the skip condition with: + + if not force_attempt and not initial["needs_sync"]: + return {"freshness": initial, "sync": skipped, "post_sync_status": None} + + Reject non-boolean values as `NOT_VERIFIED` without invoking CodeGraph. + +- [ ] **Step 4: Verify GREEN and legacy conditional behavior** + + Run the new test plus existing pending, timeout, failure, and `sync_if_needed` + tests. Expected: all PASS and the legacy wrapper still skips clean status. + +- [ ] **Step 5: Commit Task 1** + + git add tests/test_codegraph.py scripts/internal/codegraph_adapter.py + git commit -m "fix: support mandatory bounded CodeGraph sync" + +--- + +### Task 2: Enforce the mandatory proxy query window + +**Files:** +- Modify: `tests/test_codegraph.py` +- Modify: `scripts/internal/code_intelligence_proxy.py:52-56,340-486,489-785` + +**Interfaces:** +- Consumes: Task 1 `synchronize_observed_status(..., force_attempt=True)`. +- Produces: runtime bundle v3 with `REFRESH_POLICY_V3`; retains `REFRESH_POLICY_V2` as frozen historical data. + +- [ ] **Step 1: Add failing proxy behavior tests** + + Add tests that assert: + + - a clean status calls `status`, `sync`, `status`, `explore`, `status`; + - the new bundle is version 3 and policy mode is + `AUTO_INCREMENTAL_BEFORE_QUERY`; + - an unreadable pre-status still syncs and explores but remains `UNKNOWN`; + - a failed mandatory sync explores once and returns `STALE`; + - identity mismatch calls only status. + +- [ ] **Step 2: Run the focused proxy tests and verify RED** + + Expected failures: clean status omits sync, bundle version is 2, and the + unreadable-status call sequence lacks sync. + +- [ ] **Step 3: Implement the fixed window** + + Preserve the original `pre_status`, call the adapter once with + `force_attempt=True`, and derive `effective_pre` from post-sync freshness. + Pass original verification failure evidence into `_delivery` so a successful + sync cannot erase an unreadable pre-status. Do not attempt sync or explore + after a proven identity mismatch. + +- [ ] **Step 4: Verify GREEN and all proxy state combinations** + + Run every test whose name starts with `test_proxy_`. Expected: all PASS. + +- [ ] **Step 5: Commit Task 2** + + git add tests/test_codegraph.py scripts/internal/code_intelligence_proxy.py + git commit -m "fix: sync every Polaris CodeGraph query window" + +--- + +### Task 3: Preserve bundle compatibility and clarify authority + +**Files:** +- Modify: `tests/test_codegraph.py` +- Modify: `scripts/internal/code_intelligence_protocol.py:1205-1367` +- Modify: `scripts/internal/code_intelligence_proxy.py:788-822` +- Modify: `scripts/code_intelligence_mcp.py:166-188` + +**Interfaces:** +- Consumes: bundle versions 1, 2, and 3. +- Produces: new bundle v3 projections into unchanged durable record v3; envelope line `source_of_truth: false`. + +- [ ] **Step 1: Add failing compatibility and envelope tests** + + Assert that bundle v2 is checked against frozen + `AUTO_INCREMENTAL_ON_PENDING`, bundle v3 against + `AUTO_INCREMENTAL_BEFORE_QUERY`, mutated policies are rejected, and the first + MCP content block contains `source_of_truth: false` before graph content. + +- [ ] **Step 2: Run the focused tests and verify RED** + + Expected: bundle v3 is unsupported and the envelope lacks the authority line. + +- [ ] **Step 3: Implement version-aware bundle projection** + + Accept exact base keys for v1, frozen v2 keys/policy for v2, and the same keys + with the new policy for v3. Continue projecting record version 3. Add the + authority line to every rendered envelope without changing delivery enums. + +- [ ] **Step 4: Verify GREEN across bundle, record, and MCP tests** + + Run bundle v1/v2/v3, proxy record projection, MCP ordering, and source fallback + tests. Expected: all PASS. + +- [ ] **Step 5: Commit Task 3** + + git add tests/test_codegraph.py scripts/internal/code_intelligence_protocol.py scripts/internal/code_intelligence_proxy.py scripts/code_intelligence_mcp.py + git commit -m "feat: publish CodeGraph bundle v3 freshness evidence" + +--- + +### Task 4: Add real clean-HEAD regression coverage + +**Files:** +- Modify: `tests/test_codegraph.py` + +**Interfaces:** +- Consumes: installed CodeGraph CLI when available; otherwise the test follows the existing optional real-CLI skip convention. +- Produces: disposable Git repositories proving clean committed and clean branch-switch drift is reconciled by one sync. + +- [ ] **Step 1: Add the committed-symbol regression test** + + Initialize and index a disposable repository, commit a new uniquely named + function without syncing, verify status reports zero pending and explore does + not find it, run one sync, then verify explore finds it. + +- [ ] **Step 2: Add the clean-branch regression test** + + Index the initial branch, create and commit a second branch with a unique + symbol, verify the clean status blind spot, then verify one sync reconciles it. + +- [ ] **Step 3: Run both tests** + + Expected: PASS with CodeGraph installed; clean skip when unavailable. Both + disposable repositories must be outside the Polaris and CodeGraph workspaces. + +- [ ] **Step 4: Commit Task 4** + + git add tests/test_codegraph.py + git commit -m "test: cover clean HEAD CodeGraph drift" + +--- + +### Task 5: Publish protocol 0.1.23 without changing Workflow + +**Files:** +- Modify: `VERSION` +- Modify: `pyproject.toml` +- Modify: `templates/project.json` +- Modify: `templates/task-sources/state.json` +- Modify: `templates/task/state.json` +- Modify: `workflow/migrations.json` +- Modify: `scripts/internal/migration_protocol.py` +- Modify: `tests/test_codegraph.py` +- Modify: `tests/test_core.py` + +**Interfaces:** +- Produces: adjacent migration `0.1.22-to-0.1.23` using `replace_version` and `append_version_event`; Workflow remains 0.1.3. + +- [ ] **Step 1: Add failing version and migration tests** + + Assert every authority surface publishes 0.1.23, the migration is adjacent and + version-only, it preserves existing record v3 bytes, and a workflow change is + rejected. + +- [ ] **Step 2: Run focused tests and verify RED** + + Expected: current authorities remain 0.1.22 and the migration is absent. + +- [ ] **Step 3: Update versions and append the migration** + + Change only Polaris/package versions to 0.1.23. Append exactly one migration + entry from 0.1.22 to 0.1.23 with Workflow 0.1.3 on both sides. Keep its + retirement inventory empty through the existing non-inventory behavior. + +- [ ] **Step 4: Verify GREEN across migration and vendoring tests** + + Run version, migration, install-manifest, vendoring, and self-contained target + tests. Expected: all PASS. + +- [ ] **Step 5: Commit Task 5** + + git add VERSION pyproject.toml templates/project.json templates/task-sources/state.json templates/task/state.json workflow/migrations.json scripts/internal/migration_protocol.py tests/test_codegraph.py tests/test_core.py + git commit -m "chore: advance Polaris protocol to 0.1.23" + +--- + +### Task 6: Synchronize product authority and verify end to end + +**Files:** +- Modify: `plan.md` +- Modify: `README.md` +- Modify: `README.zh-CN.md` +- Modify: `docs/USAGE.md` +- Modify: `skills/code-intelligence/SKILL.md` +- Modify: `skills/architecture-planning/SKILL.md` +- Modify: `skills/implementation/SKILL.md` +- Modify: `skills/documentation-sync/SKILL.md` +- Modify: `skills/adversarial-review/SKILL.md` +- Modify: `templates/AGENTS.md` +- Modify: `tests/test_codegraph.py` +- Modify: `tests/test_core.py` + +**Interfaces:** +- Produces: one consistent user and Agent contract for mandatory incremental sync, finite freshness, and non-authoritative graph usage. + +- [ ] **Step 1: Add failing surface-contract tests** + + Require all CodeGraph-capable Skills and user authorities to state that every + proxy query attempts one incremental sync, raw results remain unverified, and + CodeGraph is never source of truth. Require release notes to name protocol + 0.1.23 and Workflow 0.1.3. + +- [ ] **Step 2: Run focused tests and verify RED** + + Expected: existing surfaces still say sync occurs only when pending exists. + +- [ ] **Step 3: Update canonical Skills, template, plan, and documentation** + + Replace pending-only wording with mandatory per-query incremental sync. State + that status pending counts alone do not bind the graph to clean HEAD. Retain + all source/Git fallback, raw-tool, ownership, optionality, and Validation + boundaries. + +- [ ] **Step 4: Run focused documentation and Skill tests** + + Expected: all surface and documentation consistency tests PASS. + +- [ ] **Step 5: Run complete verification** + + python3 tests/run_tests.py + python3 scripts/check_docs.py --help + git diff --check + git status --short + git -C /Users/zero/Documents/work/ai/codegraph status --short + + Expected: complete suite PASS, no whitespace errors, only intended Polaris + files changed, and the CodeGraph repository remains clean. + +- [ ] **Step 6: Commit Task 6** + + git add plan.md README.md README.zh-CN.md docs/USAGE.md skills templates/AGENTS.md tests/test_codegraph.py tests/test_core.py docs/superpowers/specs/2026-08-20-codegraph-clean-head-freshness-design.md docs/superpowers/plans/2026-08-20-codegraph-clean-head-freshness.md + git commit -m "docs: define clean HEAD CodeGraph freshness" diff --git a/docs/superpowers/specs/2026-08-20-codegraph-clean-head-freshness-design.md b/docs/superpowers/specs/2026-08-20-codegraph-clean-head-freshness-design.md new file mode 100644 index 0000000..49d4702 --- /dev/null +++ b/docs/superpowers/specs/2026-08-20-codegraph-clean-head-freshness-design.md @@ -0,0 +1,116 @@ +# Polaris CodeGraph clean-HEAD freshness hardening design + +## Status + +- Date: 2026-08-20 +- Status: approved for implementation +- Scope: Polaris CodeGraph proxy, evidence, documentation, and adjacent version migration +- CodeGraph repository: read-only dependency; do not modify it + +## Problem + +Polaris 0.1.22 runs `codegraph sync` only when `codegraph status --json` +reports pending added, modified, or removed files. CodeGraph 1.5.0 uses Git +status as its fast-path candidate list. After an indexed file is changed and +committed, or after switching to another clean branch in the same worktree, +Git status can be empty even though the CodeGraph database still describes the +previous clean HEAD. Both Polaris status observations can therefore report zero +pending changes, a neutral explore response can omit a newly committed symbol, +and the proxy can incorrectly deliver `CURRENT`. + +## Goals + +1. Every Polaris proxy query automatically attempts exactly one bounded + incremental `codegraph sync`, even when the initial status reports zero + pending changes. +2. A clean committed change or clean branch switch must not be delivered as + current merely because Git status is empty. +3. `CURRENT` remains a finite at-check observation and is explicitly never a + source of truth. +4. Status, sync, response, or identity failures remain conservative and + non-gating. +5. Existing durable Code Intelligence records and runtime bundle v1/v2 remain + readable without rewriting them. + +## Non-goals + +- Do not run `codegraph index`. +- Do not start, configure, wait for, or manage a watcher or daemon. +- Do not make CodeGraph a workflow gate. +- Do not change Workflow 0.1.3 states or transitions. +- Do not claim that CodeGraph is strictly bound to a Git commit. +- Do not modify the CodeGraph repository. + +## Query window + +The proxy-owned window is: + +1. Validate the fixed repository, task, stage, query identity, policy, marker, + and CLI availability. +2. Run pre-status. +3. Refuse sync and explore when pre-status proves a project or worktree identity + mismatch. +4. Otherwise run exactly one `codegraph sync --quiet`, regardless of pending + counts. +5. Run one post-sync status check. +6. Run exactly one explore when repository identity remains safe. +7. Classify the response and run one post-query status check. +8. Conservatively merge every observation and emit the envelope before graph + content. + +An unreadable pre-status does not block a safe sync or explore, but it remains +verification-failure evidence: later clean observations must not promote the +delivery above `UNKNOWN`. A known stale signal remains `STALE` even when another +observation is unreadable. A failed sync remains `STALE` and the graph may still +be returned only for navigation. + +## Evidence compatibility + +New proxy calls write runtime bundle version 3 with this fixed policy: + + { + "mode": "AUTO_INCREMENTAL_BEFORE_QUERY", + "max_sync_attempts": 1, + "full_rebuild": "USER_ONLY" + } + +Bundle v1 and v2 remain projectable under their frozen historical contracts. +Durable Code Intelligence records remain record version 3; new bundle +provenance guarantees the stronger write path without invalidating already +committed record v3 evidence. + +## User-visible authority + +Every freshness envelope includes `source_of_truth: false`. Delivery semantics +remain: + +- `CURRENT / NON_AUTHORITATIVE_CONTEXT`: bounded context observed clean after + the mandatory sync; source and Git remain authority. +- `STALE / NAVIGATION_ONLY`: known stale signal; exact source/Git fallback is + required. +- `UNKNOWN / TREAT_AS_STALE / NAVIGATION_ONLY`: freshness cannot be proved; + exact source/Git fallback is required. +- `UNAVAILABLE / NO_GRAPH`: no graph content. + +Raw CodeGraph tools remain out-of-band, unverified, and unable to support +Polaris `CURRENT` evidence. + +## Versioning + +Polaris advances from 0.1.22 to 0.1.23. Workflow remains 0.1.3. The adjacent +0.1.22-to-0.1.23 migration replaces only the Polaris version and appends the +normal version event; it neither inventories nor rewrites Code Intelligence +records. + +## Acceptance tests + +- A clean initial status still causes exactly one sync before explore. +- A clean committed new symbol is discovered after the mandatory sync. +- A clean branch switch is reconciled before explore. +- Unknown pre-status is never promoted to current by later clean observations. +- Sync failure or unhealthy post-sync status cannot become current. +- Project/worktree mismatch runs neither sync nor explore. +- Bundle v1/v2 remain readable; new bundles are v3 with the fixed policy. +- The first MCP content block states `source_of_truth: false`. +- The full standard-library-only test suite passes without CodeGraph installed. + diff --git a/plan.md b/plan.md index b10eb1e..8399c2b 100644 --- a/plan.md +++ b/plan.md @@ -2,7 +2,7 @@ > 状态:Implementation underway > 目标版本:v0.1 -> 当前协议:`0.1.22`;Workflow:`0.1.3` +> 当前协议:`0.1.23`;Workflow:`0.1.3` > 产品形态:Repo-native Skill System > 宿主 Runtime:声明式可扩展;v0.1 内置 Codex、Claude Code > @@ -52,7 +52,7 @@ v0.1 的目标是验证这套工程方法能否提高 Horizon / Vision 上复杂 - 项目初始化、任务初始化、状态转换、结构校验、文档影响检查、工作集生成脚本。 - 通过 pip 安装、只暴露用户命令的薄 `polaris` CLI;保留原 Python 脚本入口。 - 只读聚合 Doctor;复用现有 Validator,一次输出环境、协议、Authority、任务与操作残留的证据和人工动作。 -- 可选 Code Intelligence 协议;唯一正式 Provider 是 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph),按阶段检查新鲜度、必要时有限同步、保存精简证据,并在任何不可用或失败时非阻断降级。 +- 可选 Code Intelligence 协议;唯一正式 Provider 是 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph),每次安全代理查询前有限同步一次、保存精简证据,并在任何不可用或失败时非阻断降级。 - 独立 Implementer worker、不可变 Implementation handoff 与事件驱动实时进度快照。 - 验收标准绑定的线性 `implementation_steps`;步骤只能依次推进或在末尾追加,最终结果冻结进 Implementation artifact。 - 独立 worker context 的对抗审查协议。 @@ -452,7 +452,7 @@ v0.1 不设置 `FAILED`:可修复失败通过治理回路处理,外部阻塞 `.polaris/workflow.json` 保存当前项目实际使用且版本锁定的节点、边、依赖和门禁 ID;`tools/polaris/workflow/default-workflow.json` 只用于初始化。`transition_task.py` 只接受图中边并先运行对应 validators,Skill 不直接编辑 `state` 字段。v0.1 遇到 `polaris_version` 或 `workflow_version` 不匹配时拒绝正常执行,不做隐式迁移。 -版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。Migration protocol v2 保留 `replace_version` / `append_version_event`,并增加 `replace_version_and_workflow` / `append_mapped_workflow_event`。`0.1.19 → 0.1.20` 原子替换冻结 workflow 为 `0.1.3`,追加带源/目标状态及旧版本字段的迁移事件;旧 `IMPLEMENTED`、`DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`,旧 R0/R1 `VERIFIED` 映射到 `VALIDATING` 以重新提交 `PASS_AND_CLOSE`,仅 R2 保持 `VERIFIED`。`0.1.20 → 0.1.21` 保持 Workflow `0.1.3`,新增项目级 CodeGraph 代理、Host Adapter v3 与 record v3,并把 canonical v1/v2 record 作为仅可读取的不可变历史证据按路径和 SHA-256 清点;迁移恢复前必须重算并比对清单。`0.1.21 → 0.1.22` 同样保持 Workflow `0.1.3`,但 retirement inventory 固定为空,不重新清点或重写任何 record v3 证据。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED`、各任务 sequence 和状态映射;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、未声明的 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。 +版本升级必须先 vendoring 目标协议,再显式运行 vendored `migrate_project.py`。`workflow/migrations.json` 是迁移路径唯一且 append-only 的注册表,一次只执行一个从当前版本到目标版本的相邻步骤;历史步骤必须保留以校验已提交记录。Migration protocol v2 保留 `replace_version` / `append_version_event`,并增加 `replace_version_and_workflow` / `append_mapped_workflow_event`。`0.1.19 → 0.1.20` 原子替换冻结 workflow 为 `0.1.3`,追加带源/目标状态及旧版本字段的迁移事件;旧 `IMPLEMENTED`、`DOCS_SYNCED` 映射到 `IMPLEMENTING`,旧 `REVIEWED` 映射到 `VALIDATING`,旧 R0/R1 `VERIFIED` 映射到 `VALIDATING` 以重新提交 `PASS_AND_CLOSE`,仅 R2 保持 `VERIFIED`。`0.1.20 → 0.1.21` 保持 Workflow `0.1.3`,新增项目级 CodeGraph 代理、Host Adapter v3 与 record v3,并把 canonical v1/v2 record 作为仅可读取的不可变历史证据按路径和 SHA-256 清点;迁移恢复前必须重算并比对清单。`0.1.21 → 0.1.22` 与 `0.1.22 → 0.1.23` 同样保持 Workflow `0.1.3`,retirement inventory 固定为空,不重新清点或重写任何 record v3 证据。迁移以 `.polaris/migrations/MIG-*.json` 记录 `IN_PROGRESS/COMPLETED`、各任务 sequence 和状态映射;重跑必须可恢复且不得重复事件。未知路径、跨版本跳跃、未声明的 workflow 变化、任务集合并发变化和不完整记录都必须机械拒绝。 迁移占用任务转换锁时必须写入结构化 owner:迁移 ID、任务 ID、主机名、PID 和创建时间。重跑只允许接管同一迁移在同一主机上、且原 PID 已确认不存在的锁;活跃 PID、其他迁移、其他主机、空锁或损坏锁一律拒绝。这样既能从进程崩溃或机器重启恢复,又不把真实并发误判为遗留锁。 @@ -488,7 +488,7 @@ AGENTS.md - v0.1 的唯一正式 Provider 是 [colbymchenry/codegraph](https://github.com/colbymchenry/codegraph)。项目级 `polaris-codegraph` MCP 只暴露 `polaris_codegraph_explore`;raw `codegraph_explore` 与 shell 仍可带外使用,但不能支持 Polaris `CURRENT` 证据。 - `.codegraph/` 与 CodeGraph 安装、初始化、配置、raw MCP、watcher 和 daemon 由用户拥有。Polaris 只非破坏地管理自身项目代理注册;缺少 marker 或策略禁用时直接回退源码,不生成阶段 record。 -- 代理在一个有界窗口内完成 pre-status、存在 pending changes 时自动且至多一次增量 `codegraph sync`、一次 explore 和 post-status,并先返回 freshness envelope。阶段不分别选择 status/sync;不等待、轮询或重试;全量 `codegraph index` 始终由用户主动执行。 +- 代理确认仓库身份安全后,在每次代理查询前尝试且至多尝试一次增量 `codegraph sync`,再执行一次 explore 和 post-status,并先返回 freshness envelope;零 pending changes 不能证明索引对应 clean HEAD 或当前分支。阶段不分别选择 status/sync;不等待、轮询或重试;全量 `codegraph index` 始终由用户主动执行。CodeGraph 永远不是 source of truth,任何 graph 状态都不能取代当前源码、Git、构建或测试。 - envelope 状态为 `CURRENT / NON_AUTHORITATIVE_CONTEXT`、`STALE / NAVIGATION_ONLY`、`UNKNOWN / TREAT_AS_STALE / NAVIGATION_ONLY` 或 `UNAVAILABLE / NO_GRAPH`。`STALE`/`UNKNOWN` 必须先完成具名 `READ_SOURCE`、删除路径 `INSPECT_GIT_DIFF` 或索引级 `SEARCH_SOURCE` 的精确源码/Git fallback;`UNKNOWN` 必须按 `TREAT_AS_STALE` 处理,不得提升为 current。 - 当 status 无法验证但仓库身份安全时,代理仍执行 `polaris_codegraph_explore` 并返回 `UNKNOWN`/`TREAT_AS_STALE`;图仅用于导航,任何结论都必须先完成精确源码/Git fallback。 - Planning、Implementation 与 Reviewer 只在冻结范围内使用图关系;Implementation 修改关系后必须 fresh proxy call,Reviewer 必须独立调用且不得继承 Implementer envelope。Documentation Sync 仅在 supported source 改变时,对 changed paths/symbols 执行一次查询,自动增量同步由代理负责。Validation 完全不调用 CodeGraph。 diff --git a/pyproject.toml b/pyproject.toml index b58db4a..93637b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "corona-polaris" -version = "0.1.22" +version = "0.1.23" description = "Repo-native AI engineering workflow command dispatcher" requires-python = ">=3.10" dependencies = [] diff --git a/scripts/code_intelligence_mcp.py b/scripts/code_intelligence_mcp.py index 468182d..72a2e4d 100644 --- a/scripts/code_intelligence_mcp.py +++ b/scripts/code_intelligence_mcp.py @@ -24,7 +24,10 @@ SERVER_ROOT = Path(__file__).resolve().parent.parent TOOL = { "name": TOOL_NAME, - "description": "Run one bounded Polaris CodeGraph freshness window.", + "description": ( + "Run one bounded Polaris CodeGraph window with one pre-query " + "incremental sync; graph output is never source of truth." + ), "inputSchema": { "type": "object", "required": [ diff --git a/scripts/internal/code_intelligence_protocol.py b/scripts/internal/code_intelligence_protocol.py index 0e17d2d..c5930db 100644 --- a/scripts/internal/code_intelligence_protocol.py +++ b/scripts/internal/code_intelligence_protocol.py @@ -1030,7 +1030,11 @@ def _validate_v3_record_value( for point in delivery["stale_points"]: _validate_v3_stale_point(repo, point) effective = post_sync if post_sync is not None else pre - observed_points: list[dict[str, Any]] = [] + observed_points: list[dict[str, Any]] = [ + point + for point in pre["stale_points"] + if point["reason"] == "STATUS_UNREADABLE" + ] for observation in (effective, post): if observation is None: continue @@ -1219,7 +1223,11 @@ def record_proxy_bundle( require_regular_file(candidate, "CodeGraph proxy bundle") bundle_digest = file_sha256(candidate) bundle = read_json(candidate) - from .code_intelligence_proxy import REFRESH_POLICY, resolve_stage_context + from .code_intelligence_proxy import ( + LEGACY_REFRESH_POLICY_V2, + REFRESH_POLICY, + resolve_stage_context, + ) version = bundle.get("bundle_version") base_keys = { @@ -1231,11 +1239,29 @@ def record_proxy_bundle( if version == 1: _require_exact_keys(bundle, base_keys, "CodeGraph proxy bundle") elif version == 2: + _require_exact_keys( + bundle, {*base_keys, "refresh_policy"}, "CodeGraph proxy bundle" + ) + if bundle["refresh_policy"] != LEGACY_REFRESH_POLICY_V2: + raise RuleFailure("CodeGraph proxy bundle has an invalid refresh policy") + elif version == 3: _require_exact_keys( bundle, {*base_keys, "refresh_policy"}, "CodeGraph proxy bundle" ) if bundle["refresh_policy"] != REFRESH_POLICY: raise RuleFailure("CodeGraph proxy bundle has an invalid refresh policy") + delivery = bundle.get("delivery") + if ( + isinstance(delivery, dict) + and delivery.get("state") != "UNAVAILABLE" + and ( + not isinstance(bundle.get("sync"), dict) + or bundle["sync"].get("status") not in {"SUCCESS", "FAILED"} + ) + ): + raise RuleFailure( + "CodeGraph v3 proxy bundle lacks its mandatory sync attempt" + ) else: raise RuleFailure("CodeGraph proxy bundle has an unsupported identity") if bundle["proxy"] != { diff --git a/scripts/internal/code_intelligence_proxy.py b/scripts/internal/code_intelligence_proxy.py index 0317829..453f113 100644 --- a/scripts/internal/code_intelligence_proxy.py +++ b/scripts/internal/code_intelligence_proxy.py @@ -49,11 +49,16 @@ "DOCUMENTATION_SYNC": {"IMPLEMENTING"}, "REVIEW": {"REVIEWING"}, } -REFRESH_POLICY = { +LEGACY_REFRESH_POLICY_V2 = { "mode": "AUTO_INCREMENTAL_ON_PENDING", "max_sync_attempts": 1, "full_rebuild": "USER_ONLY", } +REFRESH_POLICY = { + "mode": "AUTO_INCREMENTAL_BEFORE_QUERY", + "max_sync_attempts": 1, + "full_rebuild": "USER_ONLY", +} _INDEX_FALLBACK = { "scope": "INDEX", "path": None, @@ -343,8 +348,10 @@ def _delivery( classification: dict[str, Any] | None, post_status: dict[str, Any] | None, *, + initial_pre: dict[str, Any] | None = None, forced_unknown: str | None = None, ) -> dict[str, Any]: + initial_observation = initial_pre or effective_pre checked_at = ( (post_status or {}).get("checked_at") or (classification or {}).get("checked_at") @@ -353,11 +360,19 @@ def _delivery( or utc_now() ) points = _deduplicate([ + *( + point + for point in _observation_points(initial_observation) + if point.get("reason") == "STATUS_UNREADABLE" + ), *_observation_points(effective_pre), *((classification or {}).get("stale_points", [])), *_observation_points(post_status), ]) - pre_status_blocks_query = _pre_status_blocks_query(effective_pre) + initial_status_blocks_query = _pre_status_blocks_query(initial_observation) + pre_status_blocks_query = ( + initial_status_blocks_query or _pre_status_blocks_query(effective_pre) + ) post_status_blocks_query = ( post_status is not None and _pre_status_blocks_query(post_status) ) @@ -368,7 +383,9 @@ def _delivery( or response_identity_mismatch or forced_unknown is not None ) - pre_status_unknown = _is_unknown(effective_pre) + pre_status_unknown = ( + _is_unknown(initial_observation) or _is_unknown(effective_pre) + ) known_stale = ( any(point.get("reason") != "STATUS_UNREADABLE" for point in points) or effective_pre.get("status") in {"PARTIAL_STALE", "INDEX_STALE"} @@ -400,6 +417,7 @@ def _delivery( ) errors = [ forced_unknown, + initial_observation.get("error"), effective_pre.get("error"), query_result.get("error"), (classification or {}).get("error"), @@ -414,7 +432,11 @@ def _delivery( reason = "WORKTREE_MISMATCH" elif pre_status_blocks_query or post_status_blocks_query: identity_observation = ( - effective_pre if pre_status_blocks_query else post_status + initial_observation + if initial_status_blocks_query + else effective_pre + if _pre_status_blocks_query(effective_pre) + else post_status ) assert identity_observation is not None reason = ( @@ -454,7 +476,8 @@ def _delivery( if pre_status_unknown: reason = ( "PROJECT_MISMATCH" - if "different project" in str(effective_pre.get("error", "")).lower() + if "different project" + in str(initial_observation.get("error", "")).lower() else "STATUS_UNREADABLE" ) elif query_result.get("status") != "SUCCESS": @@ -496,7 +519,7 @@ def _bundle_base( ) -> dict[str, Any]: project = read_json(repo / ".polaris/project.json") return { - "bundle_version": 2, + "bundle_version": 3, "refresh_policy": dict(REFRESH_POLICY), "proxy": { "server_id": "polaris-codegraph", @@ -622,25 +645,24 @@ def execute_proxy_query( "envelope": render_freshness_envelope(bundle), } - if pre_status.get("needs_sync"): - synchronized = synchronize_observed_status( - repo, descriptor, pre_status, runner=runner + synchronized = synchronize_observed_status( + repo, descriptor, pre_status, runner=runner, force_attempt=True + ) + bundle["sync"] = synchronized["sync"] + bundle["post_sync_status"] = synchronized["post_sync_status"] + effective_pre = synchronized["freshness"] + if ( + bundle["post_sync_status"] is not None + and ( + _pre_status_blocks_query(bundle["post_sync_status"]) + or bundle["post_sync_status"].get("status") == "UNAVAILABLE" ) - bundle["sync"] = synchronized["sync"] - bundle["post_sync_status"] = synchronized["post_sync_status"] - effective_pre = synchronized["freshness"] - if ( - bundle["post_sync_status"] is not None - and ( - _pre_status_blocks_query(bundle["post_sync_status"]) - or bundle["post_sync_status"].get("status") == "UNAVAILABLE" - ) - and bundle["post_sync_status"].get("error") - ): - effective_pre = { - **effective_pre, - "error": bundle["post_sync_status"]["error"], - } + and bundle["post_sync_status"].get("error") + ): + effective_pre = { + **effective_pre, + "error": bundle["post_sync_status"]["error"], + } post_sync_status = bundle["post_sync_status"] if post_sync_status is not None and ( @@ -657,6 +679,7 @@ def execute_proxy_query( bundle["query"], None, None, + initial_pre=pre_status, ) _write_bundle(bundle_path, bundle) return { @@ -699,6 +722,7 @@ def execute_proxy_query( bundle["query"], None, None, + initial_pre=pre_status, ) _write_bundle(bundle_path, bundle) return { @@ -772,6 +796,7 @@ def execute_proxy_query( bundle["query"], classification, post_status, + initial_pre=pre_status, forced_unknown=forced_unknown, ) if response is None: @@ -806,6 +831,7 @@ def render_freshness_envelope(bundle: dict[str, Any]) -> str: "[POLARIS_CODEGRAPH_FRESHNESS]", f"state: {delivery['state']}", f"record_status: {delivery['record_status']}", + "source_of_truth: false", f"reason: {delivery['reason']}", f"checked_at: {delivery['checked_at']}", f"pending_added: {pending.get('added', 0)}", @@ -815,7 +841,7 @@ def render_freshness_envelope(bundle: dict[str, Any]) -> str: f"required_fallback: {delivery['required_fallback']}", f"evidence_bundle: {bundle_path}", ] - lines.insert(3, f"freshness: {freshness}") + lines.insert(4, f"freshness: {freshness}") if error: lines.append(f"error: {error}") lines.append("[/POLARIS_CODEGRAPH_FRESHNESS]") diff --git a/scripts/internal/codegraph_adapter.py b/scripts/internal/codegraph_adapter.py index 5096a12..5c419ad 100644 --- a/scripts/internal/codegraph_adapter.py +++ b/scripts/internal/codegraph_adapter.py @@ -823,9 +823,12 @@ def synchronize_observed_status( runner: Runner = subprocess.run, status_timeout_seconds: float = 15, sync_timeout_seconds: float = 120, + force_attempt: bool = False, ) -> dict[str, Any]: """Synchronize one already-observed status at most once, then recheck once.""" try: + if type(force_attempt) is not bool: + raise ValueError("CodeGraph forced sync policy must be a boolean") status_timeout = _validated_timeout(status_timeout_seconds) sync_timeout = _validated_timeout(sync_timeout_seconds) except ValueError as error: @@ -839,7 +842,7 @@ def synchronize_observed_status( unavailable = _sync_result("UNAVAILABLE", None, None) if initial["status"] == "UNAVAILABLE" or _marker_path(repo, descriptor) is None: return {"freshness": initial, "sync": unavailable, "post_sync_status": None} - if not initial["needs_sync"]: + if not force_attempt and not initial["needs_sync"]: return {"freshness": initial, "sync": skipped, "post_sync_status": None} try: diff --git a/skills/adversarial-review/SKILL.md b/skills/adversarial-review/SKILL.md index c54406b..414d061 100644 --- a/skills/adversarial-review/SKILL.md +++ b/skills/adversarial-review/SKILL.md @@ -22,4 +22,4 @@ Return a concise structured result to the dispatcher with verdict, Review attemp Only the Reviewer context may write `ACCEPT`. -Proxy evidence contract: the proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. `CURRENT` is `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is out-of-band and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. Graph evidence cannot determine the Review verdict. +Proxy evidence contract: the proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only `NON_AUTHORITATIVE_CONTEXT`. `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is unverified, out-of-band, and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. Graph evidence cannot determine the Review verdict. diff --git a/skills/architecture-planning/SKILL.md b/skills/architecture-planning/SKILL.md index f1dd333..e31fe9a 100644 --- a/skills/architecture-planning/SKILL.md +++ b/skills/architecture-planning/SKILL.md @@ -21,4 +21,4 @@ After the transition succeeds, reload state and emit `[POLARIS:PLAN_READY]` with Do not modify the frozen Work Item or start implementation from this stage. -Proxy evidence contract: the proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. `CURRENT` is `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is out-of-band and cannot back `CURRENT` Polaris evidence. After a proxy operation, write annotations and run `record_code_intelligence.py --repo . --bundle --annotations ` to project v3; without a proxy operation, omit the Code Intelligence record. Never run `codegraph init` or manage the Provider. +Proxy evidence contract: the proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only `NON_AUTHORITATIVE_CONTEXT`. `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is unverified, out-of-band, and cannot back `CURRENT` Polaris evidence. After a proxy operation, write annotations and run `record_code_intelligence.py --repo . --bundle --annotations ` to project v3; without a proxy operation, omit the Code Intelligence record. Never run `codegraph init` or manage the Provider. diff --git a/skills/code-intelligence/SKILL.md b/skills/code-intelligence/SKILL.md index 3c84a32..83720fd 100644 --- a/skills/code-intelligence/SKILL.md +++ b/skills/code-intelligence/SKILL.md @@ -8,7 +8,7 @@ description: Internal optional Polaris stage support for bounded CodeGraph relat CodeGraph is optional navigation context. Source, Git, builds, tests, frozen artifacts, Review, Validation, and Human decisions remain authority. 1. Load `.polaris/code-intelligence.json` and project rules. If policy disables Code Intelligence or the repository root lacks `.codegraph/`, skip the proxy, use source/Git, and omit the Code Intelligence record because no proxy operation ran. Never run `codegraph init`. -2. Call only `polaris_codegraph_explore` with task ID, stage, next `CIQ-NNN`, purpose, and query. The proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. When status cannot be verified but the project has a safe repository identity, the proxy still calls `polaris_codegraph_explore` and returns `UNKNOWN`/`TREAT_AS_STALE`; graph content remains navigation-only and any conclusion requires the exact source/Git fallback. The project registration fixes the repository root. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after an envelope requires the stage fallback. +2. Call only `polaris_codegraph_explore` with task ID, stage, next `CIQ-NNN`, purpose, and query. The proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only non-authoritative context. When status cannot be verified but the project has a safe repository identity, the proxy still calls `polaris_codegraph_explore` and returns `UNKNOWN`/`TREAT_AS_STALE`; graph content remains navigation-only and any conclusion requires the exact source/Git fallback. The project registration fixes the repository root. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after an envelope requires the stage fallback. 3. Read the `freshness envelope` before any graph content: - `CURRENT` with `usage: NON_AUTHORITATIVE_CONTEXT` permits the graph only as non-authoritative context. - `STALE` and `UNKNOWN`/`TREAT_AS_STALE` with `usage: NAVIGATION_ONLY` require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only the resulting current source/Git evidence does. Index-wide uncertainty affects the entire graph response. `UNKNOWN` is never current. diff --git a/skills/documentation-sync/SKILL.md b/skills/documentation-sync/SKILL.md index b5bb40b..a7849bd 100644 --- a/skills/documentation-sync/SKILL.md +++ b/skills/documentation-sync/SKILL.md @@ -20,4 +20,4 @@ Do not run workflow transitions or emit a Polaris checkpoint marker. The main `{ Do not edit Review, Validation, Result, event, or state artifacts directly. -Proxy evidence contract: the proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. `CURRENT` is `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is out-of-band and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. Graph evidence never gates documentation checks or state changes. +Proxy evidence contract: the proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only `NON_AUTHORITATIVE_CONTEXT`. `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is unverified, out-of-band, and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. Graph evidence never gates documentation checks or state changes. diff --git a/skills/implementation/SKILL.md b/skills/implementation/SKILL.md index 5888516..ac921e5 100644 --- a/skills/implementation/SKILL.md +++ b/skills/implementation/SKILL.md @@ -19,4 +19,4 @@ description: Internal Polaris worker stage for an explicitly started `{{skill:en Do not run workflow transitions, Review, Validation, or task closure. Do not emit a Polaris checkpoint marker; the main `{{skill:engineering-task}}` validates the artifact and continues this same task for `{{skill:documentation-sync}}` while authority remains `IMPLEMENTING`. -Proxy evidence contract: the proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. `CURRENT` is `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is out-of-band and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. +Proxy evidence contract: the proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only `NON_AUTHORITATIVE_CONTEXT`. `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. A raw `codegraph_explore` or `codegraph explore` result is unverified, out-of-band, and cannot back `CURRENT` Polaris evidence. Never run `codegraph init` or manage the Provider. diff --git a/templates/AGENTS.md b/templates/AGENTS.md index 6eaaddb..27cb984 100644 --- a/templates/AGENTS.md +++ b/templates/AGENTS.md @@ -16,8 +16,8 @@ ## Optional CodeGraph rules - Use CodeGraph only when project policy permits it and the repository root already contains `.codegraph/`. Otherwise skip the proxy, use source/Git, and omit the Code Intelligence record; agents never run `codegraph init`. -- For Polaris evidence call only `polaris_codegraph_explore` and read its freshness envelope before graph content. The proxy automatically runs at most one incremental `codegraph sync` when pending changes exist and never runs `codegraph index`. When status cannot be verified but the project has a safe repository identity, the proxy still calls `polaris_codegraph_explore` and returns `UNKNOWN`/`TREAT_AS_STALE`; graph content remains navigation-only and any conclusion requires the exact source/Git fallback. `CURRENT` is `NON_AUTHORITATIVE_CONTEXT`; `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. `UNAVAILABLE` means no graph. +- For Polaris evidence call only `polaris_codegraph_explore` and read its freshness envelope before graph content. The proxy attempts exactly one incremental `codegraph sync` before every proxy query, including when status reports zero pending changes, and never runs `codegraph index`. Zero pending status does not prove clean HEAD. CodeGraph is never a source of truth; `CURRENT` is only `NON_AUTHORITATIVE_CONTEXT`. When status cannot be verified but the project has a safe repository identity, the proxy still calls `polaris_codegraph_explore` and returns `UNKNOWN`/`TREAT_AS_STALE`; graph content remains navigation-only and any conclusion requires the exact source/Git fallback. `STALE` and `UNKNOWN`/`TREAT_AS_STALE` are `NAVIGATION_ONLY` and require the exact source/Git fallback before any conclusion is used. `NAVIGATION_ONLY` never substantiates an edit or conclusion, even after fallback; only completed current source/Git fallback evidence does, and index-wide uncertainty affects the entire graph response. Use no separate status/sync MCP tool and do not retry, poll, wait, or run another query after fallback is required. `UNAVAILABLE` means no graph. - Complete fallbacks exactly: a safe current regular file uses `READ_SOURCE` with current SHA-256; a safe missing/deleted path uses `INSPECT_GIT_DIFF` with null observed SHA-256 and bound base/head/diff hashes; unsafe or index-wide stale/unknown results use `SEARCH_SOURCE` with finite confined POSIX result paths and current hashes. -- A raw `codegraph_explore` or `codegraph explore` result is out-of-band and cannot back `CURRENT` Polaris evidence. If the proxy ran, write annotations and run `record_code_intelligence.py --repo . --bundle --annotations ` to project v3; do not hand-author a record. +- A raw `codegraph_explore` or `codegraph explore` result is unverified, out-of-band, and cannot back `CURRENT` Polaris evidence. If the proxy ran, write annotations and run `record_code_intelligence.py --repo . --bundle --annotations ` to project v3; do not hand-author a record. - Never install, initialize, start, authenticate, configure, reconfigure, or manage CodeGraph, its watcher, daemon, lock, raw MCP registration, or index. CodeGraph cannot expand frozen scope or replace source, Git, builds, tests, Review, Validation, or Human gates. - Preserve any installer-managed marker block exactly as owned by that installer; Polaris does not add, edit, or remove installer marker fences. diff --git a/templates/project.json b/templates/project.json index 124869f..7e38742 100644 --- a/templates/project.json +++ b/templates/project.json @@ -1,6 +1,6 @@ { "project_id": "PROJECT_ID", - "polaris_version": "0.1.22", + "polaris_version": "0.1.23", "workflow_version": "0.1.3", "active_tasks": [] } diff --git a/templates/task-sources/state.json b/templates/task-sources/state.json index 256dcb7..c851135 100644 --- a/templates/task-sources/state.json +++ b/templates/task-sources/state.json @@ -1,6 +1,6 @@ { "task_id": "TASK-0001", - "polaris_version": "0.1.22", + "polaris_version": "0.1.23", "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", diff --git a/templates/task/state.json b/templates/task/state.json index 256dcb7..c851135 100644 --- a/templates/task/state.json +++ b/templates/task/state.json @@ -1,6 +1,6 @@ { "task_id": "TASK-0001", - "polaris_version": "0.1.22", + "polaris_version": "0.1.23", "workflow_version": "0.1.3", "current_revision": 1, "status": "DRAFT", diff --git a/tests/test_codegraph.py b/tests/test_codegraph.py index 22594ae..3f77930 100644 --- a/tests/test_codegraph.py +++ b/tests/test_codegraph.py @@ -177,7 +177,7 @@ def test_managed_surfaces_only_name_the_official_codegraph(self) -> None: self.assertIn(official, path.read_text(encoding="utf-8"), path.relative_to(ROOT).as_posix()) for path in [ROOT / "README.md", ROOT / "README.zh-CN.md"]: text = path.read_text(encoding="utf-8") - self.assertIn("0.1.22", text, path.relative_to(ROOT).as_posix()) + self.assertIn("0.1.23", text, path.relative_to(ROOT).as_posix()) self.assertIn("0.1.3", text, path.relative_to(ROOT).as_posix()) def test_authority_surfaces_publish_workflow_013(self) -> None: @@ -188,9 +188,50 @@ def test_authority_surfaces_publish_workflow_013(self) -> None: ROOT / "plan.md", ]: text = path.read_text(encoding="utf-8") - self.assertIn("0.1.22", text, path.relative_to(ROOT).as_posix()) + self.assertIn("0.1.23", text, path.relative_to(ROOT).as_posix()) self.assertIn("0.1.3", text, path.relative_to(ROOT).as_posix()) + def test_codegraph_surfaces_publish_clean_head_freshness_contract(self) -> None: + for relative in [ + "skills/architecture-planning/SKILL.md", + "skills/implementation/SKILL.md", + "skills/documentation-sync/SKILL.md", + "skills/adversarial-review/SKILL.md", + "skills/code-intelligence/SKILL.md", + "templates/AGENTS.md", + ]: + text = (ROOT / relative).read_text(encoding="utf-8") + self.assertIn("before every proxy query", text, relative) + self.assertIn("never a source of truth", text, relative) + self.assertIn("raw", text, relative) + self.assertIn("unverified", text, relative) + localized_anchors = { + "README.md": [ + "before every proxy query", + "zero pending changes", + "never a source of truth", + ], + "README.zh-CN.md": [ + "每次代理查询前", + "零 pending changes", + "永远不是 source of truth", + ], + "docs/USAGE.md": [ + "每次代理查询前", + "零 pending changes", + "永远不是 source of truth", + ], + "plan.md": [ + "每次代理查询前", + "零 pending changes", + "永远不是 source of truth", + ], + } + for relative, anchors in localized_anchors.items(): + text = (ROOT / relative).read_text(encoding="utf-8") + for anchor in anchors: + self.assertIn(anchor, text, relative) + def test_readmes_keep_codegraph_operational_boundaries(self) -> None: """User-facing authorities retain the source-fallback and ownership boundaries.""" shared_anchors = [ @@ -337,6 +378,7 @@ def record_current_v3_fixture( self, *, legacy_bundle: bool = False, + legacy_v2_bundle: bool = False, invalid_refresh_policy: bool = False, ) -> tuple[dict[str, object], dict[str, object]]: self.qualify_task() @@ -346,6 +388,8 @@ def record_current_v3_fixture( source.write_text("class A:\n pass\n", encoding="utf-8") proxy = self.proxy_module() responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed("A is defined in src/a.py\n"), completed(healthy_status(self.repo)), @@ -369,9 +413,16 @@ def runner(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess if legacy_bundle: bundle["bundle_version"] = 1 bundle.pop("refresh_policy") + elif legacy_v2_bundle: + bundle["bundle_version"] = 2 + bundle["refresh_policy"] = { + "mode": "AUTO_INCREMENTAL_ON_PENDING", + "max_sync_attempts": 1, + "full_rebuild": "USER_ONLY", + } elif invalid_refresh_policy: bundle["refresh_policy"]["max_sync_attempts"] = 2 - if legacy_bundle or invalid_refresh_policy: + if legacy_bundle or legacy_v2_bundle or invalid_refresh_policy: write_json_atomic(bundle_path, bundle) protocol = importlib.import_module("internal.code_intelligence_protocol") result = protocol.record_proxy_bundle( @@ -481,6 +532,8 @@ def test_proxy_window_requires_clean_pre_and_post_status_for_current(self) -> No proxy = self.proxy_module() calls: list[tuple[list[str], dict[str, object]]] = [] responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed("graph bytes\n"), completed(healthy_status(self.repo)), @@ -507,7 +560,10 @@ def runner(command: list[str], **kwargs: object) -> subprocess.CompletedProcess[ self.assertEqual(bundle["delivery"]["state"], "CURRENT") self.assertEqual(bundle["delivery"]["usage"], "NON_AUTHORITATIVE_CONTEXT") self.assertEqual(bundle["delivery"]["record_status"], "CURRENT_AT_CHECK") - self.assertEqual([call[0][1] for call in calls], ["status", "explore", "status"]) + self.assertEqual( + [call[0][1] for call in calls], + ["status", "sync", "status", "explore", "status"], + ) self.assertTrue(all(Path(call[1]["cwd"]).resolve() == self.repo.resolve() for call in calls)) self.assertEqual(result["response"], "graph bytes\n") self.assertTrue(result["envelope"].startswith("[POLARIS_CODEGRAPH_FRESHNESS]\n")) @@ -549,9 +605,9 @@ def runner(command, **_kwargs): self.assertEqual([item[1] for item in calls], [ "status", "sync", "status", "explore", "status" ]) - self.assertEqual(result["bundle"]["bundle_version"], 2) + self.assertEqual(result["bundle"]["bundle_version"], 3) self.assertEqual(result["bundle"]["refresh_policy"], { - "mode": "AUTO_INCREMENTAL_ON_PENDING", + "mode": "AUTO_INCREMENTAL_BEFORE_QUERY", "max_sync_attempts": 1, "full_rebuild": "USER_ONLY", }) @@ -683,6 +739,8 @@ def test_proxy_discards_response_after_post_query_project_mismatch(self) -> None wrong = json.loads(healthy_status(self.repo)) wrong["projectPath"] = str(self.repo / "other-checkout") responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed("graph bytes must be discarded\n"), completed(json.dumps(wrong)), @@ -708,7 +766,8 @@ def runner(command, **_kwargs): / "CIQ-001.response.txt" ) self.assertEqual( - [item[1] for item in calls], ["status", "explore", "status"] + [item[1] for item in calls], + ["status", "sync", "status", "explore", "status"], ) self.assertIsNone(result["response"]) self.assertIsNone(result["bundle"]["response_path"]) @@ -728,6 +787,8 @@ def test_proxy_discards_response_classified_as_worktree_mismatch(self) -> None: "graph bytes must be discarded\n" ) responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed(response), completed(healthy_status(self.repo)), @@ -755,7 +816,8 @@ def runner( / "CIQ-001.response.txt" ) self.assertEqual( - [item[1] for item in calls], ["status", "explore", "status"] + [item[1] for item in calls], + ["status", "sync", "status", "explore", "status"], ) self.assertEqual( result["bundle"]["response_classification"]["classification"], @@ -780,6 +842,8 @@ def test_proxy_discards_misplaced_worktree_mismatch_banner(self) -> None: "graph bytes must be discarded\n" ) responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed(response), completed(healthy_status(self.repo)), @@ -808,7 +872,8 @@ def runner( ) classification = result["bundle"]["response_classification"] self.assertEqual( - [item[1] for item in calls], ["status", "explore", "status"] + [item[1] for item in calls], + ["status", "sync", "status", "explore", "status"], ) self.assertEqual(classification["classification"], "NOT_VERIFIED") self.assertEqual( @@ -841,6 +906,8 @@ def runner( calls.append(command) if command[1] == "status": return completed(healthy_status(self.repo)) + if command[1] == "sync": + return completed("synced\n") if command[1] == "explore": marker.rmdir() return completed("graph bytes\n") @@ -855,7 +922,10 @@ def runner( "locate A", "symbol A", runner=runner, ) - self.assertEqual([item[1] for item in calls], ["status", "explore"]) + self.assertEqual( + [item[1] for item in calls], + ["status", "sync", "status", "explore"], + ) self.assertEqual( result["bundle"]["post_query_status"]["status"], "UNAVAILABLE" ) @@ -871,6 +941,8 @@ def test_proxy_queries_unknown_pre_status_and_treats_result_as_stale(self) -> No (self.repo / ".codegraph").mkdir() responses = [ completed("not-json\n"), + completed("synced\n"), + completed(healthy_status(self.repo)), completed("graph bytes\n"), completed(healthy_status(self.repo)), ] @@ -889,11 +961,50 @@ def runner(command, **_kwargs): "locate A", "symbol A", runner=runner, ) - self.assertEqual([item[1] for item in calls], ["status", "explore", "status"]) + self.assertEqual( + [item[1] for item in calls], + ["status", "sync", "status", "explore", "status"], + ) self.assertEqual(result["response"], "graph bytes\n") self.assertEqual(result["bundle"]["delivery"]["state"], "UNKNOWN") self.assertIn("freshness: TREAT_AS_STALE", result["envelope"]) + def test_proxy_sync_failure_keeps_graph_for_navigation_and_marks_it_stale(self) -> None: + self.qualify_task() + (self.repo / ".codegraph").mkdir() + responses = [ + completed(healthy_status(self.repo)), + completed("", 1, "sync failed"), + completed("graph bytes\n"), + completed(healthy_status(self.repo)), + ] + calls: list[list[str]] = [] + + def runner( + command: list[str], **_kwargs: object + ) -> subprocess.CompletedProcess[str]: + calls.append(command) + return responses.pop(0) + + with mock.patch( + "internal.code_intelligence_proxy.shutil.which", + return_value="/bin/codegraph", + ): + result = self.proxy_module().execute_proxy_query( + self.repo, "TASK-0001", "PLANNING", "CIQ-001", + "locate A", "symbol A", runner=runner, + ) + + self.assertEqual( + [item[1] for item in calls], + ["status", "sync", "explore", "status"], + ) + self.assertEqual(result["response"], "graph bytes\n") + self.assertEqual(result["bundle"]["sync"]["status"], "FAILED") + self.assertEqual(result["bundle"]["delivery"]["state"], "STALE") + self.assertEqual(result["bundle"]["delivery"]["usage"], "NAVIGATION_ONLY") + self.assertEqual(result["bundle"]["delivery"]["reason"], "SYNC_FAILED") + def test_proxy_known_stale_overrides_unknown_pre_status(self) -> None: cases = [ ( @@ -919,6 +1030,8 @@ def test_proxy_known_stale_overrides_unknown_pre_status(self) -> None: post_status["pendingChanges"]["modified"] = 1 responses = [ completed("not-json\n"), + completed("synced\n"), + completed(healthy_status(self.repo)), completed(response), completed(json.dumps(post_status)), ] @@ -938,7 +1051,8 @@ def runner(command, **_kwargs): ) self.assertEqual( - [item[1] for item in calls], ["status", "explore", "status"] + [item[1] for item in calls], + ["status", "sync", "status", "explore", "status"], ) self.assertEqual(result["response"], response) self.assertEqual(result["bundle"]["delivery"]["state"], "STALE") @@ -1012,7 +1126,7 @@ def runner(command, **_kwargs): def test_proxy_window_downgrades_pending_unknown_and_unavailable_states(self) -> None: cases = [ ("pending", "STALE", 5), - ("malformed", "UNKNOWN", 3), + ("malformed", "UNKNOWN", 5), ("missing_marker", "UNAVAILABLE", 0), ] for index, (case, expected_state, expected_calls) in enumerate(cases, start=1): @@ -1037,6 +1151,8 @@ def test_proxy_window_downgrades_pending_unknown_and_unavailable_states(self) -> else: responses = [ completed("not-json\n"), + completed("synced\n"), + completed(healthy_status(self.repo)), completed("graph bytes\n"), completed(healthy_status(self.repo)), ] @@ -1105,8 +1221,16 @@ def runner(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess def test_proxy_window_never_promotes_failed_or_post_stale_queries(self) -> None: cases = [ - ("explore_failed", "UNKNOWN", ["status", "explore"]), - ("post_pending", "STALE", ["status", "explore", "status"]), + ( + "explore_failed", + "UNKNOWN", + ["status", "sync", "status", "explore"], + ), + ( + "post_pending", + "STALE", + ["status", "sync", "status", "explore", "status"], + ), ] for index, (case, expected_state, expected_calls) in enumerate(cases, start=1): with self.subTest(case=case): @@ -1119,9 +1243,16 @@ def test_proxy_window_never_promotes_failed_or_post_stale_queries(self) -> None: post = json.loads(healthy_status(self.repo)) post["pendingChanges"]["added"] = 1 responses = ( - [completed(healthy_status(self.repo)), completed("", 1, "failed")] + [ + completed(healthy_status(self.repo)), + completed("synced\n"), + completed(healthy_status(self.repo)), + completed("", 1, "failed"), + ] if case == "explore_failed" else [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed("graph bytes\n"), completed(json.dumps(post)), @@ -1175,6 +1306,8 @@ def test_proxy_window_classifies_banners_and_discards_unsafe_paths(self) -> None source.write_text("value = 1\n", encoding="utf-8") proxy = self.proxy_module() responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed(response), completed(healthy_status(self.repo)), @@ -1298,6 +1431,7 @@ def test_proxy_envelope_is_finite_and_truncates_diagnostics(self) -> None: self.assertTrue(envelope.startswith("[POLARIS_CODEGRAPH_FRESHNESS]\n")) self.assertTrue(envelope.endswith("[/POLARIS_CODEGRAPH_FRESHNESS]\n")) + self.assertIn("source_of_truth: false\n", envelope) error_line = next(line for line in envelope.splitlines() if line.startswith("error: ")) self.assertEqual(len(error_line.removeprefix("error: ")), 240) @@ -1340,6 +1474,8 @@ def test_mcp_server_initializes_and_lists_one_proxy_tool(self) -> None: ) tools = responses[1]["result"]["tools"] self.assertEqual([item["name"] for item in tools], ["polaris_codegraph_explore"]) + self.assertIn("one pre-query incremental sync", tools[0]["description"]) + self.assertIn("never source of truth", tools[0]["description"]) schema = tools[0]["inputSchema"] self.assertNotIn("repository", schema["properties"]) self.assertNotIn("sync_if_needed", schema["properties"]) @@ -1792,10 +1928,15 @@ def test_v3_record_projects_exact_proxy_bundle(self) -> None: ) self.assertEqual(recorded["query"]["symbols"][0]["path"], "src/a.py") - def test_bundle_v1_remains_projectable_but_v2_policy_is_fixed(self) -> None: + def test_bundle_v1_and_v2_remain_projectable_but_policies_are_fixed(self) -> None: recorded, _query = self.record_current_v3_fixture(legacy_bundle=True) self.assertEqual(recorded["record_version"], 3) + self.tearDown() + self.setUp() + recorded, _query = self.record_current_v3_fixture(legacy_v2_bundle=True) + self.assertEqual(recorded["record_version"], 3) + self.tearDown() self.setUp() with self.assertRaisesRegex(RuleFailure, "refresh policy"): @@ -1953,6 +2094,8 @@ def test_failed_explore_proxy_bundle_projects_to_unknown_v3(self) -> None: source.parent.mkdir() source.write_text("class A:\n pass\n", encoding="utf-8") responses = [ + completed(healthy_status(self.repo)), + completed("synced\n"), completed(healthy_status(self.repo)), completed("failed explore output\n", returncode=1), ] @@ -2012,6 +2155,8 @@ def test_failed_explore_with_known_stale_projects_to_failed_v3(self) -> None: stale = json.loads(healthy_status(self.repo)) stale["index"]["state"] = "partial" responses = [ + completed(json.dumps(stale)), + completed("synced\n"), completed(json.dumps(stale)), completed("failed explore output\n", returncode=1), ] @@ -2164,6 +2309,8 @@ def test_v3_record_preserves_stale_unknown_and_unavailable_restrictions(self) -> elif case == "unknown": responses = [ completed("not-json\n"), + completed("synced\n"), + completed(healthy_status(self.repo)), completed("A is defined in src/a.py\n"), completed(healthy_status(self.repo)), ] @@ -2430,6 +2577,28 @@ def test_0122_migration_resume_rejects_nonempty_retirement_inventory( with self.assertRaisesRegex(RuleFailure, "inventory changed"): migrate_project(self.repo) + def test_0123_migration_preserves_v3_code_intelligence_records(self) -> None: + recorded, _query = self.record_current_v3_fixture() + actual_path = ( + self.repo + / ".polaris/tasks/TASK-0001/code-intelligence/r001/planning.json" + ) + self.assertEqual( + json.loads(actual_path.read_text(encoding="utf-8")), recorded + ) + before = actual_path.read_bytes() + self.set_protocol_version("0.1.22") + + with protocol_source_at("0.1.23") as source: + vendor(source, self.repo, False) + result = migrate_project(self.repo) + + self.assertEqual(result["from"], "0.1.22") + self.assertEqual(result["to"], "0.1.23") + self.assertEqual(actual_path.read_bytes(), before) + migration = json.loads(Path(result["record"]).read_text(encoding="utf-8")) + self.assertEqual(migration["retired_code_intelligence_records"], []) + def test_legacy_v1_records_remain_readable_but_cannot_be_written(self) -> None: self.initialize_task() protocol = importlib.import_module("internal.code_intelligence_protocol") @@ -3694,8 +3863,10 @@ def test_all_agent_surfaces_require_automatic_freshness_policy(self) -> None: ROOT / "templates/AGENTS.md", ] required = ( - "automatically runs at most one incremental `codegraph sync`", + "before every proxy query", + "zero pending changes", "never runs `codegraph index`", + "never a source of truth", "UNKNOWN", "TREAT_AS_STALE", "source/Git fallback", @@ -3905,6 +4076,66 @@ def runner( self.assertEqual(result["freshness"]["status"], "CURRENT_AT_CHECK") self.assertIn("SYNC_ACKNOWLEDGED", result["freshness"]["basis"]) + def test_clean_status_can_force_one_sync_and_recheck(self) -> None: + adapter = self.adapter_module() + (self.repo / ".codegraph").mkdir() + initial = adapter._status_result( + self.repo, + json.loads(healthy_status(self.repo)), + "2026-08-20T00:00:00Z", + "a" * 64, + ) + responses = iter( + [ + completed("Synced clean committed changes\n"), + completed(healthy_status(self.repo)), + ] + ) + calls: list[list[str]] = [] + + def runner( + command: list[str], **_kwargs: object + ) -> subprocess.CompletedProcess[str]: + calls.append(command) + return next(responses) + + result = adapter.synchronize_observed_status( + self.repo, + load_providers(ROOT)["codegraph"], + initial, + runner=runner, + force_attempt=True, + ) + + self.assertEqual([call[1] for call in calls], ["sync", "status"]) + self.assertEqual(result["sync"]["status"], "SUCCESS") + self.assertEqual(result["freshness"]["status"], "CURRENT_AT_CHECK") + self.assertIn("SYNC_ACKNOWLEDGED", result["freshness"]["basis"]) + + def test_forced_sync_rejects_non_boolean_policy_without_running_codegraph(self) -> None: + adapter = self.adapter_module() + (self.repo / ".codegraph").mkdir() + initial = adapter._status_result( + self.repo, + json.loads(healthy_status(self.repo)), + "2026-08-20T00:00:00Z", + "a" * 64, + ) + + result = adapter.synchronize_observed_status( + self.repo, + load_providers(ROOT)["codegraph"], + initial, + runner=lambda *_args, **_kwargs: self.fail( + "invalid policy must not run CodeGraph" + ), + force_attempt="yes", + ) + + self.assertEqual(result["freshness"]["status"], "NOT_VERIFIED") + self.assertEqual(result["sync"]["status"], "SKIPPED") + self.assertIsNone(result["post_sync_status"]) + def test_pending_changes_still_sync_once_with_an_index_stale_reason(self) -> None: _, sync_if_needed = self.adapter_functions() (self.repo / ".codegraph").mkdir() @@ -5199,6 +5430,214 @@ def test_real_codegraph_status_shape_when_cli_is_available(self) -> None: ) self.assertEqual(result["status"], "CURRENT_AT_CHECK") + @unittest.skipUnless(shutil.which("codegraph"), "codegraph CLI is not installed") + def test_real_codegraph_sync_reconciles_a_clean_committed_symbol(self) -> None: + """A clean commit can evade status, but one explicit sync must reconcile it.""" + temporary_repo = validated_disposable_codegraph_repo( + self.repo, self.temp.name + ) + symbol = "polaris_clean_head_committed_symbol_6f82c1" + (temporary_repo / ".gitignore").write_text(".codegraph/\n", encoding="utf-8") + source = temporary_repo / "indexed.py" + source.write_text("def indexed_symbol():\n return 1\n", encoding="utf-8") + subprocess.run( + ["git", "add", ".gitignore", "indexed.py"], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["git", "commit", "-qm", "add indexed symbol"], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["codegraph", "init", str(temporary_repo)], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=120, + ) + new_source = temporary_repo / "clean_head_new.py" + new_source.write_text( + f"def {symbol}():\n return 2\n", + encoding="utf-8", + ) + subprocess.run( + ["git", "add", "clean_head_new.py"], cwd=temporary_repo, check=True + ) + subprocess.run( + ["git", "commit", "-qm", "add clean head symbol"], + cwd=temporary_repo, + check=True, + ) + + self.assertEqual( + subprocess.run( + ["git", "status", "--porcelain"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + ).stdout, + "", + ) + status = json.loads(subprocess.run( + ["codegraph", "status", "--json"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout) + self.assertEqual( + status["pendingChanges"], + {"added": 0, "modified": 0, "removed": 0}, + ) + before = subprocess.run( + ["codegraph", "explore", symbol], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout + self.assertNotIn("`clean_head_new.py`", before) + + subprocess.run( + ["codegraph", "sync", "--quiet"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=120, + ) + after = subprocess.run( + ["codegraph", "explore", symbol], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout + self.assertIn("`clean_head_new.py`", after) + self.assertIn(symbol, after) + + @unittest.skipUnless(shutil.which("codegraph"), "codegraph CLI is not installed") + def test_real_codegraph_sync_reconciles_a_clean_branch_switch(self) -> None: + """A clean branch switch can evade status, but one explicit sync repairs it.""" + temporary_repo = validated_disposable_codegraph_repo( + self.repo, self.temp.name + ) + symbol = "polaris_clean_branch_symbol_9d31a4" + initial_branch = subprocess.run( + ["git", "branch", "--show-current"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + ).stdout.strip() + (temporary_repo / ".gitignore").write_text(".codegraph/\n", encoding="utf-8") + source = temporary_repo / "main_branch.py" + source.write_text("def main_symbol():\n return 1\n", encoding="utf-8") + subprocess.run( + ["git", "add", ".gitignore", "main_branch.py"], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["git", "commit", "-qm", "add main branch symbol"], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["git", "switch", "-q", "-c", "clean-feature"], + cwd=temporary_repo, + check=True, + ) + feature_source = temporary_repo / "feature_branch.py" + feature_source.write_text( + f"def {symbol}():\n return 2\n", + encoding="utf-8", + ) + subprocess.run( + ["git", "add", "feature_branch.py"], cwd=temporary_repo, check=True + ) + subprocess.run( + ["git", "commit", "-qm", "add feature branch symbol"], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["git", "switch", "-q", initial_branch], + cwd=temporary_repo, + check=True, + ) + subprocess.run( + ["codegraph", "init", str(temporary_repo)], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=120, + ) + subprocess.run( + ["git", "switch", "-q", "clean-feature"], + cwd=temporary_repo, + check=True, + ) + + self.assertEqual( + subprocess.run( + ["git", "status", "--porcelain"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + ).stdout, + "", + ) + status = json.loads(subprocess.run( + ["codegraph", "status", "--json"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout) + self.assertEqual( + status["pendingChanges"], + {"added": 0, "modified": 0, "removed": 0}, + ) + before = subprocess.run( + ["codegraph", "explore", symbol], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout + self.assertNotIn("`feature_branch.py`", before) + + subprocess.run( + ["codegraph", "sync", "--quiet"], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=120, + ) + after = subprocess.run( + ["codegraph", "explore", symbol], + cwd=temporary_repo, + check=True, + text=True, + capture_output=True, + timeout=30, + ).stdout + self.assertIn("`feature_branch.py`", after) + self.assertIn(symbol, after) + def test_real_cli_fixture_rejects_temporary_paths_nested_in_workspace(self) -> None: """A hostile TMPDIR beneath the workspace must never become an init target.""" nested_workspace_temp = ROOT / "nested-temporary-repository" diff --git a/tests/test_core.py b/tests/test_core.py index d8f010d..83f61e5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -377,6 +377,8 @@ def record_current_proxy_intelligence(self, stage: str) -> dict[str, object]: }, }) responses = [ + subprocess.CompletedProcess([], 0, status, ""), + subprocess.CompletedProcess([], 0, "synced\n", ""), subprocess.CompletedProcess([], 0, status, ""), subprocess.CompletedProcess([], 0, "graph context\n", ""), subprocess.CompletedProcess([], 0, status, ""), @@ -1439,6 +1441,28 @@ def test_cli_packaging_declares_no_runtime_dependencies(self) -> None: self.assertIn('dependencies = []', metadata) self.assertIn('polaris = "polaris_cli:main"', metadata) + def test_0123_authorities_publish_one_version_only_adjacent_migration(self) -> None: + self.assertEqual((ROOT / "VERSION").read_text(encoding="utf-8").strip(), "0.1.23") + self.assertIn('version = "0.1.23"', (ROOT / "pyproject.toml").read_text(encoding="utf-8")) + for relative in [ + "templates/project.json", + "templates/task/state.json", + "templates/task-sources/state.json", + ]: + self.assertEqual(read_json(ROOT / relative)["polaris_version"], "0.1.23") + step = read_json(ROOT / "workflow/migrations.json")["steps"][-1] + self.assertEqual(step, { + "migration_id": "0.1.22-to-0.1.23", + "from_polaris_version": "0.1.22", + "to_polaris_version": "0.1.23", + "from_workflow_version": "0.1.3", + "to_workflow_version": "0.1.3", + "project_strategy": "replace_version", + "task_strategy": "append_version_event", + }) + workflow = read_json(ROOT / "workflow/default-workflow.json") + self.assertEqual(workflow["workflow_version"], "0.1.3") + def test_artifact_protocol_rejects_escape_and_registered_hash_drift(self) -> None: """共享 artifact 引用层拒绝越界路径和注册后的内容漂移。""" with self.assertRaises(RuleFailure): @@ -2618,6 +2642,19 @@ def test_0122_version_only_migration_rejects_workflow_change(self) -> None: ): migrate_project(self.repo) + def test_0123_version_only_migration_rejects_workflow_change(self) -> None: + self.set_protocol_version("0.1.22") + with protocol_source_at("0.1.23") as source: + migrations_path = source / "workflow/migrations.json" + migrations = read_json(migrations_path) + migrations["steps"][-1]["to_workflow_version"] = "0.1.4" + write_json_atomic(migrations_path, migrations) + vendor(source, self.repo, False) + with self.assertRaisesRegex( + RuleFailure, "workflow migration requires replacement" + ): + migrate_project(self.repo) + def test_code_intelligence_auto_detects_available_operations_and_can_be_disabled(self) -> None: """已初始化的可选代码情报按 MCP 工具能力发现;缺失或禁用时不产生硬依赖。""" (self.repo / ".codegraph").mkdir() diff --git a/workflow/migrations.json b/workflow/migrations.json index d97f961..d14259c 100644 --- a/workflow/migrations.json +++ b/workflow/migrations.json @@ -117,6 +117,15 @@ "to_workflow_version": "0.1.3", "project_strategy": "replace_version", "task_strategy": "append_version_event" + }, + { + "migration_id": "0.1.22-to-0.1.23", + "from_polaris_version": "0.1.22", + "to_polaris_version": "0.1.23", + "from_workflow_version": "0.1.3", + "to_workflow_version": "0.1.3", + "project_strategy": "replace_version", + "task_strategy": "append_version_event" } ] }