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
34 changes: 32 additions & 2 deletions .agents/docs/2026-06-26-identity-first-resolution-no-filename.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

**Date:** 2026-06-26
**Status:** Step 0 landed in v0.0.67 (candidate selection is now identity-first);
§5 `PackageLocator` / `IdentityIndex` choke-point consolidation remains follow-up
§5 `PackageLocator` / `IdentityIndex` choke-point consolidation remains follow-up.
**Partly superseded by #278 (v0.0.105):** §4.5's emit half landed (`mcpp emit xpkg`
now writes both `namespace` and the FQN `name`); its "catalog keys on canonical
(ns,name)" half is **unachievable as written** — the catalog belongs to xlings, is
keyed by the literal `package.name` with exact `find()`, and mcpp's normalization
has no authority over it, so the split/FQN equivalence is instead resolved by
constraining the wire key's spelling (INV-NAME). §4.6(c)'s discovery rung is
narrowed — see the SUPERSEDED note at that row.
**Extends:** [`2026-06-20-package-resolution-architecture.md`](2026-06-20-package-resolution-architecture.md)
(realizes its deferred §5 `PackageLocator` / identity-indexed slow path),
[`2026-05-11-namespace-field-design.md`](2026-05-11-namespace-field-design.md),
Expand Down Expand Up @@ -389,7 +396,30 @@ declared `(∅, a.b.c)` in the `a.b`-owned index. The user's point, encoded.
| candidate kind | rule | matches `(aimol, tensorvia-cpu)`? |
|---|---|---|
| qualified, ns non-empty | **exact tuple equality** `cand == declared` | `(aimol, tensorvia-cpu)` ✅ · `(mcpplibs.aimol, …)` ❌ · `(mcpplibs, …)` ❌ |
| 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)` |
| 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)`~~ |

> **SUPERSEDED by #278 (mcpp 0.0.105).** The discovery rung is **not** a
> cross-namespace wildcard. It is the "upstream package that declares no
> `namespace`" rung, and a hit whose descriptor declares a non-empty namespace is
> now REJECTED at the dependency-resolution call site (`selectDependencyCandidate`
> in `prepare.cppm`; the gate itself is unchanged, because `mcpp new --template X`
> still discovers by short name). A bare `[dependencies]` key therefore resolves in
> exactly three places: `mcpplibs`, `compat`, and no-namespace upstream packages.
>
> Why the reversal: "match by name alone across the precedence path" makes
> resolution depend on which indices happen to be present. Two namespaces owning
> the same short name would be settled by an index ordering that has **no total
> order** across user-added `[indices]`, and adding an index could silently
> retarget an existing dependency. Reproducibility beats the convenience.
>
> The **P3 half of this row still stands and is now actually implemented**: a
> discovery hit writes back the namespace the DESCRIPTOR declares, not the
> candidate's empty one. Caveat: an empty declared namespace is a legal identity
> for upstream bare packages, not a hole to fill — the "no empty namespace ever
> reaches the install layer" claim in §4.4 presumes §4.1 index-owned namespace
> attribution, which remains unimplemented.
>
> See `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md` §3.2.

**Selection** = first candidate (in the §(a) order) that finds a declared identity by
rule §(c). Crucially, "finds" means **an entry with that declared identity exists in
Expand Down
224 changes: 224 additions & 0 deletions .agents/docs/2026-07-24-embedded-platform-support-design.md

Large diffs are not rendered by default.

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions .agents/docs/2026-07-25-issue278-implementation-plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# #278 包身份双侧收敛 — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 实现 `.agents/docs/2026-07-25-issue278-descriptor-name-form-canonicalization-design.md` 的两条不变式 → 单 PR(`Fixes #278`)→ CI 全绿 → bypass squash 合入 → 0.0.105 release → xlings 全生态验证。

**Architecture:** 一个纯谓词 `xpkg_name_form_violation` 两处调用(lint + 运行期),消灭"同一决策两处推导";依赖侧候选阶梯收敛为封闭三档,全索引扫描降级为**仅失败路径**的 did-you-mean 诊断,绝不回灌解析。

## Global Constraints

- `canonical_xpkg_identity` / `xpkg_lua_identity_matches` **一个字不改** —— 收紧只发生在发布侧(lint)与依赖解析调用点。
- 谓词必须收窄为"声明了非空 namespace 且 name 不以 `ns.` 开头",**不得**写成"字面 name != 推导 fqname"通用比较(会打断裸 `gtest` → `compat.gtest`)。
- did-you-mean 扫描三约束:仅失败路径触发、只进错误文案、扫描空不改退出码。
- 无 `namespace` 声明的上游包(`opencv`/`musl-gcc`)空 ns 是**合法身份**,T10 不得强行填充。
- T11 收紧 scoped 到 `selectDependencyCandidate`,不动闸门(保 `mcpp new --template`)。
- 既有单测与 e2e(160 例)全绿。

## Tasks

### C1 索引侧谓词与两个调用点

- [x] **T1** `xpkg_name_form_violation(declaredNs, declaredName)` + `_from_lua` 导出(`src/manifest/xpkg.cppm`)
- [x] **T2** `mcpp xpkg parse` 接入谓词,违规 exit 1;`--json` 输出 `error` 字段(`src/cli/cmd_xpkg.cppm`)
- [x] **T3** `loadVersionDep` install 分支 fail-fast;已解析到安装物的路径 `ui::warn`(`src/build/prepare.cppm`)

### C2 生成源

- [x] **T4** `emit_xpkg` 输出 `namespace` + FQN `name`;无 namespace 时 stderr 提示;`--namespace` 覆盖开关(`src/pm/publisher.cppm` + emit CLI)

### C3 依赖侧收敛

- [x] **T9** `selectDependencyCandidate` 全候选落空 → 明确失败(`prepare.cppm:1516-1541`)
- [x] **T10** discovery 档命中后 `extract_xpkg_namespace(*lua)` 回填真实 ns(声明为空则保持空)
- [x] **T11** discovery 档拒绝声明了非空 `namespace` 的描述符
- [x] **T12** did-you-mean:仅失败路径的全索引扫描,三约束写进实现注释

### C4 测试

- [x] **T5a** 单测:谓词 6 例(含 `CompatAliasIsClean` 回归锁)
- [x] **T5b** 单测:依赖侧解析(三档成功 / 第三方 ns 裸名失败 / P3 空 ns 锁)
- [x] **T5c** e2e:split 描述符 → 秒级自解释失败;改 FQN → 通过
- [x] **T5d** e2e:裸名请求第三方 ns 包 → 失败 + did-you-mean;改写后通过

### C5 文档与版本

- [x] **T13** 用户文档 §2.5(`docs/05-mcpp-toml.md` + `docs/zh/05-mcpp-toml.md`)
- [x] **T14** CHANGELOG 0.0.105 段(含 breaking:裸名不再解析第三方命名空间包)
- [x] **T15** `mcpp.toml` version → 0.0.105
- [x] **T11b** 修订 `2026-06-26 §4.4/§4.6(a)` 表述;更新 `prepare.cppm:1456-1459` 注释

### C6 验收与合入

- [x] **T6** 全索引 `mcpp xpkg parse` 回归,结果贴 PR。**实测 49/49 mcpplibs 描述符全过、0 违规**(非设计预期的 2 —— 上游 mcpp-index 已先行修好两例);完整 registry(200+)另见 §2.1b 的 xlings-native 修正
- [ ] **T7** 单 PR(`Fixes #278`)→ CI 全绿 → `gh pr merge --squash --admin`

### C7 生态

- [ ] **T16** release 0.0.105 四平台 + 镜像 xlings-res 双端 + xim-pkgindex PR + `xlings install mcpp` 真装验证
- [x] **T8** mcpp-index:**上游已完成** —— `chriskohlhoff.asio` / `tensorvia-cpu` 均已是 FQN,#116 的 `tests/check_package_name.lua` 已合入。该 Lua 守卫现与 `mcpp xpkg parse` 冗余但无害,可留作保险(其 CI 本就跑 `mcpp xpkg parse pkgs/*/*.lua`,升级 MCPP_VERSION 到 0.0.105 后自动获得引擎侧防护)
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,30 @@
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。

## [0.0.105] — 2026-07-25

> 包身份口径双侧收敛(#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`。

### ⚠️ 破坏性变更

- **裸依赖名不再解析到第三方命名空间的包。** 命名空间缺省时,`[dependencies]` 里的裸名**只**解析三类:`mcpplibs`(默认)、`compat`(包装)、无 `namespace` 声明的上游包。此前裸名会跨命名空间命中(例如裸 `tensorvia-cpu` 能装上 `aimol` 下的包),现在必须写全:`"aimol.tensorvia-cpu" = "…"` 或 `[dependencies.aimol] tensorvia-cpu = "…"`。
取舍理由:全域按名发现的便捷性换来三条稳定性损失——同名包的裁决依赖索引优先级(而用户 `[indices]` 添加的索引之间**无全序**)、**新增一个索引可能悄悄改变既有依赖解析到的包**(供应链隐患)、同一份 `mcpp.toml` 在不同机器上可能解析到不同包。依赖解析的可复现性优先于书写便捷性。
迁移无需查文档:失败时 mcpp 会扫描索引并直接给出应当改写成的那两行(见下)。

### 新增

- **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`,免费获得秒级防护。
- **`mcpp xpkg parse --allow-split-name`**:跳过 INV-NAME 检查。xlings 原生索引(xim-pkgindex、-scode)里 `package.namespace` 是**安装目录分类**(`config`/`scode`/`awesome`)而非包命名空间,索引按裸 `package.name` 建键,split 形式在那个世界里是正确的——这些树用此开关 lint。
- **运行期 fail-fast**:`mcpp build` 在构造安装目标**之前**用同一个谓词校验手上已有的描述符(零额外 I/O),把"三平台一小时后的 E_NOT_FOUND"变成秒级自解释失败。lint 与运行期共用一份判定,不会再各自推导。已装旧快照的路径降级为 warning,让"本机绿、干净 CI 红"的遮蔽陷阱可见。
- **依赖解析失败的 did-you-mean**:候选全部落空时(且**仅**在此时)扫描索引,若该短名存在于其他命名空间,直接列出 FQN 与两种可直接抄写的正确写法。该扫描是**纯诊断**——结果只进错误文案,绝不回灌解析、lockfile 或安装层(否则就退化成被否决的全域模糊匹配)。
- **`mcpp emit xpkg --namespace <NS>`**:为归档场景提供命名空间,无需改 `mcpp.toml`。

### 修复

- **`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 里就已写明,一直未落地。
- **依赖候选全部落空时不再静默回退**:此前会退到第一个候选并把 mcpp **自己编造的**命名空间当作结论继续跑,失败被推迟到下载/安装阶段,错误文本里还带着用户从未写过的命名空间。现在当场失败并列出试过的每一个身份。
- **discovery 档不再泄漏空命名空间(P3)**:`(∅, name)` 命中后写回的是**描述符声明的**命名空间,而非候选的空值——空命名空间此前会流进 lockfile 与安装层。无 `namespace` 声明的上游包(`opencv`/`musl-gcc`)保持空命名空间,那是它们的合法身份。

## [0.0.104] — 2026-07-24

> `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`。
Expand Down
55 changes: 55 additions & 0 deletions docs/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,61 @@ baz = "=1.2.3" # Exact match
qux = ">=1.0, <2.0" # Range combination
```

#### Namespace resolution rules

Every package has a two-part identity: a **namespace** and a **name**. How you write
a dependency key decides which namespaces mcpp will look in.

**A bare name resolves in exactly three places**, in order:

| # | Namespace | Example |
|---|---|---|
| 1 | `mcpplibs` — the default namespace | `cmdline = "0.0.2"` |
| 2 | `compat` — the wrapper namespace for third-party C/C++ libraries | `gtest = "1.15.2"` → `compat.gtest` |
| 3 | upstream packages that declare no namespace at all | `opencv = "4.10.0"` |

**Any other namespace must be written out in full.** There is no fuzzy, index-wide
search by short name:

```toml
# ✅ Correct — dotted selector
[dependencies]
"chriskohlhoff.asio" = "1.38.1"

# ✅ Correct — namespace sub-table (preferred when you have several from one org)
[dependencies.chriskohlhoff]
asio = "1.38.1"

# ❌ Wrong — a bare name never reaches the `chriskohlhoff` namespace
[dependencies]
asio = "1.38.1"
```

The third form fails with an error that lists the namespaces that were searched and,
when a package with that short name exists elsewhere, the exact line to write instead.

**Why not resolve bare names across every namespace?** Because dependency resolution
has to be reproducible. A global short-name search would mean that (a) two namespaces
owning the same short name are settled by index ordering, and (b) **adding an index
could silently change which package an existing dependency resolves to**. Requiring
the namespace keeps a `mcpp.toml` resolving to the same packages on every machine.

**For xpkg authors:** in an index descriptor, `package.name` must be the *fully
qualified* name — it has to start with `package.namespace` followed by a dot:

```lua
package = {
namespace = "chriskohlhoff",
name = "chriskohlhoff.asio", -- NOT "asio"
...
}
```

The package index is a flat key space keyed by the literal `package.name`, so the
namespace has to be carried in the name itself — otherwise `mcpplibs`'s `zlib` and
`compat`'s `zlib` would collide. A descriptor that gets this wrong parses fine but can
never be installed. `mcpp xpkg parse` checks it, so run it in your index CI.

### 2.6 `[dev-dependencies]` — Test Dependencies

```toml
Expand Down
44 changes: 44 additions & 0 deletions docs/zh/05-mcpp-toml.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,50 @@ baz = "=1.2.3" # 精确匹配
qux = ">=1.0, <2.0" # 范围组合
```

#### 命名空间解析规则

每个包的身份是**命名空间 + 名字**二元组。依赖 key 的写法决定 mcpp 到哪些命名空间里找。

**裸名只在三个地方解析**,按序:

| # | 命名空间 | 示例 |
|---|---|---|
| 1 | `mcpplibs` — 默认命名空间 | `cmdline = "0.0.2"` |
| 2 | `compat` — 第三方 C/C++ 库的包装命名空间 | `gtest = "1.15.2"` → `compat.gtest` |
| 3 | 完全没有声明命名空间的上游包 | `opencv = "4.10.0"` |

**其他命名空间一律必须写全。** 不存在按短名的全索引模糊搜索:

```toml
# ✅ 正确 —— 点式选择器
[dependencies]
"chriskohlhoff.asio" = "1.38.1"

# ✅ 正确 —— 命名空间子表(同一组织有多个包时更推荐)
[dependencies.chriskohlhoff]
asio = "1.38.1"

# ❌ 错误 —— 裸名永远到不了 chriskohlhoff 命名空间
[dependencies]
asio = "1.38.1"
```

第三种写法会明确报错,并列出搜索过的命名空间;若该短名的包存在于别处,错误信息会直接给出应当改写成的那一行。

**为什么不让裸名跨所有命名空间去找?** 因为依赖解析必须可复现。全域短名搜索意味着:(a) 两个命名空间拥有同名包时,胜负由索引顺序决定;(b) **新增一个索引可能悄悄改变某个既有依赖解析到的包**。要求写出命名空间,才能让同一份 `mcpp.toml` 在每台机器上解析到相同的包。

**给 xpkg 作者:** 索引描述符里的 `package.name` 必须是**完全限定名** —— 以 `package.namespace` 加一个点开头:

```lua
package = {
namespace = "chriskohlhoff",
name = "chriskohlhoff.asio", -- 不是 "asio"
...
}
```

包索引是以 `package.name` 字面值为键的**扁平键空间**,命名空间的区分度必须由名字本身携带 —— 否则 `mcpplibs` 的 `zlib` 与 `compat` 的 `zlib` 会撞键。写错的描述符能被正常解析,却永远装不上。`mcpp xpkg parse` 会校验这一点,请在索引 CI 里跑它。

### 2.6 `[dev-dependencies]` — 测试依赖

```toml
Expand Down
2 changes: 1 addition & 1 deletion mcpp.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mcpp"
version = "0.0.104"
version = "0.0.105"
description = "Modern C++ build & package management tool"
license = "Apache-2.0"
authors = ["mcpp-community"]
Expand Down
Loading
Loading