Skip to content

Commit ba39f9e

Browse files
committed
fix: #278 package identity convergence — index side + dependency side (0.0.105)
mcpp-index changed `chriskohlhoff.asio`'s `name` from "chriskohlhoff.asio" to "asio" (namespace untouched). Lint went green; all three platform workspace jobs died after 20-58 minutes with E_NOT_FOUND. The descriptor parsed, passed mcpp's own identity gate, and no consumer spelling could install it. Root cause is a split in how package identity is spoken on the two sides: index side — identity normalization treats (ns="a", name="b") and (ns="a", name="a.b") as equivalent, while install-target construction hard-assumes the latter. The index is a FLAT key space keyed by the LITERAL package.name (libxpkg: build_index is called with no namespace, lookup is exact entries.find), so the two never meet. dependency side — the bare-name candidate ladder's discovery rung was specified as "match by name across the precedence path" but implemented as "try two more filenames", and a total miss fell through to candidates.front() SILENTLY, so mcpp continued with a namespace it had invented. Both contracts existed only as prose comments. They are now predicates. INV-NAME (index side): package.name must BE the fully-qualified name whenever package.namespace is declared. One predicate, two call sites — `mcpp xpkg parse` (index CI, seconds) and the install path (catches descriptors that never passed through lint, zero extra I/O since the descriptor is already in memory). The predicate is deliberately narrow: written as the general "literal != derived fqname" comparison it would flag the working compat-alias path and break every bare dependency. CompatAliasIsClean locks that. INV-RESOLVE (dependency side): a bare name resolves in exactly three places — mcpplibs, compat, and no-namespace upstream packages. Everything else must be written out. Global short-name search is rejected: it makes resolution depend on which indices happen to be present, so adding an index could silently retarget an existing dependency. BREAKING: bare names no longer reach third-party namespaces. The index-wide scan survives only as a did-you-mean on the already-failed path — result goes into error text, never back into resolution. Also fixes the generator that produced these descriptors: `mcpp emit xpkg` wrote a bare name and no namespace at all, so filing a package into a namespaced index meant hand-adding `namespace = "<org>"` — and that edit is what created the split form. aimol.tensorvia-cpu was born exactly this way. Prescribed in 2026-06-26 design §4.5; never landed until now. Design: .agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md Fixes #278
1 parent 4a08b48 commit ba39f9e

18 files changed

Lines changed: 1258 additions & 20 deletions

.agents/docs/2026-06-26-identity-first-resolution-no-filename.md

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
**Date:** 2026-06-26
44
**Status:** Step 0 landed in v0.0.67 (candidate selection is now identity-first);
5-
§5 `PackageLocator` / `IdentityIndex` choke-point consolidation remains follow-up
5+
§5 `PackageLocator` / `IdentityIndex` choke-point consolidation remains follow-up.
6+
**Partly superseded by #278 (v0.0.105):** §4.5's emit half landed (`mcpp emit xpkg`
7+
now writes both `namespace` and the FQN `name`); its "catalog keys on canonical
8+
(ns,name)" half is **unachievable as written** — the catalog belongs to xlings, is
9+
keyed by the literal `package.name` with exact `find()`, and mcpp's normalization
10+
has no authority over it, so the split/FQN equivalence is instead resolved by
11+
constraining the wire key's spelling (INV-NAME). §4.6(c)'s discovery rung is
12+
narrowed — see the SUPERSEDED note at that row.
613
**Extends:** [`2026-06-20-package-resolution-architecture.md`](2026-06-20-package-resolution-architecture.md)
714
(realizes its deferred §5 `PackageLocator` / identity-indexed slow path),
815
[`2026-05-11-namespace-field-design.md`](2026-05-11-namespace-field-design.md),
@@ -389,7 +396,30 @@ declared `(∅, a.b.c)` in the `a.b`-owned index. The user's point, encoded.
389396
| candidate kind | rule | matches `(aimol, tensorvia-cpu)`? |
390397
|---|---|---|
391398
| qualified, ns non-empty | **exact tuple equality** `cand == declared` | `(aimol, tensorvia-cpu)` ✅ · `(mcpplibs.aimol, …)` ❌ · `(mcpplibs, …)`|
392-
| discovery, ns = `` | match by `name` alone across the precedence path; **resolve to the declared `(ns, name)`** before returning | `(∅, tensorvia-cpu)` ✅ → resolves to `(aimol, tensorvia-cpu)` |
399+
| discovery, ns = `` | ~~match by `name` alone across the precedence path~~ **SUPERSEDED — see the note below**; **resolve to the declared `(ns, name)`** before returning | ~~`(∅, tensorvia-cpu)` ✅ → resolves to `(aimol, tensorvia-cpu)`~~ |
400+
401+
> **SUPERSEDED by #278 (mcpp 0.0.105).** The discovery rung is **not** a
402+
> cross-namespace wildcard. It is the "upstream package that declares no
403+
> `namespace`" rung, and a hit whose descriptor declares a non-empty namespace is
404+
> now REJECTED at the dependency-resolution call site (`selectDependencyCandidate`
405+
> in `prepare.cppm`; the gate itself is unchanged, because `mcpp new --template X`
406+
> still discovers by short name). A bare `[dependencies]` key therefore resolves in
407+
> exactly three places: `mcpplibs`, `compat`, and no-namespace upstream packages.
408+
>
409+
> Why the reversal: "match by name alone across the precedence path" makes
410+
> resolution depend on which indices happen to be present. Two namespaces owning
411+
> the same short name would be settled by an index ordering that has **no total
412+
> order** across user-added `[indices]`, and adding an index could silently
413+
> retarget an existing dependency. Reproducibility beats the convenience.
414+
>
415+
> The **P3 half of this row still stands and is now actually implemented**: a
416+
> discovery hit writes back the namespace the DESCRIPTOR declares, not the
417+
> candidate's empty one. Caveat: an empty declared namespace is a legal identity
418+
> for upstream bare packages, not a hole to fill — the "no empty namespace ever
419+
> reaches the install layer" claim in §4.4 presumes §4.1 index-owned namespace
420+
> attribution, which remains unimplemented.
421+
>
422+
> See `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md` §3.2.
393423
394424
**Selection** = first candidate (in the §(a) order) that finds a declared identity by
395425
rule §(c). Crucially, "finds" means **an entry with that declared identity exists in

.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md

Lines changed: 399 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# #278 包身份双侧收敛 — Implementation Plan
2+
3+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4+
5+
**Goal:** 实现 `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md` 的两条不变式 → 单 PR(`Fixes #278`)→ CI 全绿 → bypass squash 合入 → 0.0.105 release → xlings 全生态验证。
6+
7+
**Architecture:** 一个纯谓词 `xpkg_name_form_violation` 两处调用(lint + 运行期),消灭"同一决策两处推导";依赖侧候选阶梯收敛为封闭三档,全索引扫描降级为**仅失败路径**的 did-you-mean 诊断,绝不回灌解析。
8+
9+
## Global Constraints
10+
11+
- `canonical_xpkg_identity` / `xpkg_lua_identity_matches` **一个字不改** —— 收紧只发生在发布侧(lint)与依赖解析调用点。
12+
- 谓词必须收窄为"声明了非空 namespace 且 name 不以 `ns.` 开头",**不得**写成"字面 name != 推导 fqname"通用比较(会打断裸 `gtest``compat.gtest`)。
13+
- did-you-mean 扫描三约束:仅失败路径触发、只进错误文案、扫描空不改退出码。
14+
-`namespace` 声明的上游包(`opencv`/`musl-gcc`)空 ns 是**合法身份**,T10 不得强行填充。
15+
- T11 收紧 scoped 到 `selectDependencyCandidate`,不动闸门(保 `mcpp new --template`)。
16+
- 既有单测与 e2e(160 例)全绿。
17+
18+
## Tasks
19+
20+
### C1 索引侧谓词与两个调用点
21+
22+
- [ ] **T1** `xpkg_name_form_violation(declaredNs, declaredName)` + `_from_lua` 导出(`src/manifest/xpkg.cppm`)
23+
- [ ] **T2** `mcpp xpkg parse` 接入谓词,违规 exit 1;`--json` 输出 `error` 字段(`src/cli/cmd_xpkg.cppm`)
24+
- [ ] **T3** `loadVersionDep` install 分支 fail-fast;已解析到安装物的路径 `ui::warn`(`src/build/prepare.cppm`)
25+
26+
### C2 生成源
27+
28+
- [ ] **T4** `emit_xpkg` 输出 `namespace` + FQN `name`;无 namespace 时 stderr 提示;`--namespace` 覆盖开关(`src/pm/publisher.cppm` + emit CLI)
29+
30+
### C3 依赖侧收敛
31+
32+
- [ ] **T9** `selectDependencyCandidate` 全候选落空 → 明确失败(`prepare.cppm:1516-1541`)
33+
- [ ] **T10** discovery 档命中后 `extract_xpkg_namespace(*lua)` 回填真实 ns(声明为空则保持空)
34+
- [ ] **T11** discovery 档拒绝声明了非空 `namespace` 的描述符
35+
- [ ] **T12** did-you-mean:仅失败路径的全索引扫描,三约束写进实现注释
36+
37+
### C4 测试
38+
39+
- [ ] **T5a** 单测:谓词 6 例(含 `CompatAliasIsClean` 回归锁)
40+
- [ ] **T5b** 单测:依赖侧解析(三档成功 / 第三方 ns 裸名失败 / P3 空 ns 锁)
41+
- [ ] **T5c** e2e:split 描述符 → 秒级自解释失败;改 FQN → 通过
42+
- [ ] **T5d** e2e:裸名请求第三方 ns 包 → 失败 + did-you-mean;改写后通过
43+
44+
### C5 文档与版本
45+
46+
- [x] **T13** 用户文档 §2.5(`docs/05-mcpp-toml.md` + `docs/zh/05-mcpp-toml.md`)
47+
- [ ] **T14** CHANGELOG 0.0.105 段(含 breaking:裸名不再解析第三方命名空间包)
48+
- [ ] **T15** `mcpp.toml` version → 0.0.105
49+
- [ ] **T11b** 修订 `2026-06-26 §4.4/§4.6(a)` 表述;更新 `prepare.cppm:1456-1459` 注释
50+
51+
### C6 验收与合入
52+
53+
- [ ] **T6** 全索引 `mcpp xpkg parse` 回归(期望恰好 2 报错),结果贴 PR
54+
- [ ] **T7** 单 PR(`Fixes #278`)→ CI 全绿 → `gh pr merge --squash --admin`
55+
56+
### C7 生态
57+
58+
- [ ] **T16** release 0.0.105 四平台 + 镜像 xlings-res 双端 + xim-pkgindex PR + `xlings install mcpp` 真装验证
59+
- [ ] **T8** mcpp-index:`chriskohlhoff.asio` / `tensorvia-cpu` 改 FQN;lint job 改调 `mcpp xpkg parse`

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,30 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.105] — 2026-07-25
7+
8+
> 包身份口径双侧收敛(#278)。事故:mcpp-index`chriskohlhoff.asio``name``"chriskohlhoff.asio"` 改成 `"asio"`(namespace 不变),lint 全绿,三个平台的 workspace job 跑满 20~58 分钟后全挂 `E_NOT_FOUND` —— 描述符能解析、能过 mcpp 的身份闸门,却没有任何消费写法能装上。根因是身份归一化(容忍三种拼写)与安装目标构造(只支持一种)口径断层,契约只写在注释里、无人执行。设计见 `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md`
9+
10+
### ⚠️ 破坏性变更
11+
12+
- **裸依赖名不再解析到第三方命名空间的包。** 命名空间缺省时,`[dependencies]` 里的裸名****解析三类:`mcpplibs`(默认)、`compat`(包装)、无 `namespace` 声明的上游包。此前裸名会跨命名空间命中(例如裸 `tensorvia-cpu` 能装上 `aimol` 下的包),现在必须写全:`"aimol.tensorvia-cpu" = "…"``[dependencies.aimol] tensorvia-cpu = "…"`
13+
取舍理由:全域按名发现的便捷性换来三条稳定性损失——同名包的裁决依赖索引优先级(而用户 `[indices]` 添加的索引之间**无全序**)、**新增一个索引可能悄悄改变既有依赖解析到的包**(供应链隐患)、同一份 `mcpp.toml` 在不同机器上可能解析到不同包。依赖解析的可复现性优先于书写便捷性。
14+
迁移无需查文档:失败时 mcpp 会扫描索引并直接给出应当改写成的那两行(见下)。
15+
16+
### 新增
17+
18+
- **INV-NAME 校验(`mcpp xpkg parse`)**:描述符声明了非空 `package.namespace` 时,`package.name` 必须是完全限定名(以 `<namespace>.` 开头),否则报错退出。索引是以 `package.name` **字面值**为键的扁平空间,而 mcpp 按 `<ns>.<short>` 寻址,两者不相交即永久不可安装。诊断直接给出应写的字面量(`fix: name = "chriskohlhoff.asio"`),`--json` 同步 `error` 字段供索引 CI 机读。mcpp-index 的 CI 本就跑 `mcpp xpkg parse pkgs/*/*.lua`,免费获得秒级防护。
19+
- **`mcpp xpkg parse --allow-split-name`**:跳过 INV-NAME 检查。xlings 原生索引(xim-pkgindex、-scode)里 `package.namespace`**安装目录分类**(`config`/`scode`/`awesome`)而非包命名空间,索引按裸 `package.name` 建键,split 形式在那个世界里是正确的——这些树用此开关 lint。
20+
- **运行期 fail-fast**:`mcpp build` 在构造安装目标**之前**用同一个谓词校验手上已有的描述符(零额外 I/O),把"三平台一小时后的 E_NOT_FOUND"变成秒级自解释失败。lint 与运行期共用一份判定,不会再各自推导。已装旧快照的路径降级为 warning,让"本机绿、干净 CI 红"的遮蔽陷阱可见。
21+
- **依赖解析失败的 did-you-mean**:候选全部落空时(且****在此时)扫描索引,若该短名存在于其他命名空间,直接列出 FQN 与两种可直接抄写的正确写法。该扫描是**纯诊断**——结果只进错误文案,绝不回灌解析、lockfile 或安装层(否则就退化成被否决的全域模糊匹配)。
22+
- **`mcpp emit xpkg --namespace <NS>`**:为归档场景提供命名空间,无需改 `mcpp.toml`
23+
24+
### 修复
25+
26+
- **`mcpp emit xpkg` 不再生成破损雏形**:此前只写裸 `name`、完全不输出 `namespace`,维护者归档进命名空间索引时手补一行 `namespace = "<org>"`,那一刻描述符就变成无法安装的 split 形式(`aimol.tensorvia-cpu` 正是这么来的,文件头还留着"AUTO-GENERATED, do not edit by hand")。现在 `[package] namespace` 非空时同时输出 `namespace` 与 FQN `name`;为空时给出明确警告,说明不要事后手补。此修复在 2026-06-26 设计 §4.5 里就已写明,一直未落地。
27+
- **依赖候选全部落空时不再静默回退**:此前会退到第一个候选并把 mcpp **自己编造的**命名空间当作结论继续跑,失败被推迟到下载/安装阶段,错误文本里还带着用户从未写过的命名空间。现在当场失败并列出试过的每一个身份。
28+
- **discovery 档不再泄漏空命名空间(P3)**:`(∅, name)` 命中后写回的是**描述符声明的**命名空间,而非候选的空值——空命名空间此前会流进 lockfile 与安装层。无 `namespace` 声明的上游包(`opencv`/`musl-gcc`)保持空命名空间,那是它们的合法身份。
29+
630
## [0.0.104] — 2026-07-24
731

832
> `mcpp test` 能力批次(两轮):逐测试编译隔离、子目录路径命名、过滤器、JSON 输出——四项均为通用能力(cargo/ctest 同形),首个下游消费者是 d2mcpp「练习即测试」重设计(见 d2mcpp 仓 `.agents/docs/2026-07-23-exercises-as-tests-design.md` §4,验收标准即出自该文档)。实施计划见 `.agents/docs/2026-07-23-test-isolation-json-plan.md`

docs/05-mcpp-toml.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,61 @@ baz = "=1.2.3" # Exact match
321321
qux = ">=1.0, <2.0" # Range combination
322322
```
323323

324+
#### Namespace resolution rules
325+
326+
Every package has a two-part identity: a **namespace** and a **name**. How you write
327+
a dependency key decides which namespaces mcpp will look in.
328+
329+
**A bare name resolves in exactly three places**, in order:
330+
331+
| # | Namespace | Example |
332+
|---|---|---|
333+
| 1 | `mcpplibs` — the default namespace | `cmdline = "0.0.2"` |
334+
| 2 | `compat` — the wrapper namespace for third-party C/C++ libraries | `gtest = "1.15.2"``compat.gtest` |
335+
| 3 | upstream packages that declare no namespace at all | `opencv = "4.10.0"` |
336+
337+
**Any other namespace must be written out in full.** There is no fuzzy, index-wide
338+
search by short name:
339+
340+
```toml
341+
# ✅ Correct — dotted selector
342+
[dependencies]
343+
"chriskohlhoff.asio" = "1.38.1"
344+
345+
# ✅ Correct — namespace sub-table (preferred when you have several from one org)
346+
[dependencies.chriskohlhoff]
347+
asio = "1.38.1"
348+
349+
# ❌ Wrong — a bare name never reaches the `chriskohlhoff` namespace
350+
[dependencies]
351+
asio = "1.38.1"
352+
```
353+
354+
The third form fails with an error that lists the namespaces that were searched and,
355+
when a package with that short name exists elsewhere, the exact line to write instead.
356+
357+
**Why not resolve bare names across every namespace?** Because dependency resolution
358+
has to be reproducible. A global short-name search would mean that (a) two namespaces
359+
owning the same short name are settled by index ordering, and (b) **adding an index
360+
could silently change which package an existing dependency resolves to**. Requiring
361+
the namespace keeps a `mcpp.toml` resolving to the same packages on every machine.
362+
363+
**For xpkg authors:** in an index descriptor, `package.name` must be the *fully
364+
qualified* name — it has to start with `package.namespace` followed by a dot:
365+
366+
```lua
367+
package = {
368+
namespace = "chriskohlhoff",
369+
name = "chriskohlhoff.asio", -- NOT "asio"
370+
...
371+
}
372+
```
373+
374+
The package index is a flat key space keyed by the literal `package.name`, so the
375+
namespace has to be carried in the name itself — otherwise `mcpplibs`'s `zlib` and
376+
`compat`'s `zlib` would collide. A descriptor that gets this wrong parses fine but can
377+
never be installed. `mcpp xpkg parse` checks it, so run it in your index CI.
378+
324379
### 2.6 `[dev-dependencies]` — Test Dependencies
325380

326381
```toml

docs/zh/05-mcpp-toml.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,50 @@ baz = "=1.2.3" # 精确匹配
293293
qux = ">=1.0, <2.0" # 范围组合
294294
```
295295

296+
#### 命名空间解析规则
297+
298+
每个包的身份是**命名空间 + 名字**二元组。依赖 key 的写法决定 mcpp 到哪些命名空间里找。
299+
300+
**裸名只在三个地方解析**,按序:
301+
302+
| # | 命名空间 | 示例 |
303+
|---|---|---|
304+
| 1 | `mcpplibs` — 默认命名空间 | `cmdline = "0.0.2"` |
305+
| 2 | `compat` — 第三方 C/C++ 库的包装命名空间 | `gtest = "1.15.2"``compat.gtest` |
306+
| 3 | 完全没有声明命名空间的上游包 | `opencv = "4.10.0"` |
307+
308+
**其他命名空间一律必须写全。** 不存在按短名的全索引模糊搜索:
309+
310+
```toml
311+
# ✅ 正确 —— 点式选择器
312+
[dependencies]
313+
"chriskohlhoff.asio" = "1.38.1"
314+
315+
# ✅ 正确 —— 命名空间子表(同一组织有多个包时更推荐)
316+
[dependencies.chriskohlhoff]
317+
asio = "1.38.1"
318+
319+
# ❌ 错误 —— 裸名永远到不了 chriskohlhoff 命名空间
320+
[dependencies]
321+
asio = "1.38.1"
322+
```
323+
324+
第三种写法会明确报错,并列出搜索过的命名空间;若该短名的包存在于别处,错误信息会直接给出应当改写成的那一行。
325+
326+
**为什么不让裸名跨所有命名空间去找?** 因为依赖解析必须可复现。全域短名搜索意味着:(a) 两个命名空间拥有同名包时,胜负由索引顺序决定;(b) **新增一个索引可能悄悄改变某个既有依赖解析到的包**。要求写出命名空间,才能让同一份 `mcpp.toml` 在每台机器上解析到相同的包。
327+
328+
**给 xpkg 作者:** 索引描述符里的 `package.name` 必须是**完全限定名** —— 以 `package.namespace` 加一个点开头:
329+
330+
```lua
331+
package = {
332+
namespace = "chriskohlhoff",
333+
name = "chriskohlhoff.asio", -- 不是 "asio"
334+
...
335+
}
336+
```
337+
338+
包索引是以 `package.name` 字面值为键的**扁平键空间**,命名空间的区分度必须由名字本身携带 —— 否则 `mcpplibs``zlib``compat``zlib` 会撞键。写错的描述符能被正常解析,却永远装不上。`mcpp xpkg parse` 会校验这一点,请在索引 CI 里跑它。
339+
296340
### 2.6 `[dev-dependencies]` — 测试依赖
297341

298342
```toml

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.104"
3+
version = "0.0.105"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

0 commit comments

Comments
 (0)