Skip to content

feat(pm): refresh the package index on a resolution miss, not on a timer (2026.7.30.3, fixes #315) - #320

Merged
speak-agent merged 3 commits into
mainfrom
feat/index-refresh-policy
Jul 30, 2026
Merged

feat(pm): refresh the package index on a resolution miss, not on a timer (2026.7.30.3, fixes #315)#320
speak-agent merged 3 commits into
mainfrom
feat/index-refresh-policy

Conversation

@speak-agent

Copy link
Copy Markdown
Member

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):

index: mcpplibs:cmdline@0.0.1: resolvable locally
index: xim.nasm@2.16.03: no index can refute this
index: somevendor.thing@1.0.0: no index can refute this

单测 InconclusiveNamespaceNeverRefreshes + e2e 173 第 3 步双闸锁住。

顺带修掉的东西

  • mcpp update 此前是空操作:只删 mcpp.lock 条目然后叫用户去 mcpp build,而构建路径从不读 mcpp.lock(prepare 只写不读)—— 删了等于没删,行为影响为零。现在它强制刷索引(显式意图 ⇒ 不看 TTL/去抖)并报告 rev 变化;工程里没有走共享 registry 的依赖时跳过。
  • 未来时间戳的 marker 被判「永远新鲜」:age < ttl 对负数恒真。不可用的时间戳现在读作 unknown,unknown 必须是 stale。
  • 索引刷新此前零并发保护(BMI 缓存早就有 flock)。改为非阻塞互斥:拿不到锁就跳过 —— 持锁者正在做的就是我们想要的事,排队只会把本 issue 要消除的症状原样复制一遍。
  • 项目级 env 下刷新永不打标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 updatemcpp update 才可见 —— 这正是那两个命令存在的意义。docs/05-mcpp-toml.md + 中文版都补了「mcpp 何时刷新包索引」一节。

测试

  • 单测 tests/unit/test_pm_index_refresh.cpp:13 条,逐行锁判据表(含 per-OS 版本表陷阱:精确版本不判 VersionMiss)
  • 单测 test_xlings.cpp:未来时间戳 → stale;rev 读取三态(缺失/空白/带 \r)+ 不透明性
  • e2e 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

…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.
@speak-agent
speak-agent merged commit 3448fe6 into main Jul 30, 2026
15 checks passed
@speak-agent
speak-agent deleted the feat/index-refresh-policy branch July 30, 2026 14:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

建议:mcpp build/test/run 手动刷新包索引而不是自动刷新

2 participants