diff --git a/.spec/knowledge/standards/repository-architecture.md b/.spec/knowledge/standards/repository-architecture.md index 7864028..51d211d 100644 --- a/.spec/knowledge/standards/repository-architecture.md +++ b/.spec/knowledge/standards/repository-architecture.md @@ -28,4 +28,5 @@ metadata: - 逐 Entity/逐 Voxel/逐包 FFI 必须先证明批处理不足;不得把跨边界调用开销扩散到上层。 - 结果必须可诊断、可取消、可重复;不能以“调用方自行保证”替代契约,panic 必须在 ABI 边界转换为稳定错误。 - ABI/Capability/Error Schema、ID 与 Fixture 只在架构源维护;本仓消费已发布 Baseline,不复制生成器或第二套 Schema。 +- Root ABI 消费机制(ADR-040 §7):上游发布物字节级镜像在 [`docs/architecture/abi/`](../../../docs/architecture/abi/README.md)(钉 revision + `.baseline.sha256` 钉 Hash),Rust 侧数值经 `cargo xtask gen-contracts` 从镜像生成,测试与镜像互证;ErrorCode 数值权威只有 `ids/index.json`,Capability bit(D-015)与非 `linux-x86_64-glibc` 布局(D-016)保持不绑定。 - 性能改动记录吞吐、p95/p99、分配、峰值内存、硬件/构建配置和结果确定性范围。 diff --git a/crates/lumio-contract-types/src/generated.rs b/crates/lumio-contract-types/src/generated.rs index a10c183..4c21be0 100644 --- a/crates/lumio-contract-types/src/generated.rs +++ b/crates/lumio-contract-types/src/generated.rs @@ -1,54 +1,202 @@ //! Architecture-source generated-contract adapter. //! -//! The architecture source has published baseline id `LGE-V1.4-2026-08-27` and, -//! under ADR-040, the Root ABI bundle at `origin/main:packages/abi/` — this -//! repository is a registered consumer of that bundle (`rootAbi.consumers`) and -//! binds its C Header directly. It is deliberately NOT a consumer of the Rust / -//! C# generated packages. +//! Binds the published Root ABI bundle (`origin/main:packages/abi/`, recorded +//! by ADR-040 — Draft, but the artifacts themselves are published) for which +//! this repository holds `rootAbi.consumers` standing. The values below are +//! transcriptions of the byte-pinned mirror under `docs/architecture/abi/` +//! (pinned revision in its README, hashes in `.baseline.sha256`); the crate's +//! integration tests re-read the mirror and reject any drift, so nothing here +//! is invented. Per ADR-040 §7 this repository consumes the C Header plus the +//! four indices, never the Rust/C# generated packages. //! -//! Binding is not done yet, so this module is still the internal seam only: -//! opaque newtypes, no public numeric registries, no copied schemas. What the -//! bundle publishes (handle / buffer / status layout, ABI version) is bindable; -//! ErrorCode, Capability bits and Operation ids are still unpublished for this -//! repository's needs. See `layout.rs` for the layout-profile caveat. +//! Still deliberately unbound (treated as absent, not inferred): +//! - `capability_bits` semantics and any bit position (D-015 pending); +//! - any layout profile other than `linux-x86_64-glibc` (D-016 pending); +//! - an `OperationId` namespace (does not exist; identity is the published +//! (`apiTable[].name`, `slots[].slotIndex`) pair). /// Published architecture baseline this crate binds to. pub(crate) const ARCHITECTURE_BASELINE_ID: &str = "LGE-V1.4-2026-08-27"; /// Revision recorded by this adapter. /// -/// Until a generated package exists, the seam records the published baseline id -/// rather than inventing a second schema. +/// The bundle carries the baseline id as its revision anchor; the digest +/// chain (`RootAbiBinding`) carries the byte-level identity. pub(crate) const GENERATED_CONTRACT_REVISION: &str = ARCHITECTURE_BASELINE_ID; -/// ABI package version scalar. Width and layout remain blocked. +const ROOT_ABI_BUNDLE_ID: &str = "root-abi-v1"; +const ROOT_ABI_BUNDLE_DIGEST: &str = + "03ca75361fed3ca95f8efd55af2e311ea8300b2635b590ae6d46394d58bc6a39"; +const ROOT_ABI_HEADER_DIGEST: &str = + "040451bbde5a4dec3726be5f5a7be4bb934c3f68a1ca87f9c55559cae738efc7"; +const ROOT_ABI_COMPILER_NAME: &str = "lumio-abi-compiler"; +const ROOT_ABI_COMPILER_VERSION: &str = "1.0.0"; +const ROOT_ABI_COMPILER_DIGEST: &str = + "217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290"; +const ROOT_ABI_INPUT_HASH: &str = + "696a58d0525b897b549dd1e432166ae1020835902a5984221a8e60d5d8285bb3"; +const ROOT_ABI_LAYOUT_PROFILE_ID: &str = "linux-x86_64-glibc"; +const ROOT_ABI_SYMBOL_PREFIX: &str = "lumio_"; +const ROOT_ABI_ABI_VERSION: u32 = 1; + +/// Identity record of the bound Root ABI bundle (ADR-040 §7 verification +/// obligations: bundle digest, compiler identity, input hash, layout profile). +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct RootAbiBinding { + pub baseline_id: &'static str, + pub bundle_id: &'static str, + pub bundle_digest: &'static str, + pub header_digest: &'static str, + pub compiler_name: &'static str, + pub compiler_version: &'static str, + pub compiler_digest: &'static str, + pub input_hash: &'static str, + pub layout_profile_id: &'static str, + pub symbol_prefix: &'static str, +} + +pub fn root_abi_binding() -> RootAbiBinding { + RootAbiBinding { + baseline_id: ARCHITECTURE_BASELINE_ID, + bundle_id: ROOT_ABI_BUNDLE_ID, + bundle_digest: ROOT_ABI_BUNDLE_DIGEST, + header_digest: ROOT_ABI_HEADER_DIGEST, + compiler_name: ROOT_ABI_COMPILER_NAME, + compiler_version: ROOT_ABI_COMPILER_VERSION, + compiler_digest: ROOT_ABI_COMPILER_DIGEST, + input_hash: ROOT_ABI_INPUT_HASH, + layout_profile_id: ROOT_ABI_LAYOUT_PROFILE_ID, + symbol_prefix: ROOT_ABI_SYMBOL_PREFIX, + } +} + +/// ABI package version scalar (`abi.abiVersion` of the bundle). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct AbiVersion { - _private: (), +pub struct AbiVersion(u32); + +impl AbiVersion { + pub const fn raw(self) -> u32 { + self.0 + } } -/// Architecture error-code newtype. No public numeric constants. +/// The published ABI version of the bound bundle. +pub fn abi_version() -> AbiVersion { + AbiVersion(ROOT_ABI_ABI_VERSION) +} + +/// One registered `ErrorCode` value. `ids/index.json` is the sole numeric +/// authority (ADR-040 §7); instances exist only in the generated registry +/// tables, so no caller can mint an unregistered numeric. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct ArchitectureErrorCode { - _private: (), + id: &'static str, + numeric: i32, } -/// Architecture operation-id newtype. No public numeric constants. +impl ArchitectureErrorCode { + /// Only the generated registry tables construct instances. + pub(crate) const fn new(id: &'static str, numeric: i32) -> Self { + Self { id, numeric } + } + + /// Registered id string, e.g. `"InvalidHandle"`. + pub const fn id(self) -> &'static str { + self.id + } + + /// Registered numeric; the value carried by `lumio_status_t` (ADR-040 §3). + pub const fn numeric(self) -> i32 { + self.numeric + } +} + +/// Architecture operation-id newtype. Permanently uninhabited: no +/// `OperationId` namespace exists or is reserved — the public identity of a +/// callable operation is (`apiTable[].name`, `slots[].slotIndex`) (ADR-040 +/// §7, B-ABI-004 adjudicated not-applicable). Kept only because `lumio-job`'s +/// negative gate consumes the empty iterator. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct ArchitectureOperationId { _private: (), } -/// Capability-bit newtype. No public numeric constants. +/// Capability-bit newtype. Uninhabited until D-015 lands: V1 freezes neither +/// mask-vs-count semantics nor any bit position, and the ID Registry +/// `Capability` numerics are CoreEngine package-capability enumeration +/// ordinals, not bit positions — deriving a key from either is forbidden +/// (ADR-040 §7). #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct CapabilityBits { _private: (), } -/// Generated struct size token. No public ABI sizes while the Header is blocked. +/// Byte size of a generated type or struct, as published by the bundle +/// Golden. Constructed only from generated data or measured Rust layouts. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] -pub struct StructSize { - _private: (), +pub struct StructSize(u32); + +impl StructSize { + pub(crate) const fn new(bytes: u32) -> Self { + Self(bytes) + } + + pub const fn bytes(self) -> u32 { + self.0 + } +} + +/// `lumio_status_t`: `int32_t` carrying a registered `ErrorCode` numeric; +/// `0` is success and no other value is reused (ADR-040 §3, ADR-046). +/// Constructible only as success or from a registered code, so an +/// unregistered non-zero status cannot originate in this workspace. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[repr(transparent)] +pub struct LumioStatus(i32); + +impl LumioStatus { + pub const SUCCESS: LumioStatus = LumioStatus(0); + + pub const fn from_error_code(code: ArchitectureErrorCode) -> Self { + Self(code.numeric()) + } + + pub const fn raw(self) -> i32 { + self.0 + } + + pub const fn is_success(self) -> bool { + self.0 == 0 + } +} + +/// `lumio_handle_t`: the Index+Generation+Context encoding of ADR-006 +/// (16 bytes, align 8 on the published profile). +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[repr(C)] +pub struct LumioHandle { + pub index: u32, + pub generation: u32, + pub context: u64, +} + +/// `lumio_buffer_t`: the Ptr+Len+Capacity layout of ADR-017 (24 bytes, +/// align 8). `len`/`capacity` are fixed-width `u64`, never `usize`. +#[derive(Clone, Copy, Debug)] +#[repr(C)] +pub struct LumioBuffer { + pub ptr: *mut core::ffi::c_void, + pub len: u64, + pub capacity: u64, +} + +/// `struct lumio_core_config_v1`: caller-owned opaque payload. The body is +/// not part of the Root ABI at this granularity and stays guarded by its own +/// leading `struct_size` (ADR-040 §3); it crosses the boundary by pointer +/// only, so this type is deliberately not constructible. +#[repr(C)] +pub struct LumioCoreConfigV1 { + _private: [u8; 0], } /// Generated revision does not match the expected architecture baseline. @@ -77,3 +225,16 @@ pub fn verify_generated_contract_revision_against( Err(ContractMismatch { expected, found }) } } + +/// Drift gate for the bundle bytes: an observed bundle digest that differs +/// from the bound `rootAbi.bundleDigest` is a contract drift, not a warning. +/// (CI's `sha256sum -c` proves the mirror file still hashes to the pin; the +/// crate tests prove pin == published digest == this constant.) +pub fn verify_root_abi_bundle_digest_against(found: &'static str) -> Result<(), ContractMismatch> { + let expected = ROOT_ABI_BUNDLE_DIGEST; + if found == expected { + Ok(()) + } else { + Err(ContractMismatch { expected, found }) + } +} diff --git a/crates/lumio-contract-types/src/generated_data.rs b/crates/lumio-contract-types/src/generated_data.rs new file mode 100644 index 0000000..ac96c3b --- /dev/null +++ b/crates/lumio-contract-types/src/generated_data.rs @@ -0,0 +1,107 @@ +//! @generated by `cargo xtask gen-contracts` — DO NOT EDIT BY HAND. +//! +//! Source of truth: the byte-pinned mirrors under `docs/architecture/abi/` +//! (upstream revision in that directory's README). Regenerate with +//! `cargo xtask gen-contracts` after a mirror update; commit together. + +use crate::generated::ArchitectureErrorCode; +use crate::layout::{AbiStructGolden, AbiTypeGolden}; + +pub(crate) const ABI_POINTER_BYTES: u32 = 8; +pub(crate) const ABI_MAX_ALIGNMENT: u32 = 8; + +#[rustfmt::skip] +pub(crate) const ABI_TYPE_GOLDEN: &[AbiTypeGolden] = &[ + AbiTypeGolden { name: "lumio_status_t", size: 4, align: 4 }, + AbiTypeGolden { name: "lumio_handle_t", size: 16, align: 8 }, + AbiTypeGolden { name: "lumio_buffer_t", size: 24, align: 8 }, +]; + +#[rustfmt::skip] +pub(crate) const ABI_STRUCT_GOLDEN: &[AbiStructGolden] = &[ + AbiStructGolden { name: "lumio_root_api", declared_size: 64, minimum_size: 32, members: &[ + ("abi_version", 0), + ("struct_size", 4), + ("capability_bits", 8), + ("lumio_core_api", 16), + ("lumio_voxel_api", 24), + ] }, + AbiStructGolden { name: "lumio_core_api", declared_size: 48, minimum_size: 48, members: &[ + ("version", 0), + ("struct_size", 4), + ("reserved0", 8), + ("lumio_core_init", 16), + ("lumio_core_shutdown", 24), + ("lumio_core_last_error_detail", 32), + ] }, + AbiStructGolden { name: "lumio_voxel_api", declared_size: 32, minimum_size: 32, members: &[ + ("version", 0), + ("struct_size", 4), + ("reserved0", 8), + ("lumio_voxel_world_create", 16), + ("lumio_voxel_world_destroy", 24), + ] }, +]; + +#[rustfmt::skip] +pub(crate) const ABI_TABLE_VERSIONS: &[(&str, u32)] = &[ + ("lumio_core_api", 1), + ("lumio_voxel_api", 1), +]; + +#[rustfmt::skip] +pub(crate) const ERROR_CODES: &[ArchitectureErrorCode] = &[ + ArchitectureErrorCode::new("RevisionConflict", 1001), + ArchitectureErrorCode::new("MaintenanceKick", 1002), + ArchitectureErrorCode::new("ReleaseMismatch", 1003), + ArchitectureErrorCode::new("NativeAbiMismatch", 1004), + ArchitectureErrorCode::new("StaleEpoch", 1005), + ArchitectureErrorCode::new("FencingTokenStale", 1006), + ArchitectureErrorCode::new("ManifestMalformed", 1007), + ArchitectureErrorCode::new("ManifestUnsupportedVersion", 1008), + ArchitectureErrorCode::new("ManifestDigestMismatch", 1009), + ArchitectureErrorCode::new("ArtifactMissing", 1010), + ArchitectureErrorCode::new("ArtifactDigestMismatch", 1011), + ArchitectureErrorCode::new("SignatureMissing", 1012), + ArchitectureErrorCode::new("SignatureInvalid", 1013), + ArchitectureErrorCode::new("TrustRootUnknown", 1014), + ArchitectureErrorCode::new("TrustPolicyRejected", 1015), + ArchitectureErrorCode::new("KeyRevoked", 1016), + ArchitectureErrorCode::new("EvidenceMissing", 1017), + ArchitectureErrorCode::new("EvidenceDigestMismatch", 1018), + ArchitectureErrorCode::new("TargetProfileMismatch", 1019), + ArchitectureErrorCode::new("CapabilityMissing", 1020), + ArchitectureErrorCode::new("SymbolMissing", 1021), + ArchitectureErrorCode::new("SymbolCollision", 1022), + ArchitectureErrorCode::new("PackageIdentityConflict", 1023), + ArchitectureErrorCode::new("WorkerPoolDuplicate", 1024), + ArchitectureErrorCode::new("LoaderTimeout", 1025), + ArchitectureErrorCode::new("LoaderCancelled", 1026), + ArchitectureErrorCode::new("LoaderOutOfMemory", 1027), + ArchitectureErrorCode::new("PartialLoadRolledBack", 1028), + ArchitectureErrorCode::new("InvalidHandle", 1029), + ArchitectureErrorCode::new("HandleDoubleRelease", 1030), + ArchitectureErrorCode::new("MessagePermissionDenied", 1031), + ArchitectureErrorCode::new("StaleConnectionGeneration", 1032), + ArchitectureErrorCode::new("ChunkUnavailable", 1033), + ArchitectureErrorCode::new("TargetRevisionUnavailable", 1034), + ArchitectureErrorCode::new("BudgetExceeded", 1035), + ArchitectureErrorCode::new("QueueFull", 1036), + ArchitectureErrorCode::new("CoordinateOutOfBounds", 1037), + ArchitectureErrorCode::new("DirtyChunkNotDurable", 1038), + ArchitectureErrorCode::new("SnapshotBaseMismatch", 1039), + ArchitectureErrorCode::new("SessionMismatch", 1040), + ArchitectureErrorCode::new("RoleMismatch", 1041), + ArchitectureErrorCode::new("ClaimNotGranted", 1042), + ArchitectureErrorCode::new("SessionAntiReplay", 1043), + ArchitectureErrorCode::new("InvalidArgument", 1044), + ArchitectureErrorCode::new("WrongContext", 1045), + ArchitectureErrorCode::new("BufferTooSmall", 1046), + ArchitectureErrorCode::new("CapacityExceeded", 1047), + ArchitectureErrorCode::new("Cancelled", 1048), + ArchitectureErrorCode::new("TimedOut", 1049), + ArchitectureErrorCode::new("ContextClosing", 1050), + ArchitectureErrorCode::new("ContextDestroyed", 1051), + ArchitectureErrorCode::new("PanicBoundary", 1052), + ArchitectureErrorCode::new("InternalInvariant", 1053), +]; diff --git a/crates/lumio-contract-types/src/layout.rs b/crates/lumio-contract-types/src/layout.rs index efd3800..902ac0d 100644 --- a/crates/lumio-contract-types/src/layout.rs +++ b/crates/lumio-contract-types/src/layout.rs @@ -1,18 +1,36 @@ -//! ABI layout assertions against the architecture Header / manifest. +//! ABI layout assertions against the published Root ABI bundle Golden. //! -//! The architecture source now publishes a C Header (ADR-040 Root ABI bundle, -//! `origin/main:packages/abi/lumio_core.h`), but its bundle certifies exactly -//! one `layoutProfileId` — `linux-x86_64-glibc`. Transcribing those sizes here -//! unconditionally would assert layouts on darwin / windows that the -//! architecture source has not certified, which is the same red line as -//! inventing them. Binding therefore stays deferred until the bundle carries -//! the remaining target profiles, or until this gate is target-gated. -//! -//! This gate must not invent ABI sizes; an empty table is a match. +//! The golden rows live in `generated_data.rs` (derived from the byte-pinned +//! bundle mirror by `cargo xtask gen-contracts`); this module only compares. +//! V1 publishes a Golden for exactly one layout profile — +//! `linux-x86_64-glibc` — and a consumer must not assert a layout on any +//! other target (ADR-040 §7, D-016 pending). The Rust-type comparisons are +//! therefore compile- and run-time gated to that profile; on every other +//! target `verify_layout` succeeds without asserting, and the data-vs-mirror +//! equality tests still run. use crate::generated::StructSize; +use crate::generated_data::{ABI_MAX_ALIGNMENT, ABI_POINTER_BYTES, ABI_STRUCT_GOLDEN}; + +/// Published size/alignment of one named shared POD C type. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AbiTypeGolden { + pub name: &'static str, + pub size: u32, + pub align: u32, +} -/// Layout row that does not match the generated Header / manifest. +/// Published layout of one generated struct: declared/minimum size plus the +/// byte offset of every header field, table pointer and slot pointer. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AbiStructGolden { + pub name: &'static str, + pub declared_size: u32, + pub minimum_size: u32, + pub members: &'static [(&'static str, u32)], +} + +/// Layout row that does not match the generated Header / bundle Golden. #[derive(Clone, Debug, Eq, PartialEq)] pub struct LayoutMismatch { pub struct_name: &'static str, @@ -20,19 +38,103 @@ pub struct LayoutMismatch { pub found: StructSize, } -/// Generated Header layout rows. Empty until binding is target-gated (see -/// the module docs): the published bundle certifies `linux-x86_64-glibc` only. -pub fn entries() -> &'static [(&'static str, StructSize)] { - &[] +/// Published pointer width in bytes (`layoutProfile.pointerBytes`). +pub fn pointer_bytes() -> u32 { + ABI_POINTER_BYTES +} + +/// Published maximum alignment (`layoutProfile.maxAlignment`). +pub fn max_alignment() -> u32 { + ABI_MAX_ALIGNMENT +} + +/// Named C type golden rows from the bundle's `typeMapping`. +pub fn type_entries() -> &'static [AbiTypeGolden] { + crate::generated_data::ABI_TYPE_GOLDEN +} + +/// Struct golden rows (root table plus every published API table). +pub fn struct_entries() -> &'static [AbiStructGolden] { + ABI_STRUCT_GOLDEN } -/// Verify generated struct layouts against the architecture manifest. +/// Published version of one API table (`tables[].version`). +pub fn table_version(table_name: &str) -> Option { + crate::generated_data::ABI_TABLE_VERSIONS + .iter() + .find(|(name, _)| *name == table_name) + .map(|(_, version)| *version) +} + +/// Generated Header layout rows as (name, declared size) — POD types first, +/// then structs, in published order. +pub fn entries() -> Vec<(&'static str, StructSize)> { + let mut rows: Vec<(&'static str, StructSize)> = crate::generated_data::ABI_TYPE_GOLDEN + .iter() + .map(|t| (t.name, StructSize::new(t.size))) + .collect(); + rows.extend( + ABI_STRUCT_GOLDEN + .iter() + .map(|s| (s.name, StructSize::new(s.declared_size))), + ); + rows +} + +#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] +mod certified { + //! Golden comparisons for the one published layout profile. + + use super::LayoutMismatch; + use crate::generated::StructSize; + use crate::generated::{LumioBuffer, LumioHandle, LumioStatus}; + + // A mismatch is a build failure, never a runtime discovery (ADR-040 §4). + const _: () = { + assert!(size_of::() == 4); + assert!(align_of::() == 4); + assert!(size_of::() == 16); + assert!(align_of::() == 8); + assert!(size_of::() == 24); + assert!(align_of::() == 8); + assert!(size_of::<*mut core::ffi::c_void>() == 8); + }; + + pub(super) fn verify() -> Result<(), LayoutMismatch> { + let bound: &[(&'static str, usize)] = &[ + ("lumio_status_t", size_of::()), + ("lumio_handle_t", size_of::()), + ("lumio_buffer_t", size_of::()), + ]; + for &(name, actual) in bound { + let golden = super::type_entries() + .iter() + .find(|t| t.name == name) + .unwrap_or_else(|| panic!("bundle golden missing {name}")); + if actual as u32 != golden.size { + return Err(LayoutMismatch { + struct_name: golden.name, + expected: StructSize::new(golden.size), + found: StructSize::new(actual as u32), + }); + } + } + Ok(()) + } +} + +/// Verify the bound Rust POD types against the published Golden. /// -/// With no generated Header there are no structs to check, so this succeeds -/// without inventing sizes. +/// On the certified `linux-x86_64-glibc` profile this compares every bound +/// type; on any other target it succeeds without asserting, because no other +/// Golden is published (ADR-040 §7). pub fn verify_layout() -> Result<(), LayoutMismatch> { - for &(name, expected) in entries() { - let _ = (name, expected); + #[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] + { + certified::verify() + } + #[cfg(not(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu")))] + { + Ok(()) } - Ok(()) } diff --git a/crates/lumio-contract-types/src/lib.rs b/crates/lumio-contract-types/src/lib.rs index 46447a4..b2913df 100644 --- a/crates/lumio-contract-types/src/lib.rs +++ b/crates/lumio-contract-types/src/lib.rs @@ -4,20 +4,23 @@ //! 错误码/能力位常量;不含任何行为逻辑。边界与依赖图见 //! `docs/specs/native-core-module-map.md`。 //! -//! Gate-0 只提供内部 seam 与负向 Gate。架构源已发布 baseline id -//! `LGE-V1.4-2026-08-27`,并按 ADR-040 发布了 Root ABI bundle;本仓已登记为该 -//! bundle 的 consumer,直接绑定其 C Header,**不**消费 Rust/C# 生成包。绑定本身 -//! 尚未落地:ErrorCode / Capability / Operation 数值对本仓的需求仍未发布, -//! 一律不得手写,也不得声称公共 ABI 已完成。 +//! 架构源已发布 baseline id `LGE-V1.4-2026-08-27` 与 ADR-040 Root ABI bundle; +//! 本仓登记为该 bundle 的 consumer,按 ADR-040 §7 直接绑定其 C Header 与四个 +//! 索引(字节级镜像见 `docs/architecture/abi/`),**不**消费 Rust/C# 生成包。 +//! ErrorCode 数值权威只有 `ids/index.json`(含 ADR-046 kernel band); +//! Capability bit 语义(D-015)、非 `linux-x86_64-glibc` 布局档(D-016)与 +//! OperationId(不存在,B-ABI-004 不适用)保持不绑定,一律不得手写。 #![forbid(unsafe_code)] mod generated; +mod generated_data; pub mod layout; pub mod registry; pub use generated::{ AbiVersion, ArchitectureErrorCode, ArchitectureOperationId, CapabilityBits, ContractMismatch, - StructSize, architecture_baseline_id, verify_generated_contract_revision, - verify_generated_contract_revision_against, + LumioBuffer, LumioCoreConfigV1, LumioHandle, LumioStatus, RootAbiBinding, StructSize, + abi_version, architecture_baseline_id, root_abi_binding, verify_generated_contract_revision, + verify_generated_contract_revision_against, verify_root_abi_bundle_digest_against, }; diff --git a/crates/lumio-contract-types/src/registry.rs b/crates/lumio-contract-types/src/registry.rs index d04af91..1d78afc 100644 --- a/crates/lumio-contract-types/src/registry.rs +++ b/crates/lumio-contract-types/src/registry.rs @@ -1,21 +1,42 @@ -//! Read-only Error / Capability / Operation registry queries. +//! Read-only registry queries over the published ID Registry. //! -//! Tables stay empty until the architecture source publishes a generated -//! package. This crate must not hand-write public numeric ids. +//! `ids/index.json` is the sole numeric authority (ADR-040 §7); the table +//! consumed here is generated from its byte-pinned mirror by +//! `cargo xtask gen-contracts`, so no numeric in this crate is hand-written. +//! Only the `ErrorCode` namespace is bound: +//! +//! - `Capability` numerics are CoreEngine package-capability enumeration +//! ordinals, not bit positions; deriving any kernel capability key from +//! them is forbidden until D-015 lands, so they stay unbound here. +//! - No `OperationId` namespace exists or is reserved (B-ABI-004 adjudicated +//! not applicable): the public identity of a callable operation is the +//! published (`apiTable[].name`, `slots[].slotIndex`) pair. +//! - `MessageType` / `FaultClass` are GameRuntime-owned and outside this +//! repository's consumption surface. use crate::generated::{ArchitectureErrorCode, ArchitectureOperationId, CapabilityBits}; +use crate::generated_data::ERROR_CODES; -/// Architecture error codes from the generated registry. +/// Architecture error codes from the generated registry, in published order +/// (includes the ADR-046 kernel status band). pub fn error_codes() -> impl Iterator { - core::iter::empty() + ERROR_CODES.iter().copied() +} + +/// Look up one registered error code by its published id string. +pub fn error_code(id: &str) -> Option { + ERROR_CODES.iter().copied().find(|code| code.id() == id) } -/// Architecture operation ids from the generated registry. +/// Architecture operation ids. Permanently empty: the namespace does not +/// exist and none is reserved (see module docs); kept for `lumio-job`'s +/// non-overlap negative gate. pub fn operation_ids() -> impl Iterator { core::iter::empty() } -/// Capability bit entries from the generated registry. +/// Capability bit entries. Empty until D-015 freezes the `capability_bits` +/// semantics and bit assignment (see module docs). pub fn capability_bits() -> impl Iterator { core::iter::empty() } diff --git a/crates/lumio-contract-types/tests/common/mod.rs b/crates/lumio-contract-types/tests/common/mod.rs new file mode 100644 index 0000000..5a4b1eb --- /dev/null +++ b/crates/lumio-contract-types/tests/common/mod.rs @@ -0,0 +1,218 @@ +//! Test-only JSON reader for the vendored Root ABI mirror files +//! (`docs/architecture/abi/`, see its README for the pinned revision). +//! +//! Strict recursive descent over the machine-generated mirrors; any +//! malformed byte panics the test. This is deliberately not a public or +//! reusable parser — production code must never parse the mirrors at +//! runtime, the crate binds generated constants instead. + +#![allow(dead_code)] + +use std::path::PathBuf; + +#[derive(Clone, Debug, PartialEq)] +pub enum Json { + Null, + Bool(bool), + Num(f64), + Str(String), + Arr(Vec), + Obj(Vec<(String, Json)>), +} + +impl Json { + pub fn get(&self, key: &str) -> &Json { + match self { + Json::Obj(pairs) => pairs + .iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v) + .unwrap_or_else(|| panic!("missing key `{key}`")), + other => panic!("get(`{key}`) on non-object {other:?}"), + } + } + + pub fn as_str(&self) -> &str { + match self { + Json::Str(s) => s, + other => panic!("expected string, got {other:?}"), + } + } + + pub fn as_i64(&self) -> i64 { + match self { + Json::Num(n) => { + let v = *n as i64; + assert!((v as f64 - n).abs() < f64::EPSILON, "non-integer {n}"); + v + } + other => panic!("expected number, got {other:?}"), + } + } + + pub fn as_arr(&self) -> &[Json] { + match self { + Json::Arr(items) => items, + other => panic!("expected array, got {other:?}"), + } + } +} + +pub fn mirror_path(file_name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../docs/architecture/abi") + .join(file_name) +} + +pub fn parse_mirror(file_name: &str) -> Json { + let path = mirror_path(file_name); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("read mirror {}: {e}", path.display())); + parse(&text) +} + +pub fn parse(text: &str) -> Json { + let bytes = text.as_bytes(); + let mut pos = 0usize; + let value = parse_value(bytes, &mut pos); + skip_ws(bytes, &mut pos); + assert_eq!(pos, bytes.len(), "trailing bytes after JSON document"); + value +} + +fn skip_ws(b: &[u8], pos: &mut usize) { + while *pos < b.len() && matches!(b[*pos], b' ' | b'\t' | b'\n' | b'\r') { + *pos += 1; + } +} + +fn expect(b: &[u8], pos: &mut usize, byte: u8) { + assert!( + *pos < b.len() && b[*pos] == byte, + "expected `{}` at byte {}", + byte as char, + *pos + ); + *pos += 1; +} + +fn parse_value(b: &[u8], pos: &mut usize) -> Json { + skip_ws(b, pos); + assert!(*pos < b.len(), "unexpected end of JSON"); + match b[*pos] { + b'{' => parse_obj(b, pos), + b'[' => parse_arr(b, pos), + b'"' => Json::Str(parse_string(b, pos)), + b't' => parse_lit(b, pos, "true", Json::Bool(true)), + b'f' => parse_lit(b, pos, "false", Json::Bool(false)), + b'n' => parse_lit(b, pos, "null", Json::Null), + _ => parse_num(b, pos), + } +} + +fn parse_lit(b: &[u8], pos: &mut usize, lit: &str, value: Json) -> Json { + assert!( + b[*pos..].starts_with(lit.as_bytes()), + "bad literal at byte {}", + *pos + ); + *pos += lit.len(); + value +} + +fn parse_obj(b: &[u8], pos: &mut usize) -> Json { + expect(b, pos, b'{'); + let mut pairs = Vec::new(); + skip_ws(b, pos); + if *pos < b.len() && b[*pos] == b'}' { + *pos += 1; + return Json::Obj(pairs); + } + loop { + skip_ws(b, pos); + let key = parse_string(b, pos); + skip_ws(b, pos); + expect(b, pos, b':'); + pairs.push((key, parse_value(b, pos))); + skip_ws(b, pos); + match b.get(*pos) { + Some(b',') => *pos += 1, + Some(b'}') => { + *pos += 1; + return Json::Obj(pairs); + } + other => panic!("expected `,` or `}}`, got {other:?} at byte {}", *pos), + } + } +} + +fn parse_arr(b: &[u8], pos: &mut usize) -> Json { + expect(b, pos, b'['); + let mut items = Vec::new(); + skip_ws(b, pos); + if *pos < b.len() && b[*pos] == b']' { + *pos += 1; + return Json::Arr(items); + } + loop { + items.push(parse_value(b, pos)); + skip_ws(b, pos); + match b.get(*pos) { + Some(b',') => *pos += 1, + Some(b']') => { + *pos += 1; + return Json::Arr(items); + } + other => panic!("expected `,` or `]`, got {other:?} at byte {}", *pos), + } + } +} + +fn parse_string(b: &[u8], pos: &mut usize) -> String { + expect(b, pos, b'"'); + let mut out = String::new(); + loop { + assert!(*pos < b.len(), "unterminated string"); + match b[*pos] { + b'"' => { + *pos += 1; + return out; + } + b'\\' => { + *pos += 1; + match b.get(*pos) { + Some(b'"') => out.push('"'), + Some(b'\\') => out.push('\\'), + Some(b'/') => out.push('/'), + Some(b'n') => out.push('\n'), + Some(b't') => out.push('\t'), + Some(b'r') => out.push('\r'), + // The mirrors carry no other escapes; fail loud if one appears. + other => panic!("unsupported escape {other:?} at byte {}", *pos), + } + *pos += 1; + } + _ => { + let ch = std::str::from_utf8(&b[*pos..]) + .expect("utf-8 mirror") + .chars() + .next() + .expect("non-empty"); + out.push(ch); + *pos += ch.len_utf8(); + } + } + } +} + +fn parse_num(b: &[u8], pos: &mut usize) -> Json { + let start = *pos; + while *pos < b.len() && matches!(b[*pos], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9') { + *pos += 1; + } + let text = std::str::from_utf8(&b[start..*pos]).expect("utf-8 number"); + Json::Num( + text.parse() + .unwrap_or_else(|e| panic!("bad number `{text}`: {e}")), + ) +} diff --git a/crates/lumio-contract-types/tests/generated_contract_revision_is_readable.rs b/crates/lumio-contract-types/tests/generated_contract_revision_is_readable.rs index 90e7455..a8f3ed3 100644 --- a/crates/lumio-contract-types/tests/generated_contract_revision_is_readable.rs +++ b/crates/lumio-contract-types/tests/generated_contract_revision_is_readable.rs @@ -1,6 +1,9 @@ +mod common; + use lumio_contract_types::{ - AbiVersion, ArchitectureErrorCode, ArchitectureOperationId, CapabilityBits, StructSize, - architecture_baseline_id, verify_generated_contract_revision, + AbiVersion, ArchitectureErrorCode, ArchitectureOperationId, CapabilityBits, LumioBuffer, + LumioCoreConfigV1, LumioHandle, LumioStatus, StructSize, abi_version, architecture_baseline_id, + root_abi_binding, verify_generated_contract_revision, }; #[test] @@ -22,6 +25,10 @@ fn generated_contract_revision_is_readable() { core::any::type_name::(), core::any::type_name::(), core::any::type_name::(), + core::any::type_name::(), + core::any::type_name::(), + core::any::type_name::(), + core::any::type_name::(), ] { assert!( name.starts_with("lumio_contract_types::"), @@ -29,3 +36,96 @@ fn generated_contract_revision_is_readable() { ); } } + +/// The bound identity constants must equal the published mirror, field by +/// field — the adapter binds, it never invents (ADR-040 §7). +#[test] +fn bound_identity_matches_published_bundle_mirror() { + let bundle = common::parse_mirror("root-abi-bundle.json"); + let binding = root_abi_binding(); + + assert_eq!(binding.baseline_id, bundle.get("baselineId").as_str()); + assert_eq!(binding.bundle_id, bundle.get("bundleId").as_str()); + let compiler = bundle.get("compiler"); + assert_eq!(binding.compiler_name, compiler.get("name").as_str()); + assert_eq!(binding.compiler_version, compiler.get("version").as_str()); + assert_eq!(binding.compiler_digest, compiler.get("digest").as_str()); + assert_eq!(binding.input_hash, bundle.get("inputHash").as_str()); + assert_eq!( + binding.layout_profile_id, + bundle.get("layoutProfile").get("targetProfileId").as_str() + ); + + let abi = bundle.get("abi"); + assert_eq!( + i64::from(abi_version().raw()), + abi.get("abiVersion").as_i64() + ); + assert_eq!(binding.symbol_prefix, abi.get("symbolPrefix").as_str()); +} + +/// The recorded bundle digest must match what the package inventory +/// publishes for this bundle, and this repository must hold consumer +/// standing in `rootAbi.consumers` (ADR-040 §5/§7). +#[test] +fn bundle_digest_and_consumer_standing_match_packages_index_mirror() { + let packages = common::parse_mirror("packages-index.json"); + let root_abi = packages.get("rootAbi"); + let binding = root_abi_binding(); + + assert_eq!(binding.bundle_digest, root_abi.get("bundleDigest").as_str()); + assert_eq!( + binding.compiler_digest, + root_abi.get("compiler").get("digest").as_str() + ); + assert_eq!(binding.input_hash, root_abi.get("inputHash").as_str()); + assert_eq!( + binding.layout_profile_id, + root_abi.get("layoutProfileId").as_str() + ); + + let consumers: Vec<&str> = root_abi + .get("consumers") + .as_arr() + .iter() + .map(|c| c.as_str()) + .collect(); + assert!( + consumers.contains(&"LumioNativeCore"), + "this repository must be a registered rootAbi consumer, got {consumers:?}" + ); + + let header = root_abi + .get("outputFiles") + .as_arr() + .iter() + .find(|f| f.get("role").as_str() == "CHeader") + .expect("published CHeader output"); + assert_eq!(binding.header_digest, header.get("digest").as_str()); +} + +/// The `.baseline.sha256` pin for the mirrored bundle file must equal the +/// published `rootAbi.bundleDigest`: CI's `sha256sum -c` proves file bytes +/// match the pin, this test proves the pin matches the publication. +#[test] +fn baseline_pin_for_bundle_mirror_equals_published_digest() { + let pin_path = common::mirror_path("../.baseline.sha256"); + let pin_body = std::fs::read_to_string(&pin_path) + .unwrap_or_else(|e| panic!("read {}: {e}", pin_path.display())); + let binding = root_abi_binding(); + + let mut pinned = None; + for line in pin_body.lines() { + let mut parts = line.split_whitespace(); + if let (Some(hex), Some(rel)) = (parts.next(), parts.next()) + && rel == "docs/architecture/abi/root-abi-bundle.json" + { + pinned = Some(hex.to_ascii_lowercase()); + } + } + assert_eq!( + pinned.as_deref(), + Some(binding.bundle_digest), + ".baseline.sha256 pin for root-abi-bundle.json must equal the published bundleDigest" + ); +} diff --git a/crates/lumio-contract-types/tests/generated_layout_matches_manifest.rs b/crates/lumio-contract-types/tests/generated_layout_matches_manifest.rs index 79d3533..408044c 100644 --- a/crates/lumio-contract-types/tests/generated_layout_matches_manifest.rs +++ b/crates/lumio-contract-types/tests/generated_layout_matches_manifest.rs @@ -1,12 +1,170 @@ +mod common; + use lumio_contract_types::layout; +/// The generated golden rows must equal the published bundle mirror field by +/// field — the data is derived, never invented, and any drift fails here. #[test] fn generated_layout_matches_manifest() { - layout::verify_layout().expect("an empty layout table has no structs to check"); + layout::verify_layout().expect("bound Rust layouts must match the published Golden"); + + let bundle = common::parse_mirror("root-abi-bundle.json"); + let profile = bundle.get("layoutProfile"); + assert_eq!( + i64::from(layout::pointer_bytes()), + profile.get("pointerBytes").as_i64() + ); + assert_eq!( + i64::from(layout::max_alignment()), + profile.get("maxAlignment").as_i64() + ); + assert_eq!( + i64::from(layout::pointer_bytes()) * 8, + bundle.get("abi").get("pointerWidth").as_i64() + ); + + // typeMapping:镜像中每个 lumio_ 前缀命名 C 类型都必须出现在 golden 中, + // 且 size/align 一致;golden 不得多出镜像没有的行。 + let mut mirror_named: Vec<(&str, i64, i64)> = Vec::new(); + for row in bundle.get("typeMapping").as_arr() { + let c_name = row.get("c").as_str(); + if !c_name.starts_with("lumio_") || c_name.contains('*') { + continue; + } + let size = row.get("size").as_i64(); + let align = row.get("align").as_i64(); + if let Some(existing) = mirror_named.iter().find(|(n, _, _)| *n == c_name) { + assert_eq!( + (existing.1, existing.2), + (size, align), + "mirror typeMapping rows disagree for {c_name}" + ); + } else { + mirror_named.push((c_name, size, align)); + } + } + let golden_types = layout::type_entries(); + assert_eq!(golden_types.len(), mirror_named.len()); + for (name, size, align) in &mirror_named { + let g = golden_types + .iter() + .find(|t| t.name == *name) + .unwrap_or_else(|| panic!("golden missing type {name}")); + assert_eq!(i64::from(g.size), *size, "size mismatch for {name}"); + assert_eq!(i64::from(g.align), *align, "align mismatch for {name}"); + } + + // root + tables:declared/minimum 尺寸与每个成员偏移逐项一致。 + let golden_structs = layout::struct_entries(); + let root = bundle.get("root"); + check_struct( + golden_structs, + "lumio_root_api", + root, + &["fields", "tables"], + ); + let tables = bundle.get("tables").as_arr(); assert_eq!( - layout::entries().len(), - 0, - "must not assert ABI struct sizes beyond the one layout profile the \ - architecture bundle certifies (linux-x86_64-glibc)" + golden_structs.len(), + tables.len() + 1, + "golden must carry the root plus every published table" ); + for table in tables { + check_struct( + golden_structs, + table.get("name").as_str(), + table, + &["fields", "slots"], + ); + } + + // entries():POD 行在前、struct 行在后,尺寸取自 golden。 + let entries = layout::entries(); + assert_eq!(entries.len(), golden_types.len() + golden_structs.len()); + for t in golden_types { + assert!( + entries + .iter() + .any(|(name, size)| *name == t.name && size.bytes() == t.size) + ); + } + for s in golden_structs { + assert!( + entries + .iter() + .any(|(name, size)| *name == s.name && size.bytes() == s.declared_size) + ); + } +} + +fn check_struct( + golden: &[layout::AbiStructGolden], + name: &str, + mirror: &common::Json, + member_keys: &[&str], +) { + let g = golden + .iter() + .find(|s| s.name == name) + .unwrap_or_else(|| panic!("golden missing struct {name}")); + assert_eq!( + i64::from(g.declared_size), + mirror.get("declaredStructSize").as_i64(), + "declared size mismatch for {name}" + ); + assert_eq!( + i64::from(g.minimum_size), + mirror.get("minimumStructSize").as_i64(), + "minimum size mismatch for {name}" + ); + let mut expected: Vec<(String, i64)> = Vec::new(); + for key in member_keys { + for row in mirror.get(key).as_arr() { + expected.push(( + row.get("name").as_str().to_string(), + row.get("offset").as_i64(), + )); + } + } + let actual: Vec<(String, i64)> = g + .members + .iter() + .map(|(n, off)| ((*n).to_string(), i64::from(*off))) + .collect(); + assert_eq!(actual, expected, "member offsets mismatch for {name}"); +} + +/// Golden comparisons of the bound Rust types run only on the certified +/// profile; other targets publish no Golden to compare against (ADR-040 §7). +#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] +#[test] +fn bound_rust_types_match_golden_on_certified_profile() { + use lumio_contract_types::{LumioBuffer, LumioHandle, LumioStatus}; + + let sizes: &[(&str, usize, usize)] = &[ + ( + "lumio_status_t", + size_of::(), + align_of::(), + ), + ( + "lumio_handle_t", + size_of::(), + align_of::(), + ), + ( + "lumio_buffer_t", + size_of::(), + align_of::(), + ), + ]; + for &(name, size, align) in sizes { + let g = layout::type_entries() + .iter() + .find(|t| t.name == name) + .unwrap_or_else(|| panic!("golden missing {name}")); + assert_eq!(size as u32, g.size, "size mismatch for {name}"); + assert_eq!(align as u32, g.align, "align mismatch for {name}"); + } + assert_eq!(size_of::() as u32, layout::pointer_bytes()); } diff --git a/crates/lumio-contract-types/tests/registry_values_are_unique.rs b/crates/lumio-contract-types/tests/registry_values_are_unique.rs index 6ca02af..6093e81 100644 --- a/crates/lumio-contract-types/tests/registry_values_are_unique.rs +++ b/crates/lumio-contract-types/tests/registry_values_are_unique.rs @@ -1,55 +1,102 @@ +mod common; + use lumio_contract_types::registry; use lumio_contract_types::{ArchitectureErrorCode, ArchitectureOperationId, CapabilityBits}; -use std::collections::HashSet; -use std::fs; -use std::path::Path; - -fn assert_unique(items: &[T]) { - let mut seen = HashSet::new(); - for item in items { - assert!(seen.insert(item), "registry value is not unique: {item:?}"); + +/// The bound `ErrorCode` table must equal the published registry mirror +/// entry for entry (ids unique, numerics unique and status-range safe), and +/// the namespaces that stay unbound must stay empty. +#[test] +fn registry_values_are_unique() { + let ids = common::parse_mirror("ids-index.json"); + let error_ns = ids + .get("namespaces") + .as_arr() + .iter() + .find(|ns| ns.get("namespace").as_str() == "ErrorCode") + .expect("mirror ErrorCode namespace"); + assert_eq!(error_ns.get("owner").as_str(), "Architecture"); + + let mirror: Vec<(&str, i64)> = error_ns + .get("values") + .as_arr() + .iter() + .map(|v| { + assert_eq!(v.get("status").as_str(), "Active"); + (v.get("id").as_str(), v.get("numeric").as_i64()) + }) + .collect(); + + let bound: Vec = registry::error_codes().collect(); + assert_eq!( + bound.len(), + mirror.len(), + "bound table must carry every published ErrorCode value" + ); + for (code, (mirror_id, mirror_numeric)) in bound.iter().zip(&mirror) { + assert_eq!(code.id(), *mirror_id); + assert_eq!(i64::from(code.numeric()), *mirror_numeric); + // ADR-040 §3 / ADR-046:status 值域 (0, i32::MAX],0 只留给成功。 + assert!(code.numeric() > 0); } -} -fn is_public_numeric_const(line: &str) -> bool { - let trimmed = line.trim(); - if !trimmed.starts_with("pub const ") { - return false; + for (i, a) in bound.iter().enumerate() { + for b in &bound[i + 1..] { + assert_ne!(a.id(), b.id(), "duplicate ErrorCode id"); + assert_ne!(a.numeric(), b.numeric(), "duplicate ErrorCode numeric"); + } } - trimmed.contains(": u") - || trimmed.contains(": i") - || trimmed.contains(": usize") - || trimmed.contains(": isize") -} -#[test] -fn registry_values_are_unique() { - let error_codes: Vec = registry::error_codes().collect(); + for code in &bound { + assert_eq!(registry::error_code(code.id()), Some(*code)); + } + assert_eq!(registry::error_code("NotARegisteredId"), None); + + // OperationId 不存在(B-ABI-004 不适用);Capability 绑定待 D-015。 let operation_ids: Vec = registry::operation_ids().collect(); let capability_bits: Vec = registry::capability_bits().collect(); - - // Generated Error/Capability/Operation package is unpublished: empty is unique. - assert_eq!(error_codes.len(), 0); assert_eq!(operation_ids.len(), 0); assert_eq!(capability_bits.len(), 0); +} - assert_unique(&error_codes); - assert_unique(&operation_ids); - assert_unique(&capability_bits); +/// The ADR-046 kernel status band must be present through the registry — +/// numerics compared against the mirror, never hard-coded here. +#[test] +fn kernel_status_band_is_bound() { + let ids = common::parse_mirror("ids-index.json"); + let error_ns = ids + .get("namespaces") + .as_arr() + .iter() + .find(|ns| ns.get("namespace").as_str() == "ErrorCode") + .expect("mirror ErrorCode namespace"); - let src_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("src"); - for entry in fs::read_dir(&src_dir).expect("crate src") { - let path = entry.expect("src entry").path(); - if path.extension().and_then(|ext| ext.to_str()) != Some("rs") { - continue; - } - let source = fs::read_to_string(&path).expect("read crate source"); - for line in source.lines() { - assert!( - !is_public_numeric_const(line), - "{} leaks a public numeric constant while the generated package is blocked: {line}", - path.display() - ); - } + for band_id in [ + "InvalidArgument", + "WrongContext", + "BufferTooSmall", + "CapacityExceeded", + "Cancelled", + "TimedOut", + "ContextClosing", + "ContextDestroyed", + "PanicBoundary", + "InternalInvariant", + // ADR-046 §2:由既有值承担的三类。 + "InvalidHandle", + "HandleDoubleRelease", + "CapabilityMissing", + ] { + let mirror_numeric = error_ns + .get("values") + .as_arr() + .iter() + .find(|v| v.get("id").as_str() == band_id) + .unwrap_or_else(|| panic!("mirror missing ErrorCode {band_id}")) + .get("numeric") + .as_i64(); + let code = registry::error_code(band_id) + .unwrap_or_else(|| panic!("registry missing ErrorCode {band_id}")); + assert_eq!(i64::from(code.numeric()), mirror_numeric); } } diff --git a/crates/lumio-contract-types/tests/wrong_baseline_is_rejected.rs b/crates/lumio-contract-types/tests/wrong_baseline_is_rejected.rs index 55d9259..3bf7a9e 100644 --- a/crates/lumio-contract-types/tests/wrong_baseline_is_rejected.rs +++ b/crates/lumio-contract-types/tests/wrong_baseline_is_rejected.rs @@ -1,6 +1,9 @@ +mod common; + use lumio_contract_types::{ - ContractMismatch, architecture_baseline_id, verify_generated_contract_revision, - verify_generated_contract_revision_against, + ContractMismatch, architecture_baseline_id, root_abi_binding, + verify_generated_contract_revision, verify_generated_contract_revision_against, + verify_root_abi_bundle_digest_against, }; #[test] @@ -20,3 +23,45 @@ fn wrong_baseline_is_rejected() { assert_eq!(verify_generated_contract_revision(), Ok(())); assert_eq!(verify_generated_contract_revision_against(current), Ok(())); } + +/// A bundle whose digest differs from the bound `rootAbi.bundleDigest` is a +/// drift, even under the same baseline id: the baseline names the contract +/// revision, the digest names the exact published bytes. +#[test] +fn wrong_bundle_digest_is_rejected() { + let binding = root_abi_binding(); + // 上一个已发布的 bundle digest(compiler.digest 变更前),真实的历史漂移样本。 + let stale = "88321f1c3374c40ce2513d258df0f8c58661ef816ddc21811a5b0371bf3b309f"; + assert_ne!(stale, binding.bundle_digest); + + assert_eq!( + verify_root_abi_bundle_digest_against(stale), + Err(ContractMismatch { + expected: binding.bundle_digest, + found: stale, + }) + ); + assert_eq!( + verify_root_abi_bundle_digest_against(binding.bundle_digest), + Ok(()) + ); +} + +/// Every mirrored index must agree on one baseline id, and it must be the +/// baseline this adapter binds — a mixed-revision mirror set is a drift. +#[test] +fn mirror_set_agrees_on_one_baseline() { + let bound = architecture_baseline_id(); + for (file, key) in [ + ("root-abi-bundle.json", "baselineId"), + ("packages-index.json", "baselineId"), + ("ids-index.json", "baselineId"), + ] { + let mirror = common::parse_mirror(file); + assert_eq!( + mirror.get(key).as_str(), + bound, + "{file} baselineId must match the bound baseline" + ); + } +} diff --git a/crates/lumio-kernel/src/error/mapping.rs b/crates/lumio-kernel/src/error/mapping.rs index a6b9f6b..d28991d 100644 --- a/crates/lumio-kernel/src/error/mapping.rs +++ b/crates/lumio-kernel/src/error/mapping.rs @@ -1,34 +1,45 @@ -//! Architecture ErrorCode mapping seam. +//! Architecture ErrorCode mapping — the single category→code conversion. //! -//! The generated registry is empty and `ArchitectureErrorCode` has no public -//! constructor, so every category is `Err(MappingBlocked)`. That is the -//! blocked-ABI seam; public numeric mapping is not complete. +//! ADR-046 (Draft; values published in `ids/index.json` on `origin/main`) +//! allocates the kernel status band 1044–1053 and adjudicates three +//! categories onto existing values (1020/1029/1030), so every frozen +//! `ErrorCategory` now maps to a registered numeric. Codes are resolved +//! through the generated registry by id string — no numeric is written here, +//! and an unregistered non-zero status cannot originate from this mapping +//! (ADR-046 §4). +//! +//! `AlreadyReleased` maps to `HandleDoubleRelease` (1030), the §2 release-path +//! ruling. Known gap: the arena also reports empty-slot hits on *use* paths +//! as `AlreadyReleased`, which §2 expects to surface as `InvalidHandle` +//! (1029); resolving that is a handle-module check-order question outside +//! this mapping (tracked in the R-00079 delivery notes). -use lumio_contract_types::ArchitectureErrorCode; +use lumio_contract_types::{ArchitectureErrorCode, registry}; use super::{ErrorCategory, KernelError}; -/// Architecture ErrorCode values are unpublished; mapping cannot complete. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct MappingBlocked; +/// Panics only if the generated registry lost a registered id — that is a +/// broken build of the generated tables, not a runtime condition. +fn registered(id: &str) -> ArchitectureErrorCode { + registry::error_code(id).unwrap_or_else(|| panic!("generated ids registry is missing `{id}`")) +} -/// Maps a kernel error to an architecture ErrorCode, or the blocked seam. -pub fn to_architecture_error_code( - error: &KernelError, -) -> Result { - match error.category() { - ErrorCategory::InvalidArgument => Err(MappingBlocked), - ErrorCategory::InvalidHandle => Err(MappingBlocked), - ErrorCategory::WrongContext => Err(MappingBlocked), - ErrorCategory::AlreadyReleased => Err(MappingBlocked), - ErrorCategory::BufferTooSmall => Err(MappingBlocked), - ErrorCategory::CapacityExceeded => Err(MappingBlocked), - ErrorCategory::CapabilityUnavailable => Err(MappingBlocked), - ErrorCategory::Cancelled => Err(MappingBlocked), - ErrorCategory::TimedOut => Err(MappingBlocked), - ErrorCategory::ContextClosing => Err(MappingBlocked), - ErrorCategory::ContextDestroyed => Err(MappingBlocked), - ErrorCategory::PanicBoundary => Err(MappingBlocked), - ErrorCategory::InternalInvariant => Err(MappingBlocked), - } +/// Maps a kernel error to its registered architecture ErrorCode. +pub fn to_architecture_error_code(error: &KernelError) -> ArchitectureErrorCode { + let id = match error.category() { + ErrorCategory::InvalidArgument => "InvalidArgument", + ErrorCategory::InvalidHandle => "InvalidHandle", + ErrorCategory::WrongContext => "WrongContext", + ErrorCategory::AlreadyReleased => "HandleDoubleRelease", + ErrorCategory::BufferTooSmall => "BufferTooSmall", + ErrorCategory::CapacityExceeded => "CapacityExceeded", + ErrorCategory::CapabilityUnavailable => "CapabilityMissing", + ErrorCategory::Cancelled => "Cancelled", + ErrorCategory::TimedOut => "TimedOut", + ErrorCategory::ContextClosing => "ContextClosing", + ErrorCategory::ContextDestroyed => "ContextDestroyed", + ErrorCategory::PanicBoundary => "PanicBoundary", + ErrorCategory::InternalInvariant => "InternalInvariant", + }; + registered(id) } diff --git a/crates/lumio-kernel/src/error/mod.rs b/crates/lumio-kernel/src/error/mod.rs index a6f9b69..14b4d19 100644 --- a/crates/lumio-kernel/src/error/mod.rs +++ b/crates/lumio-kernel/src/error/mod.rs @@ -4,7 +4,7 @@ mod category; mod mapping; pub use category::{ErrorCategory, ErrorDetail, KernelError}; -pub use mapping::{MappingBlocked, to_architecture_error_code}; +pub use mapping::to_architecture_error_code; pub type KernelResult = Result; diff --git a/crates/lumio-kernel/tests/error_contract.rs b/crates/lumio-kernel/tests/error_contract.rs index 7cf89f2..655fbae 100644 --- a/crates/lumio-kernel/tests/error_contract.rs +++ b/crates/lumio-kernel/tests/error_contract.rs @@ -1,21 +1,35 @@ //! T-error-04 / R-00082: error hot path does not heap-allocate. //! //! `#[global_allocator]` is test-binary-only (this integration target). +//! Counting is gated by a const-initialized thread-local window: a global +//! count also sees libtest-harness allocations from other threads, which +//! race into the window under machine load and made the test flaky. The +//! const initializer keeps the TLS access allocation-free, so the gate +//! itself cannot recurse into the hook. use std::alloc::{GlobalAlloc, Layout, System}; +use std::cell::Cell; use std::sync::atomic::{AtomicUsize, Ordering}; -use lumio_kernel::error::{ - ErrorCategory, ErrorDetail, KernelError, MappingBlocked, to_architecture_error_code, -}; +use lumio_kernel::error::{ErrorCategory, ErrorDetail, KernelError, to_architecture_error_code}; struct CountingAllocator; static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0); +thread_local! { + static COUNT_THIS_THREAD: Cell = const { Cell::new(false) }; +} + +fn counting_here() -> bool { + COUNT_THIS_THREAD.try_with(Cell::get).unwrap_or(false) +} + unsafe impl GlobalAlloc for CountingAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { - ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + if counting_here() { + ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + } unsafe { System.alloc(layout) } } @@ -24,12 +38,16 @@ unsafe impl GlobalAlloc for CountingAllocator { } unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 { - ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + if counting_here() { + ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + } unsafe { System.alloc_zeroed(layout) } } unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 { - ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + if counting_here() { + ALLOC_COUNT.fetch_add(1, Ordering::SeqCst); + } unsafe { System.realloc(ptr, layout, new_size) } } } @@ -55,6 +73,7 @@ fn assert_detail_has_no_string(detail: &ErrorDetail) { #[test] fn error_hot_path_does_not_allocate() { ALLOC_COUNT.store(0, Ordering::SeqCst); + COUNT_THIS_THREAD.with(|flag| flag.set(true)); let none = KernelError::new(ErrorCategory::Cancelled, ErrorDetail::None); let too_small = KernelError::buffer_too_small(64, 8); @@ -79,9 +98,26 @@ fn error_hot_path_does_not_allocate() { let static_category = static_msg.category(); let static_detail = static_msg.detail(); + // 映射也在热路径上:一并纳入零分配窗口。 + let none_code = to_architecture_error_code(&none); + let too_small_code = to_architecture_error_code(&too_small); + let limit_code = to_architecture_error_code(&limit); + let static_code = to_architecture_error_code(&static_msg); + + COUNT_THIS_THREAD.with(|flag| flag.set(false)); let allocs = ALLOC_COUNT.load(Ordering::SeqCst); assert_eq!(allocs, 0, "error hot path heap-allocated {allocs} time(s)"); + // 自证:门内的真实分配必须被计数,防止门控把测试弄成永真。 + COUNT_THIS_THREAD.with(|flag| flag.set(true)); + let boxed = Box::new(0u64); + COUNT_THIS_THREAD.with(|flag| flag.set(false)); + assert!( + ALLOC_COUNT.load(Ordering::SeqCst) >= 1, + "counting hook must observe a real allocation on this thread" + ); + drop(boxed); + assert_eq!(none_category, ErrorCategory::Cancelled); assert_eq!(too_small_category, ErrorCategory::BufferTooSmall); assert_eq!(limit_category, ErrorCategory::CapacityExceeded); @@ -115,8 +151,11 @@ fn error_hot_path_does_not_allocate() { other => panic!("unexpected detail: {other:?}"), } - assert_eq!(to_architecture_error_code(&none), Err(MappingBlocked)); - assert_eq!(to_architecture_error_code(&too_small), Err(MappingBlocked)); - assert_eq!(to_architecture_error_code(&limit), Err(MappingBlocked)); - assert_eq!(to_architecture_error_code(&static_msg), Err(MappingBlocked)); + assert_eq!(none_code.id(), "Cancelled"); + assert_eq!(too_small_code.id(), "BufferTooSmall"); + assert_eq!(limit_code.id(), "CapacityExceeded"); + assert_eq!(static_code.id(), "InternalInvariant"); + for code in [none_code, too_small_code, limit_code, static_code] { + assert!(code.numeric() > 0, "0 is reserved for success"); + } } diff --git a/crates/lumio-kernel/tests/mapping_is_total_for_all_categories.rs b/crates/lumio-kernel/tests/mapping_is_total_for_all_categories.rs index f37b8f6..9239fb3 100644 --- a/crates/lumio-kernel/tests/mapping_is_total_for_all_categories.rs +++ b/crates/lumio-kernel/tests/mapping_is_total_for_all_categories.rs @@ -1,36 +1,54 @@ -//! T-error-03 / R-00079: mapping covers every ErrorCategory and stays blocked. +//! T-error-03 / R-00079: every frozen `ErrorCategory` maps to a registered +//! architecture ErrorCode (ADR-046 kernel band plus 1020/1029/1030). +//! +//! Numerics are never written here: expectations go through the generated +//! registry, whose values the contract-types tests verify against the +//! byte-pinned `ids/index.json` mirror. -use lumio_kernel::error::{ - ErrorCategory, ErrorDetail, KernelError, MappingBlocked, to_architecture_error_code, -}; +use lumio_contract_types::registry; +use lumio_kernel::error::{ErrorCategory, ErrorDetail, KernelError, to_architecture_error_code}; #[test] fn mapping_is_total_for_all_categories() { - assert!( - lumio_contract_types::registry::error_codes() - .next() - .is_none() - ); - - let categories = [ - ErrorCategory::InvalidArgument, - ErrorCategory::InvalidHandle, - ErrorCategory::WrongContext, - ErrorCategory::AlreadyReleased, - ErrorCategory::BufferTooSmall, - ErrorCategory::CapacityExceeded, - ErrorCategory::CapabilityUnavailable, - ErrorCategory::Cancelled, - ErrorCategory::TimedOut, - ErrorCategory::ContextClosing, - ErrorCategory::ContextDestroyed, - ErrorCategory::PanicBoundary, - ErrorCategory::InternalInvariant, + // (kernel category, adjudicated registry id) — ADR-046 §1–§2. + let expected = [ + (ErrorCategory::InvalidArgument, "InvalidArgument"), + (ErrorCategory::InvalidHandle, "InvalidHandle"), + (ErrorCategory::WrongContext, "WrongContext"), + // §2 release-path ruling; the use-path nuance is a known gap + // recorded on the card, not silently resolved here. + (ErrorCategory::AlreadyReleased, "HandleDoubleRelease"), + (ErrorCategory::BufferTooSmall, "BufferTooSmall"), + (ErrorCategory::CapacityExceeded, "CapacityExceeded"), + (ErrorCategory::CapabilityUnavailable, "CapabilityMissing"), + (ErrorCategory::Cancelled, "Cancelled"), + (ErrorCategory::TimedOut, "TimedOut"), + (ErrorCategory::ContextClosing, "ContextClosing"), + (ErrorCategory::ContextDestroyed, "ContextDestroyed"), + (ErrorCategory::PanicBoundary, "PanicBoundary"), + (ErrorCategory::InternalInvariant, "InternalInvariant"), ]; - for category in categories { + let mut mapped = Vec::new(); + for (category, registry_id) in expected { let err = KernelError::new(category, ErrorDetail::None); assert_eq!(err.category(), category); - assert_eq!(to_architecture_error_code(&err), Err(MappingBlocked)); + + let code = to_architecture_error_code(&err); + assert_eq!(code.id(), registry_id, "wrong mapping for {category:?}"); + assert_eq!( + Some(code), + registry::error_code(registry_id), + "mapped code must be the registered instance for {registry_id}" + ); + assert!(code.numeric() > 0, "0 is reserved for success"); + mapped.push(code); + } + + // 13 类映射到 13 个互不相同的注册值(单射)。 + for (i, a) in mapped.iter().enumerate() { + for b in &mapped[i + 1..] { + assert_ne!(a.numeric(), b.numeric(), "mapping must stay injective"); + } } } diff --git a/crates/lumio-native-ffi/src/boundary.rs b/crates/lumio-native-ffi/src/boundary.rs index beadc59..02614c9 100644 --- a/crates/lumio-native-ffi/src/boundary.rs +++ b/crates/lumio-native-ffi/src/boundary.rs @@ -1,10 +1,10 @@ //! Unified FFI panic/error boundary. //! -//! `to_architecture_error_code` currently returns `Err(MappingBlocked)` for -//! every category, including `PanicBoundary` (T-error-03). Public numeric -//! Panic ErrorCode is blocked, so this seam returns `KernelError` rather than -//! `ArchitectureErrorCode`. After mapping is unblocked, FFI exports can call -//! `to_architecture_error_code` at the C ABI. +//! `to_architecture_error_code` is total since ADR-046 published the kernel +//! status band (a caught panic maps to the registered `PanicBoundary` code). +//! This seam still returns `KernelError` so internal callers keep the +//! category and bounded detail; C exports convert to `LumioStatus` via the +//! single mapping at the ABI edge. use lumio_kernel::error::{ErrorCategory, ErrorDetail, KernelError}; diff --git a/crates/lumio-native-ffi/src/exports.rs b/crates/lumio-native-ffi/src/exports.rs index e93fb96..210849e 100644 --- a/crates/lumio-native-ffi/src/exports.rs +++ b/crates/lumio-native-ffi/src/exports.rs @@ -1,34 +1,114 @@ -//! Blocked-header FFI smoke helper. +//! Provider `lumio_core_api` table, bound to the published Root ABI Header. //! -//! The architecture source publishes the Root ABI C Header (ADR-040), but the -//! entry symbol it declares belongs to CoreEngine root-abi, not to this -//! repository (ADR 0001; enforced by `cargo xtask dump-symbols` and by the -//! source-text guard in this module's tests — do not spell that symbol here). -//! The provider symbol list this crate would export is still unpublished -//! (T-ffi-04). -//! This module composes existing Rust seams; it is not a C ABI surface. -//! Do not add `#[no_mangle]` or `extern "C"` names here. - -use lumio_kernel::error::KernelError; +//! The generated Header (`docs/architecture/abi/lumio_core.h`, ADR-040) is +//! the source of the table layout and slot signatures transcribed here; the +//! layout tests compare against the bundle Golden through +//! `lumio_contract_types::layout`. Per ADR-006 the entry symbol belongs to +//! CoreEngine `root-abi` — this crate assembles the provider table as a Rust +//! value and exports **no** C symbol (enforced by `cargo xtask dump-symbols` +//! and the source-text guard in this module's tests; do not spell that +//! symbol here). Still blocked, recorded on R-00179: +//! +//! - how CoreEngine obtains this table (provider composition contract / +//! symbol list is unpublished), so nothing is `#[no_mangle]`; +//! - `lumio_core_init`: the `lumio_core_config_v1` body is opaque by +//! contract and the published slot returns the context handle through a +//! by-value `out_context` parameter, which cannot carry a result — raised +//! upstream; the slot stays unpopulated rather than invented. + +use core::ffi::c_void; + +use lumio_contract_types::{LumioBuffer, LumioCoreConfigV1, LumioHandle, LumioStatus, layout}; +use lumio_kernel::error::{ErrorCategory, ErrorDetail, KernelError, to_architecture_error_code}; use lumio_kernel::handle::{ContextKey, HandleKey}; use crate::boundary::ffi_boundary; use crate::handles::decode_handle_for_context; +/// `lumio_core_api` with the published layout (48 bytes on the certified +/// profile: header fields at 0/4/8, slots at 16/24/32, one reserved pointer +/// word). Field names and slot signatures follow the generated Header +/// verbatim; `Option` has the guaranteed nullable-pointer +/// representation, so an unpopulated slot is a null function pointer. +#[repr(C)] +pub struct LumioCoreApi { + pub version: u32, + pub struct_size: u32, + pub reserved0: u64, + pub lumio_core_init: + Option LumioStatus>, + pub lumio_core_shutdown: Option LumioStatus>, + pub lumio_core_last_error_detail: + Option LumioStatus>, + pub reserved: [*mut c_void; 1], +} + +fn status_of(result: Result<(), KernelError>) -> LumioStatus { + match result { + Ok(()) => LumioStatus::SUCCESS, + Err(error) => LumioStatus::from_error_code(to_architecture_error_code(&error)), + } +} + +/// With `lumio_core_init` unpopulated no context can exist, so every handle +/// fails as the registered `InvalidHandle` code — through the panic boundary +/// and the single mapping, never as an invented numeric. +extern "C" fn core_shutdown(_context: LumioHandle) -> LumioStatus { + status_of(ffi_boundary(|| { + Err(KernelError::new( + ErrorCategory::InvalidHandle, + ErrorDetail::None, + )) + })) +} + +/// Same degenerate state as [`core_shutdown`]: the context cannot exist, so +/// the buffer is left untouched and the registered `InvalidHandle` returns. +extern "C" fn core_last_error_detail( + _context: LumioHandle, + _out_detail: LumioBuffer, +) -> LumioStatus { + status_of(ffi_boundary(|| { + Err(KernelError::new( + ErrorCategory::InvalidHandle, + ErrorDetail::None, + )) + })) +} + +/// Assemble the provider table with published header fields (version and +/// struct_size come from the bundle Golden, not literals). +pub fn provider_core_api_table() -> LumioCoreApi { + let golden = layout::struct_entries() + .iter() + .find(|s| s.name == "lumio_core_api") + .expect("bundle golden carries lumio_core_api"); + LumioCoreApi { + version: layout::table_version("lumio_core_api").expect("published table version"), + struct_size: golden.declared_size, + reserved0: 0, + lumio_core_init: None, + lumio_core_shutdown: Some(core_shutdown), + lumio_core_last_error_detail: Some(core_last_error_detail), + reserved: [core::ptr::null_mut(); 1], + } +} + /// Decode `key` for `expected` inside the FFI panic/error boundary. /// -/// Wrong-context handles return `ErrorCategory::WrongContext`. Mapping that -/// category to a public architecture ErrorCode remains `MappingBlocked`. +/// Wrong-context handles return `ErrorCategory::WrongContext`, which maps to +/// the registered `WrongContext` ErrorCode (ADR-046). pub fn smoke_decode_handle(key: HandleKey, expected: ContextKey) -> Result<(), KernelError> { ffi_boundary(move || decode_handle_for_context(key, expected).map(|_| ())) } #[cfg(test)] mod tests { - use super::smoke_decode_handle; + use super::{provider_core_api_table, smoke_decode_handle}; use crate::boundary::ffi_boundary; use crate::handles::decode_handle_for_context; - use lumio_kernel::error::{ErrorCategory, MappingBlocked, to_architecture_error_code}; + use lumio_contract_types::{LumioHandle, layout, registry}; + use lumio_kernel::error::{ErrorCategory, to_architecture_error_code}; use lumio_kernel::handle::{ContextKey, Generation, HandleKey, SlotIndex}; fn wrong_context_key() -> HandleKey { @@ -51,7 +131,7 @@ mod tests { assert_eq!(err.category(), ErrorCategory::WrongContext); assert_ne!(err.category(), ErrorCategory::InvalidHandle); assert_ne!(err.category(), ErrorCategory::AlreadyReleased); - assert_eq!(to_architecture_error_code(&err), Err(MappingBlocked)); + assert_eq!(to_architecture_error_code(&err).id(), "WrongContext"); let via_boundary = match ffi_boundary(|| decode_handle_for_context(key, expected).map(|_| ())) { @@ -62,11 +142,42 @@ mod tests { assert_eq!(via_boundary.category(), err.category()); assert_eq!( to_architecture_error_code(&via_boundary), - Err(MappingBlocked) + to_architecture_error_code(&err) ); assert!(smoke_decode_handle(key, ContextKey::new(1)).is_ok()); + // 经槽位函数指针的 C ABI 调用:init 未发布即无 context,任何 handle + // 都必须返回注册的 InvalidHandle numeric(经注册表取值,不写字面量)。 + let table = provider_core_api_table(); + assert!(table.lumio_core_init.is_none(), "init stays blocked"); + let shutdown = table.lumio_core_shutdown.expect("shutdown slot populated"); + let status = shutdown(LumioHandle { + index: 7, + generation: 3, + context: 999, + }); + let invalid = registry::error_code("InvalidHandle").expect("registered InvalidHandle"); + assert_eq!(status.raw(), invalid.numeric()); + assert!(!status.is_success()); + + let last_error = table + .lumio_core_last_error_detail + .expect("last_error_detail slot populated"); + let status = last_error( + LumioHandle { + index: 0, + generation: 1, + context: 1, + }, + lumio_contract_types::LumioBuffer { + ptr: core::ptr::null_mut(), + len: 0, + capacity: 0, + }, + ); + assert_eq!(status.raw(), invalid.numeric()); + let exports_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src/exports.rs"); let exports_src = std::fs::read_to_string(&exports_path) .unwrap_or_else(|e| panic!("read {}: {e}", exports_path.display())); @@ -75,4 +186,60 @@ mod tests { "exports.rs must not mention the Root API symbol" ); } + + /// Table header fields carry the published values (data vs golden; no + /// platform layout claim). + #[test] + fn provider_table_header_fields_match_golden() { + let table = provider_core_api_table(); + let golden = layout::struct_entries() + .iter() + .find(|s| s.name == "lumio_core_api") + .expect("golden lumio_core_api"); + assert_eq!(table.struct_size, golden.declared_size); + assert_eq!(Some(table.version), layout::table_version("lumio_core_api")); + assert_eq!(table.reserved0, 0); + } + + /// Rust-side table layout equals the bundle Golden — asserted only on + /// the one certified profile (ADR-040 §7, D-016 pending). + #[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] + #[test] + fn provider_table_layout_matches_golden_on_certified_profile() { + use super::LumioCoreApi; + + let golden = layout::struct_entries() + .iter() + .find(|s| s.name == "lumio_core_api") + .expect("golden lumio_core_api"); + assert_eq!(size_of::() as u32, golden.declared_size); + let offsets: &[(&str, usize)] = &[ + ("version", core::mem::offset_of!(LumioCoreApi, version)), + ( + "struct_size", + core::mem::offset_of!(LumioCoreApi, struct_size), + ), + ("reserved0", core::mem::offset_of!(LumioCoreApi, reserved0)), + ( + "lumio_core_init", + core::mem::offset_of!(LumioCoreApi, lumio_core_init), + ), + ( + "lumio_core_shutdown", + core::mem::offset_of!(LumioCoreApi, lumio_core_shutdown), + ), + ( + "lumio_core_last_error_detail", + core::mem::offset_of!(LumioCoreApi, lumio_core_last_error_detail), + ), + ]; + for &(name, actual) in offsets { + let (_, expected) = golden + .members + .iter() + .find(|(member, _)| *member == name) + .unwrap_or_else(|| panic!("golden member {name}")); + assert_eq!(actual as u32, *expected, "offset mismatch for {name}"); + } + } } diff --git a/crates/lumio-native-ffi/src/lib.rs b/crates/lumio-native-ffi/src/lib.rs index 87a64d4..78cf7b9 100644 --- a/crates/lumio-native-ffi/src/lib.rs +++ b/crates/lumio-native-ffi/src/lib.rs @@ -16,7 +16,9 @@ mod handles; pub use handles::decode_handle_for_context; mod exports; -pub use exports::smoke_decode_handle; +pub use exports::{LumioCoreApi, provider_core_api_table, smoke_decode_handle}; mod symbol_guard; -pub use symbol_guard::{crate_sources_contain_root_symbol, forbidden_root_symbol_name}; +pub use symbol_guard::{ + crate_sources_contain_root_symbol, forbidden_root_symbol_name, mirror_entry_symbol, +}; diff --git a/crates/lumio-native-ffi/src/symbol_guard.rs b/crates/lumio-native-ffi/src/symbol_guard.rs index b57a93c..5747869 100644 --- a/crates/lumio-native-ffi/src/symbol_guard.rs +++ b/crates/lumio-native-ffi/src/symbol_guard.rs @@ -9,10 +9,39 @@ use std::fs; use std::path::{Path, PathBuf}; /// Cross-crate Root symbol owned by CoreEngine `root-abi` (ADR 0001). +/// +/// The published bundle's `entrySymbol` is the authority; the tests assert +/// this hardcoded copy equals [`mirror_entry_symbol`] so neither can drift. pub fn forbidden_root_symbol_name() -> &'static str { "lumio_core_get_api_v1" } +/// `abi.entrySymbol` extracted textually from the mirrored bundle +/// (`docs/architecture/abi/root-abi-bundle.json`), keeping this guard free +/// of crate dependencies. +pub fn mirror_entry_symbol() -> String { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../docs/architecture/abi/root-abi-bundle.json"); + let text = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + // 镜像可能是 minified 或 pretty 格式:定位键名后跳过 `:` 与空白再取值。 + let key = "\"entrySymbol\""; + let after_key = text + .find(key) + .unwrap_or_else(|| panic!("{} has no entrySymbol field", path.display())) + + key.len(); + let rest = text[after_key..] + .trim_start() + .strip_prefix(':') + .map(str::trim_start); + let Some(value) = rest.and_then(|r| r.strip_prefix('"')) else { + panic!("{} entrySymbol is not a string", path.display()); + }; + let end = value + .find('"') + .unwrap_or_else(|| panic!("{} entrySymbol unterminated", path.display())); + value[..end].to_string() +} + /// True when any `src/**/*.rs` file uses the Root symbol as a Rust identifier. /// /// Comments and string literals do not count, so naming the forbidden symbol @@ -165,6 +194,13 @@ fn strip_comments_and_strings(src: &str) -> String { out.push(' '); continue; } + // 字符/字节字面量(含引号内容如 `'"'` / `b'\''`)会让字符串识别失步, + // 必须整体跳过;`'ident`(无闭合引号)是生命周期,原样保留。 + if let Some(end) = char_literal_end(b, i) { + i = end; + out.push(' '); + continue; + } let ch = src[i..].chars().next().expect("utf-8"); out.push(ch); i += ch.len_utf8(); @@ -172,12 +208,111 @@ fn strip_comments_and_strings(src: &str) -> String { out } +/// `Some(end)` when `b[i..]` starts a char/byte-char literal (optionally +/// `b`-prefixed); `None` for lifetimes and everything else. +fn char_literal_end(b: &[u8], i: usize) -> Option { + let mut j = i; + if b[j] == b'b' && j + 1 < b.len() && b[j + 1] == b'\'' { + j += 1; + } + if b[j] != b'\'' { + return None; + } + let content = j + 1; + if content >= b.len() { + return None; + } + if b[content] == b'\\' { + // 转义字面量:从转义序列后找最近的闭合引号。 + let mut k = content + 2; + while k < b.len() && b[k] != b'\'' { + k += 1; + } + return (k < b.len()).then_some(k + 1); + } + // 单字符字面量 `'x'`;`'a`(无闭合)是生命周期。 + let ch_len = core::str::from_utf8(&b[content..]) + .ok()? + .chars() + .next()? + .len_utf8(); + let close = content + ch_len; + (close < b.len() && b[close] == b'\'').then_some(close + 1) +} + #[cfg(test)] mod tests { - use super::{crate_sources_contain_root_symbol, forbidden_root_symbol_name, rust_sources}; + use super::{ + contains_ident, crate_sources_contain_root_symbol, forbidden_root_symbol_name, + mirror_entry_symbol, rust_sources, strip_comments_and_strings, + }; use std::fs; use std::path::Path; + /// Regression: a quote inside a char/byte-char literal (`'"'`, `b'"'`) + /// must not desync the string stripper — a desynced scanner can both + /// miss real identifiers and report string contents as code. + #[test] + fn scanner_survives_quote_char_literals() { + let ident = forbidden_root_symbol_name(); + let in_string_only = format!("let q = b'\"'; let s = \"{ident}\"; let c = '\"';"); + assert!( + !contains_ident(&strip_comments_and_strings(&in_string_only), ident), + "string contents after a quote char literal must stay stripped" + ); + let in_code = format!("let q = '\"'; let bad = {ident};"); + assert!( + contains_ident(&strip_comments_and_strings(&in_code), ident), + "identifiers after a quote char literal must stay visible" + ); + let lifetime = "fn f<'a>(x: &'a str) -> &'a str { x }"; + assert!(!contains_ident( + &strip_comments_and_strings(lifetime), + ident + )); + } + + /// The hardcoded Root symbol must equal the published `entrySymbol`, and + /// it must sit under the published `symbolPrefix` — the gate binds the + /// mirror, it does not merely repeat a string. + #[test] + fn forbidden_symbol_matches_published_entry_symbol() { + let published = mirror_entry_symbol(); + assert_eq!(published, forbidden_root_symbol_name()); + assert!( + published.starts_with("lumio_"), + "published entry symbol must carry the published symbolPrefix" + ); + } + + /// Dependency half of the negative gate: only this crate may declare a + /// C artifact crate-type (`cdylib`/`staticlib`), so no other workspace + /// crate can grow a symbol surface. + #[test] + fn only_native_ffi_declares_a_c_artifact_crate_type() { + let crates_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join(".."); + let mut checked = 0usize; + for entry in fs::read_dir(&crates_dir).expect("read crates/") { + let dir = entry.expect("crates entry").path(); + let manifest = dir.join("Cargo.toml"); + if !manifest.is_file() { + continue; + } + checked += 1; + let text = fs::read_to_string(&manifest) + .unwrap_or_else(|e| panic!("read {}: {e}", manifest.display())); + let declares_c_artifact = text.contains("cdylib") || text.contains("staticlib"); + let is_ffi = dir.file_name().and_then(|n| n.to_str()) == Some("lumio-native-ffi"); + assert_eq!( + declares_c_artifact, + is_ffi, + "{} crate-type violates the single-symbol-surface rule", + manifest.display() + ); + } + assert!(checked >= 2, "expected multiple crates under crates/"); + } + #[test] fn root_symbol_is_absent() { let name = forbidden_root_symbol_name(); diff --git a/crates/lumio-native-ffi/tests/c_smoke_invalid_handle_returns_stable_code.rs b/crates/lumio-native-ffi/tests/c_smoke_invalid_handle_returns_stable_code.rs index 216acee..6cda675 100644 --- a/crates/lumio-native-ffi/tests/c_smoke_invalid_handle_returns_stable_code.rs +++ b/crates/lumio-native-ffi/tests/c_smoke_invalid_handle_returns_stable_code.rs @@ -1,4 +1,6 @@ -//! T-ffi-04 / R-00179: blocked-header handle smoke returns WrongContext. +//! T-ffi-04 / R-00179: provider-table C smoke — registered status codes +//! through the published slot signatures, plus a standalone compile of the +//! mirrored generated Header (its static asserts are the layout Golden). //! //! `lumio-native-ffi` is `cdylib`+`staticlib` only, so Cargo does not pass //! `--extern lumio_native_ffi` (no rlib) to integration tests on this host. @@ -17,7 +19,7 @@ mod handles; use boundary::ffi_boundary; use exports::smoke_decode_handle; use handles::decode_handle_for_context; -use lumio_kernel::error::{ErrorCategory, MappingBlocked, to_architecture_error_code}; +use lumio_kernel::error::{ErrorCategory, to_architecture_error_code}; use lumio_kernel::handle::{ContextKey, Generation, HandleKey, SlotIndex}; fn wrong_context_key() -> HandleKey { @@ -40,7 +42,7 @@ fn c_smoke_invalid_handle_returns_stable_code() { assert_eq!(err.category(), ErrorCategory::WrongContext); assert_ne!(err.category(), ErrorCategory::InvalidHandle); assert_ne!(err.category(), ErrorCategory::AlreadyReleased); - assert_eq!(to_architecture_error_code(&err), Err(MappingBlocked)); + assert_eq!(to_architecture_error_code(&err).id(), "WrongContext"); let via_boundary = match ffi_boundary(|| decode_handle_for_context(key, expected).map(|_| ())) { Err(e) => e, @@ -50,7 +52,7 @@ fn c_smoke_invalid_handle_returns_stable_code() { assert_eq!(via_boundary.category(), err.category()); assert_eq!( to_architecture_error_code(&via_boundary), - Err(MappingBlocked) + to_architecture_error_code(&err) ); assert!(smoke_decode_handle(key, ContextKey::new(1)).is_ok()); @@ -62,4 +64,64 @@ fn c_smoke_invalid_handle_returns_stable_code() { !exports_src.contains("lumio_core_get_api_v1"), "exports.rs must not mention the Root API symbol" ); + + // 经 provider 表函数指针的 C ABI 调用:无 context 存在,任何 handle 都返回 + // 注册的 InvalidHandle numeric(经注册表取值,不写字面量)。 + let table = exports::provider_core_api_table(); + assert!(table.lumio_core_init.is_none(), "init stays blocked"); + let shutdown = table.lumio_core_shutdown.expect("shutdown slot populated"); + let status = shutdown(lumio_contract_types::LumioHandle { + index: 7, + generation: 3, + context: 999, + }); + let invalid = lumio_contract_types::registry::error_code("InvalidHandle") + .expect("registered InvalidHandle"); + assert_eq!(status.raw(), invalid.numeric()); + assert!(!status.is_success()); +} + +/// C smoke: the mirrored generated Header must compile standalone — its +/// `LUMIO_STATIC_ASSERT` rows are the layout Golden, so a successful compile +/// is the C-side layout check. Skips (with a log line) when the host has no +/// C compiler, mirroring the `nm` host-gap precedent in `symbol_guard`. +#[test] +fn c_header_compile_smoke_asserts_published_layout() { + use std::process::Command; + + let header_dir = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/architecture/abi"); + assert!( + header_dir.join("lumio_core.h").is_file(), + "mirrored lumio_core.h must exist" + ); + + let compiler = ["cc", "gcc", "clang"].into_iter().find(|c| { + Command::new(c) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + }); + let Some(compiler) = compiler else { + eprintln!("skip: no C compiler (cc/gcc/clang) on this host"); + return; + }; + + let tu = std::path::Path::new(env!("CARGO_TARGET_TMPDIR")).join("lumio_core_smoke.c"); + std::fs::write(&tu, "#include \"lumio_core.h\"\nint lumio_smoke_unused;\n") + .unwrap_or_else(|e| panic!("write {}: {e}", tu.display())); + + let out = Command::new(compiler) + .arg("-fsyntax-only") + .arg("-I") + .arg(&header_dir) + .arg(&tu) + .output() + .unwrap_or_else(|e| panic!("run {compiler}: {e}")); + assert!( + out.status.success(), + "{compiler} rejected the published Header (layout Golden failed?):\n{}", + String::from_utf8_lossy(&out.stderr) + ); } diff --git a/docs/architecture/.baseline.sha256 b/docs/architecture/.baseline.sha256 index 44b5304..f035929 100644 --- a/docs/architecture/.baseline.sha256 +++ b/docs/architecture/.baseline.sha256 @@ -1 +1,5 @@ f1d36acf33a1f5e8326a9e58d609fcf7d9fa85177f9b5b60bb3f4742c1afebd0 docs/architecture/LumioGameEngine_Architecture_v1.4.md +515c1626163d935a85fabf04afa743d38b4ba0fb49a7bf38602d2155f4f097f1 docs/architecture/abi/ids-index.json +040451bbde5a4dec3726be5f5a7be4bb934c3f68a1ca87f9c55559cae738efc7 docs/architecture/abi/lumio_core.h +d33c2c90bee1318cb0829c567259593f9b1fd388bc7691604fee521929780fbb docs/architecture/abi/packages-index.json +03ca75361fed3ca95f8efd55af2e311ea8300b2635b590ae6d46394d58bc6a39 docs/architecture/abi/root-abi-bundle.json diff --git a/docs/architecture/abi/README.md b/docs/architecture/abi/README.md new file mode 100644 index 0000000..19387c5 --- /dev/null +++ b/docs/architecture/abi/README.md @@ -0,0 +1,36 @@ +# Root ABI 发布物只读镜像 + +本目录镜像 `LumioGameEngineArchitecture` 已发布的 Root ABI 消费面(ADR-040 Root ABI Generated Bundle;该 ADR 当前状态为 **Draft**,但发布物本身已在上游 `origin/main`)。本仓在 `packages/index.json` 的 `rootAbi.consumers` 中登记为消费方,按 ADR-040 §7 只消费 C Header 与索引,不消费 Rust/C# 生成包。 + +## 钉住的上游 revision(钉 revision,不钉分支名) + +- 上游仓库:`LumioGameEngineArchitecture`(`https://github.com/LumioGames/LumioGameEngineArchitecture.git`) +- 镜像 revision:`origin/main` 提交 `1f2ead332b3dfc3042e1495bfbe6febb8699df7e`(含内容提交 `5c222c4`,2026-08-28) +- 架构基线:`LGE-V1.4-2026-08-27` + +## 文件清单与上游路径 + +| 本目录文件 | 上游路径 | 完整性保证 | +| --- | --- | --- | +| `lumio_core.h` | `packages/abi/lumio_core.h` | bundle `outputFiles[].digest`(自校验)+ `.baseline.sha256` | +| `root-abi-bundle.json` | `packages/abi/root-abi-bundle.json` | `packages/index.json` 的 `rootAbi.bundleDigest`(自校验)+ `.baseline.sha256` | +| `ids-index.json` | `ids/index.json` | V1 无 per-file digest;钉住的镜像 revision 对象身份 + `.baseline.sha256` | +| `packages-index.json` | `packages/index.json` | 同上 | + +`.baseline.sha256`(`docs/architecture/.baseline.sha256`)钉住上表四个镜像文件的 SHA-256(本 README 是本仓维护的说明文档,不入 pin),由 Repository Policy CI 的 `sha256sum -c` 与 `cargo xtask check-baseline` 共同校验;`root-abi-bundle.json` 与 `lumio_core.h` 另有上游自发布 digest 交叉校验(见 `crates/lumio-contract-types` 测试)。 + +## 消费纪律(ADR-040 §7) + +- 数值权威只有 `ids/index.json`;生成包只发布 id 字符串,从生成包读 ordinal 等于读未发布之物。 +- V1 布局 Golden 只发布 `linux-x86_64-glibc` 一档,其余平台布局不得断言(D-016 待裁决)。 +- `capability_bits` 是掩码还是计数、以及任何 bit 位指派,V1 均未冻结;`Capability` 命名空间 numeric 是枚举序号,不是 bit 位(D-015 待裁决)。 +- 不存在 `OperationId` 命名空间;公共操作身份是 (`apiTable[].name`, `slots[].slotIndex`)。 +- 跨仓 Root 符号(`LUMIO_ENTRY_SYMBOL`)由 CoreEngine `root-abi`/`composition` 独占导出;本仓发布物不得导出。 + +## 更新流程 + +1. 在上游仓核实目标提交已在 `origin/main`(`git branch -r --contains `)。 +2. `git show :<上游路径>` 覆盖本目录对应文件(字节级,不得手改)。 +3. 重算四个文件的 SHA-256 更新 `docs/architecture/.baseline.sha256`,并更新本文件的 revision 记录。 +4. 运行 `cargo xtask gen-contracts` 重新生成 `crates/lumio-contract-types/src/registry_data.rs`,与镜像一起提交。 +5. 跑收口门槛(workspace 测试 + clippy + `cargo xtask check-baseline`)确认绑定测试全绿。 diff --git a/docs/architecture/abi/ids-index.json b/docs/architecture/abi/ids-index.json new file mode 100644 index 0000000..4f665d6 --- /dev/null +++ b/docs/architecture/abi/ids-index.json @@ -0,0 +1,103 @@ +{ + "registryVersion": 1, + "baselineId": "LGE-V1.4-2026-08-27", + "namespaces": [ + { + "namespace": "MessageType", + "owner": "GameRuntime", + "values": [ + { "id": "Handshake", "numeric": 1, "status": "Active", "since": "V1" }, + { "id": "FullSnapshot", "numeric": 2, "status": "Active", "since": "V1" }, + { "id": "Delta", "numeric": 3, "status": "Active", "since": "V1" }, + { "id": "ResyncRequest", "numeric": 4, "status": "Active", "since": "V1" }, + { "id": "MaintenanceKick", "numeric": 5, "status": "Active", "since": "V1" }, + { "id": "BaselineAck", "numeric": 6, "status": "Active", "since": "V1" }, + { "id": "DeltaAck", "numeric": 7, "status": "Active", "since": "V1" }, + { "id": "Error", "numeric": 8, "status": "Active", "since": "V1" } + ] + }, + { + "namespace": "ErrorCode", + "owner": "Architecture", + "values": [ + { "id": "RevisionConflict", "numeric": 1001, "status": "Active", "since": "V1" }, + { "id": "MaintenanceKick", "numeric": 1002, "status": "Active", "since": "V1" }, + { "id": "ReleaseMismatch", "numeric": 1003, "status": "Active", "since": "V1" }, + { "id": "NativeAbiMismatch", "numeric": 1004, "status": "Active", "since": "V1" }, + { "id": "StaleEpoch", "numeric": 1005, "status": "Active", "since": "V1" }, + { "id": "FencingTokenStale", "numeric": 1006, "status": "Active", "since": "V1" }, + { "id": "ManifestMalformed", "numeric": 1007, "status": "Active", "since": "V1" }, + { "id": "ManifestUnsupportedVersion", "numeric": 1008, "status": "Active", "since": "V1" }, + { "id": "ManifestDigestMismatch", "numeric": 1009, "status": "Active", "since": "V1" }, + { "id": "ArtifactMissing", "numeric": 1010, "status": "Active", "since": "V1" }, + { "id": "ArtifactDigestMismatch", "numeric": 1011, "status": "Active", "since": "V1" }, + { "id": "SignatureMissing", "numeric": 1012, "status": "Active", "since": "V1" }, + { "id": "SignatureInvalid", "numeric": 1013, "status": "Active", "since": "V1" }, + { "id": "TrustRootUnknown", "numeric": 1014, "status": "Active", "since": "V1" }, + { "id": "TrustPolicyRejected", "numeric": 1015, "status": "Active", "since": "V1" }, + { "id": "KeyRevoked", "numeric": 1016, "status": "Active", "since": "V1" }, + { "id": "EvidenceMissing", "numeric": 1017, "status": "Active", "since": "V1" }, + { "id": "EvidenceDigestMismatch", "numeric": 1018, "status": "Active", "since": "V1" }, + { "id": "TargetProfileMismatch", "numeric": 1019, "status": "Active", "since": "V1" }, + { "id": "CapabilityMissing", "numeric": 1020, "status": "Active", "since": "V1" }, + { "id": "SymbolMissing", "numeric": 1021, "status": "Active", "since": "V1" }, + { "id": "SymbolCollision", "numeric": 1022, "status": "Active", "since": "V1" }, + { "id": "PackageIdentityConflict", "numeric": 1023, "status": "Active", "since": "V1" }, + { "id": "WorkerPoolDuplicate", "numeric": 1024, "status": "Active", "since": "V1" }, + { "id": "LoaderTimeout", "numeric": 1025, "status": "Active", "since": "V1" }, + { "id": "LoaderCancelled", "numeric": 1026, "status": "Active", "since": "V1" }, + { "id": "LoaderOutOfMemory", "numeric": 1027, "status": "Active", "since": "V1" }, + { "id": "PartialLoadRolledBack", "numeric": 1028, "status": "Active", "since": "V1" }, + { "id": "InvalidHandle", "numeric": 1029, "status": "Active", "since": "V1" }, + { "id": "HandleDoubleRelease", "numeric": 1030, "status": "Active", "since": "V1" }, + { "id": "MessagePermissionDenied", "numeric": 1031, "status": "Active", "since": "V1" }, + { "id": "StaleConnectionGeneration", "numeric": 1032, "status": "Active", "since": "V1" }, + { "id": "ChunkUnavailable", "numeric": 1033, "status": "Active", "since": "V1" }, + { "id": "TargetRevisionUnavailable", "numeric": 1034, "status": "Active", "since": "V1" }, + { "id": "BudgetExceeded", "numeric": 1035, "status": "Active", "since": "V1" }, + { "id": "QueueFull", "numeric": 1036, "status": "Active", "since": "V1" }, + { "id": "CoordinateOutOfBounds", "numeric": 1037, "status": "Active", "since": "V1" }, + { "id": "DirtyChunkNotDurable", "numeric": 1038, "status": "Active", "since": "V1" }, + { "id": "SnapshotBaseMismatch", "numeric": 1039, "status": "Active", "since": "V1" }, + { "id": "SessionMismatch", "numeric": 1040, "status": "Active", "since": "V1" }, + { "id": "RoleMismatch", "numeric": 1041, "status": "Active", "since": "V1" }, + { "id": "ClaimNotGranted", "numeric": 1042, "status": "Active", "since": "V1" }, + { "id": "SessionAntiReplay", "numeric": 1043, "status": "Active", "since": "V1" }, + { "id": "InvalidArgument", "numeric": 1044, "status": "Active", "since": "V1" }, + { "id": "WrongContext", "numeric": 1045, "status": "Active", "since": "V1" }, + { "id": "BufferTooSmall", "numeric": 1046, "status": "Active", "since": "V1" }, + { "id": "CapacityExceeded", "numeric": 1047, "status": "Active", "since": "V1" }, + { "id": "Cancelled", "numeric": 1048, "status": "Active", "since": "V1" }, + { "id": "TimedOut", "numeric": 1049, "status": "Active", "since": "V1" }, + { "id": "ContextClosing", "numeric": 1050, "status": "Active", "since": "V1" }, + { "id": "ContextDestroyed", "numeric": 1051, "status": "Active", "since": "V1" }, + { "id": "PanicBoundary", "numeric": 1052, "status": "Active", "since": "V1" }, + { "id": "InternalInvariant", "numeric": 1053, "status": "Active", "since": "V1" } + ] + }, + { + "namespace": "Capability", + "owner": "Architecture", + "values": [ + { "id": "Native", "numeric": 1, "status": "Active", "since": "V1" }, + { "id": "HybridCLR", "numeric": 2, "status": "Reserved", "since": "V1" }, + { "id": "ReferenceVoxel", "numeric": 3, "status": "Active", "since": "V1" }, + { "id": "VoxelSnapshot", "numeric": 4, "status": "Active", "since": "V1" }, + { "id": "VoxelStreaming", "numeric": 5, "status": "Active", "since": "V1" }, + { "id": "VoxelSpatial", "numeric": 6, "status": "Active", "since": "V1" }, + { "id": "VoxelMeshCollision", "numeric": 7, "status": "Active", "since": "V1" }, + { "id": "VoxelAllResident", "numeric": 8, "status": "Active", "since": "V1" }, + { "id": "VoxelVolatileChunks", "numeric": 9, "status": "Active", "since": "V1" } + ] + }, + { + "namespace": "FaultClass", + "owner": "GameRuntime", + "values": [ + { "id": "SessionLocalProven", "numeric": 1, "status": "Active", "since": "V1" }, + { "id": "SlotStateUnproven", "numeric": 2, "status": "Active", "since": "V1" }, + { "id": "ProcessFault", "numeric": 3, "status": "Active", "since": "V1" } + ] + } + ] +} diff --git a/docs/architecture/abi/lumio_core.h b/docs/architecture/abi/lumio_core.h new file mode 100644 index 0000000..f783427 --- /dev/null +++ b/docs/architecture/abi/lumio_core.h @@ -0,0 +1,89 @@ +/* Generated Root ABI header. Do not hand-edit. */ +/* Publisher: LumioGameEngineArchitecture / LGE-V1.4-2026-08-27. */ +/* Compiler: lumio-abi-compiler 1.0.0. ADR-040. */ +/* Layout profile: linux-x86_64-glibc (pointer 8 bytes, max align 8). */ + +#ifndef LUMIO_CORE_H +#define LUMIO_CORE_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define LUMIO_ABI_VERSION 1 +#define LUMIO_ENTRY_SYMBOL "lumio_core_get_api_v1" +#define LUMIO_SYMBOL_PREFIX "lumio_" +#define LUMIO_CAPABILITY_BITS 7u + +typedef int32_t lumio_status_t; + +typedef struct lumio_handle_t { + uint32_t index; + uint32_t generation; + uint64_t context; +} lumio_handle_t; + +typedef struct lumio_buffer_t { + void* ptr; + uint64_t len; + uint64_t capacity; +} lumio_buffer_t; + +/* Opaque caller-owned payloads; bodies are guarded by their own struct_size. */ +struct lumio_core_config_v1; +struct lumio_voxel_world_desc_v1; + +typedef struct lumio_core_api { + uint32_t version; + uint32_t struct_size; + uint64_t reserved0; + lumio_status_t (*lumio_core_init)(const struct lumio_core_config_v1* config, lumio_handle_t out_context); + lumio_status_t (*lumio_core_shutdown)(lumio_handle_t context); + lumio_status_t (*lumio_core_last_error_detail)(lumio_handle_t context, lumio_buffer_t out_detail); + void* reserved[1]; +} lumio_core_api; + +typedef struct lumio_voxel_api { + uint32_t version; + uint32_t struct_size; + uint64_t reserved0; + lumio_status_t (*lumio_voxel_world_create)(lumio_handle_t context, const struct lumio_voxel_world_desc_v1* desc, lumio_handle_t out_world); + lumio_status_t (*lumio_voxel_world_destroy)(lumio_handle_t world); +} lumio_voxel_api; + +typedef struct lumio_root_api { + uint32_t abi_version; + uint32_t struct_size; + uint64_t capability_bits; + const lumio_core_api* lumio_core_api; + const lumio_voxel_api* lumio_voxel_api; + unsigned char reserved_tail[32]; +} lumio_root_api; + +lumio_status_t lumio_core_get_api_v1(uint32_t requested_version, const lumio_root_api** out_table); + +/* Layout Golden assertions: a mismatch is a build failure, never a runtime discovery. */ +#define LUMIO_STATIC_ASSERT(cond, tag) typedef char lumio_assert_##tag[(cond) ? 1 : -1] +LUMIO_STATIC_ASSERT(sizeof(lumio_handle_t) == 16, handle_size); +LUMIO_STATIC_ASSERT(sizeof(lumio_buffer_t) == 24, buffer_size); +LUMIO_STATIC_ASSERT(sizeof(lumio_status_t) == 4, status_size); +LUMIO_STATIC_ASSERT(sizeof(void*) == 8, pointer_size); +LUMIO_STATIC_ASSERT(sizeof(lumio_core_api) == 48, lumio_core_api_size); +LUMIO_STATIC_ASSERT(offsetof(lumio_core_api, lumio_core_init) == 16, lumio_core_init_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_core_api, lumio_core_shutdown) == 24, lumio_core_shutdown_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_core_api, lumio_core_last_error_detail) == 32, lumio_core_last_error_detail_offset); +LUMIO_STATIC_ASSERT(sizeof(lumio_voxel_api) == 32, lumio_voxel_api_size); +LUMIO_STATIC_ASSERT(offsetof(lumio_voxel_api, lumio_voxel_world_create) == 16, lumio_voxel_world_create_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_voxel_api, lumio_voxel_world_destroy) == 24, lumio_voxel_world_destroy_offset); +LUMIO_STATIC_ASSERT(sizeof(lumio_root_api) == 64, root_size); +LUMIO_STATIC_ASSERT(offsetof(lumio_root_api, lumio_core_api) == 16, root_lumio_core_api_offset); +LUMIO_STATIC_ASSERT(offsetof(lumio_root_api, lumio_voxel_api) == 24, root_lumio_voxel_api_offset); + +#ifdef __cplusplus +} +#endif + +#endif /* LUMIO_CORE_H */ diff --git a/docs/architecture/abi/packages-index.json b/docs/architecture/abi/packages-index.json new file mode 100644 index 0000000..9081b8c --- /dev/null +++ b/docs/architecture/abi/packages-index.json @@ -0,0 +1 @@ +{"artifacts":[{"artifactId":"protocol-permission-validator-rust","artifactKind":"ProtocolPermissionValidator","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"fdd1bdc57b95447e09f15008069bd6c6b6eb355897f075eddb0d71770feee5e2","packagePath":"rust/lumio-gen-protocol-permission-validator/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"protocol-permission-validator-csharp","artifactKind":"ProtocolPermissionValidator","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"6bd347ff929c0446ce801bfae2b22f9fc0d65812f47c1c9e1ea8270ba03750a2","packagePath":"csharp/Lumio.Gen.ProtocolPermissionValidator/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"mapping-table-rust","artifactKind":"MappingTable","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"bd248c0442f84c5ac8c0bf18ab13f855bc610056f5be106a124ed7bed85bf56c","packagePath":"rust/lumio-gen-mapping-table/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"mapping-table-csharp","artifactKind":"MappingTable","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"c3f8e85858e2048ba31fe945f547ca68448eddbbd823c32569b47400307e0e59","packagePath":"csharp/Lumio.Gen.MappingTable/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"canonical-serializer-rust","artifactKind":"CanonicalSerializer","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"fd9720649389fd6f49f63ecac2727c98237492c8637c4f2d15a97252d7c45e50","packagePath":"rust/lumio-gen-canonical-serializer/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"canonical-serializer-csharp","artifactKind":"CanonicalSerializer","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"b16885850b80278870c09ef6bdf2dd41a5f67f54574800fbd44ea23aed409bbf","packagePath":"csharp/Lumio.Gen.CanonicalSerializer/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"language-binding-rust","artifactKind":"LanguageBinding","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"6b8e2a390e6beb9bd918ea5db58a61b7483555134b65c1dc04ee7373a0effeea","packagePath":"rust/lumio-gen-language-binding/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"language-binding-csharp","artifactKind":"LanguageBinding","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"dae2beac6c36b8715a1ebe61dc36a5082299d9525666ea3877409b1accff381d","packagePath":"csharp/Lumio.Gen.LanguageBinding/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"contract-types-rust","artifactKind":"ContractTypes","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"5c686850855bdf111c5ebf6128cd0dfba14a46dd74eaeabe3cf7ffe2cde23b34","packagePath":"rust/lumio-gen-contract-types/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"contract-types-csharp","artifactKind":"ContractTypes","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"69a2f24659520258fe4cc594c77e26a99ddac254fab6662e4cf6c58d594945c4","packagePath":"csharp/Lumio.Gen.ContractTypes/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"contract-runtime-rust","artifactKind":"ContractRuntime","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"rust","outputHash":"3f9357242b67ce513cd3e1c102f9e96d7402922ba0a04ec976ed70a60d45cc52","packagePath":"rust/lumio-gen-contract-runtime/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1},{"artifactId":"contract-runtime-csharp","artifactKind":"ContractRuntime","baselineId":"LGE-V1.4-2026-08-27","compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","consumers":["LumioClient","LumioGame","LumioGameRuntime","LumioServer"],"forbiddenDependents":["LumioClient","LumioGame"],"implementationDependencies":[],"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","language":"csharp","outputHash":"fbc3e82cb63cbca338e85ccef045dab0f7604ede3910f695fb17b9e70e23a4dd","packagePath":"csharp/Lumio.Gen.ContractRuntime/","publisher":"LumioGameEngineArchitecture","schemaEpoch":1}],"baselineId":"LGE-V1.4-2026-08-27","blocked":[{"id":"D-009","reason":"protocol-dispatch not frozen"},{"id":"D-011","reason":"Auth wire not frozen"}],"canonicalDigest":{"consumers":["LumioCoreEngine"],"digestAlgorithm":{"framing":"PrefixFreeOverCanonicalBytes","name":"SHA-256"},"formId":"CanonicalJsonV1","goldenCount":10,"profileDigest":"4bf1976f6811357bd74b922358afd6f4c1e6d68fa925ab5249139bf781ecef46","profileId":"canonical-digest-v1","profilePath":"canonical/canonical-digest-profile.json"},"compilerHash":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","evidence":{"consumers":["LumioCoreEngine"],"profileCount":3,"profileDigest":"61b635c60c5ac7bad6155fdbfd52feb91b1ac80c21924cddc333331e7fb724e3","profileId":"evidence-profile-v1","profilePath":"evidence/evidence-profile.json"},"inputHash":"4e2542521219896cd1534e8e3064fe01e8737d62df5c846cca2033f9548f37e0","loader":{"consumers":["LumioCoreEngine"],"errorPriorityLength":10,"profileDigest":"9eefca4d1a809755db9614aa1924e247f3d9f60ef942357355408a1a1bce975f","profileId":"loader-profile-v1","profilePath":"loader/loader-profile.json"},"rootAbi":{"bundleDigest":"03ca75361fed3ca95f8efd55af2e311ea8300b2635b590ae6d46394d58bc6a39","bundleId":"root-abi-v1","bundlePath":"abi/root-abi-bundle.json","compiler":{"digest":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","name":"lumio-abi-compiler","version":"1.0.0"},"consumers":["LumioCoreEngine","LumioNativeCore"],"inputHash":"696a58d0525b897b549dd1e432166ae1020835902a5984221a8e60d5d8285bb3","layoutProfileId":"linux-x86_64-glibc","outputFiles":[{"digest":"040451bbde5a4dec3726be5f5a7be4bb934c3f68a1ca87f9c55559cae738efc7","path":"abi/lumio_core.h","role":"CHeader"},{"digest":"5e81bdfb6e879d849e2cb77a847a07167e5a459f2f23fd43f07609e726043bec","path":"rust/lumio-gen-language-binding/src/root_abi.rs","role":"RustBinding"},{"digest":"d89ff35434438773055ce4108b9f04ef6ff2b42335101249163f65c734975cd1","path":"csharp/Lumio.Gen.LanguageBinding/RootAbi.cs","role":"CSharpBinding"}]},"schemaEpoch":1,"stateMachineCount":12,"stateMachineIds":["ClientReplicaSession","CoreEngineLoader","CrossWorldTxn","EcsCommandBuffer","GameplayScopeActivation","GasAbility","GasEffect","ReleasePool","SimulationSession","VoxelChunkResidency","VoxelSnapshotCapture","WorldSlotHost"],"trust":{"consumers":["LumioCoreEngine"],"profileDigest":"fa4bd14550d7e21e9e1996e21e5e157b708c08a6692754d859e13f615b4d5399","profileId":"trust-profile-v1","profilePath":"trust/trust-profile.json","signatureProfileId":"LumioSignatureV1","trustDomain":"Test","vectorCount":8}} diff --git a/docs/architecture/abi/root-abi-bundle.json b/docs/architecture/abi/root-abi-bundle.json new file mode 100644 index 0000000..1f19c86 --- /dev/null +++ b/docs/architecture/abi/root-abi-bundle.json @@ -0,0 +1 @@ +{"abi":{"abiVersion":1,"callingConvention":"C","capabilityBits":7,"endianness":"Little","entrySymbol":"lumio_core_get_api_v1","pointerWidth":64,"symbolPrefix":"lumio_"},"baselineId":"LGE-V1.4-2026-08-27","bundleId":"root-abi-v1","compiler":{"digest":"217437fd4755e1a339e2029838cc4a2d2fb305fa05520c8cfd10ea98cc2ff290","name":"lumio-abi-compiler","version":"1.0.0"},"inputHash":"696a58d0525b897b549dd1e432166ae1020835902a5984221a8e60d5d8285bb3","inputSet":["schemas/native-managed-abi.schema.json","fixtures/valid/native-managed-abi.json"],"layoutProfile":{"abiRuntime":"glibc","arch":"x86_64","maxAlignment":8,"os":"LinuxServer","pointerBytes":8,"rootHeaderBytes":16,"tableHeaderBytes":16,"targetProfileId":"linux-x86_64-glibc"},"outputFiles":[{"digest":"040451bbde5a4dec3726be5f5a7be4bb934c3f68a1ca87f9c55559cae738efc7","path":"abi/lumio_core.h","role":"CHeader"},{"digest":"5e81bdfb6e879d849e2cb77a847a07167e5a459f2f23fd43f07609e726043bec","path":"rust/lumio-gen-language-binding/src/root_abi.rs","role":"RustBinding"},{"digest":"d89ff35434438773055ce4108b9f04ef6ff2b42335101249163f65c734975cd1","path":"csharp/Lumio.Gen.LanguageBinding/RootAbi.cs","role":"CSharpBinding"}],"root":{"declaredStructSize":64,"fields":[{"c":"uint32_t","csharp":"uint","name":"abi_version","offset":0,"rust":"u32","size":4},{"c":"uint32_t","csharp":"uint","name":"struct_size","offset":4,"rust":"u32","size":4},{"c":"uint64_t","csharp":"ulong","name":"capability_bits","offset":8,"rust":"u64","size":8}],"minimumStructSize":32,"tables":[{"name":"lumio_core_api","offset":16},{"name":"lumio_voxel_api","offset":24}]},"schemaEpoch":1,"tables":[{"declaredStructSize":48,"fields":[{"c":"uint32_t","csharp":"uint","name":"version","offset":0,"rust":"u32","size":4},{"c":"uint32_t","csharp":"uint","name":"struct_size","offset":4,"rust":"u32","size":4},{"c":"uint64_t","csharp":"ulong","name":"reserved0","offset":8,"rust":"u64","size":8}],"functionCount":3,"minimumStructSize":48,"name":"lumio_core_api","reservedSlots":1,"slots":[{"cSignature":"lumio_status_t (*lumio_core_init)(const struct lumio_core_config_v1* config, lumio_handle_t out_context)","csharpSignature":"LumioStatus lumio_core_init(IntPtr config, LumioHandle out_context)","name":"lumio_core_init","offset":16,"params":[{"name":"config","typeRef":"struct:core_config:v1"},{"name":"out_context","typeRef":"handle:core_context"}],"returns":"status","rustSignature":"extern \"C\" fn(config: *const LumioCoreConfigV1, out_context: LumioHandle) -> LumioStatus","slotIndex":0},{"cSignature":"lumio_status_t (*lumio_core_shutdown)(lumio_handle_t context)","csharpSignature":"LumioStatus lumio_core_shutdown(LumioHandle context)","name":"lumio_core_shutdown","offset":24,"params":[{"name":"context","typeRef":"handle:core_context"}],"returns":"status","rustSignature":"extern \"C\" fn(context: LumioHandle) -> LumioStatus","slotIndex":1},{"cSignature":"lumio_status_t (*lumio_core_last_error_detail)(lumio_handle_t context, lumio_buffer_t out_detail)","csharpSignature":"LumioStatus lumio_core_last_error_detail(LumioHandle context, LumioBuffer out_detail)","name":"lumio_core_last_error_detail","offset":32,"params":[{"name":"context","typeRef":"handle:core_context"},{"name":"out_detail","typeRef":"buffer:out"}],"returns":"status","rustSignature":"extern \"C\" fn(context: LumioHandle, out_detail: LumioBuffer) -> LumioStatus","slotIndex":2}],"version":1},{"declaredStructSize":32,"fields":[{"c":"uint32_t","csharp":"uint","name":"version","offset":0,"rust":"u32","size":4},{"c":"uint32_t","csharp":"uint","name":"struct_size","offset":4,"rust":"u32","size":4},{"c":"uint64_t","csharp":"ulong","name":"reserved0","offset":8,"rust":"u64","size":8}],"functionCount":2,"minimumStructSize":32,"name":"lumio_voxel_api","reservedSlots":0,"slots":[{"cSignature":"lumio_status_t (*lumio_voxel_world_create)(lumio_handle_t context, const struct lumio_voxel_world_desc_v1* desc, lumio_handle_t out_world)","csharpSignature":"LumioStatus lumio_voxel_world_create(LumioHandle context, IntPtr desc, LumioHandle out_world)","name":"lumio_voxel_world_create","offset":16,"params":[{"name":"context","typeRef":"handle:core_context"},{"name":"desc","typeRef":"struct:voxel_world_desc:v1"},{"name":"out_world","typeRef":"handle:voxel_world"}],"returns":"status","rustSignature":"extern \"C\" fn(context: LumioHandle, desc: *const LumioVoxelWorldDescV1, out_world: LumioHandle) -> LumioStatus","slotIndex":0},{"cSignature":"lumio_status_t (*lumio_voxel_world_destroy)(lumio_handle_t world)","csharpSignature":"LumioStatus lumio_voxel_world_destroy(LumioHandle world)","name":"lumio_voxel_world_destroy","offset":24,"params":[{"name":"world","typeRef":"handle:voxel_world"}],"returns":"status","rustSignature":"extern \"C\" fn(world: LumioHandle) -> LumioStatus","slotIndex":1}],"version":1}],"typeMapping":[{"align":1,"c":"uint8_t","csharp":"byte","rust":"u8","size":1,"typeRef":"u8"},{"align":2,"c":"uint16_t","csharp":"ushort","rust":"u16","size":2,"typeRef":"u16"},{"align":4,"c":"uint32_t","csharp":"uint","rust":"u32","size":4,"typeRef":"u32"},{"align":8,"c":"uint64_t","csharp":"ulong","rust":"u64","size":8,"typeRef":"u64"},{"align":1,"c":"int8_t","csharp":"sbyte","rust":"i8","size":1,"typeRef":"i8"},{"align":2,"c":"int16_t","csharp":"short","rust":"i16","size":2,"typeRef":"i16"},{"align":4,"c":"int32_t","csharp":"int","rust":"i32","size":4,"typeRef":"i32"},{"align":8,"c":"int64_t","csharp":"long","rust":"i64","size":8,"typeRef":"i64"},{"align":4,"c":"float","csharp":"float","rust":"f32","size":4,"typeRef":"f32"},{"align":8,"c":"double","csharp":"double","rust":"f64","size":8,"typeRef":"f64"},{"align":4,"c":"uint32_t","csharp":"uint","rust":"u32","size":4,"typeRef":"bool32"},{"align":4,"c":"lumio_status_t","csharp":"LumioStatus","rust":"LumioStatus","size":4,"typeRef":"status"},{"align":8,"c":"lumio_handle_t","csharp":"LumioHandle","rust":"LumioHandle","size":16,"typeRef":"handle:"},{"align":8,"c":"lumio_buffer_t","csharp":"LumioBuffer","rust":"LumioBuffer","size":24,"typeRef":"buffer:in"},{"align":8,"c":"lumio_buffer_t","csharp":"LumioBuffer","rust":"LumioBuffer","size":24,"typeRef":"buffer:out"},{"align":8,"c":"lumio_buffer_t","csharp":"LumioBuffer","rust":"LumioBuffer","size":24,"typeRef":"buffer:inout"},{"align":8,"c":"const lumio__v*","csharp":"IntPtr","rust":"*const LumioV","size":8,"typeRef":"struct::v"},{"align":8,"c":"const lumio_*","csharp":"IntPtr","rust":"*const Lumio","size":8,"typeRef":"ptr:const:"},{"align":8,"c":"lumio_*","csharp":"IntPtr","rust":"*mut Lumio","size":8,"typeRef":"ptr:mut:"}]} diff --git a/xtask/src/baseline.rs b/xtask/src/baseline.rs index 3776209..c7cbfa8 100644 --- a/xtask/src/baseline.rs +++ b/xtask/src/baseline.rs @@ -10,6 +10,13 @@ use std::process::Command; pub const BASELINE_ID: &str = "LGE-V1.4-2026-08-27"; pub const MIRROR_REL: &str = "docs/architecture/LumioGameEngine_Architecture_v1.4.md"; pub const BASELINE_SHA_REL: &str = "docs/architecture/.baseline.sha256"; +/// Root ABI 发布物镜像(ADR-040 §7 消费面;来源与 revision 见 docs/architecture/abi/README.md)。 +pub const ABI_MIRROR_RELS: [&str; 4] = [ + "docs/architecture/abi/lumio_core.h", + "docs/architecture/abi/root-abi-bundle.json", + "docs/architecture/abi/ids-index.json", + "docs/architecture/abi/packages-index.json", +]; pub const FRAME_REL: &str = "docs/2026-08-27-native-core-module-implementation-frame.md"; const STALE_MIRROR: &str = "LumioGameEngine_Architecture_v1.2.md"; const STALE_BASELINE: &str = "LGE-V1.2-2026-08-27"; @@ -73,24 +80,28 @@ pub fn file_sha256_hex(path: &Path) -> Result { )) } -pub fn parse_baseline_sha_file(body: &str) -> Result<(String, String), String> { - let line = body - .lines() - .find(|l| !l.trim().is_empty()) - .ok_or_else(|| ".baseline.sha256 is empty".to_string())?; - let mut parts = line.split_whitespace(); - let hex = parts - .next() - .ok_or_else(|| ".baseline.sha256 missing digest".to_string())? - .to_ascii_lowercase(); - let rel = parts - .next() - .ok_or_else(|| ".baseline.sha256 missing path".to_string())? - .replace('\\', "/"); - if hex.len() != 64 { - return Err(format!(".baseline.sha256 digest length {}", hex.len())); +/// Every ` ` row of `.baseline.sha256`, in file order. +pub fn parse_baseline_sha_file(body: &str) -> Result, String> { + let mut rows = Vec::new(); + for line in body.lines().filter(|l| !l.trim().is_empty()) { + let mut parts = line.split_whitespace(); + let hex = parts + .next() + .ok_or_else(|| ".baseline.sha256 missing digest".to_string())? + .to_ascii_lowercase(); + let rel = parts + .next() + .ok_or_else(|| ".baseline.sha256 missing path".to_string())? + .replace('\\', "/"); + if hex.len() != 64 { + return Err(format!(".baseline.sha256 digest length {}", hex.len())); + } + rows.push((hex, rel)); + } + if rows.is_empty() { + return Err(".baseline.sha256 is empty".to_string()); } - Ok((hex, rel)) + Ok(rows) } fn audit_agents(root: &Path, errors: &mut Vec) { @@ -229,25 +240,34 @@ fn audit_mirror_hash(root: &Path, errors: &mut Vec) { return; } }; - let (expected, rel) = match parse_baseline_sha_file(&sha_body) { + let rows = match parse_baseline_sha_file(&sha_body) { Ok(v) => v, Err(e) => { errors.push(e); return; } }; - if rel != MIRROR_REL { + if !rows.iter().any(|(_, rel)| rel == MIRROR_REL) { errors.push(format!( - ".baseline.sha256 path `{rel}` is not the v1.4 activity mirror" + ".baseline.sha256 does not pin the v1.4 activity mirror {MIRROR_REL}" )); } - let path = root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)); - match file_sha256_hex(&path) { - Ok(actual) if actual == expected => {} - Ok(actual) => errors.push(format!( - "v1.4 mirror SHA-256 {actual} != .baseline.sha256 {expected}" - )), - Err(e) => errors.push(e), + for required in ABI_MIRROR_RELS { + if !rows.iter().any(|(_, rel)| rel == required) { + errors.push(format!( + ".baseline.sha256 does not pin the Root ABI mirror file {required}" + )); + } + } + for (expected, rel) in &rows { + let path = root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)); + match file_sha256_hex(&path) { + Ok(actual) if actual == *expected => {} + Ok(actual) => errors.push(format!( + "{rel} SHA-256 {actual} != .baseline.sha256 {expected}" + )), + Err(e) => errors.push(e), + } } } diff --git a/xtask/src/contracts.rs b/xtask/src/contracts.rs new file mode 100644 index 0000000..ae5bb59 --- /dev/null +++ b/xtask/src/contracts.rs @@ -0,0 +1,413 @@ +//! `gen-contracts`:从 `docs/architecture/abi/` 镜像生成 +//! `crates/lumio-contract-types/src/generated_data.rs`。 +//! +//! 镜像是唯一生成源(钉住的上游 revision 见镜像 README);生成物不得手改, +//! 只能经本命令更新并与镜像一起提交。`registry_values_are_unique` 等 +//! crate 测试与本 crate 的回归测试共同断言生成物与镜像零漂移。 +//! +//! 解析器是面向这两个机器生成 JSON 文件的严格最小实现:任何意外字节直接 +//! 报错退出,不做宽容恢复。 + +use std::fmt::Write as _; +use std::path::Path; + +// ---------- 最小 JSON 解析 ---------- + +#[derive(Clone, Debug, PartialEq)] +pub enum Json { + Null, + Bool(bool), + Num(f64), + Str(String), + Arr(Vec), + Obj(Vec<(String, Json)>), +} + +impl Json { + fn get(&self, key: &str) -> Result<&Json, String> { + match self { + Json::Obj(pairs) => pairs + .iter() + .find(|(k, _)| k == key) + .map(|(_, v)| v) + .ok_or_else(|| format!("missing key `{key}`")), + other => Err(format!("get(`{key}`) on non-object {other:?}")), + } + } + + fn as_str(&self) -> Result<&str, String> { + match self { + Json::Str(s) => Ok(s), + other => Err(format!("expected string, got {other:?}")), + } + } + + fn as_i64(&self) -> Result { + match self { + Json::Num(n) => { + let v = *n as i64; + if (v as f64 - n).abs() >= f64::EPSILON { + return Err(format!("non-integer {n}")); + } + Ok(v) + } + other => Err(format!("expected number, got {other:?}")), + } + } + + fn as_arr(&self) -> Result<&[Json], String> { + match self { + Json::Arr(items) => Ok(items), + other => Err(format!("expected array, got {other:?}")), + } + } +} + +pub fn parse(text: &str) -> Result { + let bytes = text.as_bytes(); + let mut pos = 0usize; + let value = parse_value(bytes, &mut pos)?; + skip_ws(bytes, &mut pos); + if pos != bytes.len() { + return Err(format!("trailing bytes at {pos}")); + } + Ok(value) +} + +fn skip_ws(b: &[u8], pos: &mut usize) { + while *pos < b.len() && matches!(b[*pos], b' ' | b'\t' | b'\n' | b'\r') { + *pos += 1; + } +} + +fn expect(b: &[u8], pos: &mut usize, byte: u8) -> Result<(), String> { + if *pos >= b.len() || b[*pos] != byte { + return Err(format!("expected `{}` at byte {}", byte as char, *pos)); + } + *pos += 1; + Ok(()) +} + +fn parse_value(b: &[u8], pos: &mut usize) -> Result { + skip_ws(b, pos); + match b.get(*pos) { + Some(b'{') => parse_obj(b, pos), + Some(b'[') => parse_arr(b, pos), + Some(b'"') => Ok(Json::Str(parse_string(b, pos)?)), + Some(b't') => parse_lit(b, pos, "true", Json::Bool(true)), + Some(b'f') => parse_lit(b, pos, "false", Json::Bool(false)), + Some(b'n') => parse_lit(b, pos, "null", Json::Null), + Some(_) => parse_num(b, pos), + None => Err("unexpected end of JSON".to_string()), + } +} + +fn parse_lit(b: &[u8], pos: &mut usize, lit: &str, value: Json) -> Result { + if !b[*pos..].starts_with(lit.as_bytes()) { + return Err(format!("bad literal at byte {}", *pos)); + } + *pos += lit.len(); + Ok(value) +} + +fn parse_obj(b: &[u8], pos: &mut usize) -> Result { + expect(b, pos, b'{')?; + let mut pairs = Vec::new(); + skip_ws(b, pos); + if b.get(*pos) == Some(&b'}') { + *pos += 1; + return Ok(Json::Obj(pairs)); + } + loop { + skip_ws(b, pos); + let key = parse_string(b, pos)?; + skip_ws(b, pos); + expect(b, pos, b':')?; + pairs.push((key, parse_value(b, pos)?)); + skip_ws(b, pos); + match b.get(*pos) { + Some(b',') => *pos += 1, + Some(b'}') => { + *pos += 1; + return Ok(Json::Obj(pairs)); + } + other => return Err(format!("expected `,` or `}}`, got {other:?} at {}", *pos)), + } + } +} + +fn parse_arr(b: &[u8], pos: &mut usize) -> Result { + expect(b, pos, b'[')?; + let mut items = Vec::new(); + skip_ws(b, pos); + if b.get(*pos) == Some(&b']') { + *pos += 1; + return Ok(Json::Arr(items)); + } + loop { + items.push(parse_value(b, pos)?); + skip_ws(b, pos); + match b.get(*pos) { + Some(b',') => *pos += 1, + Some(b']') => { + *pos += 1; + return Ok(Json::Arr(items)); + } + other => return Err(format!("expected `,` or `]`, got {other:?} at {}", *pos)), + } + } +} + +fn parse_string(b: &[u8], pos: &mut usize) -> Result { + expect(b, pos, b'"')?; + let mut out = String::new(); + loop { + match b.get(*pos) { + Some(b'"') => { + *pos += 1; + return Ok(out); + } + Some(b'\\') => { + *pos += 1; + match b.get(*pos) { + Some(b'"') => out.push('"'), + Some(b'\\') => out.push('\\'), + Some(b'/') => out.push('/'), + Some(b'n') => out.push('\n'), + Some(b't') => out.push('\t'), + Some(b'r') => out.push('\r'), + other => return Err(format!("unsupported escape {other:?} at {}", *pos)), + } + *pos += 1; + } + Some(_) => { + let ch = std::str::from_utf8(&b[*pos..]) + .map_err(|e| format!("utf-8: {e}"))? + .chars() + .next() + .ok_or("empty")?; + out.push(ch); + *pos += ch.len_utf8(); + } + None => return Err("unterminated string".to_string()), + } + } +} + +fn parse_num(b: &[u8], pos: &mut usize) -> Result { + let start = *pos; + while *pos < b.len() && matches!(b[*pos], b'-' | b'+' | b'.' | b'e' | b'E' | b'0'..=b'9') { + *pos += 1; + } + let text = std::str::from_utf8(&b[start..*pos]).map_err(|e| format!("utf-8: {e}"))?; + text.parse() + .map(Json::Num) + .map_err(|e| format!("bad number `{text}`: {e}")) +} + +// ---------- 生成 ---------- + +pub const GENERATED_DATA_REL: &str = "crates/lumio-contract-types/src/generated_data.rs"; +const BUNDLE_MIRROR_REL: &str = "docs/architecture/abi/root-abi-bundle.json"; +const IDS_MIRROR_REL: &str = "docs/architecture/abi/ids-index.json"; +/// ADR-046:`ErrorCode` numeric 必须放得进 `lumio_status_t`(int32)。 +const STATUS_NUMERIC_MAX: i64 = 2_147_483_647; + +fn read_mirror(root: &Path, rel: &str) -> Result { + let path = root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)); + let text = + std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?; + parse(&text).map_err(|e| format!("{rel}: {e}")) +} + +/// 从镜像推导 `generated_data.rs` 的完整内容(确定性输出)。 +pub fn derive_generated_data(root: &Path) -> Result { + let bundle = read_mirror(root, BUNDLE_MIRROR_REL)?; + + let mut out = String::new(); + out.push_str( + "//! @generated by `cargo xtask gen-contracts` — DO NOT EDIT BY HAND.\n\ + //!\n\ + //! Source of truth: the byte-pinned mirrors under `docs/architecture/abi/`\n\ + //! (upstream revision in that directory's README). Regenerate with\n\ + //! `cargo xtask gen-contracts` after a mirror update; commit together.\n\n\ + use crate::generated::ArchitectureErrorCode;\n\ + use crate::layout::{AbiStructGolden, AbiTypeGolden};\n\n", + ); + + // layoutProfile 标量。 + let profile = bundle.get("layoutProfile")?; + let pointer_bytes = profile.get("pointerBytes")?.as_i64()?; + let max_alignment = profile.get("maxAlignment")?.as_i64()?; + writeln!( + out, + "pub(crate) const ABI_POINTER_BYTES: u32 = {pointer_bytes};\n\ + pub(crate) const ABI_MAX_ALIGNMENT: u32 = {max_alignment};" + ) + .unwrap(); + out.push('\n'); + + // typeMapping 中的命名 C 类型(lumio_ 前缀),按 C 名去重并要求各行一致。 + let mut named: Vec<(String, i64, i64)> = Vec::new(); + for row in bundle.get("typeMapping")?.as_arr()? { + let c_name = row.get("c")?.as_str()?; + if !c_name.starts_with("lumio_") || c_name.contains('*') { + continue; + } + let size = row.get("size")?.as_i64()?; + let align = row.get("align")?.as_i64()?; + if let Some(existing) = named.iter().find(|(n, _, _)| n == c_name) { + if existing.1 != size || existing.2 != align { + return Err(format!("typeMapping rows disagree for {c_name}")); + } + continue; + } + named.push((c_name.to_string(), size, align)); + } + out.push_str("#[rustfmt::skip]\npub(crate) const ABI_TYPE_GOLDEN: &[AbiTypeGolden] = &[\n"); + for (name, size, align) in &named { + writeln!( + out, + " AbiTypeGolden {{ name: \"{name}\", size: {size}, align: {align} }}," + ) + .unwrap(); + } + out.push_str("];\n\n"); + + // root + 各 API table 的结构 Golden(成员 = 头部字段与槽位/表指针偏移)。 + // (name, declared_size, minimum_size, members[(name, offset)]) + type StructRow = (String, i64, i64, Vec<(String, i64)>); + let mut structs: Vec = Vec::new(); + { + let root_obj = bundle.get("root")?; + let mut members = Vec::new(); + for field in root_obj.get("fields")?.as_arr()? { + members.push(( + field.get("name")?.as_str()?.to_string(), + field.get("offset")?.as_i64()?, + )); + } + for table in root_obj.get("tables")?.as_arr()? { + members.push(( + table.get("name")?.as_str()?.to_string(), + table.get("offset")?.as_i64()?, + )); + } + structs.push(( + "lumio_root_api".to_string(), + root_obj.get("declaredStructSize")?.as_i64()?, + root_obj.get("minimumStructSize")?.as_i64()?, + members, + )); + } + for table in bundle.get("tables")?.as_arr()? { + let mut members = Vec::new(); + for field in table.get("fields")?.as_arr()? { + members.push(( + field.get("name")?.as_str()?.to_string(), + field.get("offset")?.as_i64()?, + )); + } + for slot in table.get("slots")?.as_arr()? { + members.push(( + slot.get("name")?.as_str()?.to_string(), + slot.get("offset")?.as_i64()?, + )); + } + structs.push(( + table.get("name")?.as_str()?.to_string(), + table.get("declaredStructSize")?.as_i64()?, + table.get("minimumStructSize")?.as_i64()?, + members, + )); + } + out.push_str("#[rustfmt::skip]\npub(crate) const ABI_STRUCT_GOLDEN: &[AbiStructGolden] = &[\n"); + for (name, declared, minimum, members) in &structs { + writeln!( + out, + " AbiStructGolden {{ name: \"{name}\", declared_size: {declared}, minimum_size: {minimum}, members: &[" + ) + .unwrap(); + for (member, offset) in members { + writeln!(out, " (\"{member}\", {offset}),").unwrap(); + } + out.push_str(" ] },\n"); + } + out.push_str("];\n\n"); + + // 各 API table 的发布版本号(tables[].version)。 + out.push_str("#[rustfmt::skip]\npub(crate) const ABI_TABLE_VERSIONS: &[(&str, u32)] = &[\n"); + for table in bundle.get("tables")?.as_arr()? { + writeln!( + out, + " (\"{}\", {}),", + table.get("name")?.as_str()?, + table.get("version")?.as_i64()? + ) + .unwrap(); + } + out.push_str("];\n\n"); + + // ids/index.json 的 ErrorCode 命名空间(Architecture 所有;唯一 numeric 权威)。 + let ids = read_mirror(root, IDS_MIRROR_REL)?; + let error_ns = ids + .get("namespaces")? + .as_arr()? + .iter() + .find(|ns| ns.get("namespace").and_then(Json::as_str).ok() == Some("ErrorCode")) + .ok_or("ids-index.json missing ErrorCode namespace")?; + if error_ns.get("owner")?.as_str()? != "Architecture" { + return Err("ErrorCode namespace owner is not Architecture".to_string()); + } + let mut seen_ids: Vec = Vec::new(); + let mut seen_numerics: Vec = Vec::new(); + out.push_str("#[rustfmt::skip]\npub(crate) const ERROR_CODES: &[ArchitectureErrorCode] = &[\n"); + for value in error_ns.get("values")?.as_arr()? { + let id = value.get("id")?.as_str()?; + let numeric = value.get("numeric")?.as_i64()?; + let status = value.get("status")?.as_str()?; + if status != "Active" { + return Err(format!("ErrorCode {id} has unexpected status {status}")); + } + if !(1..=STATUS_NUMERIC_MAX).contains(&numeric) { + return Err(format!( + "ErrorCode {id} numeric {numeric} out of status range" + )); + } + if seen_ids.iter().any(|s| s == id) || seen_numerics.contains(&numeric) { + return Err(format!("ErrorCode duplicate id/numeric: {id}/{numeric}")); + } + seen_ids.push(id.to_string()); + seen_numerics.push(numeric); + writeln!(out, " ArchitectureErrorCode::new(\"{id}\", {numeric}),").unwrap(); + } + out.push_str("];\n"); + + Ok(out) +} + +/// 发布的符号面策略:`(entrySymbol, symbolPrefix)`,来自 bundle 镜像。 +pub fn abi_symbol_policy(root: &Path) -> Result<(String, String), String> { + let bundle = read_mirror(root, BUNDLE_MIRROR_REL)?; + let abi = bundle.get("abi")?; + Ok(( + abi.get("entrySymbol")?.as_str()?.to_string(), + abi.get("symbolPrefix")?.as_str()?.to_string(), + )) +} + +pub fn generated_data_path(root: &Path) -> std::path::PathBuf { + root.join(GENERATED_DATA_REL.replace('/', std::path::MAIN_SEPARATOR_STR)) +} + +/// 生成并写盘;内容与既有文件一致时不动文件。 +pub fn write_generated_data(root: &Path) -> Result { + let derived = derive_generated_data(root)?; + let path = generated_data_path(root); + let current = std::fs::read_to_string(&path).unwrap_or_default(); + if current == derived { + return Ok(false); + } + std::fs::write(&path, derived).map_err(|e| format!("write {}: {e}", path.display()))?; + Ok(true) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 72c6ce3..8e90ba6 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -5,15 +5,23 @@ //! - `dump-symbols`:构建 `lumio-native-ffi` cdylib 并断言符号表不含跨仓 Root 符号 //! (ADR 0001:`lumio_core_get_api_v1` 归 CoreEngine root-abi)。 //! - `check-baseline`:活动入口、模块 README、CI 与镜像 Hash 对齐 `LGE-V1.4-2026-08-27`。 +//! - `gen-contracts`:从 `docs/architecture/abi/` 镜像重新生成 +//! `crates/lumio-contract-types/src/generated_data.rs`(生成物不手改)。 mod baseline; +mod contracts; use std::collections::BTreeMap; use std::process::{Command, ExitCode}; /// 跨仓 Root 符号:NativeCore 产物中出现即失败(ADR 0001)。 +/// `dump-symbols` 会与镜像 bundle 的 `entrySymbol` 交叉校验,防止硬编码漂移。 const FORBIDDEN_ROOT_SYMBOL: &str = "lumio_core_get_api_v1"; +/// 已批准导出的 provider 符号。provider 组合契约未发布(T-ffi-04 blocked +/// 半边),列表保持为空:任何 `symbolPrefix` 前缀导出都视为违规。 +const APPROVED_PROVIDER_EXPORTS: &[&str] = &[]; + /// crate -> 允许的 workspace 直接依赖(normal 图)。白名单以外的一切 workspace 边都算违规; /// 非 workspace 的外部依赖必须出现在 `EXTERNAL_ALLOWLIST`。 fn allowed_deps() -> BTreeMap<&'static str, Vec<&'static str>> { @@ -224,26 +232,45 @@ fn cmd_dump_symbols() -> ExitCode { exported.push(sym.trim_start_matches('_').to_string()); } } + + // 符号策略来自镜像 bundle;与硬编码交叉校验,防止两边各自漂移。 + let (entry_symbol, symbol_prefix) = + match contracts::abi_symbol_policy(&baseline::workspace_root()) { + Ok(policy) => policy, + Err(err) => { + eprintln!("error: 读取镜像符号策略失败: {err}"); + return ExitCode::from(2); + } + }; let mut failed = false; + if entry_symbol != FORBIDDEN_ROOT_SYMBOL { + eprintln!( + "FAIL 镜像 entrySymbol `{entry_symbol}` 与硬编码 Root 符号 `{FORBIDDEN_ROOT_SYMBOL}` 不一致" + ); + failed = true; + } if exported.iter().any(|s| s == FORBIDDEN_ROOT_SYMBOL) { eprintln!( "FAIL 产物导出了跨仓 Root 符号 {FORBIDDEN_ROOT_SYMBOL}(归 CoreEngine root-abi,ADR 0001)" ); failed = true; } - let lumio_syms: Vec<&String> = exported + let unapproved: Vec<&String> = exported .iter() - .filter(|s| s.starts_with("lumio_")) + .filter(|s| s.starts_with(&symbol_prefix)) + .filter(|s| !APPROVED_PROVIDER_EXPORTS.contains(&s.as_str())) .collect(); - println!( - "dump-symbols:lumio_* 导出 {} 个{}", - lumio_syms.len(), - if lumio_syms.is_empty() { - "(脚手架阶段应为 0)".to_string() - } else { - format!(":{lumio_syms:?}") - } - ); + if unapproved.is_empty() { + println!( + "dump-symbols:{symbol_prefix}* 导出 0 个未批准符号(批准列表 {} 项)", + APPROVED_PROVIDER_EXPORTS.len() + ); + } else { + eprintln!( + "FAIL 未批准的 {symbol_prefix}* 导出:{unapproved:?}(provider 符号列表未发布,批准列表为空)" + ); + failed = true; + } if failed { ExitCode::FAILURE } else { @@ -270,14 +297,35 @@ fn cmd_check_baseline() -> ExitCode { } } +fn cmd_gen_contracts() -> ExitCode { + let root = baseline::workspace_root(); + match contracts::write_generated_data(&root) { + Ok(true) => { + println!("gen-contracts:已更新 {}", contracts::GENERATED_DATA_REL); + ExitCode::SUCCESS + } + Ok(false) => { + println!("gen-contracts:{} 已是最新", contracts::GENERATED_DATA_REL); + ExitCode::SUCCESS + } + Err(e) => { + eprintln!("error: {e}"); + ExitCode::FAILURE + } + } +} + fn main() -> ExitCode { let arg = std::env::args().nth(1); match arg.as_deref() { Some("check-dep-dag") => cmd_check_dep_dag(), Some("dump-symbols") => cmd_dump_symbols(), Some("check-baseline") => cmd_check_baseline(), + Some("gen-contracts") => cmd_gen_contracts(), _ => { - eprintln!("用法: cargo xtask "); + eprintln!( + "用法: cargo xtask " + ); ExitCode::from(2) } } @@ -367,19 +415,48 @@ mod tests { }); } + /// 生成物不得手改:从镜像重推导必须与已提交的 generated_data.rs 逐字节一致。 + #[test] + fn generated_data_matches_mirror_derivation() { + let root = crate::baseline::workspace_root(); + let derived = crate::contracts::derive_generated_data(&root) + .unwrap_or_else(|e| panic!("derive generated_data: {e}")); + let path = crate::contracts::generated_data_path(&root); + let committed = std::fs::read_to_string(&path).unwrap_or_else(|e| { + panic!( + "read {}: {e}(先跑 cargo xtask gen-contracts)", + path.display() + ) + }); + assert_eq!( + committed, derived, + "generated_data.rs 与镜像推导不一致:重跑 `cargo xtask gen-contracts` 并与镜像一起提交" + ); + } + #[test] fn v14_mirror_digest_in_baseline_file_matches_hashed_bytes() { let root = crate::baseline::workspace_root(); let sha_path = root.join(crate::baseline::BASELINE_SHA_REL); let body = std::fs::read_to_string(&sha_path).expect("read .baseline.sha256"); - let (expected, rel) = - crate::baseline::parse_baseline_sha_file(&body).expect("parse .baseline.sha256"); - assert_eq!(rel, crate::baseline::MIRROR_REL); - let actual = crate::baseline::file_sha256_hex( - &root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)), - ) - .expect("hash v1.4 mirror"); - assert_eq!(actual, expected); + let rows = crate::baseline::parse_baseline_sha_file(&body).expect("parse .baseline.sha256"); + assert!( + rows.iter() + .any(|(_, rel)| rel == crate::baseline::MIRROR_REL) + ); + for required in crate::baseline::ABI_MIRROR_RELS { + assert!( + rows.iter().any(|(_, rel)| rel == required), + ".baseline.sha256 must pin {required}" + ); + } + for (expected, rel) in &rows { + let actual = crate::baseline::file_sha256_hex( + &root.join(rel.replace('/', std::path::MAIN_SEPARATOR_STR)), + ) + .unwrap_or_else(|e| panic!("hash {rel}: {e}")); + assert_eq!(&actual, expected, "pinned digest mismatch for {rel}"); + } } #[test]