///|
/// Deterministic release identity, without coverage, credentials or local paths.
/// Obtain the manifest or its exact-byte digest through an independent trusted
/// channel. Parsing alone does not authenticate a publisher.
pub fn ObjectSpec::to_manifest(self : ObjectSpec) -> String {
  Json::array([
    Json::string("MFLARTIFACT1"),
    Json::string(self.resource),
    Json::string(self.length.to_string()),
    Json::string(self.etag),
    Json::string(self.sha256),
  ]).stringify()
}

///|
/// Exact five-field schema; unknown fields/versions are refused, never discarded.
/// Length is a canonical decimal string so all targets preserve Int64 identity.
/// Accepts JSON whitespace but never normalizes resource or validator values.
pub fn ObjectSpec::from_manifest(text : String) -> ObjectSpec raise LedgerError {
  if text.length() > 65536 {
    raise InvalidManifest("manifest exceeds 65536 code units")
  }
  let value = @json.parse(text) catch {
    _ => raise InvalidManifest("malformed manifest JSON")
  }
  let (resource, length, etag, sha256) = match value {
    [
      String("MFLARTIFACT1"),
      String(resource),
      String(length),
      String(etag),
      String(sha256),
    ] => (resource, length, etag, sha256)
    _ =>
      raise InvalidManifest(
        "unsupported manifest version, field type or extra field",
      )
  }
  let length = decimal(length) catch {
    _ =>
      raise InvalidManifest(
        "manifest length must be canonical decimal in 0..Int64::max_value",
      )
  }
  ObjectSpec::new(resource, length, etag, sha256) catch {
    InvalidSpec(message) =>
      raise InvalidManifest("invalid manifest identity: " + message)
    other => raise other
  }
}