Skip to content

Commit 6e6176a

Browse files
committed
fix(native): land and provision objects under the configured object kind
FIX-MTR-16. Native-push landing, path provisioning and their descendant / target bookkeeping now derive every object id from monorepo.object_hash_kind() instead of the thread-local HashKind: - trunk_provenance gains a fallible synthesize_with_kind that fails closed on a cross-kind tree or parent; - mono_storage::advance_descendant_refs_with takes an explicit kind, used for the descendant walk, parent/tree parsing and both continuation constructors; - trace_target_service publishes and re-binds active targets through the kind-aware resolver, keeping the legacy entry as a TLS wrapper; - push_queue_service drives tip/root/payload/previous reads, the create-path resolve, the stale pre-check, squash synthesis and target publication from the configured kind; - merge.rs parses ancestors and the root parent, and synthesizes the roll-up, under the same kind; - editing.rs, provision.rs and handler.rs mint the .gitkeep and file blobs and every provisioned directory level under the configured kind, and create and annotated-tag commits and tags hash with it (Tag::new_with_kind); - code_edit::utils resolves the build-repo root tree and repo tip commit with the kind-aware converters. The http_git commit fixture in tests/integration_traces.rs now stages until the index differs from HEAD, because a host write can lag behind a bind-mounted `git add` and make `commit` see a clean tree. Verification on this tree: cargo check --all-targets clean, nightly fmt clean, cargo nextest run --all --no-fail-fast 3121/3121 passed (3 pre-existing ignores) in 670s, acceptance target 6/6 over real Docker Git for SHA-1 and SHA-256 with /project and monorepo-root closures strict-fsck verified, and the VER-1 RED replay fails on the pre-fix tree with the original Invalid tree signature. Independent review returned VERDICT: PASS. Version 0.42.84 -> 0.42.85 (patch; dependencies unchanged). Signed-off-by: Eli Ma <eli@patch.sh>
1 parent f925d5d commit 6e6176a

15 files changed

Lines changed: 1382 additions & 103 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mega2"
3-
version = "0.42.84"
3+
version = "0.42.85"
44
edition = "2024"
55
license = "MIT OR Apache-2.0"
66
description = "Monorepo engine and Git hosting server for AI agents: trunk-based storage, Git Smart HTTP, Git LFS, OCI registry, and Agent Session Capture"

‎docs/plan/plan-20261015.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4401,7 +4401,14 @@ AC1..4是发布前四前置source资格;AC5..8在合法bump之后、C③三门
44014401

44024402
**Task type:** `implementation`
44034403

4404-
**Lifecycle / Acceptance:** `pending` / 空
4404+
**Lifecycle / Acceptance:** `in-progress` / `locally-accepted`(实现、验收与完整本地 C 已达;签名提交、推送、tag、Release 与远端 D 未达)
4405+
4406+
**2026-10-31 实施与本地验收(本卡现行):** 显式对象算法轴已落地并端到端通过。生产改动:`trunk_provenance` 新增可失败 `synthesize_with_kind`(跨 kind fail-closed);`mono_storage::advance_descendant_refs_with` 取显式 `kind` 并把 descendant 走树、父/树解析与 `Legacy`/`Trunk` 合成改为 kind 显式(新增 `load_tree_cached_with_kind`);`trace_target_service` 的 active-target 解析与 `bind_if_still_landed*` 改用 `resolve_path_tree_hash_in_txn_with_kind`(保留 TLS 包装入口);`push_queue_service` 以 `config.monorepo.object_hash_kind()` 驱动 tip/root/payload/previous 读取、创建路径解析、squash 合成与 target 发布,并新增 `assert_main_path_tree_hash_in_txn_with_kind`;`merge.rs` 祖先/根 parent 解析与合成同 kind;`provision.rs` 的 `.gitkeep` blob 与每一层新目录树、`editing.rs` 的 `.gitkeep`/文件 blob、`rewrite_parent_chains`、`commit_tree_update`、`prepare_create_entry_update` 及标注 tag(改 `Tag::new_with_kind`)、移动路径的替换 `.gitkeep`、`code_edit::utils` 的 build-repo 根树与 tip commit 读取,均改为配置 kind。
4407+
**本地实际收据:** `cargo check --all-targets` 0 error/0 warning;`cargo +nightly fmt --all --check` exit0;`native_push_hash_` 5/5(含独立 canonical-bytes 重算 oracle、opposing TLS、SHA-256↔BLAKE3 同宽拒绝);`cargo test --lib ceres::pack::` **187 passed/0 failed**;`cargo test -p mega2 --test integration_native_push_hash` **6 passed/0 failed**(SHA-1 与 SHA-256 各自的首次落地、单提交更新含父边、双提交 squash provenance、root-dir 外创建被拒且快照不变、客户端重同步后的合法恢复;每次落地后对 `/project` 与 monorepo 根 `/` 两个 clone 刷新并 `fsck --strict`)。版本候选 0.42.84→**0.42.85**(Cargo.toml 与 lock 一致,依赖不变)。
4408+
**本机 Claude(用户指定替代 Codex)复审:** R2 判 `VERDICT: FAIL`(3×P1、2×P2),R3 判 FAIL(标注 tag、移动 `.gitkeep`、oracle 覆盖面),R4 判 FAIL(目录创建落地后的 target 绑定仍走 TLS 解析、根 `/` 未被 fsck),逐项修复后 **R5 明确 `VERDICT: PASS`:`No remaining defects found in the requested scope.`**
4409+
**VER 逐项收据(2026-10-31):** VER-1 已做真实 RED 回放:把本卡 `tests/integration_native_push_hash.rs` 单独复制到 `origin/main`(`f483f57`,0.42.84)前驱树的 `/tmp/mega2-red` 副本、仅还原本卡十一个改动文件为前驱字节、独立 `CARGO_TARGET_DIR`,`hash_red_sha256_create`/`hash_red_sha256_update` 实际 **FAILED**,两份均在 `POST /api/v1/path/provision` 得 500 `Application error: Invalid tree`(trace `e9224264-d96f-409d-bb61-5edb620844bf`、`45c7c23a-9185-469b-9921-d29ec806be3c`),`test result: FAILED. 0 passed; 2 failed`;同一两个 target 在修复树上 6/6 全绿,故 RED 成立。VER-2 见上(6/6,SHA-1 与 SHA-256,含 `/project` 与根 `/` 双 closure fsck)。VER-3 `native_push_hash_` 5/5。VER-4 `cargo test --lib ceres::pack::` 187/187。VER-6 仓外脚本 `/tmp/fix-mtr16-acceptance-20261031/audit.py` 实际 **AUDIT PASSED/exit0**(20 项:卡片 8 AC、9 个生产文件存在、五个在范围内落地函数的函数体内无 `Commit::from_tree_id(`/`Blob::from_content(`/`Tree::from_tree_items(`/`Tag::new(`、`synthesize_with_kind` 可失败、descendant 显式 kind 与 kind 感知走树、target 发布与绑定 kind 显式、测试覆盖双算法/根 closure/两个 RED target);该脚本只审卡片范围内函数,attach/detach 与 CL-merge 的既有构造器按卡片声明排除,不冒认修复。**VER-5(`native_push_hash_hash_red_` 专用单元 RED)未建立**:前驱树无 `synthesize_with_kind`,同签名 RED 单元无法在前驱编译;其行为轴已由 VER-1 的端到端 RED 回放覆盖,但按卡片逐项计数仍缺一件,故不勾选。
4410+
**整卡完整 C(2026-10-31,nextest):** 环境三处已定位并解除:`NO_PROXY=127.0.0.1,localhost`(macOS 系统代理 `127.0.0.1:7897` 劫持 localhost 流量,reqwest 长退避曾致两小时超时)、`MEGA2_IT_DB_ADMIN_URL`(DEP-TR-PG-ADMIN 要求的管理 DSN)、`PATH` 前置 `git-lfs 3.8.0`(宿主 3.8.1 与测试 pin 不符)。另修一处仅测试侧的 fixture 竞态:`tests/integration_traces.rs::http_git_commit` 原先 `git add .` 后立即 `commit`,在 bind mount 落后于宿主写入时会「无可提交」而以空 stderr exit1 失败;改为按 `git diff --cached --quiet` 反复 stage 直到 index 与 HEAD 不同(最多 40 次、每次 100 毫秒),不触碰产品代码。修后 `cargo nextest run --all --no-fail-fast` 实际 **3121 run / 3121 passed(27 slow、1 leaky)/ 0 failed / 3 skipped,670.115 秒**,两项 `integration_traces::http_git_sha{1,256}_roundtrip` 由失败转为通过(`Summary [...] 2 tests run: 2 passed`)。此前 3119/3121 的两次失败已在**前驱已发布树** `/tmp/mega2-red`(0.42.84,十一个改动文件逐字取自 `origin/main`,独立 `CARGO_TARGET_DIR`)逐字复现,故该缺陷先于本卡存在、本卡以测试侧修复消除,不冒认为产品修复。
4411+
**仍未取得(故不得 done/complete、不 push、不发布):****仍未取得(故不得 done/complete、不 push、不发布):****仍未取得(故不得 done/complete、不 push、不发布):** VER-1 仅执行现树(RED target 现为绿),未做"前驱旧树仅复制本卡测试"的 RED 回放;VER-5 的 `native_push_hash_hash_red_` 专用 RED 未建立;VER-6 的仓外 `audit.py` 未重建;整卡版本化完整 C 亦未取得——本机以单进程运行全量 lib(约2800 用例)时出现 433 项与产品改动无关的 fixture 失败(同模块单独运行与 `ceres::pack::` 187/187 均通过,属本机连接/资源上限),而 `cargo test --all` 的完整五命令合同尚未按计划在受控窗口内取得。A/C/ER05/D 均零 claim。
44054412

44064413
**Description:** 空引用 discovery 的 40/64 零 OID 缺陷已拆出独立 FIX-MTR-24,须先完成其版本化 C/D;本卡只在新已发布树上处理 B3 主轴。 保留原编号给B3事务落地主轴。实施时仅消费已发布FIX18索引、FIX19签名、FIX20链解析与FIX21路径树行为,只修stale前置、typed合成/descendant和事务内active-target路径解析;原synthesize公开返回Commit入口保留,新加fallible显式kind入口由本卡3个生产调用实际消费。MST2 publication=true仍未在本卡范围,不能改fixture关闭原本开启的分支;一旦强制门需要先ER10。 自原FIX-MTR-16(拆出18/19/20/21/24)拆出,独立可发布/恢复,不以拆卡降低原矩阵。
44074414

‎src/ceres/api_service/mono_api_service/editing.rs‎

Lines changed: 47 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::{
22
CreateEntryUpdate, MonoServiceLogic, PROVISION_STALE_SNAPSHOT, RefUpdate, TreeCommitInput,
3-
TreeCommitOutcome, TreeUpdateResult, gitkeep_placeholder, logic::PATH_NOT_EXIST_RE, prelude::*,
3+
TreeCommitOutcome, TreeUpdateResult, logic::PATH_NOT_EXIST_RE, prelude::*,
44
};
55
impl super::MonoApiService {
66
/// `search_tree_for_update` with the ADR-LB-03 error mapping: a missing
@@ -36,6 +36,7 @@ impl super::MonoApiService {
3636
pub(in crate::ceres::api_service::mono_api_service) fn rewrite_parent_chains(
3737
ancestors: Vec<(PathBuf, Vec<Arc<Tree>>)>,
3838
edits: Vec<(PathBuf, Vec<TreeItem>)>,
39+
kind: git_internal::hash::HashKind,
3940
) -> Result<TreeUpdateResult, GitError> {
4041
let mut known: HashMap<PathBuf, Arc<Tree>> = HashMap::new();
4142
for (parent, chain) in ancestors {
@@ -68,7 +69,7 @@ impl super::MonoApiService {
6869
let Some(items) = pending.remove(&path) else {
6970
break;
7071
};
71-
let tree = Tree::from_tree_items(items)
72+
let tree = Tree::from_tree_items_with_kind(kind, items)
7273
.map_err(|_| GitError::CustomError("Invalid tree".to_string()))?;
7374
let clean_path = MonoServiceLogic::clean_path_str(&path.to_string_lossy());
7475
let ref_path = if clean_path == "/" || clean_path.starts_with('/') {
@@ -126,6 +127,12 @@ impl super::MonoApiService {
126127
input: TreeCommitInput,
127128
requester: Option<String>,
128129
) -> Result<TreeCommitOutcome, GitError> {
130+
let kind = self
131+
.storage
132+
.config()
133+
.monorepo
134+
.object_hash_kind()
135+
.map_err(|e| GitError::CustomError(e.to_string()))?;
129136
let TreeCommitInput {
130137
update_result,
131138
mut save_trees,
@@ -173,20 +180,22 @@ impl super::MonoApiService {
173180
return Err(GitError::CustomError(PROVISION_STALE_SNAPSHOT.to_owned()));
174181
}
175182
let base_commit =
176-
ObjectHash::from_hex_for_kind(get_hash_kind(), &src_commit.id.to_string()).map_err(
177-
|e| GitError::CustomError(format!("Invalid commit hash {}: {e}", src_commit.id)),
178-
)?;
183+
ObjectHash::from_hex_for_kind(kind, &src_commit.id.to_string()).map_err(|e| {
184+
GitError::CustomError(format!("Invalid commit hash {}: {e}", src_commit.id))
185+
})?;
179186
let target_tree_id = Self::ref_update_tree_id_for_path(&update_result, &tip_path)
180187
.ok_or_else(|| {
181188
GitError::CustomError(format!(
182189
"Missing updated tree for build repo root {tip_path}"
183190
))
184191
})?;
185-
let dst_commit = Commit::from_tree_id(
192+
let dst_commit = Commit::from_tree_id_with_kind(
193+
kind,
186194
target_tree_id,
187195
vec![base_commit],
188196
&format_commit_msg(&commit_msg, None),
189-
);
197+
)
198+
.map_err(|e| GitError::CustomError(e.to_string()))?;
190199
let new_commit_id = dst_commit.id.to_string();
191200

192201
let username = author_username.unwrap_or("Anonymous".to_string());
@@ -272,6 +281,12 @@ impl super::MonoApiService {
272281
&self,
273282
entry_info: &CreateEntryInfo,
274283
) -> Result<CreateEntryUpdate, GitError> {
284+
let kind = self
285+
.storage
286+
.config()
287+
.monorepo
288+
.object_hash_kind()
289+
.map_err(|e| GitError::CustomError(e.to_string()))?;
275290
let path = PathBuf::from(&entry_info.path);
276291
let mut save_trees = vec![];
277292
let file_content = if entry_info.is_directory {
@@ -369,8 +384,8 @@ impl super::MonoApiService {
369384
// directory can be represented as a tree with at least
370385
// one blob entry. The blob contains a timestamp so it's
371386
// unique.
372-
let (blob, tree_item) = gitkeep_placeholder();
373-
let new_dir_tree = Tree::from_tree_items(vec![tree_item]).unwrap();
387+
let (blob, tree_item) = super::gitkeep_placeholder_for_kind(kind)?;
388+
let new_dir_tree = Tree::from_tree_items_with_kind(kind, vec![tree_item]).unwrap();
374389
save_trees.push(new_dir_tree.clone());
375390
let entry_oid = new_dir_tree.id;
376391
(
@@ -385,7 +400,8 @@ impl super::MonoApiService {
385400
} else {
386401
let content = file_content
387402
.ok_or_else(|| GitError::CustomError("Missing file content".to_string()))?;
388-
let blob = Blob::from_content(content);
403+
let blob = Blob::from_content_with_kind(kind, content)
404+
.map_err(|e| GitError::CustomError(e.to_string()))?;
389405
let entry_oid = blob.id;
390406
(
391407
TreeItem {
@@ -400,7 +416,7 @@ impl super::MonoApiService {
400416

401417
target_items.push(new_item);
402418
target_items.sort_by(|a, b| a.name.cmp(&b.name));
403-
let target_tree = Tree::from_tree_items(target_items).unwrap();
419+
let target_tree = Tree::from_tree_items_with_kind(kind, target_items).unwrap();
404420
save_trees.push(target_tree.clone());
405421

406422
// Build update instructions for parent trees and refs.
@@ -433,8 +449,8 @@ impl super::MonoApiService {
433449
// Create .gitkeep blob and an initial tree for the new
434450
// directory leaf. This represents the directory's own
435451
// tree object which will be nested under new parent trees.
436-
let (blob, tree_item) = gitkeep_placeholder();
437-
let new_dir_tree = Tree::from_tree_items(vec![tree_item]).unwrap();
452+
let (blob, tree_item) = super::gitkeep_placeholder_for_kind(kind)?;
453+
let new_dir_tree = Tree::from_tree_items_with_kind(kind, vec![tree_item]).unwrap();
438454
save_trees.push(new_dir_tree.clone());
439455
let entry_oid = new_dir_tree.id;
440456
(
@@ -449,7 +465,8 @@ impl super::MonoApiService {
449465
} else {
450466
let content = file_content
451467
.ok_or_else(|| GitError::CustomError("Missing file content".to_string()))?;
452-
let blob = Blob::from_content(content);
468+
let blob = Blob::from_content_with_kind(kind, content)
469+
.map_err(|e| GitError::CustomError(e.to_string()))?;
453470
let entry_oid = blob.id;
454471
(
455472
TreeItem {
@@ -462,7 +479,7 @@ impl super::MonoApiService {
462479
)
463480
};
464481

465-
let mut current_tree = Tree::from_tree_items(vec![leaf_item]).unwrap();
482+
let mut current_tree = Tree::from_tree_items_with_kind(kind, vec![leaf_item]).unwrap();
466483
save_trees.push(current_tree.clone());
467484

468485
// Wrap the leaf tree with trees for each missing parent segment.
@@ -477,7 +494,7 @@ impl super::MonoApiService {
477494
name: part.to_string(),
478495
};
479496

480-
current_tree = Tree::from_tree_items(vec![sub_item]).unwrap();
497+
current_tree = Tree::from_tree_items_with_kind(kind, vec![sub_item]).unwrap();
481498
save_trees.push(current_tree.clone());
482499
}
483500

@@ -506,7 +523,7 @@ impl super::MonoApiService {
506523

507524
target_items.push(top_item);
508525
target_items.sort_by(|a, b| a.name.cmp(&b.name));
509-
let target_tree = Tree::from_tree_items(target_items).unwrap();
526+
let target_tree = Tree::from_tree_items_with_kind(kind, target_items).unwrap();
510527
save_trees.push(target_tree.clone());
511528

512529
// After constructing the nested trees, build update instructions
@@ -886,25 +903,35 @@ impl super::MonoApiService {
886903
tagger_info: String,
887904
message: Option<String>,
888905
) -> Result<(String, String), GitError> {
906+
let tag_kind = self
907+
.storage
908+
.config()
909+
.monorepo
910+
.object_hash_kind()
911+
.map_err(|e| GitError::CustomError(e.to_string()))?;
889912
let tag_target = target
890913
.as_ref()
891914
.ok_or(GitError::InvalidCommitObject)
892915
.and_then(|t| {
893-
ObjectHash::from_hex_for_kind(get_hash_kind(), t)
916+
ObjectHash::from_hex_for_kind(tag_kind, t)
894917
.map_err(|_| GitError::InvalidCommitObject)
895918
})?;
896919
let tagger_sig = git_internal::internal::object::signature::Signature::new(
897920
git_internal::internal::object::signature::SignatureType::Tagger,
898921
tagger_info.clone(),
899922
String::new(),
900923
);
901-
let git_internal_tag = git_internal::internal::object::tag::Tag::new(
924+
// `Tag::new` hashes through the thread-local kind, which would mint a
925+
// SHA-1 tag id pointing at a SHA-256 commit.
926+
let git_internal_tag = git_internal::internal::object::tag::Tag::new_with_kind(
927+
tag_kind,
902928
tag_target,
903929
git_internal::internal::object::types::ObjectType::Commit,
904930
name.clone(),
905931
tagger_sig,
906932
message.clone().unwrap_or_default(),
907-
);
933+
)
934+
.map_err(|e| GitError::CustomError(e.to_string()))?;
908935
Ok((
909936
git_internal_tag.id.to_string(),
910937
target.unwrap_or_else(|| "HEAD".to_string()),

‎src/ceres/api_service/mono_api_service/handler.rs‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,18 @@ impl ApiHandler for super::MonoApiService {
257257
.await;
258258
let outcome = committed?;
259259
if entry_info.is_directory && target_follows_landed_commit(&self.storage) {
260-
bind_if_still_landed(&self.storage, &entry_path).await?;
260+
let bind_kind = self
261+
.storage
262+
.config()
263+
.monorepo
264+
.object_hash_kind()
265+
.map_err(|e| GitError::CustomError(e.to_string()))?;
266+
crate::jupiter::service::trace_target_service::bind_if_still_landed_with_kind(
267+
&self.storage.mono_storage(),
268+
&entry_path,
269+
bind_kind,
270+
)
271+
.await?;
261272
}
262273
Ok(CreateEntryResult {
263274
commit_id: outcome.commit_id,
@@ -506,7 +517,13 @@ impl ApiHandler for super::MonoApiService {
506517
.ok_or_else(|| GitError::CustomError("Empty update chain".to_string()))?;
507518
ancestors.push((dest_parent_path.clone(), dest_chain));
508519
if src_items.is_empty() {
509-
let (keep, item) = gitkeep_placeholder();
520+
let move_kind = self
521+
.storage
522+
.config()
523+
.monorepo
524+
.object_hash_kind()
525+
.map_err(|e| GitError::CustomError(e.to_string()))?;
526+
let (keep, item) = super::gitkeep_placeholder_for_kind(move_kind)?;
510527
src_items.push(item);
511528
blobs.push(keep);
512529
}
@@ -530,7 +547,13 @@ impl ApiHandler for super::MonoApiService {
530547
sort_git_tree_items(&mut dest_items);
531548
edits.push((dest_parent_path, dest_items));
532549

533-
let update_result = Self::rewrite_parent_chains(ancestors, edits)?;
550+
let rewrite_kind = self
551+
.storage
552+
.config()
553+
.monorepo
554+
.object_hash_kind()
555+
.map_err(|e| GitError::CustomError(e.to_string()))?;
556+
let update_result = Self::rewrite_parent_chains(ancestors, edits, rewrite_kind)?;
534557
let landing = common_parent_path(&src_parent, &dest_parent);
535558
let moved_ids = if entry_info.is_directory && target_follows_landed_commit(&self.storage) {
536559
move_native_directory(&self.storage, &src_full, &dest_full).await?

0 commit comments

Comments
 (0)