@rhei-team/rhei 1.0.0-beta.0 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -30320,8 +30320,9 @@ async function buildLocalCodeContextForGoalSnapshot(args) {
30320
30320
  const backendIntelligenceExactRefsForSelection = routeSlotContextGoal ? [] : backendIntelligenceExactRefs;
30321
30321
  const evidenceInputPresent = Boolean(args.evidenceIntake?.length || args.refinementEvidence?.length || args.diagnosticEvidence?.length);
30322
30322
  const goalExactRefsForSelection = evidenceInputPresent ? [] : goalExactRefs;
30323
- const exactRefs = uniqueStrings16([...explicitExactRefs, ...recentTouchedRefs, ...goalScopeFileRefs, ...explicitScopeFileRefs, ...goalExactRefsForSelection, ...boringMcpExactRefs, ...backendIntelligenceExactRefsForSelection]);
30324
- const collectionExactRefs = uniqueStrings16([...exactRefs, ...goalExactRefs, ...boringMcpEvidenceRefs, ...backendIntelligenceExactRefs, ...backendIntelligenceEvidenceRefs, ...categoryCollectionRefs]);
30323
+ const goalScopeFileRefsForSelection = evidenceInputPresent ? [] : goalScopeFileRefs;
30324
+ const exactRefs = uniqueStrings16([...explicitExactRefs, ...recentTouchedRefs, ...goalScopeFileRefsForSelection, ...explicitScopeFileRefs, ...goalExactRefsForSelection, ...boringMcpExactRefs, ...backendIntelligenceExactRefsForSelection]);
30325
+ const collectionExactRefs = uniqueStrings16([...exactRefs, ...goalScopeFileRefs, ...goalExactRefs, ...boringMcpEvidenceRefs, ...backendIntelligenceExactRefs, ...backendIntelligenceEvidenceRefs, ...categoryCollectionRefs]);
30325
30326
  const maxFilesToScan = positiveInteger5(args.maxFilesToScan, DEFAULT_MAX_FILES_TO_SCAN);
30326
30327
  const collectStartedAt = Date.now();
30327
30328
  let collected = await collectCandidates(repoPath, terms, maxFilesToScan, core, { ...args, exactRefs: collectionExactRefs, allowEvalReportEvidenceRefs });
@@ -42985,10 +42986,20 @@ var rheiStatusToolOutputSchema = toolOutputSchema({
42985
42986
  var rheiLedgerToolInputSchema = toolObjectSchema({
42986
42987
  op: {
42987
42988
  type: "string",
42988
- enum: ["overview", "report_cards", "trust_diff", "explain_line"],
42989
- description: "Ledger projection. Defaults to overview."
42989
+ enum: [
42990
+ "overview",
42991
+ "report_cards",
42992
+ "trust_diff",
42993
+ "explain_line",
42994
+ "why_symbol_changed",
42995
+ "what_changed_since"
42996
+ ],
42997
+ description: "Ledger projection. Defaults to overview. why_symbol_changed needs `symbol`; what_changed_since needs `sinceMs` or `sinceRef`."
42990
42998
  },
42991
- repoPath: STRING_SCHEMA
42999
+ repoPath: STRING_SCHEMA,
43000
+ symbol: { type: "string", description: "why_symbol_changed: symbol name or qualified name." },
43001
+ sinceMs: { type: "number", description: "what_changed_since: inclusive lower bound (epoch ms)." },
43002
+ sinceRef: { type: "string", description: "what_changed_since: git ref (e.g. HEAD~1, main) resolved to its commit time." }
42992
43003
  }, { additionalProperties: true });
42993
43004
  var rheiLedgerToolOutputSchema = toolOutputSchema({
42994
43005
  kind: STRING_SCHEMA,
@@ -42999,6 +43010,8 @@ var rheiLedgerToolOutputSchema = toolOutputSchema({
42999
43010
  reportCards: OBJECT_ARRAY_SCHEMA,
43000
43011
  trustDiff: OBJECT_SCHEMA,
43001
43012
  line: STRING_SCHEMA,
43013
+ whySymbolChanged: OBJECT_SCHEMA,
43014
+ whatChangedSince: OBJECT_SCHEMA,
43002
43015
  reportOnly: BOOLEAN_SCHEMA,
43003
43016
  advisoryOnly: BOOLEAN_SCHEMA,
43004
43017
  noAuthority: BOOLEAN_SCHEMA,
@@ -44262,7 +44275,7 @@ function briefRheiCodeIoHits(search) {
44262
44275
  return;
44263
44276
  const sqlSymbolSearch = search["searchMode"] === "sql_symbol_index" || search["searchTargetMode"] === "symbol_path";
44264
44277
  const results = codeIoRecordList(search["results"]).slice(0, 5).map((result) => {
44265
- const matchedLines = arrayValue4(result["matchedLines"]).map((line) => asRecord8(line)).filter((line) => Boolean(line)).slice(0, 4).map((line) => removeUndefined13({
44278
+ const matchedLines = arrayValue4(result["matchedLines"]).map((line) => asRecord8(line)).filter((line) => Boolean(line)).slice(0, 1).map((line) => removeUndefined13({
44266
44279
  line: line["line"],
44267
44280
  preview: line["preview"]
44268
44281
  }));
@@ -45727,6 +45740,9 @@ function repoPromptSearchDto(search) {
45727
45740
  const omittedTotal = codeIoNonNegativeNumber(search["omittedResultCount"] ?? coverage?.["matchedRefsOmitted"]) ?? (omittedByResultLimit > 0 ? omittedByResultLimit : undefined);
45728
45741
  const omittedContentMatches = returnedMatchCount === undefined ? undefined : Math.max(0, totalMatchCount - returnedMatchCount);
45729
45742
  const sizeLimitHit = coverage?.["truncated"] === true;
45743
+ const carriesNonRedundantInfo = omittedTotal !== undefined && omittedTotal > 0 || omittedContentMatches !== undefined && omittedContentMatches > 0 || sizeLimitHit || Boolean(cleanString23(scopeDiagnostic?.["summary"])) || Boolean(cleanString23(scopeDiagnostic?.["suggestion"])) || arrayValue4(search["warnings"]).length > 0 || perFileTotals.length > 0 || pathMatchLines.length > 0;
45744
+ if (!carriesNonRedundantInfo)
45745
+ return;
45730
45746
  return removeUndefined13({
45731
45747
  total_matches: totalMatchCount,
45732
45748
  total_files: codeIoNonNegativeNumber(coverage?.["searchedFiles"]) ?? resultCount,
@@ -46572,7 +46588,7 @@ function briefRheiCodeIoInspect(inspectResult) {
46572
46588
  const factCounts = asRecord8(codeStructureCoverage?.["factCounts"]);
46573
46589
  const emittedFactCounts = asRecord8(codeStructureCoverage?.["emittedFactCounts"]);
46574
46590
  const fullSymbolCount = positiveNumber12(factCounts?.["symbols"]) ?? symbols.length;
46575
- const emittedSymbolCount = positiveNumber12(emittedFactCounts?.["symbols"]) ?? symbols.length;
46591
+ const emittedSymbolCount = positiveNumber12(emittedFactCounts?.["symbols"]);
46576
46592
  const nextReadPlan = arrayValue4(inspectResult["nextReadPlan"]);
46577
46593
  const codeStructureDiagnostic = asRecord8(inspect["codeStructureDiagnostic"]) ?? asRecord8(inspectResult["codeStructureDiagnostic"]);
46578
46594
  const imports = [];
@@ -46611,9 +46627,8 @@ function briefRheiCodeIoInspect(inspectResult) {
46611
46627
  mode: inspect["mode"],
46612
46628
  codemapProfile: inspect["codemapProfile"],
46613
46629
  fileCount: files.length,
46614
- symbolCount: symbols.length,
46615
- totalSymbolCount: fullSymbolCount !== symbols.length ? fullSymbolCount : undefined,
46616
- emittedSymbolCount: emittedSymbolCount !== symbols.length && emittedSymbolCount !== fullSymbolCount ? emittedSymbolCount : undefined,
46630
+ symbolCount: fullSymbolCount,
46631
+ emittedSymbolCount: emittedSymbolCount !== undefined && emittedSymbolCount !== fullSymbolCount ? emittedSymbolCount : undefined,
46617
46632
  importCount: imports.length,
46618
46633
  functionSignatureCount: functionSignatures.length,
46619
46634
  codeStructureContent,
@@ -48946,10 +48961,10 @@ function compactObservability(observability) {
48946
48961
  categoryCollectionRefCount: observability["categoryCollectionRefCount"]
48947
48962
  });
48948
48963
  }
48949
- function compactContextReadPlanItem(value) {
48964
+ function compactContextReadPlanItem(value, includeContent = true) {
48950
48965
  if (!value)
48951
48966
  return;
48952
- const content = cleanString23(value["content"]);
48967
+ const content = includeContent ? cleanString23(value["content"]) : undefined;
48953
48968
  return removeUndefined13({
48954
48969
  readId: value["readId"],
48955
48970
  path: value["path"],
@@ -49055,7 +49070,7 @@ function compactContextForGoalRead(value) {
49055
49070
  const maxContentChars = 12000;
49056
49071
  const readItems = arrayValue4(read["reads"]).map((item) => asRecord8(item)).filter((item) => Boolean(item));
49057
49072
  const singleRead = readItems.length === 0 && (cleanString23(read["path"]) || cleanString23(read["content"])) ? [read] : [];
49058
- const readPlan = arrayValue4(read["readPlan"]).slice(0, 12).map((item) => compactContextReadPlanItem(asRecord8(item))).filter((item) => Boolean(item));
49073
+ const readPlan = arrayValue4(read["readPlan"]).slice(0, 12).map((item) => compactContextReadPlanItem(asRecord8(item), false)).filter((item) => Boolean(item));
49059
49074
  const compactReads = [...readItems, ...singleRead].slice(0, 12).map((item) => compactContextReadPlanItem(asRecord8(item))).filter((item) => Boolean(item));
49060
49075
  const paths = read["paths"] ?? singleRead.map((item) => item["path"]);
49061
49076
  const compactRead = removeUndefined13({
@@ -62181,13 +62196,19 @@ async function resolveMultiEditFrontier(input) {
62181
62196
  if (proposedPaths.length === 0)
62182
62197
  return unavailable("no_proposed_paths");
62183
62198
  const advisoryDepth = Math.min(activation2.effectiveDepth, 3);
62199
+ const runAdvisory = input.includeAdvisory ?? true;
62184
62200
  let gateResult;
62185
62201
  let advisoryResult;
62186
62202
  try {
62187
- [gateResult, advisoryResult] = await Promise.all([
62188
- runQuery({ repoPath: input.repoPath, op: "impact", changedPaths: proposedPaths, depth: 1, artifactRoot: input.artifactRoot, env }),
62189
- runQuery({ repoPath: input.repoPath, op: "impact", changedPaths: proposedPaths, depth: advisoryDepth, artifactRoot: input.artifactRoot, env })
62190
- ]);
62203
+ if (runAdvisory) {
62204
+ [gateResult, advisoryResult] = await Promise.all([
62205
+ runQuery({ repoPath: input.repoPath, op: "impact", changedPaths: proposedPaths, depth: 1, artifactRoot: input.artifactRoot, env }),
62206
+ runQuery({ repoPath: input.repoPath, op: "impact", changedPaths: proposedPaths, depth: advisoryDepth, artifactRoot: input.artifactRoot, env })
62207
+ ]);
62208
+ } else {
62209
+ gateResult = await runQuery({ repoPath: input.repoPath, op: "impact", changedPaths: proposedPaths, depth: 1, artifactRoot: input.artifactRoot, env });
62210
+ advisoryResult = undefined;
62211
+ }
62191
62212
  } catch (error) {
62192
62213
  return unavailable(`graph_query_threw: ${String(error).slice(0, 200)}`);
62193
62214
  }
@@ -62288,7 +62309,8 @@ async function goalStartReadPlanWithFrontier(contextForGoal, options = {}) {
62288
62309
  return { ...plan, frontier: { status: verdict.status, reason: verdict.reason } };
62289
62310
  }
62290
62311
  const planPaths = new Set(items.map((item) => cleanString23(item["path"])).filter(Boolean));
62291
- const addedPaths = uniqueStrings24([...verdict.gateEdges, ...verdict.advisoryFrontier]).filter((path) => !planPaths.has(path)).slice(0, 4);
62312
+ const frontierPaths = uniqueStrings24([...verdict.gateEdges, ...verdict.advisoryFrontier]);
62313
+ const addedPaths = frontierPaths.filter((path) => !planPaths.has(path)).slice(0, 4);
62292
62314
  const frontierItems = addedPaths.map((path, index) => ({
62293
62315
  order: items.length + index + 1,
62294
62316
  path,
@@ -62301,9 +62323,9 @@ async function goalStartReadPlanWithFrontier(contextForGoal, options = {}) {
62301
62323
  coverage: { ...asRecord8(plan["coverage"]) ?? {}, frontier: frontierItems.length },
62302
62324
  frontier: {
62303
62325
  status: verdict.status,
62304
- gateEdges: verdict.gateEdges.slice(0, 12),
62305
- advisoryFrontier: verdict.advisoryFrontier.slice(0, 12),
62306
- addedPaths
62326
+ addedPaths,
62327
+ gateEdgeCount: verdict.gateEdges.length,
62328
+ frontierPathCount: frontierPaths.length
62307
62329
  }
62308
62330
  };
62309
62331
  }
@@ -62868,6 +62890,7 @@ async function buildWritebackGraduation(input) {
62868
62890
  repoPath,
62869
62891
  proposedPaths: changedPaths,
62870
62892
  gateEligible: true,
62893
+ includeAdvisory: false,
62871
62894
  runQuery: input.runQuery
62872
62895
  });
62873
62896
  const verdictRecorded = input.synapseVerdictReceipt?.["status"] === "recorded";
@@ -66984,6 +67007,64 @@ function directCodeIoDeleteFile(args) {
66984
67007
  });
66985
67008
  }
66986
67009
 
67010
+ // src/codeContextTools/inverseCapture.ts
67011
+ import { deriveInverseEdit } from "./vendor/rhei-core/changeEpisode.js";
67012
+ function str2(value) {
67013
+ return typeof value === "string" && value.length > 0 ? value : undefined;
67014
+ }
67015
+ function firstString2(record, keys) {
67016
+ for (const key of keys) {
67017
+ const value = str2(record[key]);
67018
+ if (value !== undefined)
67019
+ return value;
67020
+ }
67021
+ return;
67022
+ }
67023
+ var SEARCH_KEYS = ["search", "find", "old_string", "oldString", "oldText"];
67024
+ var REPLACE_KEYS = ["replace", "replacement", "with", "new_string", "newString", "newText"];
67025
+ function inverseEditsForFile(file) {
67026
+ const path = str2(file["path"]);
67027
+ if (!path)
67028
+ return [];
67029
+ const isDelete = file["delete"] === true || file["mode"] === "delete";
67030
+ if (isDelete)
67031
+ return [deriveInverseEdit({ kind: "delete", path })];
67032
+ if (file["create"] === true)
67033
+ return [deriveInverseEdit({ kind: "create", path })];
67034
+ const rewrite = str2(file["rewrite"]) ?? str2(file["content"]);
67035
+ if (rewrite !== undefined)
67036
+ return [deriveInverseEdit({ kind: "rewrite", path, afterContent: rewrite })];
67037
+ const edits = Array.isArray(file["edits"]) ? file["edits"] : [];
67038
+ const inverses = [];
67039
+ for (const raw of edits) {
67040
+ if (typeof raw !== "object" || raw === null)
67041
+ continue;
67042
+ const edit = raw;
67043
+ const search = firstString2(edit, SEARCH_KEYS);
67044
+ const replace = firstString2(edit, REPLACE_KEYS);
67045
+ if (search === undefined || replace === undefined)
67046
+ continue;
67047
+ inverses.push(deriveInverseEdit({ kind: "search_replace", path, search, replace }));
67048
+ }
67049
+ return inverses;
67050
+ }
67051
+ function inverseEditsForFiles(files) {
67052
+ if (!Array.isArray(files))
67053
+ return [];
67054
+ const bundles = [];
67055
+ for (const file of files) {
67056
+ if (typeof file !== "object" || file === null)
67057
+ continue;
67058
+ const path = str2(file["path"]);
67059
+ if (!path)
67060
+ continue;
67061
+ const inverse = inverseEditsForFile(file);
67062
+ if (inverse.length > 0)
67063
+ bundles.push({ path, inverse });
67064
+ }
67065
+ return bundles;
67066
+ }
67067
+
66987
67068
  // src/codeContextTools/codeIoToolRuntimeAndServiceIntelligence.ts
66988
67069
  var RECENT_SEARCH_TRIAL_TTL_MS = 10 * 60 * 1000;
66989
67070
  var recentSearchTrials = new Map;
@@ -67804,12 +67885,18 @@ function codeIoRuntimeRestartInfo() {
67804
67885
  restartAction: asRecord8(runtime["restartAction"])
67805
67886
  };
67806
67887
  }
67807
- function codeIoRuntimeWriteBlockRequired(op, _reloadStatus) {
67808
- return op === "edit" || op === "publish" || op === "revert";
67888
+ function codeIoRuntimeWriteBlockRequired(op, reloadStatus, targetPaths = []) {
67889
+ if (op !== "edit" && op !== "publish" && op !== "revert")
67890
+ return false;
67891
+ if (reloadStatus["editEngineStale"] === true)
67892
+ return true;
67893
+ if (targetPaths.length === 0)
67894
+ return true;
67895
+ return targetPaths.some((path) => /(?:^|\/)packages\/mcp-server\/src\//.test(path));
67809
67896
  }
67810
67897
  function codeIoRuntimeFreshnessAdvisory(op, targetPaths = []) {
67811
67898
  const restartInfo = codeIoRuntimeRestartInfo();
67812
- if (!restartInfo || codeIoRuntimeWriteBlockRequired(op, restartInfo.reloadStatus))
67899
+ if (!restartInfo || codeIoRuntimeWriteBlockRequired(op, restartInfo.reloadStatus, targetPaths))
67813
67900
  return;
67814
67901
  return removeUndefined13({
67815
67902
  kind: "rhei_code_io_runtime_freshness",
@@ -67861,7 +67948,7 @@ function codeIoRuntimeStaleWriteBlock(op, targetPaths = []) {
67861
67948
  const restartInfo = codeIoRuntimeRestartInfo();
67862
67949
  if (!restartInfo)
67863
67950
  return;
67864
- if (!codeIoRuntimeWriteBlockRequired(op, restartInfo.reloadStatus))
67951
+ if (!codeIoRuntimeWriteBlockRequired(op, restartInfo.reloadStatus, targetPaths))
67865
67952
  return;
67866
67953
  return removeUndefined13({
67867
67954
  kind: op === "publish" || op === "revert" ? "rhei_code_git_publish" : "rhei_code_edit",
@@ -68657,6 +68744,7 @@ async function recordCodeIoEditProvenance(args, edit, files, dirtyEventReceipt)
68657
68744
  goal: cleanString23(args["goal"]) ?? cleanString23(args["message"]),
68658
68745
  createdAt
68659
68746
  })}`;
68747
+ const inverseByPath = new Map(inverseEditsForFiles(files).map((bundle) => [bundle.path, bundle.inverse]));
68660
68748
  await getLedgerSink({ repoPath }).recordEditProvenance(buildEditProvenanceV1({
68661
68749
  provenanceId,
68662
68750
  source: "codex_mcp",
@@ -68669,7 +68757,11 @@ async function recordCodeIoEditProvenance(args, edit, files, dirtyEventReceipt)
68669
68757
  repoEpoch: cleanString23(asRecord8(edit["receipt"])?.["repoEpoch"]) ?? "repo_epoch_unknown_v1",
68670
68758
  scopePolicyHash: cleanString23(edit["scopePolicyHash"]) ?? UNKNOWN_SCOPE_POLICY_HASH,
68671
68759
  files: changedPaths,
68672
- hunks: changedPaths.map((path) => ({ filePath: path, startLine: 0, endLine: 0, kind: codeIoEditHunkKind(args) })),
68760
+ hunks: changedPaths.map((path) => {
68761
+ const inverse = inverseByPath.get(path);
68762
+ const hunk = { filePath: path, startLine: 0, endLine: 0, kind: codeIoEditHunkKind(args) };
68763
+ return inverse === undefined ? hunk : { ...hunk, inverse };
68764
+ }),
68673
68765
  symbols: codeIoTouchedSymbols(files, edit),
68674
68766
  mcpReceipts: receiptRefs,
68675
68767
  nonMcpCommands: [],
@@ -69259,6 +69351,8 @@ async function buildRheiCodeIoPayload(args) {
69259
69351
  const postEditIndexWarmup = codeIoPostEditIndexWarmupReceipt(args, edit, files, dirtyEventReceipt);
69260
69352
  const frontierGateForResponse = frontierGate ? removeUndefined13({
69261
69353
  ...frontierGate,
69354
+ advisoryFrontier: undefined,
69355
+ advisoryFrontierCount: frontierGate.advisoryFrontier.length,
69262
69356
  evidence: undefined,
69263
69357
  evidenceSummary: frontierGate.evidence ? removeUndefined13({
69264
69358
  sourceEpochHash: frontierGate.evidence.sourceEpochHash,
@@ -70252,8 +70346,6 @@ var RHEI_MCP_PUBLIC_LOCAL_TOOL_NAMES = [
70252
70346
  "index",
70253
70347
  "status",
70254
70348
  "ledger",
70255
- "agent_run",
70256
- "agent_manage",
70257
70349
  "oracle_send",
70258
70350
  "oracle_utils"
70259
70351
  ];
@@ -73226,8 +73318,18 @@ var rheiStatusOutputSchema = z3.object({
73226
73318
  productionAuthority: z3.literal(false).optional()
73227
73319
  }).passthrough();
73228
73320
  var rheiLedgerInputSchema = z3.object({
73229
- op: z3.enum(["overview", "report_cards", "trust_diff", "explain_line"]).optional(),
73230
- repoPath: z3.string().min(1).optional()
73321
+ op: z3.enum([
73322
+ "overview",
73323
+ "report_cards",
73324
+ "trust_diff",
73325
+ "explain_line",
73326
+ "why_symbol_changed",
73327
+ "what_changed_since"
73328
+ ]).optional(),
73329
+ repoPath: z3.string().min(1).optional(),
73330
+ symbol: z3.string().optional(),
73331
+ sinceMs: z3.number().optional(),
73332
+ sinceRef: z3.string().optional()
73231
73333
  }).passthrough();
73232
73334
  var rheiLedgerOutputSchema = z3.object({
73233
73335
  kind: z3.string().optional(),
@@ -76775,31 +76877,31 @@ async function emitGoalMemoryCandidates(syncConfig, args) {
76775
76877
  const raw = args.structured["memoryCandidates"];
76776
76878
  if (!Array.isArray(raw) || raw.length === 0)
76777
76879
  return;
76778
- const str2 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
76880
+ const str3 = (value) => typeof value === "string" && value.trim() ? value.trim() : undefined;
76779
76881
  const strArray = (value) => Array.isArray(value) ? value.filter((v) => typeof v === "string") : undefined;
76780
- const topGoal = str2(args.structured["goal"]);
76781
- const topSession = str2(args.structured["goalSessionId"]) ?? str2(args.structured["activeGoalId"]);
76882
+ const topGoal = str3(args.structured["goal"]);
76883
+ const topSession = str3(args.structured["goalSessionId"]) ?? str3(args.structured["activeGoalId"]);
76782
76884
  for (const entry of raw.slice(0, 20)) {
76783
76885
  if (!entry || typeof entry !== "object")
76784
76886
  continue;
76785
76887
  const candidate = entry;
76786
- const title = str2(candidate["title"]) ?? str2(candidate["summary"]);
76787
- const summary = str2(candidate["summary"]) ?? str2(candidate["title"]);
76888
+ const title = str3(candidate["title"]) ?? str3(candidate["summary"]);
76889
+ const summary = str3(candidate["summary"]) ?? str3(candidate["title"]);
76788
76890
  if (!title || !summary)
76789
76891
  continue;
76790
76892
  try {
76791
76893
  await postRetryableRheiSyncReceipt(syncConfig, "/v1/mcp/sync/goal-memory", {
76792
76894
  projectId: args.projectId,
76793
76895
  repoId: args.repoId,
76794
- sourceKind: str2(candidate["sourceKind"]) ?? "goal_finish",
76795
- candidateKind: str2(candidate["candidateKind"]) ?? "goal_session_learning",
76896
+ sourceKind: str3(candidate["sourceKind"]) ?? "goal_finish",
76897
+ candidateKind: str3(candidate["candidateKind"]) ?? "goal_session_learning",
76796
76898
  title,
76797
76899
  summary,
76798
- subjectKind: ["path", "symbol", "service"].includes(String(candidate["subjectKind"])) ? str2(candidate["subjectKind"]) : undefined,
76799
- subjectRef: str2(candidate["subjectRef"]),
76800
- normalizedRule: str2(candidate["normalizedRule"]) ?? str2(candidate["rule"]),
76801
- goal: str2(candidate["goal"]) ?? topGoal,
76802
- goalSessionId: str2(candidate["goalSessionId"]) ?? topSession,
76900
+ subjectKind: ["path", "symbol", "service"].includes(String(candidate["subjectKind"])) ? str3(candidate["subjectKind"]) : undefined,
76901
+ subjectRef: str3(candidate["subjectRef"]),
76902
+ normalizedRule: str3(candidate["normalizedRule"]) ?? str3(candidate["rule"]),
76903
+ goal: str3(candidate["goal"]) ?? topGoal,
76904
+ goalSessionId: str3(candidate["goalSessionId"]) ?? topSession,
76803
76905
  filePaths: strArray(candidate["filePaths"]) ?? strArray(candidate["files"]),
76804
76906
  targetRefs: strArray(candidate["targetRefs"]),
76805
76907
  evidenceRefs: strArray(candidate["evidenceRefs"])
@@ -79497,14 +79599,14 @@ async function readFocusLedger(repoPath) {
79497
79599
  const raw = JSON.parse(await readFile22(path, "utf8"));
79498
79600
  const items = asArray(raw.items).flatMap((item) => {
79499
79601
  const record = asRecord11(item);
79500
- const stableKey = str2(record?.["stableKey"]);
79501
- const source = str2(record?.["source"]);
79602
+ const stableKey = str3(record?.["stableKey"]);
79603
+ const source = str3(record?.["source"]);
79502
79604
  if (!stableKey || !source)
79503
79605
  return [];
79504
79606
  return [{
79505
79607
  stableKey,
79506
79608
  source,
79507
- goalId: str2(record?.["goalId"]),
79609
+ goalId: str3(record?.["goalId"]),
79508
79610
  showCount: num(record?.["showCount"]) ?? 0,
79509
79611
  lastShownAt: num(record?.["lastShownAt"]),
79510
79612
  snoozedUntil: num(record?.["snoozedUntil"]),
@@ -79792,7 +79894,7 @@ function extractReuse(report) {
79792
79894
  continue;
79793
79895
  const evidence2 = asRecord11(record["evidence"]);
79794
79896
  const dup = asRecord11(evidence2?.["duplicateEvidence"]);
79795
- const subtype = str2(dup?.["reuseSubtype"]) ?? str2(record["candidateKind"]) ?? "other";
79897
+ const subtype = str3(dup?.["reuseSubtype"]) ?? str3(record["candidateKind"]) ?? "other";
79796
79898
  counts.set(subtype, (counts.get(subtype) ?? 0) + 1);
79797
79899
  const member = representativeMember(asArray(evidence2?.["memberSet"]));
79798
79900
  if (member)
@@ -79815,13 +79917,13 @@ function extractDeadCode(report) {
79815
79917
  if (!record)
79816
79918
  continue;
79817
79919
  const evidence2 = asRecord11(record["evidence"]);
79818
- const kind = str2(evidence2?.["exportKind"]) ?? str2(record["candidateKind"]) ?? "other";
79920
+ const kind = str3(evidence2?.["exportKind"]) ?? str3(record["candidateKind"]) ?? "other";
79819
79921
  counts.set(kind, (counts.get(kind) ?? 0) + 1);
79820
- const path = str2(evidence2?.["path"]);
79922
+ const path = str3(evidence2?.["path"]);
79821
79923
  if (path)
79822
79924
  paths.push(path);
79823
79925
  if (!top) {
79824
- const name = str2(record["displayName"]) ?? str2(evidence2?.["exportName"]) ?? "symbol";
79926
+ const name = str3(record["displayName"]) ?? str3(evidence2?.["exportName"]) ?? "symbol";
79825
79927
  top = `${name} (${kind})${path ? ` · ${path}` : ""}`;
79826
79928
  }
79827
79929
  }
@@ -79835,14 +79937,14 @@ function extractCoverage(report) {
79835
79937
  const record = asRecord11(entry);
79836
79938
  if (!record)
79837
79939
  continue;
79838
- const path = str2(record["path"]);
79940
+ const path = str3(record["path"]);
79839
79941
  if (path) {
79840
79942
  paths.push(path);
79841
79943
  if (!top)
79842
79944
  top = path;
79843
79945
  }
79844
79946
  for (const type of asArray(record["evidenceTypes"])) {
79845
- const label = str2(type);
79947
+ const label = str3(type);
79846
79948
  if (label)
79847
79949
  counts.set(label, (counts.get(label) ?? 0) + 1);
79848
79950
  }
@@ -79852,7 +79954,7 @@ function extractCoverage(report) {
79852
79954
  function extractGeneric(report) {
79853
79955
  const paths = [];
79854
79956
  for (const candidate of asArray(report["candidates"])) {
79855
- const path = str2(asRecord11(candidate)?.["path"]) ?? str2(asRecord11(asRecord11(candidate)?.["evidence"])?.["path"]);
79957
+ const path = str3(asRecord11(candidate)?.["path"]) ?? str3(asRecord11(asRecord11(candidate)?.["evidence"])?.["path"]);
79856
79958
  if (path)
79857
79959
  paths.push(path);
79858
79960
  }
@@ -79927,21 +80029,21 @@ function extractHotspots(report) {
79927
80029
  const evidence2 = asRecord11(asRecord11(candidate)?.["evidence"]);
79928
80030
  for (const member of asArray(evidence2?.["memberSet"])) {
79929
80031
  const record = asRecord11(member);
79930
- const symbol = str2(record?.["displayName"]);
80032
+ const symbol = str3(record?.["displayName"]);
79931
80033
  const inEdges = num(record?.["inEdgeCount"]);
79932
80034
  if (!symbol || inEdges === undefined)
79933
80035
  continue;
79934
80036
  const display = symbol.includes("/") ? basename18(symbol) : symbol;
79935
80037
  const existing = best.get(symbol);
79936
80038
  if (!existing || inEdges > existing.inEdges)
79937
- best.set(symbol, { symbol: display, inEdges, path: str2(record?.["path"]) });
80039
+ best.set(symbol, { symbol: display, inEdges, path: str3(record?.["path"]) });
79938
80040
  }
79939
80041
  }
79940
80042
  return [...best.values()].sort((a, b) => b.inEdges - a.inEdges).slice(0, 4);
79941
80043
  }
79942
80044
  function extractDriftTop(report) {
79943
80045
  const first = asRecord11(asArray(report?.["findings"])[0]);
79944
- return str2(first?.["path"]) ?? str2(first?.["summary"]) ?? str2(first?.["title"]) ?? str2(first?.["detail"]);
80046
+ return str3(first?.["path"]) ?? str3(first?.["summary"]) ?? str3(first?.["title"]) ?? str3(first?.["detail"]);
79945
80047
  }
79946
80048
  async function buildModuleRows(repoPath, findings) {
79947
80049
  const weights = await scanModuleWeights(repoPath);
@@ -80035,10 +80137,10 @@ function representativeMember(members) {
80035
80137
  let fallback;
80036
80138
  for (const member of members) {
80037
80139
  const record = asRecord11(member);
80038
- const path = str2(record?.["path"]);
80140
+ const path = str3(record?.["path"]);
80039
80141
  if (!path)
80040
80142
  continue;
80041
- if (str2(record?.["role"]) === "representative")
80143
+ if (str3(record?.["role"]) === "representative")
80042
80144
  return path;
80043
80145
  fallback ??= path;
80044
80146
  }
@@ -80056,7 +80158,7 @@ function asArray(value) {
80056
80158
  function asRecord11(value) {
80057
80159
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
80058
80160
  }
80059
- function str2(value) {
80161
+ function str3(value) {
80060
80162
  return typeof value === "string" && value.length > 0 ? value : undefined;
80061
80163
  }
80062
80164
  function num(value) {
@@ -80080,6 +80182,173 @@ function tidyPath(absolute, repoPath) {
80080
80182
  return rel && !rel.startsWith("..") ? rel : absolute;
80081
80183
  }
80082
80184
 
80185
+ // src/codeContextTools/symbolHistoryReader.ts
80186
+ import { join as join25 } from "node:path";
80187
+ import {
80188
+ resolveProvenanceSymbols,
80189
+ whatChangedSince,
80190
+ whySymbolChanged
80191
+ } from "./vendor/rhei-core/evidenceLedger.js";
80192
+ function resolveSymbolHistoryDeps(repoPath, env = process.env) {
80193
+ const root = localSqlArtifactRoot(repoPath, env);
80194
+ return {
80195
+ ledgerDbPath: join25(root, "evidence-ledger.sqlite"),
80196
+ codeIndexDbPath: join25(root, "code-index.sqlite")
80197
+ };
80198
+ }
80199
+ async function loadSqlite() {
80200
+ try {
80201
+ return await import("node:sqlite");
80202
+ } catch {
80203
+ return;
80204
+ }
80205
+ }
80206
+ function openReadOnly(mod, path) {
80207
+ try {
80208
+ return new mod.DatabaseSync(path, { readOnly: true });
80209
+ } catch {
80210
+ return;
80211
+ }
80212
+ }
80213
+ function parseStringArray(value) {
80214
+ if (typeof value !== "string" || value.length === 0)
80215
+ return [];
80216
+ try {
80217
+ const parsed = JSON.parse(value);
80218
+ return Array.isArray(parsed) ? parsed.filter((entry) => typeof entry === "string") : [];
80219
+ } catch {
80220
+ return [];
80221
+ }
80222
+ }
80223
+ function toProvenanceRow(raw) {
80224
+ return {
80225
+ provenanceId: raw.provenance_id,
80226
+ goal: raw.goal ?? undefined,
80227
+ actor: raw.actor ?? undefined,
80228
+ createdAt: raw.created_at ?? undefined,
80229
+ files: parseStringArray(raw.files_json),
80230
+ symbols: parseStringArray(raw.symbols_json)
80231
+ };
80232
+ }
80233
+ function symbolSpansForPaths(db, paths) {
80234
+ const unique = [...new Set(paths)].filter(Boolean);
80235
+ if (unique.length === 0)
80236
+ return [];
80237
+ const placeholders = unique.map(() => "?").join(",");
80238
+ const rows = db.prepare(`SELECT COALESCE(s.qualified_name, s.name) AS key, f.path AS path,
80239
+ s.start_line AS startLine, s.end_line AS endLine
80240
+ FROM symbols s JOIN files f ON f.file_id = s.file_id
80241
+ WHERE f.path IN (${placeholders})`).all(...unique);
80242
+ return rows.map((row) => ({
80243
+ key: String(row["key"]),
80244
+ path: String(row["path"]),
80245
+ startLine: Number(row["startLine"]),
80246
+ endLine: Number(row["endLine"])
80247
+ }));
80248
+ }
80249
+ function definingPathsForSymbol(db, symbol) {
80250
+ const rows = db.prepare(`SELECT DISTINCT f.path AS path FROM symbols s JOIN files f ON f.file_id = s.file_id
80251
+ WHERE s.qualified_name = ? OR s.name = ?`).all(symbol, symbol);
80252
+ return rows.map((row) => String(row["path"]));
80253
+ }
80254
+ function provenanceRowsTouchingPaths(db, paths) {
80255
+ const unique = [...new Set(paths)].filter(Boolean);
80256
+ if (unique.length === 0)
80257
+ return [];
80258
+ const likeClause = unique.map(() => "files_json LIKE ?").join(" OR ");
80259
+ const likeParams = unique.map((path) => `%${JSON.stringify(path)}%`);
80260
+ const rows = db.prepare(`SELECT provenance_id, goal, actor, created_at, files_json, symbols_json
80261
+ FROM edit_provenance WHERE ${likeClause} ORDER BY created_at DESC`).all(...likeParams);
80262
+ const want = new Set(unique);
80263
+ return rows.map(toProvenanceRow).filter((row) => (row.files ?? []).some((file) => want.has(file)));
80264
+ }
80265
+ async function resolveSinceMs(query2) {
80266
+ if (typeof query2.sinceMs === "number" && Number.isFinite(query2.sinceMs))
80267
+ return query2.sinceMs;
80268
+ if (query2.sinceRef && query2.repoPath) {
80269
+ try {
80270
+ const { execFileSync: execFileSync5 } = await import("node:child_process");
80271
+ const out = execFileSync5("git", ["-C", query2.repoPath, "log", "-1", "--format=%ct", query2.sinceRef], {
80272
+ encoding: "utf8",
80273
+ timeout: 2000,
80274
+ windowsHide: true
80275
+ }).trim();
80276
+ const seconds = Number.parseInt(out, 10);
80277
+ if (Number.isFinite(seconds))
80278
+ return seconds * 1000;
80279
+ } catch {}
80280
+ }
80281
+ return;
80282
+ }
80283
+ var EMPTY_WHAT_CHANGED = {
80284
+ sinceMs: 0,
80285
+ editCount: 0,
80286
+ files: [],
80287
+ symbols: [],
80288
+ goals: []
80289
+ };
80290
+ async function whatChangedSinceFromLedger(query2, deps) {
80291
+ const sinceMs = await resolveSinceMs(query2);
80292
+ const mod = await loadSqlite();
80293
+ if (!mod || sinceMs === undefined)
80294
+ return { ...EMPTY_WHAT_CHANGED, sinceMs: sinceMs ?? 0, available: false };
80295
+ const ledger = openReadOnly(mod, deps.ledgerDbPath);
80296
+ if (!ledger)
80297
+ return { ...EMPTY_WHAT_CHANGED, sinceMs, available: false };
80298
+ try {
80299
+ const raw = ledger.prepare(`SELECT provenance_id, goal, actor, created_at, files_json, symbols_json
80300
+ FROM edit_provenance WHERE created_at >= ? ORDER BY created_at DESC`).all(sinceMs);
80301
+ const rows = raw.map(toProvenanceRow);
80302
+ const resolved = withResolvedSymbols(mod, deps, rows);
80303
+ return { ...whatChangedSince({ sinceMs }, resolved), available: true };
80304
+ } catch {
80305
+ return { ...EMPTY_WHAT_CHANGED, sinceMs, available: false };
80306
+ } finally {
80307
+ ledger.close();
80308
+ }
80309
+ }
80310
+ async function whySymbolChangedFromLedger(symbol, deps) {
80311
+ const empty = { symbol, editCount: 0, edits: [], goals: [] };
80312
+ const mod = await loadSqlite();
80313
+ if (!mod)
80314
+ return { ...empty, available: false };
80315
+ const index = openReadOnly(mod, deps.codeIndexDbPath);
80316
+ const ledger = openReadOnly(mod, deps.ledgerDbPath);
80317
+ if (!index || !ledger) {
80318
+ index?.close();
80319
+ ledger?.close();
80320
+ return { ...empty, available: false };
80321
+ }
80322
+ try {
80323
+ const paths = definingPathsForSymbol(index, symbol);
80324
+ if (paths.length === 0)
80325
+ return { ...empty, available: true };
80326
+ const rows = provenanceRowsTouchingPaths(ledger, paths).map((row) => ({
80327
+ ...row,
80328
+ symbols: [...new Set([...row.symbols ?? [], symbol])]
80329
+ }));
80330
+ return { ...whySymbolChanged(symbol, rows), available: true };
80331
+ } catch {
80332
+ return { ...empty, available: false };
80333
+ } finally {
80334
+ index.close();
80335
+ ledger.close();
80336
+ }
80337
+ }
80338
+ function withResolvedSymbols(mod, deps, rows) {
80339
+ const paths = rows.flatMap((row) => row.files ?? []);
80340
+ if (paths.length === 0)
80341
+ return [...rows];
80342
+ const index = openReadOnly(mod, deps.codeIndexDbPath);
80343
+ if (!index)
80344
+ return [...rows];
80345
+ try {
80346
+ return resolveProvenanceSymbols(rows, symbolSpansForPaths(index, paths));
80347
+ } finally {
80348
+ index.close();
80349
+ }
80350
+ }
80351
+
80083
80352
  // src/tools.ts
80084
80353
  var ENTITY_TYPES = [
80085
80354
  "character",
@@ -81741,8 +82010,46 @@ async function executeRemoteBetaTool(toolName, args, config) {
81741
82010
  });
81742
82011
  }
81743
82012
  if (toolName === "ledger") {
82013
+ const repoPath = typeof args["repoPath"] === "string" ? args["repoPath"] : undefined;
82014
+ const opRaw = typeof args["op"] === "string" ? args["op"].trim().toLowerCase() : "overview";
82015
+ if (opRaw === "why_symbol_changed" || opRaw === "what_changed_since") {
82016
+ const deps = resolveSymbolHistoryDeps(repoPath ?? process.cwd());
82017
+ if (opRaw === "why_symbol_changed") {
82018
+ const symbol = typeof args["symbol"] === "string" ? args["symbol"].trim() : "";
82019
+ const result2 = symbol ? await whySymbolChangedFromLedger(symbol, deps) : { symbol: "", editCount: 0, edits: [], goals: [], available: false };
82020
+ return textJsonResult({
82021
+ kind: "rhei_ledger",
82022
+ schemaVersion: 1,
82023
+ op: "why_symbol_changed",
82024
+ status: result2.available ? "ready" : "unavailable",
82025
+ whySymbolChanged: result2,
82026
+ reportOnly: true,
82027
+ advisoryOnly: true,
82028
+ noAuthority: true,
82029
+ candidatesNotProof: true,
82030
+ sourceWrites: false,
82031
+ productionAuthority: false
82032
+ });
82033
+ }
82034
+ const sinceMs = typeof args["sinceMs"] === "number" ? args["sinceMs"] : undefined;
82035
+ const sinceRef = typeof args["sinceRef"] === "string" ? args["sinceRef"] : undefined;
82036
+ const result = await whatChangedSinceFromLedger({ sinceMs, sinceRef, repoPath }, deps);
82037
+ return textJsonResult({
82038
+ kind: "rhei_ledger",
82039
+ schemaVersion: 1,
82040
+ op: "what_changed_since",
82041
+ status: result.available ? "ready" : "unavailable",
82042
+ whatChangedSince: result,
82043
+ reportOnly: true,
82044
+ advisoryOnly: true,
82045
+ noAuthority: true,
82046
+ candidatesNotProof: true,
82047
+ sourceWrites: false,
82048
+ productionAuthority: false
82049
+ });
82050
+ }
81744
82051
  const overview = await buildStatusOverview({
81745
- repoPath: typeof args["repoPath"] === "string" ? args["repoPath"] : undefined,
82052
+ repoPath,
81746
82053
  full: true,
81747
82054
  skipMcpSmoke: true,
81748
82055
  mcp: {
@@ -81753,7 +82060,6 @@ async function executeRemoteBetaTool(toolName, args, config) {
81753
82060
  }
81754
82061
  });
81755
82062
  const evidenceLedger = overview.overview["evidenceLedger"];
81756
- const opRaw = typeof args["op"] === "string" ? args["op"].trim().toLowerCase() : "overview";
81757
82063
  const op = opRaw === "report_cards" || opRaw === "trust_diff" || opRaw === "explain_line" ? opRaw : "overview";
81758
82064
  const line = evidenceLedger ? evidenceLedgerLine(evidenceLedger) : undefined;
81759
82065
  return textJsonResult({
@@ -82538,8 +82844,6 @@ function createLocalCodeContextServer(args) {
82538
82844
  };
82539
82845
  const explicitAllowlist = process.env["RHEI_MCP_LOCAL_TOOL_ALLOWLIST"] !== undefined;
82540
82846
  for (const tool of RHEI_LOCAL_STATUS_TOOLS) {
82541
- if (!publicLocalToolNameSet.has(tool.name))
82542
- continue;
82543
82847
  if (explicitAllowlist && localToolAllowlist && !localToolAllowlist.has(tool.name))
82544
82848
  continue;
82545
82849
  if (!explicitAllowlist && toolSurface !== "top_level_orchestrator")
@@ -0,0 +1,288 @@
1
+ // ../core/src/changeEpisode/types.ts
2
+ var CHANGE_EPISODE_SCHEMA_VERSION = 1;
3
+ // ../core/src/changeEpisode/builders.ts
4
+ var CHANGE_EPISODE_ACTORS = ["human", "agent", "mixed"];
5
+ var CLOSED_EPISODE_STATUSES = [
6
+ "validated",
7
+ "failed",
8
+ "reverted",
9
+ "committed"
10
+ ];
11
+ function openChangeEpisode(input) {
12
+ if (!CHANGE_EPISODE_ACTORS.includes(input.actor)) {
13
+ throw new Error(`openChangeEpisode: invalid actor "${String(input.actor)}"`);
14
+ }
15
+ if (!isValidTimestamp(input.startedAt)) {
16
+ throw new Error(`openChangeEpisode: invalid startedAt ${String(input.startedAt)}`);
17
+ }
18
+ const episodeId = cleanString(input.episodeId) ?? `change-episode:${stableHash({
19
+ goalId: input.goalId,
20
+ actor: input.actor,
21
+ startedAt: input.startedAt,
22
+ goal: input.goal,
23
+ gitBaseSha: input.gitBaseSha
24
+ })}`;
25
+ return removeUndefined({
26
+ episodeId,
27
+ goalId: cleanString(input.goalId),
28
+ actor: input.actor,
29
+ startedAt: input.startedAt,
30
+ goal: cleanString(input.goal),
31
+ contextReceipts: uniqueStrings(input.contextReceipts ?? []),
32
+ fileDeltas: uniqueStrings(input.fileDeltas ?? []),
33
+ symbolDeltas: uniqueStrings(input.symbolDeltas ?? []),
34
+ edgeDeltas: uniqueStrings(input.edgeDeltas ?? []),
35
+ memoryDeltas: uniqueStrings(input.memoryDeltas ?? []),
36
+ validationRuns: uniqueStrings(input.validationRuns ?? []),
37
+ gitBaseSha: cleanString(input.gitBaseSha),
38
+ worktreeDirtyHashBefore: cleanString(input.worktreeDirtyHashBefore),
39
+ status: "open"
40
+ });
41
+ }
42
+ function attachEpisodeRows(episode, rows) {
43
+ return {
44
+ ...episode,
45
+ contextReceipts: uniqueStrings([...episode.contextReceipts, ...rows.contextReceipts ?? []]),
46
+ fileDeltas: uniqueStrings([...episode.fileDeltas, ...rows.fileDeltas ?? []]),
47
+ symbolDeltas: uniqueStrings([...episode.symbolDeltas, ...rows.symbolDeltas ?? []]),
48
+ edgeDeltas: uniqueStrings([...episode.edgeDeltas, ...rows.edgeDeltas ?? []]),
49
+ memoryDeltas: uniqueStrings([...episode.memoryDeltas, ...rows.memoryDeltas ?? []]),
50
+ validationRuns: uniqueStrings([...episode.validationRuns, ...rows.validationRuns ?? []])
51
+ };
52
+ }
53
+ function closeChangeEpisode(episode, input) {
54
+ if (episode.status !== "open") {
55
+ throw new Error(`closeChangeEpisode: episode ${episode.episodeId} is already closed (status "${episode.status}")`);
56
+ }
57
+ if (!CLOSED_EPISODE_STATUSES.includes(input.status)) {
58
+ throw new Error(`closeChangeEpisode: invalid target status "${String(input.status)}"`);
59
+ }
60
+ if (!isValidTimestamp(input.endedAt) || input.endedAt < episode.startedAt) {
61
+ throw new Error(`closeChangeEpisode: invalid endedAt ${String(input.endedAt)} (startedAt ${episode.startedAt})`);
62
+ }
63
+ return removeUndefined({
64
+ ...episode,
65
+ status: input.status,
66
+ endedAt: input.endedAt,
67
+ gitHeadSha: cleanString(input.gitHeadSha) ?? episode.gitHeadSha,
68
+ worktreeDirtyHashAfter: cleanString(input.worktreeDirtyHashAfter) ?? episode.worktreeDirtyHashAfter
69
+ });
70
+ }
71
+ function planEpisodeRevert(episode, fileDeltas, opts = {}) {
72
+ const deltasById = new Map;
73
+ for (const delta of fileDeltas) {
74
+ if (!deltasById.has(delta.fileDeltaId))
75
+ deltasById.set(delta.fileDeltaId, delta);
76
+ }
77
+ const deltasToInvert = [];
78
+ const deltasExcluded = [];
79
+ const conflicts = [];
80
+ for (const fileDeltaId of uniqueStrings(episode.fileDeltas)) {
81
+ const delta = deltasById.get(fileDeltaId);
82
+ if (!delta) {
83
+ conflicts.push({ fileDeltaId, reason: "missing_delta_row" });
84
+ continue;
85
+ }
86
+ const currentPath = cleanString(delta.pathAfter) ?? cleanString(delta.pathBefore);
87
+ if (currentPath !== undefined && opts.excludePaths?.(currentPath) === true) {
88
+ deltasExcluded.push({ id: fileDeltaId, reason: "excluded_by_filter" });
89
+ continue;
90
+ }
91
+ if (cleanString(delta.inversePatch) === undefined) {
92
+ conflicts.push({ fileDeltaId, reason: "no_inverse_patch" });
93
+ continue;
94
+ }
95
+ deltasToInvert.push(fileDeltaId);
96
+ }
97
+ const status = deltasToInvert.length === 0 ? "blocked" : conflicts.length > 0 || deltasExcluded.length > 0 ? "partial" : "plannable";
98
+ return {
99
+ episodeId: episode.episodeId,
100
+ deltasToInvert,
101
+ deltasExcluded,
102
+ conflicts,
103
+ status
104
+ };
105
+ }
106
+ function fileDeltasFromProvenance(rows) {
107
+ return rows.map((row) => {
108
+ const actor = (() => {
109
+ const value = cleanString(row.actor);
110
+ return value === "human" || value?.startsWith("human_") ? "human" : "agent";
111
+ })();
112
+ return removeUndefined({
113
+ fileDeltaId: row.provenanceId,
114
+ pathBefore: cleanString(row.pathBefore),
115
+ pathAfter: cleanString(row.pathAfter),
116
+ changeKind: row.changeKind ?? "modify",
117
+ beforeHash: cleanString(row.beforeHash),
118
+ afterHash: cleanString(row.afterHash),
119
+ forwardPatch: cleanString(row.forwardPatch),
120
+ inversePatch: cleanString(row.inversePatch),
121
+ actor,
122
+ receiptId: row.provenanceId
123
+ });
124
+ });
125
+ }
126
+ function uniqueStrings(values) {
127
+ return Array.from(new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value))));
128
+ }
129
+ function cleanString(value) {
130
+ if (typeof value !== "string")
131
+ return;
132
+ const text = value.trim();
133
+ return text.length > 0 ? text : undefined;
134
+ }
135
+ function isValidTimestamp(value) {
136
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
137
+ }
138
+ function removeUndefined(value) {
139
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
140
+ }
141
+ function stableHash(value) {
142
+ const text = stableJson(value);
143
+ let hash = 2166136261;
144
+ for (let index = 0;index < text.length; index += 1) {
145
+ hash ^= text.charCodeAt(index);
146
+ hash = Math.imul(hash, 16777619);
147
+ }
148
+ return (hash >>> 0).toString(16).padStart(8, "0").slice(0, 12);
149
+ }
150
+ function stableJson(value) {
151
+ if (Array.isArray(value))
152
+ return `[${value.map(stableJson).join(",")}]`;
153
+ if (value && typeof value === "object") {
154
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(",")}}`;
155
+ }
156
+ return JSON.stringify(value);
157
+ }
158
+ // ../core/src/changeEpisode/fromLedgerRows.ts
159
+ function assembleChangeEpisodeFromLedgerRows(input) {
160
+ const provenance = input.editProvenance.filter((row) => row.goalId === undefined || row.goalId === input.goalId);
161
+ const timestamps = [
162
+ ...input.trials.map((trial) => trial.createdAt),
163
+ ...provenance.map((row) => row.createdAt)
164
+ ].filter((value) => typeof value === "number" && Number.isFinite(value));
165
+ const startedAt = timestamps.length > 0 ? Math.min(...timestamps) : input.fallbackStartedAt ?? 0;
166
+ const episode = openChangeEpisode({
167
+ goalId: input.goalId,
168
+ goal: input.goal,
169
+ actor: inferEpisodeActor(provenance),
170
+ startedAt,
171
+ gitBaseSha: input.gitBaseSha,
172
+ worktreeDirtyHashBefore: input.worktreeDirtyHashBefore
173
+ });
174
+ return attachEpisodeRows(episode, {
175
+ contextReceipts: uniqueStrings([
176
+ ...input.packItems.map((item) => item.trialId),
177
+ ...input.trials.map((trial) => trial.receiptId)
178
+ ]),
179
+ fileDeltas: provenance.map((row) => row.provenanceId),
180
+ symbolDeltas: provenance.flatMap((row) => [...row.symbols ?? []]),
181
+ validationRuns: [...input.validationRefs]
182
+ });
183
+ }
184
+ function inferEpisodeActor(provenance) {
185
+ let hasAgent = false;
186
+ let hasHuman = false;
187
+ for (const row of provenance) {
188
+ const actor = cleanString(row.actor);
189
+ if (actor === undefined)
190
+ continue;
191
+ if (actor === "human" || actor.startsWith("human_"))
192
+ hasHuman = true;
193
+ else
194
+ hasAgent = true;
195
+ }
196
+ if (hasAgent && hasHuman)
197
+ return "mixed";
198
+ if (hasHuman)
199
+ return "human";
200
+ return "agent";
201
+ }
202
+ // ../core/src/changeEpisode/deltaComputation.ts
203
+ function isUsableRange(range) {
204
+ return Number.isFinite(range.startLine) && Number.isFinite(range.endLine) && range.endLine >= range.startLine && (range.startLine > 0 || range.endLine > 0);
205
+ }
206
+ function overlaps(span, range) {
207
+ return span.startLine <= range.endLine && span.endLine >= range.startLine;
208
+ }
209
+ function resolveTouchedSymbols(changedPaths, symbols, hunks) {
210
+ const changed = new Set(changedPaths.map((path) => path.trim()).filter(Boolean));
211
+ const usableHunksByPath = new Map;
212
+ for (const hunk of hunks ?? []) {
213
+ if (!isUsableRange(hunk))
214
+ continue;
215
+ const list = usableHunksByPath.get(hunk.path) ?? [];
216
+ list.push(hunk);
217
+ usableHunksByPath.set(hunk.path, list);
218
+ }
219
+ const seen = new Set;
220
+ const touched = [];
221
+ for (const symbol of symbols) {
222
+ if (!changed.has(symbol.path))
223
+ continue;
224
+ const pathHunks = usableHunksByPath.get(symbol.path);
225
+ const isTouched = pathHunks === undefined ? true : pathHunks.some((range) => overlaps(symbol, range));
226
+ if (!isTouched)
227
+ continue;
228
+ const key = symbol.key.trim();
229
+ if (!key || seen.has(key))
230
+ continue;
231
+ seen.add(key);
232
+ touched.push(key);
233
+ }
234
+ return touched;
235
+ }
236
+ function deriveInverseEdit(spec) {
237
+ switch (spec.kind) {
238
+ case "search_replace":
239
+ return { kind: "search_replace", path: spec.path, search: spec.replace, replace: spec.search };
240
+ case "rewrite":
241
+ return spec.beforeContent === undefined ? { kind: "unavailable", path: spec.path, reason: "missing_before_content" } : { kind: "rewrite", path: spec.path, rewrite: spec.beforeContent };
242
+ case "create":
243
+ return { kind: "delete", path: spec.path };
244
+ case "delete":
245
+ return spec.beforeContent === undefined ? { kind: "unavailable", path: spec.path, reason: "missing_before_content" } : { kind: "create", path: spec.path, content: spec.beforeContent };
246
+ }
247
+ }
248
+ function edgeIdentity(edge) {
249
+ return `${edge.from}\x00${edge.to}\x00${edge.kind}`;
250
+ }
251
+ function diffGraphEdges(prev, next) {
252
+ const prevIds = new Set(prev.map(edgeIdentity));
253
+ const nextIds = new Set(next.map(edgeIdentity));
254
+ const added = [];
255
+ const seenAdded = new Set;
256
+ for (const edge of next) {
257
+ const id = edgeIdentity(edge);
258
+ if (prevIds.has(id) || seenAdded.has(id))
259
+ continue;
260
+ seenAdded.add(id);
261
+ added.push({ change: "added", from: edge.from, to: edge.to, kind: edge.kind });
262
+ }
263
+ const removed = [];
264
+ const seenRemoved = new Set;
265
+ for (const edge of prev) {
266
+ const id = edgeIdentity(edge);
267
+ if (nextIds.has(id) || seenRemoved.has(id))
268
+ continue;
269
+ seenRemoved.add(id);
270
+ removed.push({ change: "removed", from: edge.from, to: edge.to, kind: edge.kind });
271
+ }
272
+ return [...added, ...removed];
273
+ }
274
+ export {
275
+ uniqueStrings,
276
+ stableHash,
277
+ resolveTouchedSymbols,
278
+ planEpisodeRevert,
279
+ openChangeEpisode,
280
+ fileDeltasFromProvenance,
281
+ diffGraphEdges,
282
+ deriveInverseEdit,
283
+ closeChangeEpisode,
284
+ cleanString,
285
+ attachEpisodeRows,
286
+ assembleChangeEpisodeFromLedgerRows,
287
+ CHANGE_EPISODE_SCHEMA_VERSION
288
+ };
@@ -4092,11 +4092,13 @@ var EditSourceV1Schema = exports_external.enum([
4092
4092
  "shell_script",
4093
4093
  "unknown_external_agent"
4094
4094
  ]);
4095
+ var EditHunkInverseV1Schema = exports_external.object({ kind: exports_external.string() }).passthrough();
4095
4096
  var EditHunkV1Schema = exports_external.object({
4096
4097
  filePath: exports_external.string(),
4097
4098
  startLine: exports_external.number().int().nonnegative(),
4098
4099
  endLine: exports_external.number().int().nonnegative(),
4099
- kind: exports_external.enum(["insert", "update", "delete", "mixed"]).optional()
4100
+ kind: exports_external.enum(["insert", "update", "delete", "mixed"]).optional(),
4101
+ inverse: exports_external.array(EditHunkInverseV1Schema).optional()
4100
4102
  }).strict();
4101
4103
  var EditProvenanceV1Schema = exports_external.object({
4102
4104
  schemaVersion: exports_external.literal(EVIDENCE_LEDGER_SCHEMA_VERSION),
@@ -5084,6 +5086,100 @@ function buildProvenanceSourceReportCards(provenances, events, options = {}) {
5084
5086
  return card;
5085
5087
  }).sort((a, b) => b.totalEdits - a.totalEdits || a.source.localeCompare(b.source));
5086
5088
  }
5089
+ // ../core/src/changeEpisode/deltaComputation.ts
5090
+ function isUsableRange(range) {
5091
+ return Number.isFinite(range.startLine) && Number.isFinite(range.endLine) && range.endLine >= range.startLine && (range.startLine > 0 || range.endLine > 0);
5092
+ }
5093
+ function overlaps(span, range) {
5094
+ return span.startLine <= range.endLine && span.endLine >= range.startLine;
5095
+ }
5096
+ function resolveTouchedSymbols(changedPaths, symbols, hunks) {
5097
+ const changed = new Set(changedPaths.map((path) => path.trim()).filter(Boolean));
5098
+ const usableHunksByPath = new Map;
5099
+ for (const hunk of hunks ?? []) {
5100
+ if (!isUsableRange(hunk))
5101
+ continue;
5102
+ const list = usableHunksByPath.get(hunk.path) ?? [];
5103
+ list.push(hunk);
5104
+ usableHunksByPath.set(hunk.path, list);
5105
+ }
5106
+ const seen = new Set;
5107
+ const touched = [];
5108
+ for (const symbol of symbols) {
5109
+ if (!changed.has(symbol.path))
5110
+ continue;
5111
+ const pathHunks = usableHunksByPath.get(symbol.path);
5112
+ const isTouched = pathHunks === undefined ? true : pathHunks.some((range) => overlaps(symbol, range));
5113
+ if (!isTouched)
5114
+ continue;
5115
+ const key = symbol.key.trim();
5116
+ if (!key || seen.has(key))
5117
+ continue;
5118
+ seen.add(key);
5119
+ touched.push(key);
5120
+ }
5121
+ return touched;
5122
+ }
5123
+ // ../core/src/evidenceLedger/symbolHistory.ts
5124
+ function sortRowsMostRecentFirst(rows) {
5125
+ return [...rows].sort((left, right) => timestamp(right.createdAt) - timestamp(left.createdAt));
5126
+ }
5127
+ function timestamp(value) {
5128
+ return typeof value === "number" && Number.isFinite(value) ? value : Number.NEGATIVE_INFINITY;
5129
+ }
5130
+ function pushUnique(seen, into, value) {
5131
+ const text = value?.trim();
5132
+ if (!text || seen.has(text))
5133
+ return;
5134
+ seen.add(text);
5135
+ into.push(text);
5136
+ }
5137
+ function resolveProvenanceSymbols(rows, indexSymbols) {
5138
+ return rows.map((row) => {
5139
+ if ((row.symbols ?? []).length > 0)
5140
+ return row;
5141
+ const symbols = resolveTouchedSymbols([...row.files ?? []], indexSymbols);
5142
+ return { ...row, symbols };
5143
+ });
5144
+ }
5145
+ function whySymbolChanged(symbolKey, rows) {
5146
+ const target = symbolKey.trim();
5147
+ const ordered = sortRowsMostRecentFirst(rows);
5148
+ const edits = [];
5149
+ const goalSeen = new Set;
5150
+ const goals = [];
5151
+ for (const row of ordered) {
5152
+ if (!(row.symbols ?? []).some((symbol) => symbol.trim() === target))
5153
+ continue;
5154
+ edits.push({
5155
+ provenanceId: row.provenanceId,
5156
+ goalId: row.goalId,
5157
+ goal: row.goal,
5158
+ actor: row.actor,
5159
+ changedAt: row.createdAt,
5160
+ files: [...row.files ?? []]
5161
+ });
5162
+ pushUnique(goalSeen, goals, row.goal);
5163
+ }
5164
+ return { symbol: target, editCount: edits.length, edits, goals };
5165
+ }
5166
+ function whatChangedSince(input, rows) {
5167
+ const ordered = sortRowsMostRecentFirst(rows).filter((row) => timestamp(row.createdAt) >= input.sinceMs);
5168
+ const fileSeen = new Set;
5169
+ const files = [];
5170
+ const symbolSeen = new Set;
5171
+ const symbols = [];
5172
+ const goalSeen = new Set;
5173
+ const goals = [];
5174
+ for (const row of ordered) {
5175
+ for (const file of row.files ?? [])
5176
+ pushUnique(fileSeen, files, file);
5177
+ for (const symbol of row.symbols ?? [])
5178
+ pushUnique(symbolSeen, symbols, symbol);
5179
+ pushUnique(goalSeen, goals, row.goal);
5180
+ }
5181
+ return { sinceMs: input.sinceMs, editCount: ordered.length, files, symbols, goals };
5182
+ }
5087
5183
  // ../core/src/evidenceLedger/ddl.ts
5088
5184
  var EVIDENCE_LEDGER_DDL = `-- Evidence Ledger canonical DDL (SPEC ONLY).
5089
5185
  --
@@ -5236,8 +5332,11 @@ var EVIDENCE_LEDGER_TABLE_NAMES = [
5236
5332
  "memory_signals"
5237
5333
  ];
5238
5334
  export {
5335
+ whySymbolChanged,
5336
+ whatChangedSince,
5239
5337
  usefulnessForIntent,
5240
5338
  unsafeTeamEvidenceMetadataPayloadPaths,
5339
+ resolveProvenanceSymbols,
5241
5340
  rankSubjectsForIntent,
5242
5341
  parseGitNameOnlyLog,
5243
5342
  nextMemoryStatus,
@@ -5325,6 +5424,7 @@ export {
5325
5424
  EditSourceV1Schema,
5326
5425
  EditProvenanceV1Schema,
5327
5426
  EditHunkV1Schema,
5427
+ EditHunkInverseV1Schema,
5328
5428
  EVIDENCE_LEDGER_TABLE_NAMES,
5329
5429
  EVIDENCE_LEDGER_SCHEMA_VERSION,
5330
5430
  EVIDENCE_LEDGER_DDL,
@@ -4,6 +4,7 @@
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./briefs": "./briefs.js",
7
+ "./changeEpisode": "./changeEpisode.js",
7
8
  "./codeAgent": "./codeAgent.js",
8
9
  "./codeEditSession": "./codeEditSession.js",
9
10
  "./codeIntelligence": "./codeIntelligence.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhei-team/rhei",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
4
4
  "description": "Local-first MCP server for Rhei Code Context and governed agent handoffs",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",