Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# `package.name` / `package.namespace` 双向验证报告

> 触发:mcpp-index PR#117 的文档把「`name` 必须写成 FQN」写成了**设计规则**,评审指出这只是兼容写法,层级应当全在 `namespace`、`name` 只留短名。
> 目的:用当前源码 + 实机双向验证,判定哪一方成立、代价在哪、以及 PR#117 该怎么写。
> 基线:mcpp `main` @ `cee8130`(0.0.105),xlings 0.4.68 / libxpkg 当前 HEAD。

---

## 0. 结论先行

| 命题 | 判定 |
|---|---|
| 「层级属于 `namespace`,`name` 概念上是短名」 | ✅ **成立**,且与 mcpp §4.2 一致 |
| 「`name` 必须写 FQN」是 mcpp 的**设计规则** | ❌ **不成立** —— 这是我写错了 |
| 「`name` 必须写 FQN」是**当前实现的编码约束** | ✅ 成立,但根因是**一行可改的代码**,不是架构 |
| 我此前「根因在 libxpkg 的 key 空间」的判断 | ❌ **错误**,已实机证伪(见 §3) |
| 短名形式需要改 libxpkg / xlings | ❌ **不需要**,mcpp 侧一行即可 |

**修正后的根因**:`prepare.cppm:1861` 把已经读到手的字面 `name` **丢弃**,改用 `ns + "." + short` 重新渲染一遍去当 wire key。改成直接用字面 `name`,短名形式立即可用,且**现网 FQN 形式继续可用**。

---

## 1. 方向一:从源码看 mcpp 如何处理 name/namespace

### 1.1 读取 —— mcpp 完全自理,不依赖 xlings

| 环节 | 位置 | 是否依赖 xlings |
|---|---|---|
| 定位描述符文件 | `package_fetcher.cppm` `read_xpkg_lua*` | ❌ 自己扫盘 |
| 解析 `namespace` | `xpkg.cppm:670` `extract_xpkg_namespace` | ❌ 自己解析 |
| 解析 `name` | `xpkg.cppm:676` `extract_xpkg_name` | ❌ 自己解析 |
| 归一化为身份 | `xpkg.cppm` `canonical_xpkg_identity` | ❌ 自己归一 |
| 身份校验 | `xpkg_lua_identity_matches` | ❌ 自己校验 |

**评审意见在这一点上完全正确**:name/namespace 的处理是 mcpp 的自有机制,xlings 只被用于**取值/装包**。

### 1.2 身份归一化对两种写法**已经等价**

```cpp
// xpkg.cppm — canonical_xpkg_identity
std::string prefix = ns + ".";
fqn = name.starts_with(prefix) ? name : ns + "." + name; // 两种写法在此合流
auto pos = fqn.rfind('.');
return XpkgIdentity{ fqn.substr(0, pos), fqn.substr(pos + 1) };
```

`(acme, asio)` 与 `(acme, acme.asio)` 归一化到**同一身份** `(acme, asio)`。身份层对写法**无偏好**。

### 1.3 唯一的偏好点:目标构造丢弃了字面值

```cpp
// prepare.cppm:1861 —— 唯一把写法变成"有对错"的一行
auto fqname = ns.empty() ? shortName : std::format("{}.{}", ns, shortName);
...
// :1906 / :1911
auto target = std::format("{}@{}", fqname, version);
target = std::format("{}:{}@{}", idxSpec->name, fqname, version);
```

而 `luaContent`(含字面 `name`)**就在同一作用域**——`:1781` 与 `:1865` 的 INV-NAME 检查用的正是它。**信息在手,只是没用。**

### 1.4 store 目录:mcpp 自己猜,且**两种形式都已覆盖**

```cpp
// compat.cppm install_dir_candidates
candidates.push_back(std::format("{}-x-{}", ns, fqname)); // <ns>-x-<ns>.<short>
...
candidates.push_back(std::format("{}-x-{}", ns, shortName)); // <ns>-x-<short> ← 短名形式
```

短名形式的 store 目录**早已在候选表里**(现标注为 COMPAT 兜底)。

---

## 2. 方向二:实机验证(真实包 `asio` 1.38.1,path index,mcpp 0.0.104 避开 INV-NAME 干扰)

固定 `namespace = "acme"`,只改 `name` 字段:

| # | `name` | mcpp 发出的 target | 结果 | store 目录 |
|---|---|---|---|---|
| 1 | `acme.asio` | `acme:acme.asio@1.38.1` | ✅ 装上并编译 | `acme-x-acme.asio` |
| 2 | `asio` | `acme:acme.asio@1.38.1` | ❌ `E_NOT_FOUND` | — |

第 2 行即 **#278 事故的最小复现**:索引 key 是 `asio`,mcpp 却要 `acme.asio`。

### 2.1 关键实验:改一行后重测

实验补丁(**未合入**,已复原):目标构造改用描述符字面 `name`。

```cpp
if (luaContent) {
auto lit = mcpp::manifest::extract_xpkg_name(*luaContent);
if (!lit.empty()) fqname = lit;
}
```

| # | `name` | 结果 | store 目录 |
|---|---|---|---|
| 3 | `asio`(短名) | ✅ **装上并编译** | `acme-x-asio` |
| 4 | `acme.asio`(FQN) | ✅ 装上并编译 | `acme-x-acme.asio` |
| 5 | mcpp 自身依赖(真实生态) | ✅ 无回归 | — |

**两种写法同时可用**,因为字面 `name` **恒等于**索引 key。下游(payload 定位、编译)零改动即работ——`install_dir_candidates` 已含短名候选。

---

## 3. 证伪:我此前「根因在 libxpkg」的判断是错的

我曾主张「libxpkg 的 key 空间没有 namespace 维度,所以 `name` 必须自带区分度」。**实验 3 直接证伪**:key 空间没变、libxpkg 一行没动,短名形式照样装上。

正确表述:key 空间确实是扁平的、以 `pkg.name` 字面值为键 —— 但这**不构成对 `name` 写法的约束**,因为 mcpp 完全可以把字面值原样送过去。真正的约束是 mcpp 自己**重新渲染**了一个可能不同的字符串。

**撞键顾虑同样不成立**:`compat` 的 `zlib` 与 `mcpplibs` 的 `zlib` 若都写 `name = "zlib"`,确实会在**同一索引内**撞键 —— 但这是**索引内命名唯一性**问题,可由 lint 保证(同一索引内 `name` 唯一),不必让每个包都背 FQN。跨索引不撞,因为每个索引一张表。

---

## 4. 兼容矩阵

| | 现网描述符(FQN) | 新描述符(短名) |
|---|---|---|
| 现有 mcpp(≤0.0.105) | ✅ | ❌ `E_NOT_FOUND` |
| 改后 mcpp(用字面 name) | ✅ | ✅ |

**只有一个破损格**:旧 mcpp + 新描述符。含义:

- 迁移**不是**一次性切换。改后的 mcpp 对两种写法都工作,所以**先发 mcpp、再按节奏迁描述符**。
- 描述符一旦改短名,消费它的最低 mcpp 版本就抬高了 → 迁移时 `index.toml` 的 `min_mcpp` **必须**同步抬到含该修复的版本(这次是硬性的,不像 0.0.105 那次可选)。

---

## 5. 对 PR#117 的处置建议

PR#117 现有文档把**编码约束**写成了**设计规则**,方向与上述结论相反,**不应按现状合入**。三选一:

**A. 拆分(推荐)** —— PR#117 只保留无争议部分:去重(字节级重复的 `capi.lua.lua`)、规范路径(`tensorvia-cpu.lua` → `aimol.tensorvia-cpu.lua`)、规则 2(短名原子)、规则 3(规范路径)、CI 升 0.0.105。**删掉全部关于 `name` 形式的表述与规则 1 lint**,留给迁移 PR。

**B. 并入迁移** —— PR#117 改造成迁移的一部分:等 mcpp 侧修复发布后,49 个描述符全改短名、lint 规则 1 反转、`min_mcpp` 抬版。

**C. 维持现状** —— 只在文档里把 FQN 标注为「当前编码约束、非设计理想」。**不推荐**:等于把一条要回滚的约定写进 49 个包的规范。

---

## 6. 若走迁移,工作项清单

**mcpp 侧(一次发版,例如 0.0.106):**

- [ ] `prepare.cppm` 目标构造改用描述符字面 `name`(字面为空时回落到现有渲染)
- [ ] `compat.cppm` `install_dir_candidates` 把 `{ns}-x-{short}` 提为**首选**,`{ns}-x-{fqname}` 降为兼容项
- [ ] **INV-NAME 谓词的语义反转**:从「必须是 FQN」改为「`name` 不得含点(层级归 `namespace`)」;`xpkg_name_form_violation` 及 `mcpp xpkg parse` 同步
- [ ] e2e 161 断言反转;新增「短名描述符可安装」正向 e2e
- [ ] 文档 `docs/05-mcpp-toml.md` §2.5 的 xpkg 作者段改写

**mcpp-index 侧(mcpp 发布后):**

- [ ] 49 个描述符 `name` 去掉 namespace 前缀
- [ ] `check_package_name.lua` 规则 1 反转
- [ ] `check_package_filename.lua` 的规范路径公式随之调整(FQN 文件名 → ?需重新定义)
- [ ] `index.toml` `min_mcpp` / `latest_mcpp` 抬到 0.0.106
- [ ] 一次全量 `mcpp test --workspace` 三平台验证

**未决问题(需拍板):**

1. **规范文件名怎么定?** 现行公式对非默认命名空间用 `<FQN>.lua`。若 `name` 变短名,是继续用 `<ns>.<short>.lua`(与 `name` 不再字面相等)还是改 `<short>.lua`(跨命名空间会重名)?
2. **同索引内 `name` 唯一性**由谁保证?建议新增 lint:同一索引内 `name` 字面值不得重复(替代 FQN 天然提供的唯一性)。
3. **是否需要过渡期双读?** 即改后的 mcpp 已同时支持两种写法,故不需要;但若希望旧 mcpp 也能读新描述符,只能靠 `min_mcpp` 挡住,无技术手段。

---

## 附:我为什么把 PR#117 写成那样

按时间顺序,证据链是这样的,错在**没有做本报告 §2.1 这一步实验**:

1. #278 事故现场:`name` 从 FQN 改成短名 → 三平台 `E_NOT_FOUND`。→ 得出「短名不可用」。**正确但不完整**——只证明了"当前 mcpp 下不可用",没证明"必然不可用"。
2. 读 libxpkg:`entries[pkg.name]` 精确匹配、`build_index` 不传 namespace。→ 得出「key 空间无 namespace 维度,`name` 必须自带区分度」。**这一步是错的**:key 空间的形状不约束 `name` 的写法,只要 mcpp 把字面值原样送过去即可。
3. 全索引扫描:46/49 用 FQN,2 个短名的都坏了。→ 强化了「FQN 是约定」。**幸存者偏差**:它们坏是因为 mcpp 的渲染,不是因为写法本身。
4. 于是把「FQN」当作规则写进 lint 与文档。

**根本失误**:我把「当前实现的行为」当成了「架构的约束」,而没有去问"这一行改掉会怎样"。评审的两次纠正——先是「a.b.c 应在 namespace」,再是「mcpp 不依赖 xlings 的 name 机制」——正是指向这个缺口。§2.1 的实验本应在写 lint **之前**做。
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# mcpp `name` / `namespace` 规范实现(定稿)

> 依据:`2026-07-25-name-namespace-bidirectional-verification-report.md` 的双向验证 + 评审给定的规格
> 基线:mcpp `main` @ 0.0.105
> 状态:**待实现**。本文定的是目标实现,不是现状描述。

---

## 0. 边界约束(先于一切)

**mcpp 不改动 xlings 的任何规范,只基于 xlings 已定的机制去实现。**

xlings 侧属于**既定事实**,mcpp 只能遵守,不能重新定义:

| xlings 既定机制 | 内容 |
|---|---|
| 索引键 | `entries[package.name]`,以**字面 `name`** 为键,精确匹配 |
| xpkg 目录 | `{namespace}-x-{name}` —— `name` 是什么就拼什么 |

**由此推出 mcpp 唯一正确的做法:把描述符里读到的字面值原样使用,不要在 mcpp 侧重新推导一遍。** 本文全部改动都是这一句话的展开。

> **勘误**:先前曾把「FQN 形式产出 `acme-x-acme.asio`」当作"不符合 `{ns}-x-{name}` 规范"、并当成短名为正解的佐证 —— **该推论是错的**。`{ns}-x-{name}` 对两种形式都成立(`name="acme.asio"` → `acme-x-acme.asio`;`name="asio"` → `acme-x-asio`),它是确定的 xlings 机制,不偏袒任何一种写法。短名为规范形态的依据是数据模型本身(层级归 `namespace`),不是目录名。

---

## 1. 数据模型

包的身份是二元组,**两个字段都直接取自描述符声明,不做任何重新渲染**:

| 字段 | 含义 | 形态 |
|---|---|---|
| `package.namespace` | 命名空间,**点分层级路径** | 可空;`compat` / `mcpplibs.capi` / `a.b.c` |
| `package.name` | 包名,**单一原子段** | **不含点**;`zlib` / `asio` / `lua` |

> **层级一律放 `namespace`,`name` 只留最后一段。**
> `(mcpplibs.capi, lua)` 是规范形态;`namespace="mcpplibs", name="capi.lua"` 不是。

### 1.1 三个派生量,全部源自**字面 `name`**

| 派生量 | 公式 | 实测依据 |
|---|---|---|
| xlings 索引 key | `<字面 name>` | libxpkg `entries[pkg.name]`,精确匹配 |
| store 目录 | `{namespace}-x-{字面 name}` | 全量实测 48/48 相符(FQN 与短名两种形式) |
| 安装 target | `<indexName>:<字面 name>@<version>` | 实验证实短名/FQN 均可解析 |

**关键性质:三者都只依赖字面 `name`,与「短名形式 / 遗留 FQN 形式」无关。** 因此实现里**不需要按形式分支** —— 这是本方案能同时兼容新旧描述符的根本原因。

### 1.2 文件名不参与任何解析

描述符文件名**自由**。推荐(非强制)`<name>.lua` 或 `<namespace>.<name>.lua`。

这是 `2026-06-26 identity-first` 的原意("Filename Is Not a Key")的完整落实 —— 此前只兑现了「命中后按声明身份复核」,**发现仍受候选文件名约束**,本方案一并补齐(§2.5)。

---

## 2. mcpp 侧实现

### 2.1 身份归一化 —— 去掉 split-on-last-dot

```cpp
// 现状(xpkg.cppm canonical_xpkg_identity):把 name 拼成 FQN 再按最后一个点切开
fqn = name.starts_with(ns + ".") ? name : ns + "." + name;
auto pos = fqn.rfind('.');
return { fqn.substr(0, pos), fqn.substr(pos + 1) }; // ← 会把 ns 从 name 里"猜"出来

// 目标:namespace 就是 namespace,name 就是 name;只为遗留 FQN 形式剥前缀
std::string short_ = name.starts_with(ns + ".") ? name.substr(ns.size() + 1) : name;
return XpkgIdentity{ ns, short_ };
```

差别在于**不再从 `name` 反推命名空间**。`namespace="a", name="a.b.c"` 这类自相矛盾的写法,现状会静默解析成 `(a.b, c)`(一个描述符从未声明的命名空间),新实现直接由 §2.4 的校验拒绝。

### 2.2 安装 target —— 用字面 `name`,不再重新渲染

```cpp
// prepare.cppm:1861 现状 —— 丢弃已读到的字面值,重新渲染
auto fqname = ns.empty() ? shortName : std::format("{}.{}", ns, shortName);

// 目标:字面 name 就是 wire key(luaContent 已在同一作用域)
auto wireName = luaContent ? mcpp::manifest::extract_xpkg_name(*luaContent) : std::string{};
if (wireName.empty()) wireName = ns.empty() ? shortName // 兜底:读不到描述符时
: std::format("{}.{}", ns, shortName);
```

**已实验验证**(报告 §2.1):改此一行后短名形式立即可装(`acme-x-asio`),且现网 FQN 形式继续可装(`acme-x-acme.asio`),mcpp 自身依赖无回归。

### 2.3 store 目录 —— 收敛为单一公式

```cpp
// compat.cppm install_dir_candidates 现状:6 个猜测候选,{ns}-x-{short} 被标为 COMPAT 兜底
// 目标:{namespace}-x-{字面 name} 单一公式(全量实测 48/48 命中),其余降为过渡期兼容项
```

### 2.4 `name` 形态校验 —— INV-NAME 语义反转

| | 现状(0.0.105) | 目标 |
|---|---|---|
| 规则 | `namespace` 非空时 `name` **必须**是 `<ns>.<short>` | `name` **不得含点**;层级归 `namespace` |
| 违规示例 | `ns="chriskohlhoff", name="asio"` | `ns="mcpplibs", name="capi.lua"` |
| 落点 | `xpkg_name_form_violation` + `mcpp xpkg parse` + 安装路径 | 同左 |

过渡期:遗留 FQN 形式(`name` 恰以 `namespace + "."` 开头)**降级为 warning**,不阻断;其他含点写法直接报错。

### 2.5 描述符发现 —— IdentityIndex(文件名自由的必要前提)

文件名一旦自由,「候选文件名探测」就不再可靠:`read_xpkg_lua*` 三个入口目前只探测 `compat::xpkg_lua_candidates` 生成的固定名单,叫别的名字的描述符**根本看不见**。

因此必须落地 `2026-06-26 §5` 一直推迟的 `IdentityIndex`:扫描 `pkgs/*/*.lua`,读每个描述符声明的 `(namespace, name)` 建表,文件名仅作可选加速提示。

> **与 INV-RESOLVE 不冲突。** 被否决的是「裸名跨命名空间发现」(解析结果取决于装了哪些索引);IdentityIndex 是「按**已知的精确身份**查表」,确定性不受影响。消费端解析规则(裸名只解析 `mcpplibs` / `compat` / 无 ns 上游)**保持不变**。

---

## 3. 遗留约束:同索引内 `name` 必须唯一(索引侧承担)

xlings 的索引是**每索引一张扁平表、以字面 `name` 为键**(§0 的既定机制)。短名形式下,同一索引里 `compat`/`zlib` 与 `mcpplibs`/`zlib` 会落到同一个键 `zlib` 上而**互相覆盖** —— 现状靠 FQN 写法天然错开。

按 §0,这**不是 mcpp 能解决的问题**:改键空间等于改 xlings 规范,越界。因此约束由**索引侧承担**:

> **同一索引内,`package.name` 的字面值必须唯一。**

由 mcpp-index 的 lint 强制(遍历 `pkgs/*/*.lua`,`name` 字面值不得重复)。

**现网检查:mcpplibs 索引 48 个描述符,短名化后无冲突**(`compat.*` 与 `mcpplibs.*` 之间无同短名者),故该约束立即可满足,不需要为它改任何包名。

若将来某个索引确实需要容纳同短名的两个包,那是 xlings 键空间的能力缺口,应作为**上游需求**提给 xlings,而不是在 mcpp 侧绕开。

---

## 4. 兼容矩阵与迁移

| | 现网描述符(FQN) | 新描述符(短名) |
|---|---|---|
| 现有 mcpp ≤0.0.105 | ✅ | ❌ `E_NOT_FOUND` |
| 本方案实现后 | ✅ | ✅ |

**只有一个破损格**,含义:

1. **先发 mcpp**(对两种写法都工作),**再按节奏迁描述符** —— 不是一次性切换。
2. 描述符改短名时,该索引的 `index.toml` `min_mcpp` **必须**同步抬到含本实现的版本(硬性,无技术手段绕过)。
3. 迁移可**逐包**进行,索引内可长期混存两种形式。

---

## 5. 实施清单

**mcpp(一次发版):**

- [ ] `canonical_xpkg_identity` 去 split-on-last-dot(§2.1)
- [ ] `prepare.cppm` target 用字面 `name`(§2.2)
- [ ] `install_dir_candidates` 收敛为 `{ns}-x-{字面 name}`(§2.3)
- [ ] `xpkg_name_form_violation` 语义反转 + `mcpp xpkg parse` 同步(§2.4)
- [ ] `IdentityIndex` 落地,`read_xpkg_lua*` 改走它(§2.5)
- [ ] e2e 161 断言反转;新增「短名描述符可安装」「任意文件名可发现」正向 e2e
- [ ] `docs/05-mcpp-toml.md` §2.5 xpkg 作者段改写

**mcpp-index(mcpp 发布后,可逐包):**

- [ ] 新增 lint:同索引内 `name` 字面值唯一(§3)
- [ ] 描述符逐步短名化;`min_mcpp` 抬版
- [ ] 现有「文件名必须规范」的 lint **不要加**(文件名自由)

---

## 附:本方案相对现状的三处"减法"

值得强调的是,这不是加复杂度,而是**去掉三处多余的推导**:

1. 不再从 `name` 反推命名空间(split-on-last-dot)
2. 不再重新渲染 wire key(直接用字面值)
3. 不再猜 store 目录(6 个候选 → 1 个公式)

三处都是「mcpp 手里已有准确值、却选择重新推导一遍」—— 与 #278 的根因同型。
Loading
Loading