import {
  topicOverlapScore
} from "./chunk-URB2WSKZ.js";
import {
  countRecallTokenOverlap,
  normalizeRecallTokens
} from "./chunk-DT5TVLJE.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";
import {
  assertIsoRecordedAt,
  assertString,
  isRecord,
  recordStoreDay
} from "./chunk-DGXUHMOV.js";
import {
  listJsonFiles,
  readJsonFile
} from "./chunk-LPSF4OQH.js";

// src/causal-chain.ts
import path from "path";
import { mkdir, readFile, writeFile } from "fs/promises";
import { createHash } from "crypto";
var STITCH_WEIGHTS = {
  followUpToGoal: 4,
  outcomeToGoal: 2,
  entityOverlap: 3,
  tagOverlap: 1.5,
  temporalProximity: 1
};
function validateCausalEdge(raw) {
  if (!isRecord(raw)) throw new Error("CausalEdge must be an object");
  if (raw.schemaVersion !== 1) throw new Error("CausalEdge.schemaVersion must be 1");
  const edgeType = assertString(raw.edgeType, "edgeType");
  const validEdgeTypes = ["follow_up_to_goal", "retry", "continuation", "correction"];
  if (!validEdgeTypes.includes(edgeType)) {
    throw new Error(`CausalEdge.edgeType must be one of ${validEdgeTypes.join(", ")}`);
  }
  const stitchMethod = assertString(raw.stitchMethod, "stitchMethod");
  const validMethods = ["lexical", "entity", "temporal", "explicit"];
  if (!validMethods.includes(stitchMethod)) {
    throw new Error(`CausalEdge.stitchMethod must be one of ${validMethods.join(", ")}`);
  }
  const confidence = typeof raw.confidence === "number" ? raw.confidence : 0;
  if (confidence < 0 || confidence > 1) throw new Error("CausalEdge.confidence must be in [0, 1]");
  return {
    schemaVersion: 1,
    edgeId: assertString(raw.edgeId, "edgeId"),
    fromTrajectoryId: assertString(raw.fromTrajectoryId, "fromTrajectoryId"),
    toTrajectoryId: assertString(raw.toTrajectoryId, "toTrajectoryId"),
    edgeType,
    confidence,
    stitchMethod,
    createdAt: assertIsoRecordedAt(assertString(raw.createdAt, "createdAt")),
    metadata: isRecord(raw.metadata) ? Object.fromEntries(
      Object.entries(raw.metadata).filter(
        (entry) => typeof entry[1] === "string"
      )
    ) : void 0
  };
}
function makeEdgeId(fromId, toId) {
  const digest = createHash("sha256").update(`${fromId}\0${toId}`).digest("hex").slice(0, 12);
  return `edge-${digest}`;
}
function resolveChainsDir(memoryDir, causalTrajectoryStoreDir) {
  const root = causalTrajectoryStoreDir ? path.join(memoryDir, causalTrajectoryStoreDir) : path.join(memoryDir, "state", "causal-trajectories");
  return path.join(root, "chains");
}
function chainIndexPath(chainsDir) {
  return path.join(chainsDir, "chain-index.json");
}
function edgeFilePath(chainsDir, edge) {
  const day = recordStoreDay(edge.createdAt);
  return path.join(chainsDir, "edges", day, `${edge.edgeId}.json`);
}
async function readChainIndex(chainsDir) {
  try {
    const raw = JSON.parse(await readFile(chainIndexPath(chainsDir), "utf8"));
    return {
      outgoing: isRecord(raw.outgoing) ? raw.outgoing : {},
      incoming: isRecord(raw.incoming) ? raw.incoming : {},
      edges: isRecord(raw.edges) ? raw.edges : {},
      updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
    };
  } catch {
    return { outgoing: {}, incoming: {}, edges: {}, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
  }
}
async function writeChainIndex(chainsDir, index) {
  await mkdir(chainsDir, { recursive: true });
  index.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
  await writeFile(chainIndexPath(chainsDir), JSON.stringify(index, null, 2), "utf8");
}
async function persistEdge(chainsDir, edge) {
  const filePath = edgeFilePath(chainsDir, edge);
  await mkdir(path.dirname(filePath), { recursive: true });
  await writeFile(filePath, JSON.stringify(edge, null, 2), "utf8");
  return filePath;
}
function addEdgeToIndex(index, edge) {
  if (!index.outgoing[edge.fromTrajectoryId]) {
    index.outgoing[edge.fromTrajectoryId] = [];
  }
  if (!index.outgoing[edge.fromTrajectoryId].includes(edge.edgeId)) {
    index.outgoing[edge.fromTrajectoryId].push(edge.edgeId);
  }
  if (!index.incoming[edge.toTrajectoryId]) {
    index.incoming[edge.toTrajectoryId] = [];
  }
  if (!index.incoming[edge.toTrajectoryId].includes(edge.edgeId)) {
    index.incoming[edge.toTrajectoryId].push(edge.edgeId);
  }
  index.edges[edge.edgeId] = edge;
}
function scoreStitchCandidate(newTrajectory, candidate) {
  let score = 0;
  let dominantMethod = "lexical";
  let maxComponent = 0;
  const newFollowUpTokens = new Set(
    normalizeRecallTokens(newTrajectory.followUpSummary ?? "", [])
  );
  const candidateGoalTokens = normalizeRecallTokens(candidate.goal, []);
  if (newFollowUpTokens.size > 0 && candidateGoalTokens.length > 0) {
    const overlap = countRecallTokenOverlap(newFollowUpTokens, candidate.goal, []);
    const normalized = overlap / Math.max(newFollowUpTokens.size, candidateGoalTokens.length);
    const component = normalized * STITCH_WEIGHTS.followUpToGoal;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "lexical";
    }
  }
  const candidateFollowUpTokens = new Set(
    normalizeRecallTokens(candidate.followUpSummary ?? "", [])
  );
  const newGoalTokens = normalizeRecallTokens(newTrajectory.goal, []);
  if (candidateFollowUpTokens.size > 0 && newGoalTokens.length > 0) {
    const overlap = countRecallTokenOverlap(candidateFollowUpTokens, newTrajectory.goal, []);
    const normalized = overlap / Math.max(candidateFollowUpTokens.size, newGoalTokens.length);
    const component = normalized * 3;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "lexical";
    }
  }
  const newOutcomeTokens = new Set(
    normalizeRecallTokens(newTrajectory.outcomeSummary, [])
  );
  if (newOutcomeTokens.size > 0) {
    const overlap = countRecallTokenOverlap(newOutcomeTokens, candidate.goal, []);
    const normalized = overlap / Math.max(newOutcomeTokens.size, candidateGoalTokens.length || 1);
    const component = normalized * STITCH_WEIGHTS.outcomeToGoal;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "lexical";
    }
  }
  const newEntities = newTrajectory.entityRefs ?? [];
  const candidateEntities = candidate.entityRefs ?? [];
  if (newEntities.length > 0 && candidateEntities.length > 0) {
    const entityJaccard = topicOverlapScore(newEntities, candidateEntities);
    const component = entityJaccard * STITCH_WEIGHTS.entityOverlap;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "entity";
    }
  }
  const newTags = newTrajectory.tags ?? [];
  const candidateTags = candidate.tags ?? [];
  if (newTags.length > 0 && candidateTags.length > 0) {
    const tagJaccard = topicOverlapScore(newTags, candidateTags);
    const component = tagJaccard * STITCH_WEIGHTS.tagOverlap;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "lexical";
    }
  }
  const newMs = Date.parse(newTrajectory.recordedAt);
  const candidateMs = Date.parse(candidate.recordedAt);
  if (Number.isFinite(newMs) && Number.isFinite(candidateMs)) {
    const gapHours = Math.abs(newMs - candidateMs) / 36e5;
    const proximity = 1 / (1 + gapHours / 24);
    const component = proximity * STITCH_WEIGHTS.temporalProximity;
    score += component;
    if (component > maxComponent) {
      maxComponent = component;
      dominantMethod = "temporal";
    }
  }
  let edgeType = "continuation";
  const goalTokens = new Set(normalizeRecallTokens(newTrajectory.goal, []));
  const candidateGoalSet = new Set(candidateGoalTokens);
  const goalOverlap = [...goalTokens].filter((t) => candidateGoalSet.has(t)).length;
  const goalSimilarity = goalTokens.size > 0 ? goalOverlap / goalTokens.size : 0;
  if (goalSimilarity > 0.7 && candidate.outcomeKind === "failure") {
    edgeType = "retry";
  } else if (goalSimilarity > 0.7 && newTrajectory.outcomeKind !== candidate.outcomeKind) {
    edgeType = "correction";
  } else if (newFollowUpTokens.size > 0 && candidateGoalTokens.length > 0) {
    const followUpGoalOverlap = countRecallTokenOverlap(newFollowUpTokens, candidate.goal, []);
    if (followUpGoalOverlap > 0) {
      edgeType = "follow_up_to_goal";
    }
  }
  return { trajectory: candidate, score, edgeType, stitchMethod: dominantMethod };
}
async function readRecentTrajectories(memoryDir, causalTrajectoryStoreDir, currentSessionKey, lookbackDays) {
  const root = causalTrajectoryStoreDir ? path.join(memoryDir, causalTrajectoryStoreDir) : path.join(memoryDir, "state", "causal-trajectories");
  const trajectoriesDir = path.join(root, "trajectories");
  const files = await listJsonFiles(trajectoriesDir).catch(() => []);
  if (files.length === 0) return [];
  const cutoff = Date.now() - lookbackDays * 864e5;
  const results = [];
  for (const filePath of files) {
    try {
      const raw = await readJsonFile(filePath);
      if (!isRecord(raw)) continue;
      const sessionKey = typeof raw.sessionKey === "string" ? raw.sessionKey : "";
      if (sessionKey === currentSessionKey) continue;
      const recordedAt = typeof raw.recordedAt === "string" ? raw.recordedAt : "";
      const ms = Date.parse(recordedAt);
      if (!Number.isFinite(ms) || ms < cutoff) continue;
      results.push(raw);
    } catch {
    }
  }
  return results;
}
async function stitchCausalChain(options) {
  const { memoryDir, causalTrajectoryStoreDir, newTrajectory, config: stitchConfig } = options;
  const chainsDir = resolveChainsDir(memoryDir, causalTrajectoryStoreDir);
  const candidates = await readRecentTrajectories(
    memoryDir,
    causalTrajectoryStoreDir,
    newTrajectory.sessionKey,
    stitchConfig.lookbackDays
  );
  if (candidates.length === 0) return [];
  const scored = candidates.map((c) => scoreStitchCandidate(newTrajectory, c)).filter((s) => s.score >= stitchConfig.minScore).sort((a, b) => b.score - a.score).slice(0, stitchConfig.maxEdgesPerTrajectory);
  if (scored.length === 0) return [];
  const index = await readChainIndex(chainsDir);
  const newEdges = [];
  for (const candidate of scored) {
    const edgeId = makeEdgeId(candidate.trajectory.trajectoryId, newTrajectory.trajectoryId);
    if (index.edges[edgeId]) continue;
    const edge = {
      schemaVersion: 1,
      edgeId,
      fromTrajectoryId: candidate.trajectory.trajectoryId,
      toTrajectoryId: newTrajectory.trajectoryId,
      edgeType: candidate.edgeType,
      confidence: Math.min(1, candidate.score / 10),
      stitchMethod: candidate.stitchMethod,
      createdAt: (/* @__PURE__ */ new Date()).toISOString()
    };
    addEdgeToIndex(index, edge);
    await persistEdge(chainsDir, edge);
    newEdges.push(edge);
  }
  if (newEdges.length > 0) {
    await writeChainIndex(chainsDir, index);
    log.debug(`[cmc] stitched ${newEdges.length} causal edge(s) for trajectory ${newTrajectory.trajectoryId}`);
  }
  return newEdges;
}

export {
  validateCausalEdge,
  makeEdgeId,
  resolveChainsDir,
  readChainIndex,
  writeChainIndex,
  scoreStitchCandidate,
  stitchCausalChain
};
//# sourceMappingURL=chunk-XMHBH5H6.js.map