// ../core/src/changeEpisode/types.ts
var CHANGE_EPISODE_SCHEMA_VERSION = 1;
// ../core/src/changeEpisode/builders.ts
var CHANGE_EPISODE_ACTORS = ["human", "agent", "mixed"];
var CLOSED_EPISODE_STATUSES = [
  "validated",
  "failed",
  "reverted",
  "committed"
];
function openChangeEpisode(input) {
  if (!CHANGE_EPISODE_ACTORS.includes(input.actor)) {
    throw new Error(`openChangeEpisode: invalid actor "${String(input.actor)}"`);
  }
  if (!isValidTimestamp(input.startedAt)) {
    throw new Error(`openChangeEpisode: invalid startedAt ${String(input.startedAt)}`);
  }
  const episodeId = cleanString(input.episodeId) ?? `change-episode:${stableHash({
    goalId: input.goalId,
    actor: input.actor,
    startedAt: input.startedAt,
    goal: input.goal,
    gitBaseSha: input.gitBaseSha
  })}`;
  return removeUndefined({
    episodeId,
    goalId: cleanString(input.goalId),
    actor: input.actor,
    startedAt: input.startedAt,
    goal: cleanString(input.goal),
    contextReceipts: uniqueStrings(input.contextReceipts ?? []),
    fileDeltas: uniqueStrings(input.fileDeltas ?? []),
    symbolDeltas: uniqueStrings(input.symbolDeltas ?? []),
    edgeDeltas: uniqueStrings(input.edgeDeltas ?? []),
    memoryDeltas: uniqueStrings(input.memoryDeltas ?? []),
    validationRuns: uniqueStrings(input.validationRuns ?? []),
    gitBaseSha: cleanString(input.gitBaseSha),
    worktreeDirtyHashBefore: cleanString(input.worktreeDirtyHashBefore),
    status: "open"
  });
}
function attachEpisodeRows(episode, rows) {
  return {
    ...episode,
    contextReceipts: uniqueStrings([...episode.contextReceipts, ...rows.contextReceipts ?? []]),
    fileDeltas: uniqueStrings([...episode.fileDeltas, ...rows.fileDeltas ?? []]),
    symbolDeltas: uniqueStrings([...episode.symbolDeltas, ...rows.symbolDeltas ?? []]),
    edgeDeltas: uniqueStrings([...episode.edgeDeltas, ...rows.edgeDeltas ?? []]),
    memoryDeltas: uniqueStrings([...episode.memoryDeltas, ...rows.memoryDeltas ?? []]),
    validationRuns: uniqueStrings([...episode.validationRuns, ...rows.validationRuns ?? []])
  };
}
function closeChangeEpisode(episode, input) {
  if (episode.status !== "open") {
    throw new Error(`closeChangeEpisode: episode ${episode.episodeId} is already closed (status "${episode.status}")`);
  }
  if (!CLOSED_EPISODE_STATUSES.includes(input.status)) {
    throw new Error(`closeChangeEpisode: invalid target status "${String(input.status)}"`);
  }
  if (!isValidTimestamp(input.endedAt) || input.endedAt < episode.startedAt) {
    throw new Error(`closeChangeEpisode: invalid endedAt ${String(input.endedAt)} (startedAt ${episode.startedAt})`);
  }
  return removeUndefined({
    ...episode,
    status: input.status,
    endedAt: input.endedAt,
    gitHeadSha: cleanString(input.gitHeadSha) ?? episode.gitHeadSha,
    worktreeDirtyHashAfter: cleanString(input.worktreeDirtyHashAfter) ?? episode.worktreeDirtyHashAfter
  });
}
function planEpisodeRevert(episode, fileDeltas, opts = {}) {
  const deltasById = new Map;
  for (const delta of fileDeltas) {
    if (!deltasById.has(delta.fileDeltaId))
      deltasById.set(delta.fileDeltaId, delta);
  }
  const deltasToInvert = [];
  const deltasExcluded = [];
  const conflicts = [];
  for (const fileDeltaId of uniqueStrings(episode.fileDeltas)) {
    const delta = deltasById.get(fileDeltaId);
    if (!delta) {
      conflicts.push({ fileDeltaId, reason: "missing_delta_row" });
      continue;
    }
    const currentPath = cleanString(delta.pathAfter) ?? cleanString(delta.pathBefore);
    if (currentPath !== undefined && opts.excludePaths?.(currentPath) === true) {
      deltasExcluded.push({ id: fileDeltaId, reason: "excluded_by_filter" });
      continue;
    }
    if (cleanString(delta.inversePatch) === undefined) {
      conflicts.push({ fileDeltaId, reason: "no_inverse_patch" });
      continue;
    }
    deltasToInvert.push(fileDeltaId);
  }
  const status = deltasToInvert.length === 0 ? "blocked" : conflicts.length > 0 || deltasExcluded.length > 0 ? "partial" : "plannable";
  return {
    episodeId: episode.episodeId,
    deltasToInvert,
    deltasExcluded,
    conflicts,
    status
  };
}
function fileDeltasFromProvenance(rows) {
  return rows.map((row) => {
    const actor = (() => {
      const value = cleanString(row.actor);
      return value === "human" || value?.startsWith("human_") ? "human" : "agent";
    })();
    return removeUndefined({
      fileDeltaId: row.provenanceId,
      pathBefore: cleanString(row.pathBefore),
      pathAfter: cleanString(row.pathAfter),
      changeKind: row.changeKind ?? "modify",
      beforeHash: cleanString(row.beforeHash),
      afterHash: cleanString(row.afterHash),
      forwardPatch: cleanString(row.forwardPatch),
      inversePatch: cleanString(row.inversePatch),
      actor,
      receiptId: row.provenanceId
    });
  });
}
function uniqueStrings(values) {
  return Array.from(new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value))));
}
function cleanString(value) {
  if (typeof value !== "string")
    return;
  const text = value.trim();
  return text.length > 0 ? text : undefined;
}
function isValidTimestamp(value) {
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
}
function removeUndefined(value) {
  return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
}
function stableHash(value) {
  const text = stableJson(value);
  let hash = 2166136261;
  for (let index = 0;index < text.length; index += 1) {
    hash ^= text.charCodeAt(index);
    hash = Math.imul(hash, 16777619);
  }
  return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 12);
}
function stableJson(value) {
  if (Array.isArray(value))
    return `[${value.map(stableJson).join(",")}]`;
  if (value && typeof value === "object") {
    return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
  }
  return JSON.stringify(value);
}
// ../core/src/changeEpisode/fromLedgerRows.ts
function assembleChangeEpisodeFromLedgerRows(input) {
  const provenance = input.editProvenance.filter((row) => row.goalId === undefined || row.goalId === input.goalId);
  const timestamps = [
    ...input.trials.map((trial) => trial.createdAt),
    ...provenance.map((row) => row.createdAt)
  ].filter((value) => typeof value === "number" && Number.isFinite(value));
  const startedAt = timestamps.length > 0 ? Math.min(...timestamps) : input.fallbackStartedAt ?? 0;
  const episode = openChangeEpisode({
    goalId: input.goalId,
    goal: input.goal,
    actor: inferEpisodeActor(provenance),
    startedAt,
    gitBaseSha: input.gitBaseSha,
    worktreeDirtyHashBefore: input.worktreeDirtyHashBefore
  });
  return attachEpisodeRows(episode, {
    contextReceipts: uniqueStrings([
      ...input.packItems.map((item) => item.trialId),
      ...input.trials.map((trial) => trial.receiptId)
    ]),
    fileDeltas: provenance.map((row) => row.provenanceId),
    symbolDeltas: provenance.flatMap((row) => [...row.symbols ?? []]),
    validationRuns: [...input.validationRefs]
  });
}
function inferEpisodeActor(provenance) {
  let hasAgent = false;
  let hasHuman = false;
  for (const row of provenance) {
    const actor = cleanString(row.actor);
    if (actor === undefined)
      continue;
    if (actor === "human" || actor.startsWith("human_"))
      hasHuman = true;
    else
      hasAgent = true;
  }
  if (hasAgent && hasHuman)
    return "mixed";
  if (hasHuman)
    return "human";
  return "agent";
}
// ../core/src/changeEpisode/deltaComputation.ts
function isUsableRange(range) {
  return Number.isFinite(range.startLine) && Number.isFinite(range.endLine) && range.endLine >= range.startLine && (range.startLine > 0 || range.endLine > 0);
}
function overlaps(span, range) {
  return span.startLine <= range.endLine && span.endLine >= range.startLine;
}
function resolveTouchedSymbols(changedPaths, symbols, hunks) {
  const changed = new Set(changedPaths.map((path) => path.trim()).filter(Boolean));
  const usableHunksByPath = new Map;
  for (const hunk of hunks ?? []) {
    if (!isUsableRange(hunk))
      continue;
    const list = usableHunksByPath.get(hunk.path) ?? [];
    list.push(hunk);
    usableHunksByPath.set(hunk.path, list);
  }
  const seen = new Set;
  const touched = [];
  for (const symbol of symbols) {
    if (!changed.has(symbol.path))
      continue;
    const pathHunks = usableHunksByPath.get(symbol.path);
    const isTouched = pathHunks === undefined ? true : pathHunks.some((range) => overlaps(symbol, range));
    if (!isTouched)
      continue;
    const key = symbol.key.trim();
    if (!key || seen.has(key))
      continue;
    seen.add(key);
    touched.push(key);
  }
  return touched;
}
function deriveInverseEdit(spec) {
  switch (spec.kind) {
    case "search_replace":
      return { kind: "search_replace", path: spec.path, search: spec.replace, replace: spec.search };
    case "rewrite":
      return spec.beforeContent === undefined ? { kind: "unavailable", path: spec.path, reason: "missing_before_content" } : { kind: "rewrite", path: spec.path, rewrite: spec.beforeContent };
    case "create":
      return { kind: "delete", path: spec.path };
    case "delete":
      return spec.beforeContent === undefined ? { kind: "unavailable", path: spec.path, reason: "missing_before_content" } : { kind: "create", path: spec.path, content: spec.beforeContent };
  }
}
function edgeIdentity(edge) {
  return `${edge.from}\x00${edge.to}\x00${edge.kind}`;
}
function diffGraphEdges(prev, next) {
  const prevIds = new Set(prev.map(edgeIdentity));
  const nextIds = new Set(next.map(edgeIdentity));
  const added = [];
  const seenAdded = new Set;
  for (const edge of next) {
    const id = edgeIdentity(edge);
    if (prevIds.has(id) || seenAdded.has(id))
      continue;
    seenAdded.add(id);
    added.push({ change: "added", from: edge.from, to: edge.to, kind: edge.kind });
  }
  const removed = [];
  const seenRemoved = new Set;
  for (const edge of prev) {
    const id = edgeIdentity(edge);
    if (nextIds.has(id) || seenRemoved.has(id))
      continue;
    seenRemoved.add(id);
    removed.push({ change: "removed", from: edge.from, to: edge.to, kind: edge.kind });
  }
  return [...added, ...removed];
}
// ../core/src/changeEpisode/revertExecution.ts
function revertApplyPlanFromInverse(inverseEdits) {
  const ops = [];
  const skipped = [];
  for (const inverse of inverseEdits ?? []) {
    switch (inverse.kind) {
      case "search_replace":
        ops.push({
          op: "search_replace",
          path: inverse.path,
          find: inverse.search,
          replace: inverse.replace,
          expectedReplacements: 1
        });
        break;
      case "rewrite":
        ops.push({ op: "rewrite", path: inverse.path, content: inverse.rewrite });
        break;
      case "create":
        ops.push({ op: "create", path: inverse.path, content: inverse.content });
        break;
      case "delete":
        ops.push({ op: "delete", path: inverse.path });
        break;
      case "unavailable":
        skipped.push({ path: inverse.path, reason: inverse.reason });
        break;
    }
  }
  return { ops, skipped };
}
export {
  uniqueStrings,
  stableHash,
  revertApplyPlanFromInverse,
  resolveTouchedSymbols,
  planEpisodeRevert,
  openChangeEpisode,
  fileDeltasFromProvenance,
  diffGraphEdges,
  deriveInverseEdit,
  closeChangeEpisode,
  cleanString,
  attachEpisodeRows,
  assembleChangeEpisodeFromLedgerRows,
  CHANGE_EPISODE_SCHEMA_VERSION
};
