Retire the mutable design schema

Make Rust records and fixed codec tests the implementation-shape owner, remove the Mermaid category registry, and state the ownership boundary in the authoritative specification.
This commit is contained in:
ms2k 2026-07-19 02:45:32 +09:00
commit 03bfe92ca0
5 changed files with 17 additions and 1679 deletions

View file

@ -31,18 +31,14 @@ pub trait SchemaType {
} }
macro_rules! schema_types { macro_rules! schema_types {
($($name:literal => $record:ty),+; $($declared:literal),* $(,)?) => { ($($name:literal => $record:ty),+ $(,)?) => {
$( $(
impl SchemaType for $record { impl SchemaType for $record {
const NAME: &'static str = $name; const NAME: &'static str = $name;
} }
)+ )+
pub const IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[$($name),+]; pub const SERIALIZED_RECORD_TYPE_NAMES: &[&str] = &[$($name),+];
pub const DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES: &[&str] = &[
$($name),+,
$($declared),*
];
}; };
} }
@ -149,13 +145,5 @@ schema_types!(
"ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1, "ResolvedNetworkReadinessV1" => ResolvedNetworkReadinessV1,
"DomainStore" => DomainStore, "DomainStore" => DomainStore,
"StorageCatalog" => StorageCatalog, "StorageCatalog" => StorageCatalog,
"PrepareStageState" => PrepareStageState; "PrepareStageState" => PrepareStageState,
"StateResetIntentV1",
"StateResetScopeV1",
); );
pub const DECLARED_RUNTIME_SCHEMA_TYPE_NAMES: &[&str] = &[
"ResolvedDomainConfiguration",
"SandboxLayoutV1",
"ConfinedPathPlan",
];

View file

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::fmt::Debug; use std::fmt::Debug;
use std::num::NonZeroU16; use std::num::NonZeroU16;

View file

@ -670,165 +670,14 @@ image = "example.invalid/agent:v1"
&mut seen, &mut seen,
); );
assert_eq!(seen.len(), IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES.len()); assert_eq!(seen.len(), SERIALIZED_RECORD_TYPE_NAMES.len());
assert!( assert!(
IMPLEMENTED_SERIALIZED_SCHEMA_TYPE_NAMES SERIALIZED_RECORD_TYPE_NAMES
.iter() .iter()
.all(|name| seen.contains(name)) .all(|name| seen.contains(name))
); );
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SchemaCategory {
Serialized,
Runtime,
}
fn parse_schema_categories(
schema: &str,
serialized_registry: &HashSet<&str>,
runtime_registry: &HashSet<&str>,
) -> Result<HashMap<String, SchemaCategory>, String> {
if let Some(name) = serialized_registry.intersection(runtime_registry).next() {
return Err(format!("schema registries overlap at {name}"));
}
let mut documented = HashMap::new();
let mut current: Option<(&str, Option<SchemaCategory>)> = None;
for line in schema.lines() {
let trimmed = line.trim();
if let Some(declaration) = trimmed.strip_prefix("class ") {
if current.is_some() {
return Err("nested class declaration".into());
}
let name = declaration
.split(|character: char| character.is_whitespace() || character == '{')
.next()
.filter(|name| !name.is_empty())
.ok_or_else(|| "class declaration has no name".to_owned())?;
if !declaration.contains('{') {
return Err(format!("class {name} has no stereotype block"));
}
if !serialized_registry.contains(name) && !runtime_registry.contains(name) {
return Err(format!("unknown schema class {name}"));
}
current = Some((name, None));
continue;
}
let Some((name, category)) = current.as_mut() else {
continue;
};
if trimmed == "}" {
let category = category.ok_or_else(|| format!("class {name} is unmarked"))?;
match documented.insert((*name).to_owned(), category) {
Some(existing) if existing != category => {
return Err(format!("class {name} has conflicting categories"));
}
_ => {}
}
current = None;
continue;
}
let Some(stereotype) = trimmed
.strip_prefix("<<")
.and_then(|value| value.strip_suffix(">>"))
else {
continue;
};
let parsed = match stereotype {
"serialized" => Some(SchemaCategory::Serialized),
"runtime" => Some(SchemaCategory::Runtime),
"enumeration" | "union" => None,
_ => return Err(format!("class {name} has unknown stereotype {stereotype}")),
};
if let Some(parsed) = parsed
&& category.replace(parsed).is_some()
{
return Err(format!("class {name} has multiple category markers"));
}
}
if let Some((name, _)) = current {
return Err(format!("class {name} has no closing brace"));
}
for name in serialized_registry.union(runtime_registry) {
if !documented.contains_key(*name) {
return Err(format!("registered schema class {name} is undocumented"));
}
}
Ok(documented)
}
#[test]
fn schema_categories_match_exact_disjoint_registries() {
let serialized = DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES
.iter()
.copied()
.collect::<HashSet<_>>();
let runtime = DECLARED_RUNTIME_SCHEMA_TYPE_NAMES
.iter()
.copied()
.collect::<HashSet<_>>();
assert_eq!(
serialized.len(),
DECLARED_SERIALIZED_SCHEMA_TYPE_NAMES.len()
);
assert_eq!(runtime.len(), DECLARED_RUNTIME_SCHEMA_TYPE_NAMES.len());
let documented = parse_schema_categories(
include_str!("../../../../docs/schema.md"),
&serialized,
&runtime,
)
.unwrap();
let documented_serialized = documented
.iter()
.filter_map(|(name, category)| {
(*category == SchemaCategory::Serialized).then_some(name.as_str())
})
.collect::<HashSet<_>>();
let documented_runtime = documented
.iter()
.filter_map(|(name, category)| {
(*category == SchemaCategory::Runtime).then_some(name.as_str())
})
.collect::<HashSet<_>>();
assert_eq!(documented_serialized, serialized);
assert_eq!(documented_runtime, runtime);
}
#[test]
fn schema_category_parser_fails_closed() {
let serialized = HashSet::from(["Known"]);
let runtime = HashSet::from(["Runtime"]);
let overlapping = HashSet::from(["Known"]);
for invalid in [
"class Known {\nfield\n}",
"class Known {\n<<serialized>>\n<<serialized>>\n}",
"class Known {\n<<serialized>>\n<<runtime>>\n}",
"class Unknown {\n<<serialized>>\n}",
"class Known {\n<<unexpected>>\n}",
] {
assert!(parse_schema_categories(invalid, &serialized, &runtime).is_err());
}
assert!(parse_schema_categories("", &serialized, &runtime).is_err());
assert!(parse_schema_categories("", &serialized, &overlapping).is_err());
assert!(
parse_schema_categories(
"class Known {\n<<serialized>>\n}\nclass Runtime {\n<<runtime>>\n}",
&serialized,
&runtime,
)
.is_ok()
);
}
fn assert_unknown_protocol_version<T>(record: &T) fn assert_unknown_protocol_version<T>(record: &T)
where where
T: DeserializeOwned + Serialize, T: DeserializeOwned + Serialize,

File diff suppressed because it is too large Load diff

View file

@ -184,8 +184,8 @@ initiated by a host Lazaret CLI invocation, not by a resident scheduler.
### 3.2 Trusted Host Configuration Refinement ### 3.2 Trusted Host Configuration Refinement
The non-authoritative repository [design schemas](schema.md) mirror the closed Rust configuration records and their codec tests own the closed v1 document
v1 trusted-host configuration described by this refinement. Lazaret chooses its shape described by this refinement. Lazaret chooses its
configuration file from an explicit host CLI option or, when absent, from configuration file from an explicit host CLI option or, when absent, from
`$XDG_CONFIG_HOME/lazaret/config.toml`, with `$XDG_CONFIG_HOME/lazaret/config.toml`, with
`$HOME/.config/lazaret/config.toml` as the XDG fallback. It derives the Storage `$HOME/.config/lazaret/config.toml` as the XDG fallback. It derives the Storage
@ -562,7 +562,7 @@ Disconnected Client Sessions close rather than reattach to an unbounded scrollba
owned by the coordinator. owned by the coordinator.
The session connection carries the terminal events and messages described by The session connection carries the terminal events and messages described by
this refinement. The non-authoritative repository schema mirrors their v1 this refinement. Rust protocol records and fixed codec tests own their v1
encoding. One Host Coordinator event loop serializes input, resize, signal, encoding. One Host Coordinator event loop serializes input, resize, signal,
close, prompt-routing, PTY output, and exec-client completion for each Session close, prompt-routing, PTY output, and exec-client completion for each Session
ID. It uses bounded queues and PTY ID. It uses bounded queues and PTY
@ -818,10 +818,10 @@ before network ownership records readiness. Sessions and the generation mailbox
are separate generation records. Deterministic paths are lookup hints for are separate generation records. Deterministic paths are lookup hints for
physical recovery, not ownership proofs. physical recovery, not ownership proofs.
The non-authoritative repository [design schemas](schema.md) mirror the logical Rust durable records and their codec tests own the store row shapes described
records and relationships described by these refinements. Implementations must by these refinements. Implementations must preserve the distinct Domain Store
preserve the distinct Domain Store and Storage Catalog authority boundaries and and Storage Catalog authority boundaries and the independently recoverable
the independently recoverable ownership described above. ownership described above.
Lazaret v1 stores the catalog in redb. Each catalog operation holds the Lazaret v1 stores the catalog in redb. Each catalog operation holds the
host-wide Storage Catalog lock for one bounded open, transaction, and close host-wide Storage Catalog lock for one bounded open, transaction, and close
@ -915,9 +915,9 @@ and publication gates follow the protocol model.
#### Derived policy and readiness encoding #### Derived policy and readiness encoding
The non-authoritative repository [design schemas](schema.md) mirror the v1 Rust network records and fixed codec tests own the v1 policy, attestation,
policy, attestation, readiness-identity projection, and fingerprint inputs readiness-identity projection, and fingerprint encodings described by this
described by this refinement. refinement.
The v1 policy transport passes canonical bytes through a write-sealed inherited The v1 policy transport passes canonical bytes through a write-sealed inherited
memfd. The Runtime Supervisor verifies the seals and digest, installs the policy, memfd. The Runtime Supervisor verifies the seals and digest, installs the policy,
@ -970,7 +970,7 @@ handles. Host-only CLI/coordinator control may use host-private Unix sockets tha
are never mounted into the Agent Sandbox. are never mounted into the Agent Sandbox.
Mailbox files carry the signed protocol records described by this refinement; Mailbox files carry the signed protocol records described by this refinement;
the non-authoritative repository schema mirrors their v1 encoding. TLS is not Rust protocol records and fixed codec tests own their v1 encoding. TLS is not
layered over the regular-file transport. HC opens each ready entry relative to a layered over the regular-file transport. HC opens each ready entry relative to a
confined directory handle, confined directory handle,
rejects links and non-regular files, bounds the complete file before allocation, rejects links and non-regular files, bounds the complete file before allocation,