///|
// Archive collection has an explicit closed input set, unlike directory
// collection's selected-path traversal. Nothing unlisted is silently ignored.
fn collection_carrier_names(
  spec : CollectionSpec,
  names : Array[String],
  limits : @ledger.Limits,
) -> Map[String, String] raise @ledger.InputError {
  ignore(spec.encode(limits~))
  let declared : Map[String, String] = Map([])
  for file in spec.files {
    declared[file.path] = file.id
  }
  let seen : Set[String] = Set([])
  for name in names {
    if !declared.contains(name) {
      raise bad(
        "UnexpectedArtifact", name, "Archive member is not an exact declared path; provide an archive containing only declared XML files",
      )
    }
    if seen.contains(name) {
      raise bad(
        "DuplicateArtifact", name, "Duplicate archive member is ambiguous",
      )
    }
    seen.add(name)
  }
  declared
}

///|
/// Collect a bounded classic ZIP against the full expected inventory.
/// Missing declared members remain incomplete; unlisted members are refused.
/// Paths never create report/run identities; all bindings come from the spec.
pub fn CollectionSpec::collect_zip(
  self : CollectionSpec,
  data : Bytes,
  limits~ : @ledger.Limits = @ledger.Limits::default(),
  max_bytes~ : Int32 = 16777216,
) -> Evidence raise @ledger.InputError {
  ignore(self.encode(limits~))
  let entries = zip_entries(data, limits, max_bytes)
  let declared = collection_carrier_names(
    self,
    entries.map(entry => entry.name),
    limits,
  )
  let inputs = entries.map(entry => {
    id: declared[entry.name],
    xml: carrier_text(zip_expand(data, entry)),
  })
  self.collect(inputs[:], limits~)
}