# Issue #267/#269 索引组织迁移 + artifact 采纳 — 实施计划(0.0.103)

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

**Goal:** 按设计文档 `2026-07-22-issue267-269-index-artifact-and-org-migration-design.md` 落地 D1–D5,单 PR 携版本 0.0.103,合入后走发布闭环 + xlings 全生态验证。

**Architecture:** 新/旧 URL 与 artifact base 收敛为 `mcpp::config` 三常量;`canonicalize_legacy_index_names` 导出并承担"URL 归一→名字迁移→artifact 内存级填充→去重"单循环;文件层治愈只做最小文本手术(config.toml URL 替换、.xlings.json URL 替换 + artifact 注入,幂等闸=已含 res base);seed 管线由 `pair` 升级为 `SeedRepo{name,url,artifact,source}`,非空才发射,零 diff 门。

**Tech Stack:** C++23 modules(自托管 `mcpp build`/`mcpp test`)、gtest 单测(tests/unit)、bash e2e(tests/e2e)、gh CLI。

## Global Constraints

- 版本:`mcpp.toml` version 与 `src/toolchain/fingerprint.cppm` MCPP_VERSION **同 commit 同步**为 `0.0.103`(不一致 release smoke 失败)。
- 新 URL `https://github.com/mcpplibs/mcpp-index.git`;旧 URL `https://github.com/mcpp-community/mcpp-index.git`;artifact base `https://github.com/xlings-res/mcpp-index`。
- 无 artifact 的仓 seed 输出**逐字节不变**(零 diff 门)。
- `.xlings.json` 迁移**不整文件重生成**(保 subos/版本绑定),只做目标文本替换,双间距变体(mcpp pretty / xlings compact)。
- `mcpp.fallback.config_migration` 是叶子模块,不 import mcpp.config —— URL 字面量在此重复是接受的(与现有 name 迁移同风格)。
- 构建/测试命令:`mcpp build`;`MCPP_FRESH=$(realpath $(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2))`;`"$MCPP_FRESH" test`;e2e 单跑 `MCPP="$MCPP_FRESH" bash tests/e2e/<n>.sh`。
- 分支:`feat/index-artifact-0.0.103` off `origin/main`(a768f48);单 PR;合并 `gh pr merge --squash --admin`。

---

### Task 0: 分支与文档

- [ ] `git checkout -b feat/index-artifact-0.0.103 origin/main`
- [ ] `git add .agents/docs/2026-07-22-issue267-269-*.md && git commit -m "docs: index org migration + artifact adoption design & impl plan (#267 #269)"`

### Task 1: D1 — 常量 + canonicalize 导出与 URL 迁移(TDD)

**Files:**
- Modify: `src/config.cppm`(导出区加三常量与 `canonicalize_legacy_index_names` 声明;原匿名 ns 实现移出;`:360` 模板 URL、`:575` add_default URL)
- Modify: `src/fallback/config_migration.cppm`(两迁移函数加 URL 替换)
- Modify: `src/publish/pipeline.cppm:158`、`docs/00-getting-started.md:98`、`docs/04-build-from-source.md:96`、`docs/zh/00-getting-started.md:99`、`docs/zh/04-build-from-source.md:103`
- Test: `tests/unit/test_config.cpp`

**Interfaces (produces):**
```cpp
// mcpp::config, exported
inline constexpr std::string_view kMcpplibsIndexUrl       = "https://github.com/mcpplibs/mcpp-index.git";
inline constexpr std::string_view kMcpplibsIndexUrlLegacy = "https://github.com/mcpp-community/mcpp-index.git";
inline constexpr std::string_view kMcpplibsIndexArtifact  = "https://github.com/xlings-res/mcpp-index";
void canonicalize_legacy_index_names(GlobalConfig& cfg);   // 导出(可测性)
```

- [ ] **Step 1: 失败单测**(`test_config.cpp` 末尾追加;`import mcpp.fallback.config_migration;` 加到文件头 import 区)

```cpp
TEST(ConfigIndexMigration, CanonicalizeRewritesOrgAndNameAndFillsArtifact) {
    mcpp::config::GlobalConfig cfg;
    cfg.defaultIndex = "mcpp-index";
    cfg.indexRepos.push_back({ "mcpp-index",
        std::string(mcpp::config::kMcpplibsIndexUrlLegacy) });
    mcpp::config::canonicalize_legacy_index_names(cfg);
    ASSERT_EQ(cfg.indexRepos.size(), 1u);
    EXPECT_EQ(cfg.defaultIndex, "mcpplibs");
    EXPECT_EQ(cfg.indexRepos[0].name, "mcpplibs");
    EXPECT_EQ(cfg.indexRepos[0].url,  mcpp::config::kMcpplibsIndexUrl);
    EXPECT_EQ(cfg.indexRepos[0].artifact, mcpp::config::kMcpplibsIndexArtifact);
}

TEST(ConfigIndexMigration, CanonicalizeFoldsLegacyAndDefaultEntries) {
    mcpp::config::GlobalConfig cfg;
    cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrlLegacy) });
    cfg.indexRepos.push_back({ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) });
    mcpp::config::canonicalize_legacy_index_names(cfg);
    EXPECT_EQ(cfg.indexRepos.size(), 1u);
}

TEST(ConfigIndexMigration, CanonicalizeRespectsExplicitGitSourceAndForeignRepos) {
    mcpp::config::GlobalConfig cfg;
    mcpp::config::IndexRepo git{ "mcpplibs", std::string(mcpp::config::kMcpplibsIndexUrl) };
    git.source = "git";
    cfg.indexRepos.push_back(git);
    cfg.indexRepos.push_back({ "other", "https://example.com/other-index.git" });
    mcpp::config::canonicalize_legacy_index_names(cfg);
    EXPECT_TRUE(cfg.indexRepos[0].artifact.empty());
    EXPECT_TRUE(cfg.indexRepos[1].artifact.empty());
    EXPECT_EQ(cfg.indexRepos[1].url, "https://example.com/other-index.git");
}

TEST(ConfigIndexMigration, MigrateConfigTomlRewritesOrgUrlIdempotently) {
    auto dir = make_tempdir("mcpp-migrate-toml");
    auto p = dir / "config.toml";
    { std::ofstream os(p); os <<
        "[index]\ndefault = \"mcpplibs\"\n\n[index.repos.\"mcpplibs\"]\n"
        "url = \"https://github.com/mcpp-community/mcpp-index.git\"\n"; }
    EXPECT_TRUE(mcpp::fallback::migrate_config_toml_index_names(p));
    std::stringstream ss; ss << std::ifstream(p).rdbuf();
    EXPECT_NE(ss.str().find("github.com/mcpplibs/mcpp-index.git"), std::string::npos);
    EXPECT_EQ(ss.str().find("mcpp-community/mcpp-index"), std::string::npos);
    EXPECT_FALSE(mcpp::fallback::migrate_config_toml_index_names(p));  // 幂等
    std::filesystem::remove_all(dir);
}
```

注:`IndexRepo` 的 `source`/`artifact` 字段 Task 2 才加 —— 本任务与 Task 2 的实现同 commit 落(测试先全写,分两步跑)。若想严格分步,`CanonicalizeRespectsExplicitGitSource` 测试留到 Task 2。

- [ ] **Step 2: 跑测确认编译失败**(常量/导出不存在):`mcpp build` 期望 FAIL
- [ ] **Step 3: 实现**——config.cppm 导出区(`struct IndexRepo` 后)加三常量;把 `canonicalize_legacy_index_names` 从匿名 ns 移到导出区,实现:

```cpp
void canonicalize_legacy_index_names(GlobalConfig& cfg) {
    if (cfg.defaultIndex == "mcpp-index") cfg.defaultIndex = "mcpplibs";
    std::vector<IndexRepo> normalized;
    for (auto r : cfg.indexRepos) {
        // ① 组织迁移(先于名字判断与去重:老配置匹配 + 新旧条目可折叠)
        if (r.url == kMcpplibsIndexUrlLegacy) r.url = std::string(kMcpplibsIndexUrl);
        // ② 名字迁移(URL 已归一,只认新 URL)
        if (r.name == "mcpp-index" && r.url == kMcpplibsIndexUrl) r.name = "mcpplibs";
        // ③ 内存级 artifact 填充(官方仓、未显式退出、未自定义)——Task 2 落地字段后启用
        if (r.url == kMcpplibsIndexUrl && r.artifact.empty() && r.source != "git")
            r.artifact = std::string(kMcpplibsIndexArtifact);
        bool duplicate = std::any_of(normalized.begin(), normalized.end(),
            [&](const IndexRepo& e) { return e.name == r.name && e.url == r.url; });
        if (!duplicate) normalized.push_back(std::move(r));
    }
    cfg.indexRepos = std::move(normalized);
}
```

`config_migration.cppm` 两函数各加(在现有 replace 之后):
```cpp
replace_all(updated, "https://github.com/mcpp-community/mcpp-index.git",
                     "https://github.com/mcpplibs/mcpp-index.git");
```
模板 `:360`、`add_default` `:575` 改用 `kMcpplibsIndexUrl`(模板是字符串字面量,直接写新 URL);`publish/pipeline.cppm:158` Fork 行 → `https://github.com/mcpplibs/mcpp-index`;docs 4 处链接同改。

- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** 期望 PASS(新测试含 Task 2 字段的暂缓)
- [ ] **Step 5: Commit** `fix(index): migrate mcpplibs index URL to mcpplibs org (#267-A)`

### Task 2: D2 — SeedRepo 管线 + IndexRepo/TOML/模板 artifact

**Files:**
- Modify: `src/config.cppm`(IndexRepo 加字段:36;TOML 读 artifact/source:535-543;模板:359-361;add_default:571-575;write_default_xlings_json:374-396;print_env:663-668)
- Modify: `src/xlings.cppm`(SeedRepo struct + seed_xlings_json 签名/发射:250-253、1110-1150)
- Test: `tests/unit/test_xlings.cpp`、`tests/unit/test_config.cpp`

**Interfaces (produces):**
```cpp
// mcpp::config
struct IndexRepo { std::string name, url, artifact, source; };
// mcpp::xlings
struct SeedRepo { std::string name, url, artifact, source; };
void seed_xlings_json(const Env&, std::span<const SeedRepo>,
                      std::string_view mirror = "auto", const ProjectEnv& = {});
```

- [ ] **Step 1: 失败单测**(`test_xlings.cpp`,`import mcpp.xlings;` 已有则复用其 temp-dir 惯例)

```cpp
TEST(XlingsSeed, PlainRepoEmissionIsByteStable) {   // 零 diff 门
    auto dir = make_tempdir("mcpp-seed-plain");
    mcpp::xlings::Env env; env.home = dir;
    std::vector<mcpp::xlings::SeedRepo> repos{ { "mcpplibs", "https://x.git", "", "" } };
    mcpp::xlings::seed_xlings_json(env, repos);
    std::stringstream ss; ss << std::ifstream(dir / ".xlings.json").rdbuf();
    EXPECT_NE(ss.str().find(
        "    { \"name\": \"mcpplibs\", \"url\": \"https://x.git\" }\n"), std::string::npos);
    EXPECT_EQ(ss.str().find("artifact"), std::string::npos);
    std::filesystem::remove_all(dir);
}

TEST(XlingsSeed, ArtifactAndSourceEmittedWhenSet) {
    auto dir = make_tempdir("mcpp-seed-art");
    mcpp::xlings::Env env; env.home = dir;
    std::vector<mcpp::xlings::SeedRepo> repos{
        { "mcpplibs", "https://x.git", "https://github.com/xlings-res/mcpp-index", "auto" } };
    mcpp::xlings::seed_xlings_json(env, repos);
    std::stringstream ss; ss << std::ifstream(dir / ".xlings.json").rdbuf();
    EXPECT_NE(ss.str().find("\"url\": \"https://x.git\", \"artifact\": "
        "\"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\""), std::string::npos);
    std::filesystem::remove_all(dir);
}
```

- [ ] **Step 2: 编译失败确认**(SeedRepo 不存在)
- [ ] **Step 3: 实现**——`xlings.cppm` 声明区加 `struct SeedRepo { std::string name, url, artifact, source; };`,签名改 `std::span<const SeedRepo>`;发射循环:

```cpp
for (std::size_t i = 0; i < repos.size(); ++i) {
    json += std::format("    {{ \"name\": \"{}\", \"url\": \"{}\"",
                        json_escape(repos[i].name), json_escape(repos[i].url));
    if (!repos[i].artifact.empty())
        json += std::format(", \"artifact\": \"{}\"", json_escape(repos[i].artifact));
    if (!repos[i].source.empty())
        json += std::format(", \"source\": \"{}\"", json_escape(repos[i].source));
    json += std::format(" }}{}\n", i + 1 == repos.size() ? "" : ",");
}
```

`config.cppm`:IndexRepo 加 `artifact`/`source`;TOML 解析读两 key(可选 string);`write_default_xlings_json` pairs→`std::vector<mcpp::xlings::SeedRepo>` 直传四字段;模板加:
```toml
[index.repos."mcpplibs"]
url      = "https://github.com/mcpplibs/mcpp-index.git"
artifact = "https://github.com/xlings-res/mcpp-index"
# source = "auto"  # default: artifact first, git fallback; set "git" to force git
```
`add_default` lambda 加 artifact 形参并给 mcpplibs 传 `kMcpplibsIndexArtifact`;`print_env` 的 repo 行追加 ` [artifact]` 标记(`r.artifact` 非空时);`ensure_project_index_dir` 的 `customRepos` 改 `std::vector<mcpp::xlings::SeedRepo>`(artifact 字段 Task 4 接入,先留空串)。canonicalize 的 ③ 分支此时编译生效。

- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** 全绿(含 Task 1 暂缓测试)
- [ ] **Step 5: Commit** `feat(index): per-repo artifact source plumbing — IndexRepo/SeedRepo/toml/template (#269)`

### Task 3: D3 — 存量 .xlings.json artifact 注入(TDD)

**Files:**
- Modify: `src/fallback/config_migration.cppm:69-81`
- Test: `tests/unit/test_config.cpp`

- [ ] **Step 1: 失败单测**

```cpp
namespace {
std::string read_all(const std::filesystem::path& p) {
    std::stringstream ss; ss << std::ifstream(p).rdbuf(); return ss.str();
}
}  // namespace

TEST(ConfigIndexMigration, XlingsJsonHealsPrettyLegacyEntry) {
    auto dir = make_tempdir("mcpp-migrate-xj");
    auto p = dir / ".xlings.json";
    { std::ofstream os(p); os << "{\n  \"index_repos\": [\n"
        "    { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpp-community/mcpp-index.git\" }\n"
        "  ],\n  \"subos\": \"default\",\n  \"mirror\": \"auto\"\n}\n"; }
    EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p));
    auto text = read_all(p);
    EXPECT_NE(text.find("\"url\": \"https://github.com/mcpplibs/mcpp-index.git\", "
        "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), std::string::npos);
    EXPECT_NE(text.find("\"subos\": \"default\""), std::string::npos);  // 无关 key 保留
    EXPECT_EQ(text.find("mcpp-community/mcpp-index"), std::string::npos);
    EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p));   // 幂等
    std::filesystem::remove_all(dir);
}

TEST(ConfigIndexMigration, XlingsJsonHealsCompactLegacyNameAndUrl) {
    auto dir = make_tempdir("mcpp-migrate-xjc");
    auto p = dir / ".xlings.json";
    { std::ofstream os(p); os << "{\"index_repos\":[{\"name\":\"mcpp-index\","
        "\"url\":\"https://github.com/mcpp-community/mcpp-index.git\"}],\"mirror\":\"auto\"}"; }
    EXPECT_TRUE(mcpp::fallback::migrate_xlings_json_index_names(p));
    auto text = read_all(p);
    EXPECT_NE(text.find("\"name\":\"mcpplibs\""), std::string::npos);
    EXPECT_NE(text.find("\"url\":\"https://github.com/mcpplibs/mcpp-index.git\", "
        "\"artifact\": \"https://github.com/xlings-res/mcpp-index\""), std::string::npos);
    EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p));
    std::filesystem::remove_all(dir);
}

TEST(ConfigIndexMigration, XlingsJsonSkipsInjectionWhenArtifactPresent) {
    auto dir = make_tempdir("mcpp-migrate-xja");
    auto p = dir / ".xlings.json";
    std::string body = "{\n  \"index_repos\": [\n"
        "    { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpplibs/mcpp-index.git\", "
        "\"artifact\": \"https://github.com/xlings-res/mcpp-index\" }\n  ]\n}\n";
    { std::ofstream os(p); os << body; }
    EXPECT_FALSE(mcpp::fallback::migrate_xlings_json_index_names(p));
    EXPECT_EQ(read_all(p), body);
    std::filesystem::remove_all(dir);
}
```

- [ ] **Step 2: 跑测确认 FAIL**(artifact 未注入)
- [ ] **Step 3: 实现**(接在 name/URL 替换后):

```cpp
// Artifact injection (xlings 0.4.68 per-repo artifact source). Idempotency
// gate: the res base appearing anywhere means a previous run (or a fresh
// seed) already declared it — plain replace_all would re-inject on every run.
if (updated.find("xlings-res/mcpp-index") == std::string::npos) {
    constexpr std::string_view art =
        ", \"artifact\": \"https://github.com/xlings-res/mcpp-index\"";
    for (std::string_view urlkv : {
            "\"url\": \"https://github.com/mcpplibs/mcpp-index.git\"",
            "\"url\":\"https://github.com/mcpplibs/mcpp-index.git\"" })
        replace_all(updated, urlkv, std::string(urlkv) + std::string(art));
}
```

- [ ] **Step 4: `mcpp build && "$MCPP_FRESH" test`** PASS
- [ ] **Step 5: Commit** `feat(index): heal existing registry .xlings.json — org URL + artifact injection (#267-A #269)`

### Task 4: D5 — 项目级 [indices] artifact/source + pin 交互

**Files:**
- Modify: `src/pm/index_spec.cppm:14-23`(加字段 + `artifact_applicable()`)
- Modify: `src/manifest/toml.cppm:1142-1150`([indices] 长格式读 artifact/source)
- Modify: `src/config.cppm:546-563`(config.toml [indices] 同步读)+ `ensure_project_index_dir:680-691`(SeedRepo 透传 + 不适用 warn)
- Test: `tests/unit/test_manifest.cpp`

**Interfaces (produces):**
```cpp
// mcpp::pm::IndexSpec 新增
std::string artifact;   // optional artifact source base (xlings 0.4.68+)
std::string source;     // optional "auto" | "artifact" | "git"
bool artifact_applicable() const {   // pin/local forces git — artifact only tracks latest
    return !artifact.empty() && rev.empty() && tag.empty() && branch.empty() && path.empty();
}
```

- [ ] **Step 1: 失败单测**(`test_manifest.cpp`,仿现有 [indices] 解析测试写法;若无现成解析入口测试,则直接测 IndexSpec 语义 + toml 解析函数)

```cpp
TEST(ManifestIndices, ArtifactAndSourceParsedAndPinSuppresses) {
    auto m = parse_manifest_from_string(R"(
[package]
name = "p"
version = "0.1.0"
[indices]
fast   = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", source = "auto" }
pinned = { url = "https://example.com/i.git", artifact = "https://example.com/res/i", rev = "abc123" }
)");  // 按 test_manifest.cpp 现有 helper 名对齐
    auto& fast = m.indices.at("fast");
    EXPECT_EQ(fast.artifact, "https://example.com/res/i");
    EXPECT_EQ(fast.source, "auto");
    EXPECT_TRUE(fast.artifact_applicable());
    auto& pinned = m.indices.at("pinned");
    EXPECT_FALSE(pinned.artifact_applicable());
}
```

- [ ] **Step 2: FAIL 确认** → **Step 3: 实现**:两处解析各加两行 `find("artifact")`/`find("source")`;`ensure_project_index_dir` 远程分支:

```cpp
mcpp::xlings::SeedRepo r{ name, spec.url, "", "" };
if (spec.artifact_applicable()) { r.artifact = spec.artifact; r.source = spec.source; }
else if (!spec.artifact.empty())
    std::println("[warn] index '{}': artifact source ignored (rev/tag/branch/path pins force git)", name);
customRepos.push_back(std::move(r));
```

- [ ] **Step 4: 全测 PASS** → **Step 5: Commit** `feat(index): project-level [indices] artifact/source with pin-forces-git rule (#269)`

### Task 5: read_seeded_index_repos 容错锁测

**Files:**
- Test: `tests/unit/test_pm_package_fetcher.cpp:237` 附近克隆现有 fixture

- [ ] **Step 1: 新测**——entry 含 artifact/source 仍正确提取 name/url:

```cpp
TEST(PackageFetcher, SeededIndexReposTolerateArtifactFields) {
    auto project = make_tempdir("mcpp-pf-artifact");   // 对齐该文件现有 temp helper
    auto xjson = project / ".xlings.json";
    { std::ofstream os(xjson); os << "{\n  \"index_repos\": [\n"
        "    { \"name\": \"mcpplibs\", \"url\": \"https://github.com/mcpplibs/mcpp-index.git\", "
        "\"artifact\": \"https://github.com/xlings-res/mcpp-index\", \"source\": \"auto\" }\n  ]\n}\n"; }
    auto repos = mcpp::pm::read_seeded_index_repos(xjson);
    ASSERT_EQ(repos.size(), 1u);
    EXPECT_EQ(repos[0].first, "mcpplibs");
    EXPECT_EQ(repos[0].second, "https://github.com/mcpplibs/mcpp-index.git");
    std::filesystem::remove_all(project);
}
```
(返回类型按现有测试实际形态对齐 —— 以 `:244` 现有断言写法为准。)

- [ ] **Step 2: 跑测**(预期直接 PASS —— 这是回归锁,不是新功能;若 FAIL 则修 `read_seeded_index_repos` 的对象切分)
- [ ] **Step 3: Commit** `test(pm): lock read_seeded_index_repos tolerance to artifact/source fields`

### Task 6: D4 — xlings pin → 0.4.68 + 常量对齐

**Files:**
- Modify: `.github/workflows/release.yml`(`:99`、`:284`、`:341-345` 硬编码 0.4.67 全部)、`.github/workflows/cross-build-test.yml:105,218`、`.github/workflows/ci-linux-e2e.yml:126`
- Modify: `src/xlings.cppm:36`(`kXlingsVersion` "0.4.51"→"0.4.68")
- Modify: `src/config.cppm` `print_env`(加 `bundled xlings pin   = {kXlingsPinnedVersion}` 行,给常量一个消费点)

- [ ] **Step 1**: `grep -rn '0\.4\.67' .github/workflows/` 逐处替换为 0.4.68(含注释里的版本说明,注释补一句 0.4.68 = per-repo index artifact #377)
- [ ] **Step 2**: 常量 + print_env;`mcpp build && "$MCPP_FRESH" self env` 目测新行
- [ ] **Step 3: Commit** `ci: bundled xlings pin 0.4.67 -> 0.4.68 (per-repo index artifact); align kXlingsVersion`

### Task 7: e2e — 151_index_url_artifact_migration.sh

**Files:**
- Create: `tests/e2e/151_index_url_artifact_migration.sh`(仿 `10_env_command.sh` 的隔离 MCPP_HOME 手法;需 `MCPP_VENDORED_XLINGS` 或系统 xlings,CI bootstrap 已具备)

- [ ] **Step 1: 写脚本**

```bash
#!/usr/bin/env bash
# #267/#269: fresh init seeds mcpplibs-org URL + artifact source; legacy
# config.toml/.xlings.json (old org URL, no artifact) are healed in place,
# idempotently, in both pretty (mcpp) and compact (xlings) JSON spacings.
set -e
TMP=$(mktemp -d); trap "rm -rf $TMP" EXIT
export MCPP_HOME="$TMP/mcpp-home"

NEW_URL='https://github.com/mcpplibs/mcpp-index.git'
OLD_URL='https://github.com/mcpp-community/mcpp-index.git'
ART='https://github.com/xlings-res/mcpp-index'
CFG="$MCPP_HOME/config.toml"; XJ="$MCPP_HOME/registry/.xlings.json"

# 1. Fresh init: new org URL + artifact in both files.
"$MCPP" self env > /dev/null
grep -q "url      = \"$NEW_URL\"" "$CFG"      || { echo "config.toml missing new url"; exit 1; }
grep -q "artifact = \"$ART\"" "$CFG"          || { echo "config.toml missing artifact"; exit 1; }
grep -q "\"url\": \"$NEW_URL\", \"artifact\": \"$ART\"" "$XJ" || { echo "seed missing artifact"; exit 1; }
grep -q 'mcpp-community/mcpp-index' "$CFG" "$XJ" && { echo "old org leaked"; exit 1; }

# 2. Legacy heal (pretty): old URL, no artifact.
cat > "$CFG" <<EOF
[xlings]
binary = "bundled"
[index]
default = "mcpplibs"
[index.repos."mcpplibs"]
url = "$OLD_URL"
EOF
cat > "$XJ" <<EOF
{
  "index_repos": [
    { "name": "mcpplibs", "url": "$OLD_URL" }
  ],
  "subos": "default",
  "lang": "en",
  "mirror": "auto"
}
EOF
"$MCPP" self env > /dev/null
grep -q "url = \"$NEW_URL\"" "$CFG"                            || { echo "toml url not healed"; exit 1; }
grep -q "\"url\": \"$NEW_URL\", \"artifact\": \"$ART\"" "$XJ"  || { echo "xj not healed"; exit 1; }
grep -q '"subos": "default"' "$XJ"                             || { echo "unrelated key lost"; exit 1; }
cp "$CFG" "$TMP/cfg1"; cp "$XJ" "$TMP/xj1"
"$MCPP" self env > /dev/null                                   # idempotent second run
cmp -s "$CFG" "$TMP/cfg1" && cmp -s "$XJ" "$TMP/xj1"           || { echo "not idempotent"; exit 1; }

# 3. Legacy heal (compact, old name): xlings-writer spacing.
printf '{"index_repos":[{"name":"mcpp-index","url":"%s"}],"mirror":"auto"}' "$OLD_URL" > "$XJ"
"$MCPP" self env > /dev/null
grep -q '"name":"mcpplibs"' "$XJ"        || { echo "compact name not healed"; exit 1; }
grep -q "xlings-res/mcpp-index" "$XJ"    || { echo "compact artifact not injected"; exit 1; }
grep -q 'mcpp-community' "$XJ" && { echo "compact old org left"; exit 1; }

echo "OK"
```

- [ ] **Step 2: 本地跑** `MCPP="$MCPP_FRESH" bash tests/e2e/151_index_url_artifact_migration.sh` → OK
- [ ] **Step 3: Commit** `test(e2e): 151 — index org URL heal + artifact seed/injection`

### Task 8: 版本 0.0.103 + CHANGELOG

- [ ] `mcpp.toml:3` version → `0.0.103`;`src/toolchain/fingerprint.cppm:21` → `0.0.103`(同 commit!)
- [ ] CHANGELOG.md 顶部加 0.0.103 条目(#267 URL 迁移 + 存量治愈、#269 artifact 采纳 + pin 0.4.68、[indices] artifact)
- [ ] Commit `release: 0.0.103 — index org migration + per-repo artifact adoption (#267 #269)`

### Task 9: 验证 + PR + CI + 合入

- [ ] `mcpp build && "$MCPP_FRESH" test` 全绿;e2e 相关子集(10、151)绿;可选跑本机 e2e 基线抽样
- [ ] `git push -u origin feat/index-artifact-0.0.103`;`gh pr create`(标题 `feat(index): mcpplibs org migration + xlings 0.4.68 per-repo artifact adoption — 0.0.103 (#267 #269)`,body 引两 issue + 设计文档)
- [ ] 等 CI 全绿(ci-linux、ci-linux-e2e 两 shard、cross-build-test、windows/macos e2e、fresh-install)
- [ ] `gh pr merge <n> --squash --admin`(不加 `--delete-branch` 以防叠栈坑;单 PR 无叠栈,可加)

### Task 10: 发布 + xlings 全生态验证(runbook = release-publish-pipeline 记忆 + .agents/skills/mcpp-release)

- [ ] `gh workflow run release.yml --ref main`(从 mcpp.toml 派生 v0.0.103;publish-ecosystem 自动镜像 xlings-res/mcpp 双端 + 开 xim-pkgindex bump PR)
- [ ] 双端 GET 核验四平台资产(`curl -sL -o /dev/null -w '%{http_code} %{size_download}'`,GitHub + gitcode.com 直链主机,必须 200 + 真字节;HEAD 不可信)
- [ ] `gh pr merge <bump-pr> --repo openxlings/xim-pkgindex --squash --admin`(publish-artifact.yml 自动发索引)
- [ ] 实机验证:`xlings update && xlings install mcpp@0.0.103 -y` → `mcpp --version` = 0.0.103
- [ ] **artifact 路径专项**(本 PR 主功能):fresh MCPP_HOME + xlings ≥0.4.68 捆绑,`mcpp index update` → `registry/data/mcpplibs` 变 artifact 管理(`.git` 消失、`.xlings-index-version` 出现);`registry/.xlings.json` mcpplibs 条目带 artifact 字段
- [ ] 收尾 pin:工作区 `.xlings.json` bootstrap pin → 0.0.103 直推 main(`ci: workspace mcpp bootstrap pin -> 0.0.103 (released + indexed)`);`ci-fresh-install.yml` 的 0.0.102 pin 同步 bump

## Self-Review 结论

- 设计文档 D1–D5 全覆盖(D1→T1,D2→T2,D3→T3,D5→T4,兼容锁→T5,D4→T6,e2e→T7);P5(CN region 对象)按设计文档明确推迟,非本计划范围。
- 类型一致性:`SeedRepo{name,url,artifact,source}` 贯穿 T2/T4;`IndexRepo` 同形;`artifact_applicable()` 只在 T4 定义与使用。
- 已知风险:test_manifest.cpp 的解析 helper 名需现场对齐(计划内已标注);`ensure_project_index_dir` 的 `spec.is_builtin()` 分支(mcpplibs)不进 customRepos,不受 T4 影响。
