Skip to content

Commit 297cd0d

Browse files
committed
fix: a bare dependency's wire address takes BOTH halves from its descriptor (0.0.109)
main went 6-of-7 workflows red without a line of mcpp changing. The PR run for the very same commit was green 80 minutes earlier; in between, mcpp-index #120 migrated all 48 descriptors to SPEC-001 short names. The identity gate deliberately accepts a `compat` descriptor for a bare/default- namespace request — that is how `gtest = "1.15.2"` resolves to compat.gtest at all. But the wire address then took its NAME from that descriptor and its NAMESPACE from the request, emitting `mcpplibs:gtest`, which no index is keyed by. It only ever worked because the literal `package.name` used to read `compat.gtest`, so the hardcoded `compat.<short>` retry caught it. The short-name migration removed that coincidence and 34 of the index's 48 packages became uninstallable by bare name. So the main path was always broken and a legacy retry had been carrying it. This is a latent defect coming due, not a new regression. Evidence, from one CI job (same index, same xlings, same process): compat:zlib@1.3.2 OK <- [dependencies.compat] zlib compat:libarchive@3.8.7 OK mcpplibs:ftxui@6.1.9 FAIL <- bare [dependencies] ftxui compat.ftxui@6.1.9 FAIL Fix: `mcpp::manifest::xpkg_wire_address` derives ns and name together from the descriptor the gate accepted, and prepare.cppm uses it. Blast radius, checked against every descriptor in the live index: the 8 mcpplibs packages address exactly as before, the 6 under other namespaces are unreachable by bare name anyway, and the 34 compat ones — all currently broken — start working. A descriptor that cannot be read keeps the historical derivation byte for byte, so an unsynced index still fails where it used to. The compat retry now tries `compat:<short>` before the legacy `compat.<short>`, so the no-descriptor path works against both index generations. Two more failures in the same batch, same trigger: * e2e 163 built its fixture by sed'ing the literal `name = "chriskohlhoff.asio"` out of the real registry descriptor. #120 renamed it, the rewrite silently stopped matching, and the fixture carried the wrong identity — a hermetic- looking test that had upstream's exact bytes compiled into it. Rewrites now match by shape and hard-fail if they miss. * bootstrap-mcpp ran `xlings install mcpp -y` with no pin. `.xlings.json` fed only the cache key, and restore-keys prefix-matches, so jobs ran 0.0.107 and 0.0.102 against an index whose floor is 0.0.108. Warm caches hid the E0006; a cold one would not have. It now installs the pin and addresses that version by path rather than through a shim that can point elsewhere. Verification: * 9 unit cases over xpkg_wire_address covering every row of the blast-radius table, including the ones that must NOT change. * New e2e 165 reproduces the production bug hermetically: a bare dep served by a compat descriptor from a local fixture index. Run against a binary built without the fix it reports the exact production symptom (mcpplibs:widget then compat.widget); with the fix it passes. * End-to-end against the live migrated index: a bare `eigen = "5.0.1"` now resolves in one shot as `compat:eigen@5.0.1`, no fallback. Design + plan: .agents/docs/2026-07-26-bare-name-wire-address-{design,implementation-plan}.md
1 parent 1b07728 commit 297cd0d

14 files changed

Lines changed: 863 additions & 58 deletions
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
# 裸名依赖的 wire address 收敛 — Design(mcpp 0.0.109)
2+
3+
> 触发事件:mcpp-index PR #120(SPEC-001 短名迁移,2026-07-25T20:10:37Z 合入)后,
4+
> mcpp main 的 7 个 workflow 红了 6 个。诊断报告见本文 §1,那不是新引入的回归。
5+
6+
## 1. 事实与证据
7+
8+
### 1.1 时间线
9+
10+
| 时间 (UTC) | 事件 | 结果 |
11+
|---|---|---|
12+
| 2026-07-25 18:43 / 18:58 | PR #282 的 CI(**与 main 完全相同的 mcpp 代码**| ✅ 全绿 |
13+
| **2026-07-25 20:10:37** | **mcpp-index #120 合入**(48 个描述符 FQN → 短名,floor → 0.0.108) ||
14+
| 2026-07-25 20:18:22 | #282 squash 进 mcpp main,push 触发 CI | ❌ 7 个 workflow 红 6 个 |
15+
16+
mcpp 侧 diff 只有 `.xlings.json` 一行。**代码没变,索引变了。**
17+
18+
### 1.2 决定性对照(`integration: mcpp builds & runs xlings`,同一次运行、同一进程)
19+
20+
`MCPP_VERBOSE=1` 抓到的真实 xlings 调用:
21+
22+
```
23+
targets:["compat:zlib@1.3.2"] → ✅
24+
targets:["compat:lz4@1.10.0"] → ✅
25+
targets:["compat:xz@5.8.3"] → ✅
26+
targets:["compat:zstd@1.5.7"] → ✅
27+
targets:["compat:libarchive@3.8.7"] → ✅ ← 依赖写的是限定名 compat.xxx
28+
targets:["mcpplibs:ftxui@6.1.9"] → ❌
29+
targets:["compat.ftxui@6.1.9"] → ❌ ← 依赖写的是裸名 ftxui
30+
```
31+
32+
**限定名走 `compat:<short>` 全通;裸名走 `mcpplibs:<short>` 全挂。** 正确地址是
33+
`compat:ftxui@6.1.9`,两条尝试都没打中。
34+
35+
### 1.3 根因
36+
37+
裸名 `gtest = "1.15.2"` 解析为 `ns=kDefaultNamespace("mcpplibs")` + `short=gtest`
38+
`src/pm/dep_spec.cppm:58`)。
39+
40+
**第一步:身份门刻意放宽,接受 compat 描述符。**
41+
`src/manifest/xpkg.cppm` `xpkg_lua_identity_matches`
42+
43+
```cpp
44+
if (ns == kDefaultNamespace) {
45+
return id.ns == kDefaultNamespace
46+
|| id.ns == kCompatNamespace // ← compat.gtest.lua 从这里被接受
47+
|| (allowLegacyBareDefault && id.ns.empty());
48+
}
49+
```
50+
51+
**第二步:拼 wire target 时,name 取自描述符,namespace 取自请求。**
52+
`src/build/prepare.cppm:1864`
53+
54+
```cpp
55+
auto wireName = luaContent ? extract_xpkg_name(*luaContent) : ""; // "gtest" ← 描述符
56+
auto target = std::format("{}:{}@{}", ns, wireName, version); // ns="mcpplibs" ← 请求
57+
```
58+
59+
身份门认下的是 `(compat, gtest)`,发出去的却是 `mcpplibs:gtest`
60+
**同一次解析,身份有两个来源。**
61+
62+
**第三步:兜底写死迁移前的字面名。**
63+
64+
```cpp
65+
auto compatTarget = std::format("compat.{}@{}", shortName, version);
66+
```
67+
68+
### 1.4 为什么迁移前是绿的
69+
70+
迁移前 `extract_xpkg_name` 返回 `"compat.gtest"` → 首选 target `mcpplibs:compat.gtest`
71+
(同样是错的,同样挂)→ 兜底 `compat.gtest@1.15.2` **恰好命中旧字面名,救了回来**
72+
73+
**主路径一直是坏的,一直靠那条写死的 legacy 兜底在扛。** #120 抽掉了兜底命中的
74+
字面名,主路径的 bug 才现形。这是存量缺陷到期,不是新回归。
75+
76+
### 1.5 改动面(对现网索引 48 个描述符逐个核对)
77+
78+
| 裸名请求能解析到的描述符 | 数量 | 修复后 |
79+
|---|---|---|
80+
| `namespace = "mcpplibs"`(cmdline / tinyhttps / opencv / imgui / ffmpeg / llmapi / templates / xpkg) | 8 | **无变化**(wireNs == 请求 ns) |
81+
| `namespace = "compat"` | 34 | `mcpplibs:X``compat:X`**这 34 个当前 100% 是坏的** |
82+
| `namespace` 为空的 legacy 描述符 | **0** | 边界情况在真实索引里不存在 |
83+
| 其他 ns(chriskohlhoff / fmtlib / marzer / nlohmann / aimol / mcpplibs.capi) | 6 | 身份门本就拒绝裸名请求,够不到 |
84+
85+
**只有当前全坏的 34 个会变。没坏的一个都不动。**
86+
87+
### 1.6 第二个独立失效面:e2e 163 的 fixture 是对上游描述符做 sed
88+
89+
`tests/e2e/163_identity_first_resolution.sh:42` 把注册表里真实的
90+
`chriskohlhoff.asio.lua` sed 成测试 fixture:
91+
92+
```bash
93+
sed -e "s/name = \"chriskohlhoff.asio\"/name = \"$4\"/"
94+
```
95+
96+
#120 把 asio 也迁了(`name = "chriskohlhoff.asio"``"asio"`),**第二条 sed 不再匹配**
97+
fixture 身份变成 `(acme, asio)`,而 app 请求 `acme:widget`
98+
99+
```
100+
error: dependency 'acme.widget': not found in local index at '.../idx1'
101+
```
102+
103+
一个看起来 hermetic(`mktemp -d`、无网络)的测试,实际把上游描述符的**逐字文本**
104+
编进了断言。linux shard2 / windows shard2 / macOS 三处同时挂。
105+
106+
### 1.7 第三个(潜伏):bootstrap 版本漂移 vs 索引 floor
107+
108+
```
109+
error: index requires mcpp >= 0.0.108 but this is mcpp 0.0.107 [E0006] # linux
110+
error: index requires mcpp >= 0.0.108 but this is mcpp 0.0.102 [E0006] # windows
111+
```
112+
113+
`.github/actions/bootstrap-mcpp/action.yml` 跑的是 **`xlings install mcpp -y`——没有版本
114+
pin**`.xlings.json` 里的 pin 只进了 cache key;而 `restore-keys` 前缀匹配会把旧 cache
115+
捞回来,`install` 幂等地保留缓存里的任意旧版本。
116+
117+
当前被热 cache 掩盖(`mcpplibs.cmdline` 已缓存,不需要读索引)。**冷 cache 一来就是硬失败。**
118+
119+
### 1.8 索引侧的流程盲区(为什么 #120 自己 8 个 check 全绿)
120+
121+
mcpp-index 每个 example 用的都是限定名 + path 索引:
122+
123+
```toml
124+
[indices]
125+
compat = { path = "../../.." }
126+
[dependencies.compat]
127+
gtest = { version = "1.15.2", features = ["main"] }
128+
```
129+
130+
`[dependencies.compat]``compat:<short>`,正是**唯一还能通的那条路**;而所有真实
131+
消费者(含 mcpp 自己的 `mcpp.toml`)写的是裸名。**验证矩阵与消费矩阵不同构**
132+
结构性假绿。(与 `xcb-link-regression` / `spec001` 两次是同一类。)
133+
134+
---
135+
136+
## 2. 设计
137+
138+
### 2.1 原则
139+
140+
> **身份门认下谁,就用谁的两半。**
141+
142+
`read_xpkg_lua*` 家族已经用 `xpkg_lua_identity_matches` 把一个描述符**认定为**这次请求
143+
的提供者。既然认定了,wire address 的 namespace 和 name 就都必须来自它——不能一半取
144+
描述符、一半取请求。
145+
146+
这与 SPEC-001 §6 一致:wire key 是字面 `package.name`,地址是
147+
`<effectiveNamespace>:<literal name>`
148+
149+
### 2.2 新增纯函数(可单测)
150+
151+
`prepare.cppm` 里那段逻辑很难单测。把它抽成 `src/manifest/xpkg.cppm` 的纯函数——
152+
`canonical_xpkg_identity_from_lua` 就在隔壁,身份门也在那里:
153+
154+
```cpp
155+
struct XpkgWireAddress {
156+
std::string ns; // effective namespace("" = 无命名空间包)
157+
std::string name; // 字面 package.name(SPEC-001 §6 的 wire key)
158+
std::string target; // "<ns>:<name>" 或 "<name>"
159+
};
160+
161+
XpkgWireAddress xpkg_wire_address(std::string_view luaContent, // 可为空
162+
std::string_view requestNs,
163+
std::string_view shortName);
164+
```
165+
166+
语义:
167+
168+
| luaContent | ns | name |
169+
|---|---|---|
170+
| 有,且声明了 namespace | 描述符的 effective ns | 字面 `package.name` |
171+
| 有,但没声明 namespace | `requestNs`(= `indexDefaultNs` 语义) | 字面 `package.name` |
172+
| 无 / 无 name | `requestNs` | `requestNs.empty() ? shortName : "<requestNs>.<shortName>"`(**保持历史推导不变**) |
173+
174+
最后一行是关键:描述符读不到时**不改变任何现有行为**,失败点留在原地。
175+
176+
### 2.3 兜底链加宽
177+
178+
兜底只在首选 target 失败后触发。当前只试 `compat.<short>`(迁移前字面名)。
179+
新索引下该字面名已不存在,因此补上 SPEC-001 拼法:
180+
181+
1. `compat:<shortName>@<ver>` — SPEC-001 短名形态(**新增**)
182+
2. `compat.<shortName>@<ver>` — legacy 字面名(保留)
183+
184+
两条都只在与首选 target 不同时才发。这让「描述符读不到」的路径对新旧两种索引都成立。
185+
186+
### 2.4 不做的事
187+
188+
- **不改 `xpkg_lua_identity_matches` 的放宽规则。** 裸名能解析到 compat 是有意设计
189+
(`gtest = "1.15.2"` 必须能用),本次只修地址推导。
190+
- **不动 xlings 任何规范。**
191+
- **不处理 `imgui`/`ffmpeg` 在 mcpplibs 与 compat 下各有一份描述符、靠候选文件名顺序
192+
消歧这件事。** 那是独立的存量歧义(与 #278 同类),修复保持现有结果不变
193+
(`imgui.lua` 先于 `compat.imgui.lua`,mcpplibs 胜出),另开 issue。
194+
195+
---
196+
197+
## 3. 验证策略
198+
199+
### 3.1 单测(`tests/unit/test_manifest.cpp`)
200+
201+
针对 `xpkg_wire_address` 的纯函数矩阵——覆盖 §1.5 表格的每一行:
202+
203+
- compat 描述符 + 裸名请求 → `compat:gtest`(**当前 bug 的直接锁定**)
204+
- mcpplibs 描述符 + 裸名请求 → `mcpplibs:cmdline`(不变)
205+
- legacy FQN 描述符(`ns=compat, name=compat.zlib`)+ 裸名请求 → `compat:compat.zlib`
206+
- 限定请求(`ns=compat`)+ compat 描述符 → `compat:gtest`(不变)
207+
- 嵌套 ns(`ns=mcpplibs.capi, name=lua`)→ `mcpplibs.capi:lua`(不变)
208+
- 无 luaContent → 历史推导 `mcpplibs:mcpplibs.gtest`(不变)
209+
- 描述符无 namespace 声明 → 回落 requestNs
210+
211+
### 3.2 e2e(新增 `165_bare_name_cross_namespace_wire_address.sh`)
212+
213+
**必须 hermetic 地复现生产 bug**——这是本次最重要的一条,因为索引侧的假绿正是
214+
"验证矩阵与消费矩阵不同构"造成的。
215+
216+
用 `[indices] mcpplibs = { path = ... }` 把默认命名空间路由到本地 fixture 索引,
217+
索引里放一个 `namespace = "compat", name = "widget"` 的描述符,app 写**裸名**依赖:
218+
219+
```toml
220+
[indices]
221+
mcpplibs = { path = "../idx" }
222+
[dependencies]
223+
widget = "1.38.1"
224+
```
225+
226+
- 修复前:target = `mcpplibs:widget` → 装不上
227+
- 修复后:target = `compat:widget` → 装上,落在 `compat-x-widget`
228+
229+
断言 store 目录 `compat-x-widget` 存在——直接锁住 wire namespace 取值。
230+
231+
### 3.3 e2e 163 去耦合
232+
233+
sed pattern 改为不依赖旧拼写:
234+
235+
```bash
236+
sed -e "s/^\( *namespace *= *\)\"[^\"]*\"/\1\"$3\"/" \
237+
-e "s/^\( *name *= *\)\"[^\"]*\"/\1\"$4\"/"
238+
```
239+
240+
(更彻底是 fixture 完全自造,但那会丢掉「用真实描述符跑通整条 install 路径」这个
241+
本来有价值的性质,故只做 pattern 去耦合。)
242+
243+
### 3.4 bootstrap pin
244+
245+
`xlings install mcpp@<pin> -y`,pin 从 `.xlings.json` 读,装完断言
246+
`mcpp --version` 与 pin 相等——版本漂移立刻可见,而不是等冷 cache 才炸。
247+
248+
---
249+
250+
## 4. 交付
251+
252+
- 单 PR,版本 **0.0.109**`mcpp.toml` + `src/toolchain/fingerprint.cppm` 两处同步)。
253+
- CI 全绿(7 workflow)→ bypass squash 合入。
254+
- release 0.0.109 → 镜像 xlings-res 双端 → xim-pkgindex → 真装验证 → bootstrap pin bump。
255+
256+
## 5. 后续(不在本 PR)
257+
258+
- mcpp-index 补一个「裸名 + 远端 git 索引」的 example,堵住 §1.8 的盲区。
259+
- mcpp-index 清 `chriskohlhoff.asio.lua:41-48` 的陈旧注释(还写着 `name` MUST be FQN,
260+
第 49 行已是 `name = "asio"`)。
261+
- `imgui`/`ffmpeg` 双描述符靠文件名顺序消歧 → 独立 issue。
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 裸名 wire address 修复 — Implementation Plan(mcpp 0.0.109)
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans。步骤用 `- [ ]` 跟踪。
4+
5+
**Design:** `.agents/docs/2026-07-26-bare-name-wire-address-design.md`
6+
7+
**Goal:** 修复三个失效面(A 裸名 wire address / B e2e 163 fixture 耦合 / C bootstrap 版本
8+
漂移)→ 单 PR(0.0.109)→ CI 全绿 → bypass squash 合入 → release + xlings 全生态验证。
9+
10+
## Global Constraints
11+
12+
- **不改 `xpkg_lua_identity_matches` 的放宽规则**(裸名解析到 compat 是有意设计)。
13+
- **不改 xlings 任何规范。**
14+
- 描述符读不到时,行为**逐字节不变**(失败点留在原地)。
15+
- 现网 48 个描述符:8 个 mcpplibs 不变、6 个其他 ns 够不到、34 个 compat 当前全坏 → 修好。
16+
- 既有单测 + 164 例 e2e 全绿。
17+
- 版本两处同步:`mcpp.toml:3` + `src/toolchain/fingerprint.cppm:21`(0.0.105 踩过)。
18+
19+
## Tasks
20+
21+
### P1 — 失效面 A:wire address 收敛
22+
23+
- [x] **A1** `src/manifest/xpkg.cppm`:新增 `struct XpkgWireAddress` +
24+
`xpkg_wire_address(luaContent, requestNs, shortName)`,语义见 design §2.2。
25+
导出到模块接口。
26+
- [x] **A2** `tests/unit/test_manifest.cpp``xpkg_wire_address` 七格矩阵(design §3.1)。
27+
**先写,先看它红。**
28+
- [x] **A3** `src/build/prepare.cppm:1864` 起:改用 `xpkg_wire_address`
29+
target 由它给出。删掉就地的 `wireName` 推导。
30+
- [x] **A4** `src/build/prepare.cppm` 兜底链加宽:`compat:<short>` 在前、
31+
`compat.<short>` 在后,各自与首选 target 去重(design §2.3)。
32+
- [x] **A5** 新增 e2e `tests/e2e/165_bare_name_cross_namespace_wire_address.sh`
33+
(design §3.2)——hermetic 复现生产 bug,断言 store 目录 `compat-x-widget`
34+
35+
### P2 — 失效面 B:e2e 163 去耦合
36+
37+
- [x] **B1** `tests/e2e/163_identity_first_resolution.sh:42` sed pattern 改为
38+
不依赖描述符旧拼写(design §3.3)。
39+
- [x] **B2** 本地跑 163 验证四个 sub-case(step 4 依赖 xlings >= 0.4.69,按现有逻辑
40+
自动 skip 则接受)。
41+
42+
### P3 — 失效面 C:bootstrap pin
43+
44+
- [x] **C1** `.github/actions/bootstrap-mcpp/action.yml`:unix + windows 两段
45+
`xlings install mcpp -y` → 读 `.xlings.json` 的 pin 装指定版本。
46+
- [x] **C2** 装完断言 `mcpp --version` == pin,不等则 fail(不再静默漂移)。
47+
48+
### P4 — 版本 + 收口
49+
50+
- [x] **D1** 版本 0.0.108 → 0.0.109,两处同步。
51+
- [x] **D2** 本地全量单测。
52+
- [ ] **D3** 本地 e2e(至少 100 / 163 / 165 / 78 / 79 + 全量能跑的部分)。
53+
- [ ] **D4** 单 PR(附版本)→ 7 个 workflow 全绿。
54+
- [ ] **D5** bypass squash 合入 main。
55+
56+
### P5 — xlings 全生态验证
57+
58+
- [ ] **E1** release v0.0.109(四平台)。
59+
- [ ] **E2** 镜像 xlings-res 双端(github + gitcode),sha256 独立核验。
60+
- [ ] **E3** xim-pkgindex PR。
61+
- [ ] **E4** 干净 XLINGS_HOME 真装 `xlings install mcpp@0.0.109` 并跑通。
62+
- [ ] **E5** bootstrap pin `.xlings.json` → 0.0.109(独立 PR)。
63+
64+
## 风险与对策
65+
66+
| 风险 | 对策 |
67+
|---|---|
68+
| `compat:compat.zlib`(legacy FQN + 裸名请求)xlings 不认 | e2e 163 step 2 已覆盖等价形态(`acme:acme.widget``acme-x-acme.widget`),迁移前它是绿的 |
69+
| 改动波及 8 个 mcpplibs 包 | 单测显式锁 `mcpplibs:cmdline` 不变;e2e 全量兜底 |
70+
| CI 热 cache 掩盖 C1 效果 | C2 的版本断言让漂移立刻可见 |
71+
| 索引侧假绿复发 | A5 是 hermetic 的,不依赖远端索引内容 |

0 commit comments

Comments
 (0)