Remove the Timestamp kind

The Timestamp kind was a residual from an early research reference. It
was byte-identical to String everywhere (length-prefixed UTF-8) and its
only distinguishing behavior was a hand-rolled non-strict RFC 3339 check
that the docs admitted was incomplete (Feb 31 passes, seconds range
unchecked, no leap seconds). JSON-level timestamp validation is
jsonschema's job (format: date-time on the validate_json path), not
alktype's.

Removes the AlkTypeKind::Timestamp variant, its to_bast_str/from_bast_str
mapping, the builder's Schema::timestamp() constructor, the
validate_timestamp/is_rfc3339_timestamp validator arms, and the
materializer/reader/engine timestamp arms. Updates the meta-schema
primitive enum (14 -> 13), the spec docs (bast-format.md, schema-layer.md,
builder.md, validation.md, overview.md, data-access.md, README.md), and
the kind-count references (19 -> 18).

Verification: cargo test --release (407 pass), cargo clippy --all-targets
-- -D warnings (clean), cargo doc --no-deps (clean), cargo build --target
wasm32-unknown-unknown --release (clean).
This commit is contained in:
deepseek-v4-pro committed 2026-08-16 09:12:33 +00:00
1 parent 62ed009281
commit 510553d800
17 files changed
+38 -225

No files matched your search

+1 -69
View File
@@ -29,7 +29,6 @@
//! | Float finiteness (Float32/64) | `validate_float` |
//! | String `maxLength` (byte length) | `check_string` reads [`BastField::max_length`](crate::bast::BastField::max_length) |
//! | Bytes `maxLength` (array length) | `check_bytes` |
//! | RFC 3339 timestamp shape | `validate_timestamp` (non-strict, matching v0.1.0) |
//! | Enum index bounds | `validate_enum` — **fixes the v0.1.0 dead constraint** |
//! | Union variant dispatch | `validate_union` reads `__discriminator`, resolves the variant, recurses |
//! | Struct fields | `validate_struct` walks `fields`, requires each declared field present, recurses |
@@ -71,7 +70,7 @@ const DISCRIMINATOR_KEY: &str = "__discriminator";
/// from the bytes, this walker enforces the value-domain constraints
/// expressed in the BAST document. The materializer already guarantees
/// structural correctness; the validator only checks what the bytes
/// alone can't tell you (integer ranges, `maxLength`, timestamp shape,
/// alone can't tell you (integer ranges, `maxLength`,
/// enum index bounds, union variant constraints).
///
/// Returns `Err(AlkTypeError::Validation(...))` on the first violated
@@ -121,7 +120,6 @@ fn validate_typeref(
BastType::Primitive(AlkTypeKind::Bytes) => {
check_bytes(value, path, field.and_then(|f| f.max_length()))
}
BastType::Primitive(AlkTypeKind::Timestamp) => validate_timestamp(value, path),
BastType::Enum(e) => validate_enum(value, path, e),
BastType::Struct(s) => validate_struct(doc, s, value, path),
BastType::Union(u) => validate_union(doc, u, value, path),
@@ -239,13 +237,6 @@ fn check_bytes(value: &Value, path: &str, max_length: Option<usize>) -> Result<(
Ok(())
}
fn validate_timestamp(value: &Value, path: &str) -> Result<(), AlkTypeError> {
match value.as_str() {
Some(s) if is_rfc3339_timestamp(s) => Ok(()),
_ => Err(validation_err(path, "expected an RFC 3339 timestamp string")),
}
}
/// Enum validation on the bytes path: the materializer emits a numeric
/// index (`Value::Number`), and the constraint is that the index is
/// within the `values` array bounds (`0..len-1`).
@@ -381,48 +372,6 @@ fn validation_err(path: &str, reason: impl Into<String>) -> AlkTypeError {
AlkTypeError::Validation(jsonschema::ValidationError::custom(msg))
}
/// Non-strict RFC 3339 / ISO 8601 datetime check — same shape as the
/// v0.1.0 `TimestampValidator` so behavior is identical. Structural
/// sanity, not strict validation (Feb 31 passes; seconds range isn't
/// checked; leap seconds aren't handled). Strict RFC 3339 validation is
/// the consumer's responsibility if they need it (ADR-003, review #002
/// N1).
fn is_rfc3339_timestamp(s: &str) -> bool {
let parts: Vec<&str> = s.splitn(2, 'T').collect();
if parts.len() != 2 {
return false;
}
let date_parts: Vec<&str> = parts[0].split('-').collect();
if date_parts.len() != 3 {
return false;
}
let time_part = parts[1];
let time_clean = if let Some(pos) = time_part.find(['Z', '+']) {
&time_part[..pos]
} else if let Some(pos) = time_part.rfind('-') {
if pos >= 8 {
&time_part[..pos]
} else {
time_part
}
} else {
time_part
};
let time_parts: Vec<&str> = time_clean.split(':').collect();
if time_parts.len() < 2 || time_parts.len() > 3 {
return false;
}
date_parts[0].parse::<u16>().is_ok_and(|y| y > 0)
&& date_parts[1]
.parse::<u8>()
.is_ok_and(|m| (1..=12).contains(&m))
&& date_parts[2]
.parse::<u8>()
.is_ok_and(|d| (1..=31).contains(&d))
&& time_parts[0].parse::<u8>().is_ok_and(|h| h <= 23)
&& time_parts[1].parse::<u8>().is_ok_and(|m| m <= 59)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -564,23 +513,6 @@ mod tests {
assert!(validate_value(&d, &json!({"blob": [65, 255]})).is_ok());
}
// ----- Timestamp -------------------------------------------------------
#[test]
fn timestamp_valid_and_invalid() {
let root = json!({
"$defs": { "S": { "kind": "struct", "fields": [
{ "name": "ts", "kind": "timestamp" }
] } }
});
let d = doc_from(&root, "S");
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00Z")).is_ok());
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00")).is_ok());
assert!(materialize_and_validate(&d, &prefixed_str_le("2026-07-20T15:30:00+02:00")).is_ok());
let err = materialize_and_validate(&d, &prefixed_str_le("not-a-date")).unwrap_err();
assert!(matches!(err, AlkTypeError::Validation(_)), "got {err:?}");
}
// ----- Enum index bounds (the v0.1.0 dead-constraint fix) -------------
#[test]