Skip to content

Commit 25380b0

Browse files
committed
docs: separate two layers of immutability (publish policy + lock)
Add §4.5 making explicit the two-layer guarantee that an earlier reading of the doc was conflating: L1 — package-publish immutability (policy, future mcpp-index CI gate). Once `<ns>:<name>@<ver>` is published, its url / sha256 / namespace fields are frozen; only new versions may be added; deletions go through an explicit yank flow. L2 — index-snapshot lock (mechanism, mcpp.lock). Each project pins the index commit sha it was last built against; build/run/test/pack always read that sha offline. Force-push or accidental policy violations on the upstream index never reach a project until it explicitly runs `mcpp index update`. L1 is the trust layer, L2 is the failsafe. Together they give CI / team-wide reproducibility even when the index repo is just a normal git repository (no server-side immutability the way crates.io enforces). Also adjust the cargo / npm comparison table in Appendix A to surface both layers, and rewrite the "core difference" paragraph to explain why mcpp keeps L2 even after L1 is in place (third-party indices, force-push recovery, audit window).
1 parent 3ac8f43 commit 25380b0

1 file changed

Lines changed: 184 additions & 8 deletions

File tree

.agents/docs/2026-05-08-package-index-config.md

Lines changed: 184 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,100 @@ acme = { url = "git@gitlab.example.com:platform/mcpp-index.git", branch = "main"
9696
| 命令 | 行为 |
9797
|---|---|
9898
| `mcpp index list` | 列出所有索引(标注来源:project / global / built-in)。 |
99-
| `mcpp index update [<ns>...]` | 拉取索引最新提交。`branch` 跟踪式按 `git pull --ff-only`;`rev`/`tag` no-op。 |
100-
| `mcpp index pin <ns> [<rev>]` | 把当前索引解析到的 commit 写回 `mcpp.toml` 作为 `rev`。空 `<rev>` = 用当前 HEAD。 |
101-
| `mcpp index unpin <ns>` | 反向操作:删除 `rev`,改回 `branch="main"`|
99+
| `mcpp index update [<ns>...]` | 拉远端最新 sha,**写回 `mcpp.lock`**。无参 = 更新所有索引;`rev`/`tag` 锁定的索引被显式跳过(no-op + 提示)。 |
100+
| `mcpp index pin <ns> [<rev>]` | 把当前索引解析到的 commit 写回 `mcpp.toml` 作为 `rev`(从"软锁"升级为"硬锁",任何机器都能复现)。空 `<rev>` = 用 lock 里现行 sha。 |
101+
| `mcpp index unpin <ns>` | 反向操作:从 `mcpp.toml` 删除 `rev` 字段(lock 仍锁,但下次 `index update` 又能动)。 |
102+
| `mcpp update [<pkg>]` | 重新解析包版本约束(等同 cargo update)。**会先自动刷新涉及到的索引**,再重选最高匹配版本,写回 lock。无参 = 全部 dep。 |
103+
104+
> 简记:`build / run / test / pack` **永远不改** `mcpp.lock`;
105+
> `mcpp update` / `mcpp index update` / `mcpp index pin`**仅有的三个**
106+
> 改写 lock 的命令。
107+
108+
### 2.5 升级与刷新流程的四个典型场景
109+
110+
> 对应"`mcpp.lock` 锁住后,具体怎么动"的四个典型问题。
111+
112+
#### 场景 A:首次构建,无 lock,无 `[indices]`
113+
114+
```
115+
mcpp build
116+
→ 内置默认 mcpp index URL,拉远端默认分支 HEAD
117+
→ 写入 mcpp.lock 的 [indices.mcpp].rev = <sha>
118+
→ 解析依赖,写入 lock 的 [[package]] 段
119+
```
120+
121+
之后所有 `mcpp build` 都用这个 lock,完全离线复现。
122+
123+
#### 场景 B:索引上游有了新版本(包括官方 mcpp-index 加了新包 / 新版本)
124+
125+
**默认行为**:`mcpp build` **看不到**——它只读 lock 里的旧 sha,旧索引
126+
里不包含新版本,**不会报错也不会自动升级**
127+
128+
这是**特性,不是 bug**:CI / 团队成员 / 老分支构建出来的产物完全可复现,
129+
不会因为上游某天合了一个 PR 就改变行为。
130+
131+
要看到新版本,**必须主动**:
132+
133+
```bash
134+
mcpp index update mcpp # 拉新 sha,写回 lock
135+
# ↳ build 现在能看到新版本号了
136+
mcpp build # 但还是用 lock 里旧的 dep 版本
137+
# (因为 mcpp.toml 约束没变,旧版本仍满足)
138+
```
139+
140+
如果还想**升级到新版本**:
141+
142+
```bash
143+
mcpp update # 重新解析所有 dep 的版本约束
144+
# 这条会自动先刷索引,再选最高匹配版本
145+
```
146+
147+
或单独升级一个包:
148+
149+
```bash
150+
mcpp update gtest # 只重选 gtest 的版本
151+
mcpp update mcpplibs:cmdline # 命名空间形式同样支持
152+
```
153+
154+
#### 场景 C:lock 锁住的 sha 被上游删了 / force-push 没了
155+
156+
例如官方 index 仓被 force-push,本地 lock 里那个 sha 在远端找不到了。
157+
158+
- **本地 `~/.mcpp/index/mcpp/<sha>/` 已经 checkout 过**:`mcpp build`
159+
仍然离线工作——sha 在本地缓存里没事。
160+
- **从未拉过 / 缓存被 gc 了**:`mcpp build` 报错:
161+
```
162+
error: cannot fetch index 'mcpp' at sha abc123de...:
163+
remote no longer has this object.
164+
try: mcpp index update mcpp # to advance to a current sha
165+
```
166+
**不静默漂移**——必须用户决定怎么办。
167+
168+
#### 场景 D:把"软锁"升级为"硬锁"
169+
170+
`mcpp.toml` 没写 `rev` 字段时,lock 里有 sha,但 `mcpp.toml` 本身没有
171+
不可变锁定信息——别的机器 clone 项目时,如果删了 lock 重 build,会拿到
172+
**当前最新**的 sha,而不是当时构建时的 sha。
173+
174+
如果想让 `mcpp.toml` 本身也带锁定:
175+
176+
```bash
177+
mcpp index pin mcpp # 把 lock 里的 sha 写到 mcpp.toml 的 [indices.mcpp].rev
178+
git add mcpp.toml mcpp.lock
179+
git commit -m "Pin mcpp index to <sha>"
180+
```
181+
182+
之后任何机器、任何时刻 clone 这个仓库都拿到完全一样的索引。
183+
184+
反向(允许 `mcpp index update` 推动 sha 前进):
185+
186+
```bash
187+
mcpp index unpin mcpp # 从 mcpp.toml 删 rev,lock 仍保留 sha
188+
```
189+
190+
> 这两条是 cargo 没有的——cargo 的 registry 是"追加式不可变",所以不
191+
> 需要在 `Cargo.toml` 里固定 registry 版本。mcpp 的索引是普通 git 仓
192+
> 库,有 force-push / 删 commit 的可能,所以多了"硬锁到 toml"的维度。
102193
103194
## 3. 内部模型
104195

@@ -228,6 +319,67 @@ mcpp build
228319
└── 5. emit ninja, link
229320
```
230321

322+
## 4.5 不可变性的两层保证
323+
324+
**用户最在意的不是"今天的 gtest@1.15.2 跟明天的 gtest@1.15.2 是不是
325+
同一个文件"——这一层 mcpp 后期的发布政策会保证不可变。真正担心的
326+
是:索引仓库里那个 `gtest.lua` 描述符**今天写的 url + sha256,明天会
327+
不会被悄悄改成别的东西**。两层是分开的:
328+
329+
|| 保护对象 | 提供方式 | 失败时的影响 |
330+
|---|---|---|---|
331+
| **L1:包发布不可变性**(policy) | 已发布的 `<ns>:<name>@<ver>` 对应的 url + sha256 + 名字空间永不改写;只能新增版本,不能修改也不能删除已发布版本。 | mcpp-index 仓库的 CI gate(已发布版本字段被改 → reject PR);`mcpp publish` 客户端拒绝覆盖。 | 一旦违反,使用旧描述符的 build 会拿到不一致的二进制,极难追溯。 |
332+
| **L2:索引快照锁定**(mechanism) | 当前项目"看到"的索引 commit sha 在 `mcpp.lock` 里固定。 | `mcpp build` / `run` / `test` / `pack` 默认离线读 lock 的 sha,不联网。 | 即使 L1 政策被违反或上游 force-push,**已锁住的项目继续用旧索引,完全不受影响**——只有显式 `mcpp index update` 才会拉到新内容。 |
333+
334+
L1 是承诺,L2 是兜底。两层的关系类似 HTTPS 里"CA 不签发流氓证书"
335+
(policy)+ "客户端 pin 住已知证书"(mechanism)——任何一层成立,
336+
build 就稳定;两层都失效才会出问题。
337+
338+
### L1 — 后期 mcpp-index 仓的发布政策
339+
340+
设计文档不强制实现 L1 检查(那是 mcpp-index 仓 CI 的事),但设计层
341+
面记下假设,给 L2 兜底逻辑做参考:
342+
343+
- **新增**(`pkgs/.../<name>.lua` 中追加 `add_versions("0.0.3", ...)`)
344+
→ 允许,合法 PR。
345+
- **修订已发布版本**(改 `0.0.2` 那行的 sha256、url、build flags)
346+
→ CI gate 必须 reject。如果 sha256 真的算错了,**只能发** `0.0.2-fix`
347+
之类的新版本,不能就地改 `0.0.2`。
348+
- **删除已发布版本** → reject。极端情况(法律 / 安全)走人工 yank 流
349+
程,版本仍保留但标 yanked,新解析跳过它。
350+
- **整个包改名 / 改 namespace** → 等同删除 + 新增,要求作者发明确新版
351+
本号 + 旧版本保留不删。
352+
353+
> mcpp 后期可以再发一个独立 spec(类似 `xpkg-publish-policy`)详细
354+
> 列举可修字段集 / 不可修字段集,本 PR 不展开。
355+
356+
### L2 — `mcpp.lock` 的索引 sha 锁
357+
358+
L2 不依赖 L1 是否真被遵守:**任何时刻,build 看到的索引描述符 = lock
359+
里那个 commit 的 checkout**。所以:
360+
361+
- L1 完美遵守 → L2 锁的 sha 解析出来的描述符 = 远端最新一致(锁多余但
362+
无害)。
363+
- L1 偶发违反(典型场景:误 force-push、维护者手抖) → L2 仍指向旧
364+
sha 的 checkout,旧描述符内容不变,build 完全不感知。
365+
- L1 故意违反(攻击) → 同上,L2 给一段缓冲时间;同时 mcpp 可在
366+
`mcpp index update` 时检测"远端历史被改写"并报警。
367+
368+
**对用户问题的直接回答**:
369+
370+
> "如果包索引仓的包描述发生变化了呢?"
371+
372+
`mcpp build` 不受影响——lock 锁的 commit sha 还指向旧 checkout,
373+
旧 checkout 里的描述符内容不变(git object 是 content-addressed,不会
374+
被远端改写)。除非用户主动 `mcpp index update` 拉到新 sha,才会看到
375+
新内容;即便看到了,L1 政策要求新内容只是"新增版本",不会改写已选
376+
的那个版本——所以已选 dep 仍稳定。
377+
378+
> "对于具体的包发布,后期 mcpp 也是固定不可回退的"
379+
380+
正是 L1 的承诺。L2 是物理层兜底,即使 L1 (人为 / 流程)出错,build
381+
也能稳定到下一次显式 update。
382+
231383
## 5. 兼容性 / 迁移
232384

233385
### 5.1 现有项目(无 `[indices]`)— 隐式默认 + 强制 lock pinning
@@ -394,11 +546,35 @@ PR-A 已经引入 `(namespace, name)`。本 PR 把 namespace 跟"索引来源"
394546

395547
| 维度 | mcpp `[indices]` | cargo `[source]` | npm `.npmrc` |
396548
|---|---|---|---|
397-
| 锁定粒度 | git commit sha | crates.io 时间戳 | tarball sha512 |
549+
| 锁定粒度 | git commit sha + 包 checksum | 包版本 + 包 checksum | tarball sha512 |
398550
| 多源 | namespace 路由 | replace-with 链 | scoped registry |
399551
| 私有 | git URL + ssh | sparse-index + token | scoped registry + token |
400552
| 离线 | 本地 path 索引 + 缓存 | `--offline` + 缓存 | `--offline` + 缓存 |
401-
402-
mcpp 的优势:**git 原生**——不需要发明索引格式 / 索引服务,clone 一
403-
个仓就是一个索引;劣势:无 fast-path 增量(每次拉全仓)。规模大到这
404-
个成为瓶颈时再考虑 sparse-index / 索引镜像。
553+
| 刷新触发 | `mcpp index update` / `mcpp update`(显式) | 每次 `cargo build` 自动 fetch index(可 `--frozen` 关) | 每次 `npm install` 自动查 registry(可 `--offline` 关) |
554+
| 已发布不可变 (L1 policy) | mcpp-index 仓 CI gate + `mcpp publish` 拒绝覆盖(后期承诺) | crates.io 服务端强制 | npm registry 服务端强制(标 deprecated 不算改写) |
555+
| 索引快照锁 (L2 mechanism) | `mcpp.lock [indices.<ns>].rev = <sha>` | 无(L1 已足够,registry 服务承诺 immutable) | 无(同上) |
556+
| 配置层硬锁 | `mcpp.toml [indices.<ns>].rev = "<sha>"` 把 lock 抬到 toml | 无(不需要) | 无(不需要) |
557+
558+
**核心差异**:cargo 由 crates.io 这一个**受控服务**单独承担"已发布
559+
不可变"承诺;mcpp 选了**双保险**——L1 后期由 mcpp-index 仓的发布政
560+
策 + CI gate 承担(类似 crates.io),L2 在客户端 lock 文件里再锁一层
561+
索引 commit sha。
562+
563+
为什么多一层 L2?因为 mcpp 的索引是普通 git 仓库——任何人 fork 一个
564+
就是新索引,官方仓也理论上可能 force-push;不像 crates.io 那样有服务
565+
端硬约束。L2 给两类用户兜底:
566+
567+
1. 用第三方 / 私有索引的项目(`[indices.acme] = ...`)——第三方维护
568+
方未必有 L1 级别的 CI gate,L2 让 lock 锁住自己当时 build 用的索引
569+
sha。
570+
2. 即使是官方索引,L1 真的偶发出错时(误 force-push、签错 sha256
571+
patch),已经在 CI / 用户机器上锁住的项目不受影响,只有显式 update
572+
才会拉到新内容。
573+
574+
代价:用户多了一个心智负担——"什么时候该跑 `mcpp index update`"。文
575+
档里要有明显的 onboarding 提示;典型答案是"开发期跟着自己节奏更新,
576+
合并到 main 之前 lock 跟着 PR 一起进 git,CI 必走 lock"。
577+
578+
mcpp 的工程优势:**git 原生**——不需要发明索引格式 / 索引服务,clone
579+
一个仓就是一个索引;劣势:无 fast-path 增量(每次拉全仓)。规模大到
580+
这个成为瓶颈时再考虑 sparse-index / 索引镜像。

0 commit comments

Comments
 (0)