Skip to content

Commit a947817

Browse files
committed
feat(pm): refresh the package index on a resolution miss, not on a timer (2026.7.30.3, fixes #315)
`mcpp build/run/test` refreshed the index whenever its marker was older than an hour — a multi-repo `xlings update` (with 3 retries and 2s/4s backoff) run whether or not anything was missing. On a slow or blocked network that is minutes of waiting, once an hour, for data already on disk. The offline-first policy was not missing: xlings.cppm's xim install gate has had it for a while, and its comment names this exact symptom. prepare.cppm's TTL gate simply fired first, making that policy unreachable. The same decision was being derived in five places, two of which contradicted each other. One source of truth now: mcpp.pm.index_refresh. build / add / the xim gate / the install-failure retry all route through it. A refresh happens when there is no local index, when a descriptor is missing from it, or when a SemVer constraint matches none of the versions it knows — never merely because time passed. Builds whose dependencies resolve locally make no network request. Why the axis changed: "is the index fresh enough" is unanswerable and mtime is a poor proxy (restored CI caches, clock skew, tars that preserve timestamps all make the marker lie in both directions). "Can the resolver work with what is on disk" IS answerable offline — every resolution input is a local file. The marker is now only a debounce timer. The load-bearing rule (SuppressedInconclusive): a miss means nothing unless the index that would have answered is authoritative. xim descriptors declare no namespace, so (xim, x) can never match the identity gate — counting that as a miss would refresh on EVERY build with a toolchain-ish dependency, strictly worse than the TTL being removed. Judgement reused from IndexRoute (#307), locked by a unit test and by e2e 173 step 3. Also in this change: - `mcpp update` stops being a no-op. It only dropped lock entries and told the user to run `mcpp build`, but the build path never reads mcpp.lock, so it changed nothing at all. It now forces an index refresh (explicit intent: no TTL, no debounce) and reports the revision change; skipped when nothing in the project is served by the shared registry. - `--offline` / MCPP_OFFLINE: no index refresh, no downloads, no toolchain auto-install. Checked at the point of download, so an offline build with its dependencies present still succeeds. MCPP_NO_AUTO_INSTALL stays accepted as the older, narrower spelling — one concept had three names. - `[index] auto_refresh` in the global config. Deliberately a boolean, not a three-valued mode: keeping the old TTL path alive to emulate a policy being deleted is how this debt accumulated. Policy lives in the global config, not mcpp.toml — it describes the machine's network, and a project carrying it would stop being portable between a LAN, a laptop and CI. - A marker stamped in the future read as "fresh" forever (age < ttl is true for negatives). Unusable timestamps now mean unknown, and unknown means stale. - The index sync had no concurrency guard while the BMI cache has had one for a while. Non-blocking: whoever holds the lock is already doing the work, and a queue of stalled builds is the symptom this change exists to remove. - mark_known_indexes_refreshed bailed out under a project-scoped env, so machines with custom [indices] never recorded a refresh at all. - `mcpp index status` grows a revision column. .xlings-index-version is the index's content identity (artifact names carry it: mcpp-index-8d67478.tar.gz) and mcpp had zero references to it. Treated as an opaque string — probing showed sub-indexes carry a date version, not a sha. - Resolution failures now carry the index revision and age plus the explicit `mcpp index update` hint, so "no such package" and "your index is from last month" stay distinguishable once refreshes are lazy. Semantic change, documented in docs/05-mcpp-toml.md: `^1.2` resolves against the versions your local index knows. A 1.3.0 published upstream since your last refresh needs `mcpp index update` or `mcpp update` — which is what those commands are for. Design + probe results: .agents/docs/2026-07-30-issue315-index-refresh-policy-design.md
1 parent d79ab00 commit a947817

19 files changed

Lines changed: 1919 additions & 66 deletions
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# 索引刷新策略收敛 — 实施计划
2+
3+
日期:2026-07-30
4+
设计:`.agents/docs/2026-07-30-issue315-index-refresh-policy-design.md`
5+
关联:#315
6+
目标版本:**2026.7.30.3**(基线 `d79ab00` / 2026.7.30.2)
7+
形态:**单 PR**,内部按 A–F 六段推进,每段自带验收。
8+
9+
---
10+
11+
## 0. 基线核对(已完成)
12+
13+
| 事实 | 位置(@ d79ab00) |
14+
|---|---|
15+
| TTL 站点 | `src/build/prepare.cppm:1390-1433` |
16+
| 每个依赖已自带**有序候选** | `DependencySpec.candidates``src/pm/dep_spec.cppm:46`)—— 与 `mcpp add` 用的是同一份 |
17+
| 存在性查找 | `mcpp::pm::lookup_descriptor(route, candidates)``src/pm/index_route.cppm:150`|
18+
| route 工厂 | `prepare.cppm:1548``index_route(cfg)`)、`commands.cppm:151-152` |
19+
| 版本可满足性 | `mcpp::pm::resolve_semver(ns, short, req, route, platform)``src/pm/resolver.cppm:96`),失败即不可满足 |
20+
| 索引目录/marker/age | `xlings.cppm:381-490`**匿名命名空间,未导出**|
21+
| 索引状态(已导出) | `default_index_status` / `official_index_status``IndexStatus{dir,present,fresh,ageSeconds}` |
22+
| 索引内容 rev | `<indexDir>/.xlings-index-version`(实测 mcpplibs=`8d67478`,xim-pkgindex=`ebf4020`)—— **mcpp 零引用** |
23+
| 跨平台锁原语 | `mcpp::platform::fs::FileLock::try_acquire(dir)``src/platform/fs.cppm:60``bmi_cache.cppm:241` 已在用) |
24+
| 索引数据根(已导出) | `mcpp::xlings::paths::index_data(env)` |
25+
26+
---
27+
28+
## A. 探针(先跑,不写代码)
29+
30+
- **P-1 `.xlings-index-version` 更新语义**:clean-room `MCPP_HOME`,走一次真实 `xlings update`
31+
比对刷新前后该文件的值与格式(是否 7 位短 sha、有无尾随空白、artifact/git 两通道是否都写)。
32+
**降级预案**:若某通道不写 → `index_revision()` 返回 nullopt,advisory 只显示 age,**gate 不受影响**
33+
- **P-2 INV-3 误报实测**:对一个同时含 mcpplibs 依赖与 xim 依赖的工程,打印每个依赖的
34+
`RefreshDecision`,确认 xim 落 `SuppressedInconclusive` 而非 `DescriptorMiss`
35+
P-2 依赖 A 段代码,故实际在 B 段末尾以单测 + 一次真实工程 `-v` 运行完成。
36+
37+
**验收**:P-1 结论写回设计文档 §10;P-2 由单测 + e2e #6 承接。
38+
39+
---
40+
41+
## B. 策略模块(新增 `src/pm/index_refresh.cppm`
42+
43+
**导出面**
44+
45+
```
46+
enum class RefreshReason { None, IndexAbsent, DescriptorMiss, VersionMiss,
47+
SuppressedDebounce, SuppressedOffline, SuppressedInconclusive };
48+
struct RefreshPolicy { bool offline; std::int64_t debounceSeconds = 120; };
49+
struct RefreshDecision { bool shouldRefresh; RefreshReason reason; std::string subject; };
50+
51+
RefreshDecision decide_for_dependency(const IndexRoute&, const DependencySpec&,
52+
const xlings::Env&, const platform::PlatformKey&,
53+
const RefreshPolicy&);
54+
std::expected<void,std::string> apply(const RefreshDecision&, const xlings::Env&, bool quiet);
55+
RefreshPolicy policy_from_env(const config::GlobalConfig&, bool offlineFlag);
56+
std::string_view reason_text(RefreshReason);
57+
```
58+
59+
**判据顺序(短路,单测逐行锁)**
60+
61+
1. `spec.isPath() || spec.isGit()``None`
62+
2. `policy.offline``SuppressedOffline`
63+
3. 无候选路由到**内置 registry**`find_for_ns` 为 null 或 `is_builtin`)→ `None`
64+
(项目 `path`/自定义 git 索引:刷全局索引对它无用,保持现状语义)
65+
4. `lookup_descriptor` 未命中且 `!conclusive``SuppressedInconclusive`**INV-3**
66+
5. 默认索引 `pkgs/` 不存在 → `IndexAbsent` → 刷
67+
6. 未命中 → `DescriptorMiss` → 刷
68+
7. 命中 + `is_version_constraint(spec.version)` + `resolve_semver` 失败 → `VersionMiss` → 刷
69+
8. 上面判刷但 `0 <= age < debounceSeconds``SuppressedDebounce`
70+
71+
**依赖方向**`index_refresh` → {`index_route`, `resolver`, `dep_spec`, `xlings`, `config`,
72+
`platform.fs`, `ui`, `log`}。`resolver` 已 import `index_route`,无环。
73+
74+
**验收**`tests/unit/test_pm_index_refresh.cpp` 覆盖 8 条判据 + 精确版本不判 VersionMiss。
75+
76+
---
77+
78+
## C. xlings 侧最小增补(`src/xlings.cppm`
79+
80+
1. `IndexStatus` 增字段 `std::optional<std::string> rev`(读 `.xlings-index-version`,trim 空白;
81+
缺失/空 → nullopt)。→ `mcpp index status` 与 advisory 免费获得。
82+
2. 新导出 `std::optional<std::string> index_revision(const std::filesystem::path& indexDir)`
83+
3. **S1**`is_index_dir_fresh``age < 0` → 判 **stale**(未来时间戳不得等于永远新鲜)。
84+
4. **S3**`update_index` 内部用 `FileLock::try_acquire(paths::index_data(env))` 包裹;
85+
拿不到锁 → 记 verbose 日志并**返回 0(跳过,不报错、不阻塞)**
86+
5. **S4**`mark_known_indexes_refreshed` 去掉 `projectDir` 早退,改为项目模式下也打标
87+
(仅供 advisory)。
88+
89+
**验收**`tests/unit/test_xlings.cpp` 增负 age、rev 读取三态(缺失/空/带换行)。
90+
91+
---
92+
93+
## D. 五站点收敛
94+
95+
| 站点 | 动作 |
96+
|---|---|
97+
| `prepare.cppm:1390-1433` | 整段替换:遍历 `m->dependencies``decide_for_dependency`;命中即 `apply` 一次(进程内 `bool` once),reason 进 `-v` 日志 |
98+
| `commands.cppm:167`(add) | 改为 `decide_*` + `apply`,删除本地 TTL 判断 |
99+
| `xlings.cppm:1400`(xim 门) | 保留判据,`kJustRefreshedSeconds` 改由 `RefreshPolicy::debounceSeconds` 提供默认值,注释指向策略模块 |
100+
| `package_fetcher.cppm:1060` | 保留,输出走统一句式 |
101+
| `index_management.cppm:29`(search) | 保留 TTL,**加注释说明这是唯一允许时间驱动的站点** |
102+
103+
**验收**`grep -n "is_index_fresh" src/` 只剩 search 一处 + xlings 内部实现。
104+
105+
---
106+
107+
## E. D6 + 表达面
108+
109+
1. **`mcpp update` 不再空转**`commands.cppm:403-430`):先强制刷索引(不看 TTL/debounce,
110+
`offline` 下拒绝并说明),再清 lock 条目,输出 `mcpplibs 8d67478 → a1b2c3d``already at …`
111+
2. **`--offline`**`build` / `run` / `test` / `update` 注册;语义 = 本次调用不发起任何网络
112+
(索引 + 包 + 工具链自动安装)。
113+
3. **`MCPP_OFFLINE=1`**:等价 env;`MCPP_NO_AUTO_INSTALL` 保留为兼容别名(等价于 offline 的
114+
工具链子集),help/docs 标注 deprecated。
115+
4. **`[index] auto_refresh = true|false`**`config.cppm` 读取,默认 true。
116+
5. **可解释输出**:刷新一行说明主语与原因;`-v` 打印跳过原因与索引 rev/age;
117+
offline+miss 的结构化错误含 rev + age + `mcpp index update`
118+
6. `mcpp index status` 增 rev 列。
119+
120+
**优先级**:flag > env > config。
121+
122+
---
123+
124+
## F. 测试 + 版本 + 交付
125+
126+
- 新 e2e `tests/e2e/173_index_refresh_policy.sh`,9 个场景(设计 §8)。
127+
**反向断言写法**`out=$(...); echo "$out" | grep -q X && { echo FAIL; exit 1; }`
128+
`! cmd | grep` 在 errexit 下被豁免,永不失败)。
129+
- 全量本地回归:`mcpp test`(单测)+ 受影响 e2e(09/14/44/105/169 等涉及索引/离线的用例)。
130+
- 版本 `2026.7.30.3`:只改 `mcpp.toml` + `src/toolchain/fingerprint.cppm`
131+
**bootstrap pin(`.xlings.json` / `ci-fresh-install.yml MCPP_PIN`)不动**
132+
`bash .github/tools/check_version_pins.sh` 必须过。
133+
- 单 PR → CI 全绿 → bypass squash 合入。
134+
- 合入后:release 四平台 → 镜像 xlings-res(gh+gtc)→ xim-pkgindex PR → `xlings install mcpp`
135+
真装验证 → bootstrap pin bump(独立 commit)。
136+
137+
---
138+
139+
## 风险与回退
140+
141+
| 风险 | 处置 |
142+
|---|---|
143+
| INV-3 接错 → 每次构建都刷 | 单测 + e2e #6 双闸;P-2 真实工程实测 |
144+
| `.xlings-index-version` 语义不符 | gate 不依赖 rev,最坏只损失 advisory 精度 |
145+
| S3 锁在 Windows 行为不一致 |`platform/fs.cppm` 封装;拿不到锁=跳过而非阻塞 |
146+
| 语义变化引发「拉不到新版本」误报 | `mcpp update` 变成真入口 + advisory 提示 + docs 明写 |

0 commit comments

Comments
 (0)