feat(pm): refresh the package index on a resolution miss, not on a timer (2026.7.30.3, fixes #315) - #320
Merged
Merged
Conversation
…mer (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
Since the refresh is lazy, "why did I get this version" often answers to "because it is the newest one your local index knows" — which is unguessable without naming the index and its age.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #315.
问题不是「缺功能」,是同一个决策在 5 处各推导一遍
mcpp build/run/test此前只要刷新 marker 超过 1 小时,就无条件跑一次xlings update(同步全部索引仓库,失败还带 3 次 2s/4s 退避重试),不管有没有东西真的缺失。但 offline-first 的策略早就在仓库里了 ——
src/xlings.cppm的 xim 安装门(ensure_official_package_index_fresh)注释里明写「不能因为 TTL 到期就刷,那会在慢/被墙的网络上把构建挂住(Termux first-run)」。是prepare.cppm那道 TTL 门先开火,把它变成了不可达代码。判据换轴:时间 → 本地可解析性
「索引够不够新」不可判定,mtime 是它的坏代理(CI 缓存恢复、时钟回拨、tar 保留时间戳都能让 marker 两个方向说谎)。可判定的问题是「解析器能不能用磁盘上的东西干活」—— 而所有解析输入都在磁盘上(描述符是文件,
resolve_semver解析本地 xpkg.lua)。新增
src/pm/index_refresh.cppm作为唯一真源,build / add / xim 门 / 安装失败重试全部走它。只有三种情况刷新:本地没索引、描述符不在本地、SemVer 约束在本地版本集内无解。依赖全部能本地解析的构建零网络请求,无论索引多旧。最危险的一条判据
SuppressedInconclusive:「本地查不到」单独不能推出「需要刷新」。xim 描述符不写namespace,(xim, x)永远匹配不上身份门 —— 把这种 miss 当真,任何带 xim 依赖的工程会每次构建都刷,比被删掉的 TTL 更糟。判据复用IndexRoute::authoritative_for(#307)。真实工程实测(
-v):单测
InconclusiveNamespaceNeverRefreshes+ e2e 173 第 3 步双闸锁住。顺带修掉的东西
mcpp update此前是空操作:只删 mcpp.lock 条目然后叫用户去mcpp build,而构建路径从不读 mcpp.lock(prepare只写不读)—— 删了等于没删,行为影响为零。现在它强制刷索引(显式意图 ⇒ 不看 TTL/去抖)并报告 rev 变化;工程里没有走共享 registry 的依赖时跳过。age < ttl对负数恒真。不可用的时间戳现在读作 unknown,unknown 必须是 stale。mcpp index status对带自定义[indices]的机器永远显示 unknown。新增的表达面(刻意收窄到 3 个旋钮)
--offline/MCPP_OFFLINE=1[index] auto_refresh = false(全局 config)--refresh-index:mcpp index update && mcpp build完全等价,而它对构建产物零影响(不进指纹、不改产物、只有网络副作用)——挂在build上层次是错的。cargo 的先例正是一对反例:有build --offline,没有build --refresh-index。真正的需求归mcpp update。auto_refresh做成布尔而不是三值:为模拟一个正在删除的策略而留第二条代码路径,正是本次要还的那笔债。MCPP_NO_AUTO_INSTALL保留为--offline的旧式窄化拼写(只管工具链)—— 同一个概念此前有三个名字。mcpp index status增加 revision 列索引自带内容身份
.xlings-index-version(artifact 把 rev 打进文件名:mcpp-index-8d67478.tar.gz),mcpp 此前零引用。它是唯一能回答「两台机器/CI 缓存与本地是不是同一份索引」的信号。按不透明字符串处理 —— 探针实测子索引的值是日期版本号(
2026.7.30.1)而不是 sha。语义变化(已写进 docs)
^1.2对本地索引已知的版本求解。上游新发的 1.3.0 需要mcpp index update或mcpp update才可见 —— 这正是那两个命令存在的意义。docs/05-mcpp-toml.md+ 中文版都补了「mcpp 何时刷新包索引」一节。测试
tests/unit/test_pm_index_refresh.cpp:13 条,逐行锁判据表(含 per-OS 版本表陷阱:精确版本不判 VersionMiss)test_xlings.cpp:未来时间戳 → stale;rev 读取三态(缺失/空白/带\r)+ 不透明性tests/e2e/173_index_refresh_policy.sh:7 组场景,全部 hermetic(伪造内置索引 + offline/去抖闸),不依赖网络也不依赖上游状态明确不做
mcpp.lock成为构建期解析输入(它今天只写不读)。那是「零网络 + 可重现」的终局,但牵出resolve_semver绕过index_route的遗留与 lock schema 演进,必须单独立项。本 PR 与它正交:合入后即便永远不做,日常构建也已经零网络。设计与探针结果:
.agents/docs/2026-07-30-issue315-index-refresh-policy-design.md