///|
priv struct SidecarPatch {
path : String
patch : MetadataPatch
}
///|
/// A bounded, unordered set of edits to distinct existing sidecars.
/// Every patch is evaluated against the same original snapshot.
pub struct EditBatch {
priv edits : Array[SidecarPatch]
}
///|
/// Build a batch directly from typed patches. Copies/normalizes the input list.
/// A successful batch always reloads through from_json(to_json()).
pub fn EditBatch::from_patches(
patches : Array[(String, MetadataPatch)],
) -> EditBatch raise TraceError {
if patches.length() > 256 {
reject(
"edit_batch_limit", "/edits", "At most 256 distinct sidecars can be edited in one plan; split the review scope explicitly.",
)
}
let seen : Map[String, Bool] = Map([])
// Count canonical envelope/entry sizes before allocating a combined JSON.
// Each entry is patch JSON plus a path member; commas separate entries.
let mut serialized_length = 12 // {"edits":[]}
let edits = patches.map(pair => {
let (path, patch) = pair
if seen.contains(path) {
reject(
"duplicate_edit_path", path, "Sidecar occurs more than once; combine its operations into one unambiguous patch.",
)
}
if path.length() > 2097152 {
reject(
"input_limit", "", "Sidecar path exceeds the edit batch input limit; correct the target path.",
)
}
let entry_length = patch.to_json().length() +
Json::string(path).stringify().length() +
8
let separator = if seen.is_empty() { 0 } else { 1 }
if entry_length > 2097152 - serialized_length - separator {
reject(
"input_limit", "", "Canonical edit batch exceeds 2,097,152 UTF-16 code units; split the review scope explicitly.",
)
}
serialized_length += entry_length + separator
seen[path] = true
{ path, patch, }
})
edits.sort_by((left, right) => lexical_compare(left.path, right.path))
let batch : EditBatch = { edits, }
if batch.to_json().length() > 2097152 {
reject(
"input_limit", "", "Canonical edit batch exceeds 2,097,152 UTF-16 code units; split the review scope explicitly.",
)
}
batch
}
///|
/// Read {"edits":[{"path":relative_path,"set":object,"remove":[field_names]}]}.
/// Unknown controls and duplicate paths/decoded JSON keys fail. Empty is a no-op.
pub fn EditBatch::from_json(text : String) -> EditBatch raise TraceError {
guard parse_source("", text, container_limit=67) is Object(fields) else {
reject(
"invalid_edit_batch", "", "Expected an object containing an edits array.",
)
return { edits: [], }
}
for key in sorted_keys(fields) {
if key != "edits" {
reject(
"unknown_edit_batch_field",
"",
"Unknown batch control " +
Json::string(key).stringify() +
"; use only edits.",
)
}
}
guard fields.get("edits") is Some(Array(values)) else {
reject(
"invalid_edit_batch", "/edits", "Required edits control must be an array.",
)
return { edits: [], }
}
if values.length() > 256 {
reject(
"edit_batch_limit", "/edits", "At most 256 distinct sidecars can be edited in one plan; split the review scope explicitly.",
)
}
let edits = values.map(value => {
guard value is Object(entry) else {
reject(
"invalid_edit_batch", "/edits", "Each edit must be an object containing path, set and remove.",
)
abort("reject raises")
}
for key in sorted_keys(entry) {
if !["path", "set", "remove"].contains(key) {
reject(
"unknown_edit_batch_field",
"/edits",
"Unknown edit control " +
Json::string(key).stringify() +
"; use path, set and remove.",
)
}
}
guard entry.get("path") is Some(String(path)) &&
entry.get("set") is Some(assignments) &&
entry.get("remove") is Some(removals) else {
reject(
"invalid_edit_batch", "/edits", "Every edit requires a string path and both set and remove controls.",
)
abort("reject raises")
}
let patch = MetadataPatch::from_json(
canonical_json(Json::object({ "set": assignments, "remove": removals })),
)
{ path, patch, }
})
EditBatch::from_patches(edits.map(edit => (edit.path, edit.patch)))
}
///|
fn EditBatch::batch_value(self : EditBatch) -> Json {
Json::object({
"edits": Json::array(
self.edits.map(edit => {
Json::object({
"path": Json::string(edit.path),
"set": Json::object(edit.patch.assignments),
"remove": Json::array(edit.patch.removals.map(Json::string)),
})
}),
),
})
}
///|
/// Normalize sidecar/removal ordering while preserving exact metadata tokens.
pub fn EditBatch::to_json(self : EditBatch) -> String {
canonical_json(self.batch_value())
}
///|
pub fn EditBatch::sidecar_paths(self : EditBatch) -> Array[String] {
self.edits.map(edit => edit.path)
}
///|
/// One immutable candidate snapshot, never a collection of partial plans.
pub struct BatchEditPlan {
priv before : DatasetIndex
priv after : DatasetIndex
priv batch : EditBatch
priv originals : Map[String, Map[String, Json]]
priv updates : Map[String, Map[String, Json]]
priv impact : ImpactReport
}
///|
/// Validate all operations against the baseline, rebuild once, then compare.
/// Failures never mutate the baseline or return a partially updated snapshot.
pub fn DatasetIndex::plan_edits(
self : DatasetIndex,
batch : EditBatch,
) -> BatchEditPlan raise TraceError {
let originals : Map[String, Map[String, Json]] = Map([])
let updates : Map[String, Map[String, Json]] = Map([])
for edit in batch.edits {
guard self.sidecars.search_by(source => source.selector.path == edit.path)
is Some(position) else {
reject(
"unknown_sidecar_path",
edit.path,
"Sidecar is absent from the snapshot; select an existing source before planning an edit.",
)
abort("reject raises")
}
let original = self.sidecars[position].metadata
originals[edit.path] = original
updates[edit.path] = apply_metadata_patch(original, edit.patch, edit.path)
}
let inputs = self.sidecars.map(source => {
let metadata = match updates.get(source.selector.path) {
Some(updated) => updated
None => source.metadata
}
SidecarInput::new(
source.selector.path,
canonical_json(Json::object(metadata)),
)
})
// Only the final combined snapshot is bounded/revalidated. A coordinated
// shrink/grow can succeed even if one isolated intermediate plan is too big.
let after = DatasetIndex::from_manifest(self.data_paths(), inputs)
{
before: self,
after,
batch,
originals,
updates,
impact: self.compare(after),
}
}
///|
pub fn BatchEditPlan::before(self : BatchEditPlan) -> DatasetIndex {
self.before
}
///|
pub fn BatchEditPlan::after(self : BatchEditPlan) -> DatasetIndex {
self.after
}
///|
pub fn BatchEditPlan::sidecar_paths(self : BatchEditPlan) -> Array[String] {
self.batch.sidecar_paths()
}
///|
/// Return complete replacement metadata only for an explicitly edited source.
pub fn BatchEditPlan::updated_sidecar_json(
self : BatchEditPlan,
path : String,
) -> String? {
match self.updates.get(path) {
Some(metadata) => Some(canonical_json(Json::object(metadata)))
None => None
}
}
///|
pub fn BatchEditPlan::impact(self : BatchEditPlan) -> ImpactReport {
self.impact
}
///|
/// Retain every patch, old/new source values, complete snapshots and impacts.
pub fn BatchEditPlan::to_json(self : BatchEditPlan) -> String {
canonical_json(
Json::object({
"batch": self.batch.batch_value(),
"sidecars": Json::array(
self.batch.edits.map(edit => {
Json::object({
"path": Json::string(edit.path),
"patch": edit.patch.patch_value(),
"before_sidecar": Json::object(self.originals[edit.path]),
"after_sidecar": Json::object(self.updates[edit.path]),
})
}),
),
"before_manifest": self.before.manifest_value(),
"after_manifest": self.after.manifest_value(),
"impact": self.impact.report_value(),
}),
)
}