@remnic/core 1.1.0 → 1.1.2
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/access-audit.d.ts +56 -0
- package/dist/access-audit.js +9 -0
- package/dist/access-cli.js +70 -53
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +16 -9
- package/dist/access-http.js +26 -18
- package/dist/access-mcp.d.ts +16 -9
- package/dist/access-mcp.js +30 -8
- package/dist/access-schema.d.ts +124 -33
- package/dist/access-schema.js +5 -1
- package/dist/{access-service-HmO1Trrx.d.ts → access-service-Br8ZydTK.d.ts} +158 -63
- package/dist/access-service.d.ts +13 -6
- package/dist/access-service.js +23 -14
- package/dist/bootstrap.d.ts +6 -3
- package/dist/briefing.d.ts +1 -0
- package/dist/briefing.js +8 -6
- package/dist/buffer-surprise-report.d.ts +70 -0
- package/dist/buffer-surprise-report.js +7 -0
- package/dist/buffer-surprise-report.js.map +1 -0
- package/dist/buffer-surprise.d.ts +98 -0
- package/dist/buffer-surprise.js +11 -0
- package/dist/buffer-surprise.js.map +1 -0
- package/dist/buffer.d.ts +100 -2
- package/dist/buffer.js +1 -1
- package/dist/calibration.js +6 -6
- package/dist/causal-behavior.js +4 -4
- package/dist/causal-chain.js +2 -2
- package/dist/causal-consolidation.js +19 -18
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/causal-retrieval.js +4 -4
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-QNJMBKFK.js → chunk-2LGMW3DJ.js} +3 -2
- package/dist/chunk-2LGMW3DJ.js.map +1 -0
- package/dist/{chunk-QDYXG4CS.js → chunk-3FPTCC3Z.js} +4 -3
- package/dist/chunk-3FPTCC3Z.js.map +1 -0
- package/dist/chunk-3GPTTA4J.js +57 -0
- package/dist/chunk-3GPTTA4J.js.map +1 -0
- package/dist/{chunk-ITRLGI2T.js → chunk-3OGMS3PE.js} +2 -2
- package/dist/{chunk-DEPL3635.js → chunk-3YGHKTBF.js} +1446 -196
- package/dist/chunk-3YGHKTBF.js.map +1 -0
- package/dist/{chunk-BLKTA7MM.js → chunk-4HQS2HPX.js} +54 -21
- package/dist/chunk-4HQS2HPX.js.map +1 -0
- package/dist/chunk-54V4BZWP.js +139 -0
- package/dist/chunk-54V4BZWP.js.map +1 -0
- package/dist/chunk-5JRF2PZA.js +67 -0
- package/dist/chunk-5JRF2PZA.js.map +1 -0
- package/dist/chunk-64NJRYU2.js +332 -0
- package/dist/chunk-64NJRYU2.js.map +1 -0
- package/dist/{chunk-OIT5QGG4.js → chunk-6AUUAZEX.js} +72 -2
- package/dist/chunk-6AUUAZEX.js.map +1 -0
- package/dist/{chunk-3QHL5ABG.js → chunk-6YJHX2DL.js} +191 -10
- package/dist/chunk-6YJHX2DL.js.map +1 -0
- package/dist/chunk-AJU4PJGY.js +126 -0
- package/dist/chunk-AJU4PJGY.js.map +1 -0
- package/dist/chunk-ASAITVLA.js +64 -0
- package/dist/chunk-ASAITVLA.js.map +1 -0
- package/dist/{chunk-44ICJRF3.js → chunk-AYXIPSZO.js} +5 -5
- package/dist/{chunk-MBJHSA7F.js → chunk-BECYBZLX.js} +265 -20
- package/dist/chunk-BECYBZLX.js.map +1 -0
- package/dist/chunk-C4SQJZAF.js +486 -0
- package/dist/chunk-C4SQJZAF.js.map +1 -0
- package/dist/{chunk-6UJ47TVX.js → chunk-CUPFXL3J.js} +2 -2
- package/dist/chunk-DF3RVK3X.js +119 -0
- package/dist/chunk-DF3RVK3X.js.map +1 -0
- package/dist/{chunk-N42IWANG.js → chunk-DG6YMRDC.js} +3 -3
- package/dist/chunk-DGVM5SFL.js +69 -0
- package/dist/chunk-DGVM5SFL.js.map +1 -0
- package/dist/{chunk-3SV6CQHO.js → chunk-DIXB44VE.js} +102 -66
- package/dist/chunk-DIXB44VE.js.map +1 -0
- package/dist/chunk-EIR5VLIH.js +90 -0
- package/dist/chunk-EIR5VLIH.js.map +1 -0
- package/dist/{chunk-GV6NLQ4X.js → chunk-F5VP6YCB.js} +374 -16
- package/dist/chunk-F5VP6YCB.js.map +1 -0
- package/dist/{chunk-6ZH4TU6I.js → chunk-FAAFWE4G.js} +2 -1
- package/dist/chunk-FAAFWE4G.js.map +1 -0
- package/dist/{chunk-7WQ6SLIE.js → chunk-FVA6TGI3.js} +2 -2
- package/dist/{chunk-PAORGQRI.js → chunk-GA5P7RST.js} +37 -23
- package/dist/chunk-GA5P7RST.js.map +1 -0
- package/dist/chunk-GDFS42HT.js +206 -0
- package/dist/chunk-GDFS42HT.js.map +1 -0
- package/dist/chunk-IISBCCWR.js +52 -0
- package/dist/chunk-IISBCCWR.js.map +1 -0
- package/dist/chunk-JBMSGZEQ.js +441 -0
- package/dist/chunk-JBMSGZEQ.js.map +1 -0
- package/dist/{chunk-J4IYOZZ5.js → chunk-JXS5PDQ7.js} +3 -1
- package/dist/chunk-JXS5PDQ7.js.map +1 -0
- package/dist/chunk-KVBLZUKV.js +173 -0
- package/dist/chunk-KVBLZUKV.js.map +1 -0
- package/dist/{chunk-4LACOVZX.js → chunk-L7IXWRYE.js} +10 -5
- package/dist/chunk-L7IXWRYE.js.map +1 -0
- package/dist/chunk-LBLXEFWK.js +51 -0
- package/dist/chunk-LBLXEFWK.js.map +1 -0
- package/dist/{chunk-WBSAYXVI.js → chunk-LOIMBRDE.js} +201 -45
- package/dist/chunk-LOIMBRDE.js.map +1 -0
- package/dist/{chunk-3WHVNEN7.js → chunk-LTCGGW2D.js} +1 -1
- package/dist/chunk-LTCGGW2D.js.map +1 -0
- package/dist/{chunk-ZVBB3T7V.js → chunk-NBVAS5MT.js} +25 -23
- package/dist/chunk-NBVAS5MT.js.map +1 -0
- package/dist/{chunk-UEYA6UC7.js → chunk-NZLQTHS5.js} +25 -2
- package/dist/chunk-NZLQTHS5.js.map +1 -0
- package/dist/{chunk-NQEVYWX6.js → chunk-OC5OXUQ4.js} +211 -7
- package/dist/chunk-OC5OXUQ4.js.map +1 -0
- package/dist/{chunk-LK6SGL53.js → chunk-OR64ZGRZ.js} +3 -2
- package/dist/chunk-OR64ZGRZ.js.map +1 -0
- package/dist/{chunk-SYUK3VLY.js → chunk-PVICZTKG.js} +117 -5
- package/dist/chunk-PVICZTKG.js.map +1 -0
- package/dist/chunk-PVPWZSSI.js +37 -0
- package/dist/chunk-PVPWZSSI.js.map +1 -0
- package/dist/{chunk-JL2PU6AI.js → chunk-R2XRID2N.js} +2 -2
- package/dist/{chunk-4NRAJUDS.js → chunk-RBBWYEFJ.js} +1 -1
- package/dist/chunk-RFYAYKTD.js +146 -0
- package/dist/chunk-RFYAYKTD.js.map +1 -0
- package/dist/chunk-SOBJ6NEY.js +18 -0
- package/dist/chunk-SOBJ6NEY.js.map +1 -0
- package/dist/{chunk-JIU55F3X.js → chunk-SPI27QT6.js} +2 -2
- package/dist/{chunk-MVTHXUBX.js → chunk-STGWEHYR.js} +479 -20
- package/dist/chunk-STGWEHYR.js.map +1 -0
- package/dist/{chunk-6LX5ORAS.js → chunk-TMYO7B5P.js} +4 -4
- package/dist/chunk-TVVEYCNW.js +65 -0
- package/dist/chunk-TVVEYCNW.js.map +1 -0
- package/dist/chunk-ULYOGL6R.js +322 -0
- package/dist/chunk-ULYOGL6R.js.map +1 -0
- package/dist/{chunk-37UIFYWO.js → chunk-UWB5LMWY.js} +108 -9
- package/dist/chunk-UWB5LMWY.js.map +1 -0
- package/dist/{chunk-47UU5PU2.js → chunk-VBVG2M5G.js} +18 -3
- package/dist/chunk-VBVG2M5G.js.map +1 -0
- package/dist/{chunk-7ECD5ATE.js → chunk-VDX363PS.js} +2 -2
- package/dist/{chunk-O5ETUNBT.js → chunk-VTU2B4VF.js} +7 -3
- package/dist/chunk-VTU2B4VF.js.map +1 -0
- package/dist/{chunk-MTLYEMJB.js → chunk-WCLICCGB.js} +18 -3
- package/dist/chunk-WCLICCGB.js.map +1 -0
- package/dist/chunk-X6GF3FX2.js +26 -0
- package/dist/chunk-X6GF3FX2.js.map +1 -0
- package/dist/{chunk-3QFQGRHO.js → chunk-XMHBH5H6.js} +4 -4
- package/dist/{chunk-DHHP2Z4X.js → chunk-XXVWLXSG.js} +2 -2
- package/dist/{chunk-XZ2TIKGC.js → chunk-Y7R2XJ5Q.js} +25 -9
- package/dist/chunk-Y7R2XJ5Q.js.map +1 -0
- package/dist/{chunk-ALXMCZEU.js → chunk-Z2E7VW55.js} +6 -3
- package/dist/chunk-Z2E7VW55.js.map +1 -0
- package/dist/chunk-ZAIM4TUE.js +488 -0
- package/dist/chunk-ZAIM4TUE.js.map +1 -0
- package/dist/chunk-ZZTOURJI.js +91 -0
- package/dist/chunk-ZZTOURJI.js.map +1 -0
- package/dist/{cli-BneVIEvh.d.ts → cli-BkeRaYfk.d.ts} +2 -2
- package/dist/cli.d.ts +13 -6
- package/dist/cli.js +42 -31
- package/dist/config.js +2 -2
- package/dist/consolidation-operator.d.ts +41 -0
- package/dist/consolidation-operator.js +11 -0
- package/dist/consolidation-operator.js.map +1 -0
- package/dist/consolidation-provenance-check.d.ts +68 -0
- package/dist/consolidation-provenance-check.js +9 -0
- package/dist/consolidation-provenance-check.js.map +1 -0
- package/dist/consolidation-undo.d.ts +123 -0
- package/dist/consolidation-undo.js +426 -0
- package/dist/consolidation-undo.js.map +1 -0
- package/dist/{contradiction-scan-GR33PONM.js → contradiction-scan-E3GJTI4F.js} +43 -7
- package/dist/contradiction-scan-E3GJTI4F.js.map +1 -0
- package/dist/cross-namespace-budget.d.ts +133 -0
- package/dist/cross-namespace-budget.js +9 -0
- package/dist/cross-namespace-budget.js.map +1 -0
- package/dist/direct-answer-wiring.js +5 -70
- package/dist/direct-answer-wiring.js.map +1 -1
- package/dist/embedding-fallback.js +2 -1
- package/dist/{engine-5TIQBYZR.js → engine-72LSIWQP.js} +8 -7
- package/dist/engine-72LSIWQP.js.map +1 -0
- package/dist/entity-retrieval.d.ts +1 -0
- package/dist/entity-retrieval.js +7 -6
- package/dist/explicit-capture.d.ts +6 -3
- package/dist/explicit-capture.js +2 -2
- package/dist/extraction-judge-telemetry.d.ts +113 -0
- package/dist/extraction-judge-telemetry.js +14 -0
- package/dist/extraction-judge-telemetry.js.map +1 -0
- package/dist/extraction-judge-training.d.ts +85 -0
- package/dist/extraction-judge-training.js +16 -0
- package/dist/extraction-judge-training.js.map +1 -0
- package/dist/extraction-judge.d.ts +124 -2
- package/dist/extraction-judge.js +11 -1
- package/dist/extraction.js +10 -9
- package/dist/fallback-llm.js +3 -3
- package/dist/graph-recall.d.ts +100 -0
- package/dist/graph-recall.js +8 -0
- package/dist/graph-recall.js.map +1 -0
- package/dist/graph-retrieval.d.ts +271 -0
- package/dist/graph-retrieval.js +21 -0
- package/dist/graph-retrieval.js.map +1 -0
- package/dist/importance.js +1 -1
- package/dist/index.d.ts +585 -20
- package/dist/index.js +542 -344
- package/dist/index.js.map +1 -1
- package/dist/local-llm.js +2 -2
- package/dist/memory-worth-bench.d.ts +51 -0
- package/dist/memory-worth-bench.js +131 -0
- package/dist/memory-worth-bench.js.map +1 -0
- package/dist/memory-worth-filter.d.ts +128 -0
- package/dist/memory-worth-filter.js +10 -0
- package/dist/memory-worth-filter.js.map +1 -0
- package/dist/memory-worth-outcomes.d.ts +118 -0
- package/dist/memory-worth-outcomes.js +9 -0
- package/dist/memory-worth-outcomes.js.map +1 -0
- package/dist/memory-worth.d.ts +102 -0
- package/dist/memory-worth.js +7 -0
- package/dist/memory-worth.js.map +1 -0
- package/dist/operator-toolkit.d.ts +40 -1
- package/dist/operator-toolkit.js +25 -16
- package/dist/{orchestrator-DRYA6_lW.d.ts → orchestrator-CmJ-NTdJ.d.ts} +233 -8
- package/dist/orchestrator.d.ts +6 -3
- package/dist/orchestrator.js +54 -44
- package/dist/page-versioning.d.ts +12 -1
- package/dist/page-versioning.js +5 -3
- package/dist/{port-C1GZFv8h.d.ts → port-BADbLZU5.d.ts} +2 -2
- package/dist/qmd-recall-cache.d.ts +1 -1
- package/dist/qmd.d.ts +5 -3
- package/dist/qmd.js +3 -3
- package/dist/reasoning-trace-recall.d.ts +90 -0
- package/dist/reasoning-trace-recall.js +13 -0
- package/dist/reasoning-trace-recall.js.map +1 -0
- package/dist/reasoning-trace-types.d.ts +54 -0
- package/dist/reasoning-trace-types.js +17 -0
- package/dist/reasoning-trace-types.js.map +1 -0
- package/dist/recall-audit-anomaly.d.ts +112 -0
- package/dist/recall-audit-anomaly.js +11 -0
- package/dist/recall-audit-anomaly.js.map +1 -0
- package/dist/recall-audit.js +5 -44
- package/dist/recall-audit.js.map +1 -1
- package/dist/recall-explain-renderer.d.ts +49 -0
- package/dist/recall-explain-renderer.js +18 -0
- package/dist/recall-explain-renderer.js.map +1 -0
- package/dist/recall-state.d.ts +12 -1
- package/dist/recall-state.js +1 -1
- package/dist/recall-xray-cli.d.ts +40 -0
- package/dist/recall-xray-cli.js +11 -0
- package/dist/recall-xray-cli.js.map +1 -0
- package/dist/recall-xray-renderer.d.ts +44 -0
- package/dist/recall-xray-renderer.js +18 -0
- package/dist/recall-xray-renderer.js.map +1 -0
- package/dist/recall-xray.d.ts +179 -0
- package/dist/recall-xray.js +13 -0
- package/dist/recall-xray.js.map +1 -0
- package/dist/resolve-provider-secret.d.ts +5 -1
- package/dist/resolve-provider-secret.js +3 -1
- package/dist/resume-bundles.js +6 -6
- package/dist/retrieval-agents.d.ts +1 -1
- package/dist/retrieval-tiers.d.ts +17 -0
- package/dist/retrieval-tiers.js +9 -0
- package/dist/retrieval-tiers.js.map +1 -0
- package/dist/schemas.d.ts +309 -53
- package/dist/schemas.js +1 -1
- package/dist/{semantic-consolidation-DrvSYRdB.d.ts → semantic-consolidation-CxJU6MJk.d.ts} +62 -1
- package/dist/semantic-consolidation.d.ts +2 -1
- package/dist/semantic-consolidation.js +22 -7
- package/dist/semantic-rule-promotion.js +7 -6
- package/dist/semantic-rule-verifier.js +7 -6
- package/dist/storage.d.ts +82 -1
- package/dist/storage.js +6 -5
- package/dist/summarizer.js +6 -6
- package/dist/temporal-supersession.d.ts +1 -0
- package/dist/tier-migration.d.ts +2 -1
- package/dist/tokens.js +2 -1
- package/dist/types.d.ts +276 -2
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +7 -6
- package/package.json +1 -1
- package/dist/chunk-37UIFYWO.js.map +0 -1
- package/dist/chunk-3QHL5ABG.js.map +0 -1
- package/dist/chunk-3SV6CQHO.js.map +0 -1
- package/dist/chunk-3WHVNEN7.js.map +0 -1
- package/dist/chunk-47UU5PU2.js.map +0 -1
- package/dist/chunk-4LACOVZX.js.map +0 -1
- package/dist/chunk-6ZH4TU6I.js.map +0 -1
- package/dist/chunk-ALXMCZEU.js.map +0 -1
- package/dist/chunk-BLKTA7MM.js.map +0 -1
- package/dist/chunk-DEPL3635.js.map +0 -1
- package/dist/chunk-GV6NLQ4X.js.map +0 -1
- package/dist/chunk-J4IYOZZ5.js.map +0 -1
- package/dist/chunk-LAYN4LDC.js +0 -267
- package/dist/chunk-LAYN4LDC.js.map +0 -1
- package/dist/chunk-LK6SGL53.js.map +0 -1
- package/dist/chunk-MBJHSA7F.js.map +0 -1
- package/dist/chunk-MTLYEMJB.js.map +0 -1
- package/dist/chunk-MVTHXUBX.js.map +0 -1
- package/dist/chunk-NQEVYWX6.js.map +0 -1
- package/dist/chunk-O5ETUNBT.js.map +0 -1
- package/dist/chunk-OIT5QGG4.js.map +0 -1
- package/dist/chunk-PAORGQRI.js.map +0 -1
- package/dist/chunk-QDYXG4CS.js.map +0 -1
- package/dist/chunk-QNJMBKFK.js.map +0 -1
- package/dist/chunk-SYUK3VLY.js.map +0 -1
- package/dist/chunk-UEYA6UC7.js.map +0 -1
- package/dist/chunk-UVJFDP7P.js +0 -202
- package/dist/chunk-UVJFDP7P.js.map +0 -1
- package/dist/chunk-WBSAYXVI.js.map +0 -1
- package/dist/chunk-XZ2TIKGC.js.map +0 -1
- package/dist/chunk-ZVBB3T7V.js.map +0 -1
- package/dist/contradiction-scan-GR33PONM.js.map +0 -1
- /package/dist/{engine-5TIQBYZR.js.map → access-audit.js.map} +0 -0
- /package/dist/{chunk-ITRLGI2T.js.map → chunk-3OGMS3PE.js.map} +0 -0
- /package/dist/{chunk-44ICJRF3.js.map → chunk-AYXIPSZO.js.map} +0 -0
- /package/dist/{chunk-6UJ47TVX.js.map → chunk-CUPFXL3J.js.map} +0 -0
- /package/dist/{chunk-N42IWANG.js.map → chunk-DG6YMRDC.js.map} +0 -0
- /package/dist/{chunk-7WQ6SLIE.js.map → chunk-FVA6TGI3.js.map} +0 -0
- /package/dist/{chunk-JL2PU6AI.js.map → chunk-R2XRID2N.js.map} +0 -0
- /package/dist/{chunk-4NRAJUDS.js.map → chunk-RBBWYEFJ.js.map} +0 -0
- /package/dist/{chunk-JIU55F3X.js.map → chunk-SPI27QT6.js.map} +0 -0
- /package/dist/{chunk-6LX5ORAS.js.map → chunk-TMYO7B5P.js.map} +0 -0
- /package/dist/{chunk-7ECD5ATE.js.map → chunk-VDX363PS.js.map} +0 -0
- /package/dist/{chunk-3QFQGRHO.js.map → chunk-XMHBH5H6.js.map} +0 -0
- /package/dist/{chunk-DHHP2Z4X.js.map → chunk-XXVWLXSG.js.map} +0 -0
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getVersion
|
|
3
|
+
} from "./chunk-FAAFWE4G.js";
|
|
4
|
+
|
|
5
|
+
// src/consolidation-undo.ts
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { mkdir, writeFile, access, realpath, lstat } from "fs/promises";
|
|
8
|
+
import { constants as fsConstants } from "fs";
|
|
9
|
+
var DERIVED_FROM_ENTRY_RE = /^(.+):(\d+)$/;
|
|
10
|
+
function parseEntry(entry) {
|
|
11
|
+
if (typeof entry !== "string") return null;
|
|
12
|
+
const match = entry.match(DERIVED_FROM_ENTRY_RE);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
return { pagePath: match[1], versionId: match[2] };
|
|
15
|
+
}
|
|
16
|
+
function isInsideDirectory(candidate, root) {
|
|
17
|
+
const normRoot = path.resolve(root);
|
|
18
|
+
const normCandidate = path.resolve(candidate);
|
|
19
|
+
const rel = path.relative(normRoot, normCandidate);
|
|
20
|
+
if (rel.length === 0) return true;
|
|
21
|
+
if (rel.startsWith("..")) return false;
|
|
22
|
+
if (path.isAbsolute(rel)) return false;
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
async function isInsideDirectoryRealpath(candidate, root) {
|
|
26
|
+
if (!isInsideDirectory(candidate, root)) return false;
|
|
27
|
+
const rawSegments = candidate.replace(/\\/g, "/").split("/");
|
|
28
|
+
if (rawSegments.some((s) => s === "..")) return false;
|
|
29
|
+
let resolvedRoot;
|
|
30
|
+
try {
|
|
31
|
+
resolvedRoot = await realpath(path.resolve(root));
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const normCandidate = path.resolve(candidate);
|
|
36
|
+
const normRoot = path.resolve(root);
|
|
37
|
+
const relFromRoot = path.relative(normRoot, normCandidate);
|
|
38
|
+
const segments = relFromRoot.length > 0 ? relFromRoot.split(path.sep) : [];
|
|
39
|
+
for (let i = 0; i <= segments.length; i++) {
|
|
40
|
+
const probe = i === 0 ? normRoot : path.join(normRoot, ...segments.slice(0, i));
|
|
41
|
+
try {
|
|
42
|
+
const st = await lstat(probe);
|
|
43
|
+
if (st.isSymbolicLink() && probe !== normRoot) {
|
|
44
|
+
let target;
|
|
45
|
+
try {
|
|
46
|
+
target = await realpath(probe);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
const rel = path.relative(resolvedRoot, target);
|
|
51
|
+
if (rel.length === 0) continue;
|
|
52
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) return false;
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const parts = normCandidate.split(path.sep);
|
|
58
|
+
for (let i = parts.length; i > 0; i--) {
|
|
59
|
+
const probe = parts.slice(0, i).join(path.sep) || path.sep;
|
|
60
|
+
try {
|
|
61
|
+
const resolved = await realpath(probe);
|
|
62
|
+
const trailing = parts.slice(i).join(path.sep);
|
|
63
|
+
const final = trailing.length > 0 ? path.join(resolved, trailing) : resolved;
|
|
64
|
+
const rel = path.relative(resolvedRoot, final);
|
|
65
|
+
if (rel.length === 0) return true;
|
|
66
|
+
if (rel.startsWith("..")) return false;
|
|
67
|
+
if (path.isAbsolute(rel)) return false;
|
|
68
|
+
return true;
|
|
69
|
+
} catch {
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
var NON_ACTIVE_PREFIXES = ["archive/", "state/"];
|
|
76
|
+
function normalizeRelativePath(p) {
|
|
77
|
+
const parts = p.replace(/\\/g, "/").split("/");
|
|
78
|
+
const resolved = [];
|
|
79
|
+
for (const seg of parts) {
|
|
80
|
+
if (seg === "" || seg === ".") continue;
|
|
81
|
+
if (seg === "..") {
|
|
82
|
+
if (resolved.length > 0) resolved.pop();
|
|
83
|
+
} else {
|
|
84
|
+
resolved.push(seg);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return resolved.join("/");
|
|
88
|
+
}
|
|
89
|
+
function isActiveMemoryRelativePath(pagePath, sidecarDir) {
|
|
90
|
+
const normalized = normalizeRelativePath(pagePath);
|
|
91
|
+
const prefixes = [...NON_ACTIVE_PREFIXES];
|
|
92
|
+
if (sidecarDir) {
|
|
93
|
+
const normSidecar = normalizeRelativePath(sidecarDir);
|
|
94
|
+
prefixes.push(normSidecar + "/");
|
|
95
|
+
}
|
|
96
|
+
for (const prefix of prefixes) {
|
|
97
|
+
if (normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
async function isRegularFile(p) {
|
|
104
|
+
try {
|
|
105
|
+
const st = await lstat(p);
|
|
106
|
+
return st.isFile();
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function fileExists(p) {
|
|
112
|
+
try {
|
|
113
|
+
await access(p, fsConstants.F_OK);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async function runConsolidationUndo(options) {
|
|
120
|
+
const { storage, memoryDir, targetPath, versioning } = options;
|
|
121
|
+
const dryRun = options.dryRun === true;
|
|
122
|
+
const result = {
|
|
123
|
+
targetPath,
|
|
124
|
+
targetArchived: false,
|
|
125
|
+
restores: [],
|
|
126
|
+
dryRun
|
|
127
|
+
};
|
|
128
|
+
if (!await isInsideDirectoryRealpath(targetPath, memoryDir)) {
|
|
129
|
+
result.error = `target path ${targetPath} is outside memory directory ${memoryDir}`;
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
const targetRel = path.relative(memoryDir, targetPath);
|
|
133
|
+
if (!isActiveMemoryRelativePath(targetRel, versioning.sidecarDir)) {
|
|
134
|
+
result.error = `target path "${targetRel}" is inside a non-active directory \u2014 refusing to operate`;
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
const target = await storage.readMemoryByPath(targetPath);
|
|
138
|
+
if (!target) {
|
|
139
|
+
result.error = `could not load target memory at ${targetPath}`;
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
const derivedFrom = target.frontmatter.derived_from;
|
|
143
|
+
if (!Array.isArray(derivedFrom) || derivedFrom.length === 0) {
|
|
144
|
+
result.error = "target memory has no derived_from entries \u2014 nothing to undo";
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
const plans = [];
|
|
148
|
+
for (const rawEntry of derivedFrom) {
|
|
149
|
+
const entry = typeof rawEntry === "string" ? rawEntry : String(rawEntry);
|
|
150
|
+
const parsed = parseEntry(rawEntry);
|
|
151
|
+
if (!parsed) {
|
|
152
|
+
plans.push({
|
|
153
|
+
kind: "skip",
|
|
154
|
+
restore: {
|
|
155
|
+
entry,
|
|
156
|
+
sourcePath: "",
|
|
157
|
+
outcome: "skipped_malformed_entry",
|
|
158
|
+
detail: `expected "<path>:<version>" shape`
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (path.isAbsolute(parsed.pagePath)) {
|
|
164
|
+
plans.push({
|
|
165
|
+
kind: "skip",
|
|
166
|
+
restore: {
|
|
167
|
+
entry,
|
|
168
|
+
sourcePath: parsed.pagePath,
|
|
169
|
+
outcome: "skipped_malformed_entry",
|
|
170
|
+
detail: `derived_from path must be relative, got absolute: "${parsed.pagePath}"`
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const sourcePath = path.join(memoryDir, parsed.pagePath);
|
|
176
|
+
if (!await isInsideDirectoryRealpath(sourcePath, memoryDir)) {
|
|
177
|
+
plans.push({
|
|
178
|
+
kind: "skip",
|
|
179
|
+
restore: {
|
|
180
|
+
entry,
|
|
181
|
+
sourcePath,
|
|
182
|
+
outcome: "skipped_outside_memory_dir",
|
|
183
|
+
detail: `resolved path escapes memory directory ${memoryDir}`
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
let resolvedRelative = parsed.pagePath;
|
|
189
|
+
try {
|
|
190
|
+
const realBase = await realpath(memoryDir);
|
|
191
|
+
try {
|
|
192
|
+
const realSource = await realpath(sourcePath);
|
|
193
|
+
const rel = path.relative(realBase, realSource);
|
|
194
|
+
if (!rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
195
|
+
resolvedRelative = rel.replace(/\\/g, "/");
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
const parentDir = path.dirname(sourcePath);
|
|
199
|
+
try {
|
|
200
|
+
const realParent = await realpath(parentDir);
|
|
201
|
+
const parentRel = path.relative(realBase, realParent);
|
|
202
|
+
if (!parentRel.startsWith("..") && !path.isAbsolute(parentRel)) {
|
|
203
|
+
const leafName = path.basename(sourcePath);
|
|
204
|
+
resolvedRelative = path.join(parentRel, leafName).replace(/\\/g, "/");
|
|
205
|
+
}
|
|
206
|
+
} catch {
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
if (!isActiveMemoryRelativePath(parsed.pagePath, versioning.sidecarDir) || !isActiveMemoryRelativePath(resolvedRelative, versioning.sidecarDir)) {
|
|
212
|
+
plans.push({
|
|
213
|
+
kind: "skip",
|
|
214
|
+
restore: {
|
|
215
|
+
entry,
|
|
216
|
+
sourcePath,
|
|
217
|
+
outcome: "skipped_non_active_path",
|
|
218
|
+
detail: `source path "${parsed.pagePath}" is inside a non-active directory (archive/state/versions)`
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (path.resolve(sourcePath) === path.resolve(targetPath)) {
|
|
224
|
+
plans.push({
|
|
225
|
+
kind: "skip",
|
|
226
|
+
restore: {
|
|
227
|
+
entry,
|
|
228
|
+
sourcePath,
|
|
229
|
+
outcome: "skipped_self_referential",
|
|
230
|
+
detail: `derived_from entry "${entry}" resolves to the same file as the target \u2014 refusing to count as recovered`
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (await isRegularFile(sourcePath)) {
|
|
236
|
+
plans.push({ kind: "recovered_existing", entry, sourcePath });
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (await fileExists(sourcePath)) {
|
|
240
|
+
plans.push({
|
|
241
|
+
kind: "skip",
|
|
242
|
+
restore: {
|
|
243
|
+
entry,
|
|
244
|
+
sourcePath,
|
|
245
|
+
outcome: "skipped_non_regular_file",
|
|
246
|
+
detail: "source path is occupied by a non-regular-file; refusing to proceed"
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
let snapshotContent;
|
|
252
|
+
try {
|
|
253
|
+
snapshotContent = await getVersion(
|
|
254
|
+
sourcePath,
|
|
255
|
+
parsed.versionId,
|
|
256
|
+
versioning,
|
|
257
|
+
memoryDir
|
|
258
|
+
);
|
|
259
|
+
} catch {
|
|
260
|
+
plans.push({
|
|
261
|
+
kind: "skip",
|
|
262
|
+
restore: {
|
|
263
|
+
entry,
|
|
264
|
+
sourcePath,
|
|
265
|
+
outcome: "skipped_snapshot_missing",
|
|
266
|
+
detail: `no snapshot for version ${parsed.versionId}`
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
plans.push({ kind: "write", entry, sourcePath, content: snapshotContent });
|
|
272
|
+
}
|
|
273
|
+
const skipped = plans.filter((p) => p.kind === "skip");
|
|
274
|
+
if (skipped.length > 0) {
|
|
275
|
+
for (const p of plans) {
|
|
276
|
+
if (p.kind === "skip") {
|
|
277
|
+
result.restores.push(p.restore);
|
|
278
|
+
} else if (p.kind === "write") {
|
|
279
|
+
result.restores.push({
|
|
280
|
+
entry: p.entry,
|
|
281
|
+
sourcePath: p.sourcePath,
|
|
282
|
+
outcome: dryRun ? "skipped_dry_run" : "skipped_blocked_by_other_failures",
|
|
283
|
+
detail: dryRun ? "would restore from snapshot (blocked by other failures)" : "snapshot available but undo aborted due to other failures"
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
result.restores.push({
|
|
287
|
+
entry: p.entry,
|
|
288
|
+
sourcePath: p.sourcePath,
|
|
289
|
+
outcome: "skipped_file_exists",
|
|
290
|
+
detail: "source file already exists; no restore needed"
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const recovered = result.restores.filter(
|
|
295
|
+
(r) => r.outcome === "restored" || r.outcome === "skipped_file_exists"
|
|
296
|
+
).length;
|
|
297
|
+
if (recovered === 0) {
|
|
298
|
+
result.error = "no sources could be recovered (all snapshots missing or paths unsafe); target not archived to preserve data";
|
|
299
|
+
} else {
|
|
300
|
+
result.error = `${skipped.length} of ${plans.length} sources could not be recovered; target not archived (undo is all-or-nothing)`;
|
|
301
|
+
}
|
|
302
|
+
return result;
|
|
303
|
+
}
|
|
304
|
+
const seenSourcePaths = /* @__PURE__ */ new Set();
|
|
305
|
+
const dedupedPlans = [];
|
|
306
|
+
for (const p of plans) {
|
|
307
|
+
if (p.kind === "write" || p.kind === "recovered_existing") {
|
|
308
|
+
if (seenSourcePaths.has(p.sourcePath)) {
|
|
309
|
+
dedupedPlans.push({
|
|
310
|
+
kind: "skip",
|
|
311
|
+
restore: {
|
|
312
|
+
entry: p.kind === "write" ? p.entry : p.entry,
|
|
313
|
+
sourcePath: p.sourcePath,
|
|
314
|
+
outcome: "skipped_file_exists",
|
|
315
|
+
detail: "duplicate derived_from entry \u2014 source already processed"
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
seenSourcePaths.add(p.sourcePath);
|
|
321
|
+
}
|
|
322
|
+
dedupedPlans.push(p);
|
|
323
|
+
}
|
|
324
|
+
if (dryRun) {
|
|
325
|
+
for (const p of dedupedPlans) {
|
|
326
|
+
if (p.kind === "write") {
|
|
327
|
+
result.restores.push({
|
|
328
|
+
entry: p.entry,
|
|
329
|
+
sourcePath: p.sourcePath,
|
|
330
|
+
outcome: "skipped_dry_run",
|
|
331
|
+
detail: "would restore from snapshot"
|
|
332
|
+
});
|
|
333
|
+
} else if (p.kind === "recovered_existing") {
|
|
334
|
+
result.restores.push({
|
|
335
|
+
entry: p.entry,
|
|
336
|
+
sourcePath: p.sourcePath,
|
|
337
|
+
outcome: "skipped_file_exists",
|
|
338
|
+
detail: "source file already exists; no restore needed"
|
|
339
|
+
});
|
|
340
|
+
} else if (p.kind === "skip" && p.restore) {
|
|
341
|
+
result.restores.push(p.restore);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
let writeFailed = false;
|
|
347
|
+
for (const p of dedupedPlans) {
|
|
348
|
+
if (p.kind === "skip") {
|
|
349
|
+
if (p.restore) result.restores.push(p.restore);
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
if (p.kind === "recovered_existing") {
|
|
353
|
+
result.restores.push({
|
|
354
|
+
entry: p.entry,
|
|
355
|
+
sourcePath: p.sourcePath,
|
|
356
|
+
outcome: "skipped_file_exists",
|
|
357
|
+
detail: "source file already exists; no restore needed"
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (p.kind === "write") {
|
|
362
|
+
if (writeFailed) {
|
|
363
|
+
result.restores.push({
|
|
364
|
+
entry: p.entry,
|
|
365
|
+
sourcePath: p.sourcePath,
|
|
366
|
+
outcome: "skipped_blocked_by_other_failures",
|
|
367
|
+
detail: "a prior source write failed; skipping remaining writes to honor all-or-nothing contract"
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
await mkdir(path.dirname(p.sourcePath), { recursive: true });
|
|
373
|
+
await writeFile(p.sourcePath, p.content, { encoding: "utf-8", flag: "wx" });
|
|
374
|
+
result.restores.push({
|
|
375
|
+
entry: p.entry,
|
|
376
|
+
sourcePath: p.sourcePath,
|
|
377
|
+
outcome: "restored"
|
|
378
|
+
});
|
|
379
|
+
} catch (err) {
|
|
380
|
+
writeFailed = true;
|
|
381
|
+
result.restores.push({
|
|
382
|
+
entry: p.entry,
|
|
383
|
+
sourcePath: p.sourcePath,
|
|
384
|
+
outcome: "skipped_write_failed",
|
|
385
|
+
detail: `write failed: ${err instanceof Error ? err.message : String(err)}`
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (writeFailed) {
|
|
391
|
+
result.error = "one or more source writes failed mid-restore; target not archived to preserve data";
|
|
392
|
+
return result;
|
|
393
|
+
}
|
|
394
|
+
const archivedAt = await storage.archiveMemory(target, {
|
|
395
|
+
actor: "consolidate-undo",
|
|
396
|
+
reasonCode: "consolidation-undo"
|
|
397
|
+
});
|
|
398
|
+
result.targetArchived = archivedAt !== null;
|
|
399
|
+
if (!result.targetArchived) {
|
|
400
|
+
result.error = "sources restored successfully but archiving the consolidated target failed; inspect storage for manual cleanup";
|
|
401
|
+
}
|
|
402
|
+
return result;
|
|
403
|
+
}
|
|
404
|
+
function formatConsolidationUndoResult(result) {
|
|
405
|
+
const lines = [];
|
|
406
|
+
lines.push(`consolidate undo ${result.dryRun ? "(dry run) " : ""}\u2192 ${result.targetPath}`);
|
|
407
|
+
for (const r of result.restores) {
|
|
408
|
+
lines.push(` - ${r.entry} \u2192 ${r.outcome}${r.detail ? ` (${r.detail})` : ""}`);
|
|
409
|
+
}
|
|
410
|
+
if (result.error) {
|
|
411
|
+
lines.push(` ERROR: ${result.error}`);
|
|
412
|
+
return lines.join("\n");
|
|
413
|
+
}
|
|
414
|
+
lines.push(
|
|
415
|
+
result.dryRun ? " (dry run \u2014 no files were modified, target not archived)" : ` target archived: ${result.targetArchived ? "yes" : "no"}`
|
|
416
|
+
);
|
|
417
|
+
return lines.join("\n");
|
|
418
|
+
}
|
|
419
|
+
export {
|
|
420
|
+
formatConsolidationUndoResult,
|
|
421
|
+
isActiveMemoryRelativePath,
|
|
422
|
+
isInsideDirectory,
|
|
423
|
+
isInsideDirectoryRealpath,
|
|
424
|
+
runConsolidationUndo
|
|
425
|
+
};
|
|
426
|
+
//# sourceMappingURL=consolidation-undo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/consolidation-undo.ts"],"sourcesContent":["/**\n * Consolidation undo (issue #561 PR 5).\n *\n * Reverts a consolidated memory by restoring each source memory from its\n * `derived_from` snapshot and archiving the target.\n *\n * Contract:\n * - Load the target memory markdown file via its absolute path.\n * - For every `\"<rel>:<version>\"` entry in `derived_from`, fetch the\n * snapshot content via `page-versioning.getVersion` and restore it\n * to the original relative path. If the restore target file\n * already exists, we skip overwriting it (the source was never\n * archived, or was re-created since) and record the skip.\n * - Archive the target with reason code `\"consolidation-undo\"` so the\n * lifecycle ledger records the undo.\n * - Dry-run mode produces the same plan without touching disk.\n *\n * The helper is kept pure over `StorageManager` so the CLI can reuse it\n * without additional wiring, and tests can exercise the plan logic\n * directly.\n */\n\nimport path from \"node:path\";\nimport { mkdir, writeFile, access, realpath, lstat } from \"node:fs/promises\";\nimport { constants as fsConstants } from \"node:fs\";\nimport type { StorageManager } from \"./storage.js\";\nimport type { VersioningConfig } from \"./page-versioning.js\";\nimport { getVersion } from \"./page-versioning.js\";\n\n/**\n * Outcome of restoring a single `derived_from` source.\n */\nexport interface ConsolidationUndoRestore {\n /** The raw `\"<relpath>:<version>\"` entry from `derived_from`. */\n entry: string;\n /** Absolute path where the source would be / was restored. */\n sourcePath: string;\n /** What actually happened. */\n outcome:\n | \"restored\"\n | \"skipped_file_exists\"\n | \"skipped_non_regular_file\"\n | \"skipped_snapshot_missing\"\n | \"skipped_malformed_entry\"\n | \"skipped_outside_memory_dir\"\n | \"skipped_non_active_path\"\n | \"skipped_self_referential\"\n | \"skipped_write_failed\"\n | \"skipped_blocked_by_other_failures\"\n | \"skipped_dry_run\";\n /** Human-readable detail. */\n detail?: string;\n}\n\n/**\n * Plan + result of a `remnic consolidate undo` invocation.\n */\nexport interface ConsolidationUndoResult {\n /** Absolute path to the target memory. */\n targetPath: string;\n /** True when the target was archived successfully. */\n targetArchived: boolean;\n /** Per-source restore outcomes. */\n restores: ConsolidationUndoRestore[];\n /** Whether the run was a dry-run plan only. */\n dryRun: boolean;\n /** Fatal error, if any — the run bails early. */\n error?: string;\n}\n\nconst DERIVED_FROM_ENTRY_RE = /^(.+):(\\d+)$/;\n\nfunction parseEntry(entry: unknown): { pagePath: string; versionId: string } | null {\n // Non-string entries (PR #637 round-3 review, cursor Low) can arrive\n // from hostile on-disk frontmatter — guard against a .match() crash.\n if (typeof entry !== \"string\") return null;\n const match = entry.match(DERIVED_FROM_ENTRY_RE);\n if (!match) return null;\n return { pagePath: match[1], versionId: match[2] };\n}\n\n/**\n * Verify that `candidate` resolves inside `root` (defense against\n * path-traversal in `derived_from` entries and user-facing target\n * paths). Path-string normalization only; for symlink-aware checks\n * use `isInsideDirectoryRealpath`. Both paths are resolved to\n * absolute form before comparison so `..` segments, symlinks-as-\n * strings, and relative prefixes are normalized.\n */\nexport function isInsideDirectory(candidate: string, root: string): boolean {\n const normRoot = path.resolve(root);\n const normCandidate = path.resolve(candidate);\n const rel = path.relative(normRoot, normCandidate);\n if (rel.length === 0) return true;\n if (rel.startsWith(\"..\")) return false;\n if (path.isAbsolute(rel)) return false;\n return true;\n}\n\n/**\n * Symlink-aware containment check (PR #637 round-2 review, codex P1).\n *\n * `isInsideDirectory` only normalizes path strings — if a `derived_from`\n * entry resolves through a symlink inside `memoryDir` that points\n * outside, the string check passes but the subsequent `writeFile` would\n * land outside the memory tree. Use this guard for any path that is\n * about to be written.\n *\n * Walks every parent directory between `candidate` and `root`,\n * `realpath`-ing each segment that exists and rejecting when any\n * segment escapes `root`. Non-existent parents are resolved as the\n * canonicalized deepest-existing ancestor plus the trailing segments,\n * so a not-yet-created target file still gets the symlink check on its\n * existing parent directories.\n */\nexport async function isInsideDirectoryRealpath(\n candidate: string,\n root: string,\n): Promise<boolean> {\n if (!isInsideDirectory(candidate, root)) return false;\n // Reject raw `..` segments before canonicalization so that symlinks\n // cannot be hidden behind intermediate dot-dot components (PR #637\n // round-14 review, codex P1).\n const rawSegments = candidate.replace(/\\\\/g, \"/\").split(\"/\");\n if (rawSegments.some((s) => s === \"..\")) return false;\n let resolvedRoot: string;\n try {\n resolvedRoot = await realpath(path.resolve(root));\n } catch {\n return false;\n }\n const normCandidate = path.resolve(candidate);\n\n // Reject dangling symlinks (PR #637 round-3 review, codex P1).\n // If the candidate itself is a symlink (even if its target doesn't\n // exist), Node will follow it when we later call `writeFile`.\n // `lstat` inspects the link itself without dereferencing; if it\n // succeeds and reports a symlink, we treat the candidate as\n // unsafe. We must check every non-root ancestor too — a symlink\n // anywhere along the path lets an attacker redirect writes.\n const normRoot = path.resolve(root);\n const relFromRoot = path.relative(normRoot, normCandidate);\n const segments = relFromRoot.length > 0 ? relFromRoot.split(path.sep) : [];\n for (let i = 0; i <= segments.length; i++) {\n const probe = i === 0 ? normRoot : path.join(normRoot, ...segments.slice(0, i));\n try {\n const st = await lstat(probe);\n if (st.isSymbolicLink() && probe !== normRoot) {\n // A symlink on the path — resolve THIS segment and bail out\n // if the resolved target escapes `resolvedRoot`.\n let target: string;\n try {\n target = await realpath(probe);\n } catch {\n // Dangling symlink inside memoryDir — always unsafe.\n return false;\n }\n const rel = path.relative(resolvedRoot, target);\n if (rel.length === 0) continue;\n if (rel.startsWith(\"..\") || path.isAbsolute(rel)) return false;\n }\n } catch {\n // Segment doesn't exist yet — that's fine, fall through to the\n // textual containment verification below.\n }\n }\n\n // Walk up from the candidate until we hit a path that exists, then\n // realpath THAT and re-apply the trailing segments textually.\n const parts = normCandidate.split(path.sep);\n for (let i = parts.length; i > 0; i--) {\n const probe = parts.slice(0, i).join(path.sep) || path.sep;\n try {\n const resolved = await realpath(probe);\n // Re-join any trailing segments that didn't exist yet.\n const trailing = parts.slice(i).join(path.sep);\n const final = trailing.length > 0 ? path.join(resolved, trailing) : resolved;\n // Now apply the textual containment check against the canonical\n // `resolvedRoot`.\n const rel = path.relative(resolvedRoot, final);\n if (rel.length === 0) return true;\n if (rel.startsWith(\"..\")) return false;\n if (path.isAbsolute(rel)) return false;\n return true;\n } catch {\n continue;\n }\n }\n // Nothing along the path resolvable — treat as outside by default.\n return false;\n}\n\n/**\n * Directories under memoryDir that are NOT active memory locations.\n * A `derived_from` entry pointing into one of these should not be\n * counted as \"recovered_existing\" (PR #637 round-7 review, codex P2).\n * The versioning sidecar directory is included dynamically via the\n * `sidecarDir` parameter (PR #637 round-8 review, codex P2).\n */\nconst NON_ACTIVE_PREFIXES = [\"archive/\", \"state/\"];\n\n/**\n * Normalize a relative path by collapsing `.` and `..` segments so\n * that crafted entries like `\"facts/../archive/x.md\"` are reduced to\n * `\"archive/x.md\"` before the non-active-prefix check.\n */\nfunction normalizeRelativePath(p: string): string {\n // Normalize separators, split into segments, then resolve.\n const parts = p.replace(/\\\\/g, \"/\").split(\"/\");\n const resolved: string[] = [];\n for (const seg of parts) {\n if (seg === \"\" || seg === \".\") continue;\n if (seg === \"..\") {\n if (resolved.length > 0) resolved.pop();\n // If \"..\" pops past the root, we let the caller's containment\n // check catch it — don't silently drop.\n } else {\n resolved.push(seg);\n }\n }\n return resolved.join(\"/\");\n}\n\n/**\n * Check that a relative path (relative to memoryDir) points to an\n * active memory location rather than an internal/archive directory.\n * Returns `true` when the normalised `pagePath` does NOT start with\n * a known non-active prefix.\n *\n * @param pagePath Relative path from `derived_from` entry.\n * @param sidecarDir Optional versioning sidecar directory name\n * (e.g. `\".versions\"`). When provided, paths\n * under this directory are also rejected as\n * non-active.\n */\nexport function isActiveMemoryRelativePath(\n pagePath: string,\n sidecarDir?: string,\n): boolean {\n const normalized = normalizeRelativePath(pagePath);\n const prefixes = [...NON_ACTIVE_PREFIXES];\n if (sidecarDir) {\n const normSidecar = normalizeRelativePath(sidecarDir);\n prefixes.push(normSidecar + \"/\");\n }\n for (const prefix of prefixes) {\n if (normalized === prefix.slice(0, -1) || normalized.startsWith(prefix)) {\n return false;\n }\n }\n return true;\n}\n\nasync function isRegularFile(p: string): Promise<boolean> {\n try {\n const st = await lstat(p);\n return st.isFile();\n } catch {\n return false;\n }\n}\n\nasync function fileExists(p: string): Promise<boolean> {\n try {\n await access(p, fsConstants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Perform a consolidation-undo operation.\n *\n * @param options.storage Storage manager for the memory directory.\n * @param options.memoryDir Absolute memory directory root.\n * @param options.targetPath Absolute path to the consolidated memory.\n * @param options.versioning Page-versioning config (sidecarDir must\n * match the sidecar layout used when the\n * snapshots were created).\n * @param options.dryRun When true, compute the plan but do not\n * write or archive.\n */\nexport async function runConsolidationUndo(options: {\n storage: StorageManager;\n memoryDir: string;\n targetPath: string;\n versioning: VersioningConfig;\n dryRun?: boolean;\n}): Promise<ConsolidationUndoResult> {\n const { storage, memoryDir, targetPath, versioning } = options;\n const dryRun = options.dryRun === true;\n\n const result: ConsolidationUndoResult = {\n targetPath,\n targetArchived: false,\n restores: [],\n dryRun,\n };\n\n // Defense against path-traversal (PR #637 review, codex P1): refuse\n // to operate on a target outside the configured memory directory.\n // Archive moves and eventual unlink would otherwise let an operator\n // accidentally destroy an unrelated file with memory-like\n // frontmatter. Uses the realpath-aware check so a symlinked\n // directory inside `memoryDir` can't tunnel a target past the guard.\n if (!(await isInsideDirectoryRealpath(targetPath, memoryDir))) {\n result.error = `target path ${targetPath} is outside memory directory ${memoryDir}`;\n return result;\n }\n\n // Reject targets in non-active directories (archive/, state/,\n // versioning sidecar). A target inside `.versions/...` would be\n // a sidecar snapshot, not a real consolidated memory; archiving\n // it would silently delete version history (PR #637 round-8\n // review, codex P2).\n const targetRel = path.relative(memoryDir, targetPath);\n if (!isActiveMemoryRelativePath(targetRel, versioning.sidecarDir)) {\n result.error = `target path \"${targetRel}\" is inside a non-active directory — refusing to operate`;\n return result;\n }\n\n // Load the target memory. readMemoryByPath returns null when the file\n // is absent or unparseable — surface that as a fatal error because the\n // caller cannot continue without a derived_from list.\n const target = await storage.readMemoryByPath(targetPath);\n if (!target) {\n result.error = `could not load target memory at ${targetPath}`;\n return result;\n }\n\n const derivedFrom = target.frontmatter.derived_from;\n if (!Array.isArray(derivedFrom) || derivedFrom.length === 0) {\n result.error = \"target memory has no derived_from entries — nothing to undo\";\n return result;\n }\n\n // Two-pass plan + execute (PR #637 round-4 review, cursor Medium):\n // the undo is \"all-or-nothing\" both for the archive decision AND\n // for the per-source writes. First pass validates + loads every\n // snapshot into memory; second pass writes only if every source\n // would succeed. This prevents the previous eager-write behaviour\n // where a later-failing source would leave earlier sources already\n // written to disk alongside an unarchived consolidated target.\n type RestorePlan =\n | { kind: \"skip\"; restore: ConsolidationUndoRestore }\n | { kind: \"write\"; entry: string; sourcePath: string; content: string }\n | { kind: \"recovered_existing\"; entry: string; sourcePath: string };\n\n const plans: RestorePlan[] = [];\n for (const rawEntry of derivedFrom) {\n const entry = typeof rawEntry === \"string\" ? rawEntry : String(rawEntry);\n const parsed = parseEntry(rawEntry);\n if (!parsed) {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath: \"\",\n outcome: \"skipped_malformed_entry\",\n detail: `expected \"<path>:<version>\" shape`,\n },\n });\n continue;\n }\n\n // Reject absolute paths in derived_from entries (PR #637 round-10\n // review, codex P1). An absolute pagePath would cause path.join to\n // ignore memoryDir, bypassing the active-directory guard downstream.\n if (path.isAbsolute(parsed.pagePath)) {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath: parsed.pagePath,\n outcome: \"skipped_malformed_entry\",\n detail: `derived_from path must be relative, got absolute: \"${parsed.pagePath}\"`,\n },\n });\n continue;\n }\n\n const sourcePath = path.join(memoryDir, parsed.pagePath);\n\n if (!(await isInsideDirectoryRealpath(sourcePath, memoryDir))) {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath,\n outcome: \"skipped_outside_memory_dir\",\n detail: `resolved path escapes memory directory ${memoryDir}`,\n },\n });\n continue;\n }\n\n // Reject source paths inside non-active directories (archive/,\n // state/, versioning sidecar). A crafted or corrupted derived_from\n // entry like \"archive/2024-01-01/x.md:1\" would otherwise be counted\n // as \"recovered_existing\" even though no active memory was restored.\n // Also resolve symlinks before checking — a derived_from entry like\n // \"facts/link/stale.md:1\" where `facts/link` points to `archive/…`\n // must be caught (PR #637 round-8 review, cursor+codex).\n let resolvedRelative = parsed.pagePath;\n try {\n const realBase = await realpath(memoryDir);\n try {\n const realSource = await realpath(sourcePath);\n const rel = path.relative(realBase, realSource);\n if (!rel.startsWith(\"..\") && !path.isAbsolute(rel)) {\n resolvedRelative = rel.replace(/\\\\/g, \"/\");\n }\n } catch {\n // realpath on the leaf failed (file doesn't exist yet). Try\n // resolving the parent directory instead — if the parent is a\n // symlink into archive/state, the leaf would be written there\n // too (PR #637 round-12 review, codex P1).\n const parentDir = path.dirname(sourcePath);\n try {\n const realParent = await realpath(parentDir);\n const parentRel = path.relative(realBase, realParent);\n if (!parentRel.startsWith(\"..\") && !path.isAbsolute(parentRel)) {\n const leafName = path.basename(sourcePath);\n resolvedRelative = path.join(parentRel, leafName).replace(/\\\\/g, \"/\");\n }\n } catch {\n // Parent also doesn't exist — fall through to text path check\n }\n }\n } catch {\n // memoryDir realpath failed — use the text path\n }\n if (!isActiveMemoryRelativePath(parsed.pagePath, versioning.sidecarDir) ||\n !isActiveMemoryRelativePath(resolvedRelative, versioning.sidecarDir)) {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath,\n outcome: \"skipped_non_active_path\",\n detail: `source path \"${parsed.pagePath}\" is inside a non-active directory (archive/state/versions)`,\n },\n });\n continue;\n }\n\n // Reject self-referential derived_from entries (PR #637 round-9 review,\n // codex P1). If the source resolves to the same file as the target,\n // counting it as \"recovered\" would let undo archive the target without\n // restoring any independent source — leaving no active copy. This\n // guards against corrupted or manually-edited derived_from lists.\n if (path.resolve(sourcePath) === path.resolve(targetPath)) {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath,\n outcome: \"skipped_self_referential\",\n detail: `derived_from entry \"${entry}\" resolves to the same file as the target — refusing to count as recovered`,\n },\n });\n continue;\n }\n\n if (await isRegularFile(sourcePath)) {\n // Source is still active (regular file present) — nothing to\n // restore but this counts as \"recovered\" for the archive\n // decision. We require a regular file specifically (PR #637\n // round-5 review, codex P2): a directory, device node, or\n // symlink at the source path should not count as \"recovered\"\n // because a later read won't find the expected memory content.\n plans.push({ kind: \"recovered_existing\", entry, sourcePath });\n continue;\n }\n if (await fileExists(sourcePath)) {\n // Something other than a regular file is at the source path\n // (directory, device node, symlink). Refuse to overwrite AND\n // refuse to count as recovered (PR #637 round-5 review, codex\n // P2) — the operator needs to clean up manually. This is a\n // blocking skip: no source writes happen, target stays active.\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath,\n outcome: \"skipped_non_regular_file\",\n detail: \"source path is occupied by a non-regular-file; refusing to proceed\",\n },\n });\n continue;\n }\n\n let snapshotContent: string;\n try {\n snapshotContent = await getVersion(\n sourcePath,\n parsed.versionId,\n versioning,\n memoryDir,\n );\n } catch {\n plans.push({\n kind: \"skip\",\n restore: {\n entry,\n sourcePath,\n outcome: \"skipped_snapshot_missing\",\n detail: `no snapshot for version ${parsed.versionId}`,\n },\n });\n continue;\n }\n\n plans.push({ kind: \"write\", entry, sourcePath, content: snapshotContent });\n }\n\n // If any plan is a skip (anything other than \"write\" or\n // \"recovered_existing\"), the undo is over before it starts — no\n // writes happen. Reveal every per-source skip reason in the\n // result so operators can diagnose what went wrong.\n const skipped = plans.filter((p) => p.kind === \"skip\");\n if (skipped.length > 0) {\n for (const p of plans) {\n if (p.kind === \"skip\") {\n result.restores.push(p.restore);\n } else if (p.kind === \"write\") {\n // Announced-but-not-executed write — still record it so the\n // operator sees what would have been restored if the failed\n // sources had been recoverable.\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: dryRun ? \"skipped_dry_run\" : \"skipped_blocked_by_other_failures\",\n detail: dryRun\n ? \"would restore from snapshot (blocked by other failures)\"\n : \"snapshot available but undo aborted due to other failures\",\n });\n } else {\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_file_exists\",\n detail: \"source file already exists; no restore needed\",\n });\n }\n }\n const recovered = result.restores.filter(\n (r) => r.outcome === \"restored\" || r.outcome === \"skipped_file_exists\",\n ).length;\n if (recovered === 0) {\n result.error =\n \"no sources could be recovered (all snapshots missing or paths unsafe); target not archived to preserve data\";\n } else {\n result.error = `${skipped.length} of ${plans.length} sources could not be recovered; target not archived (undo is all-or-nothing)`;\n }\n return result;\n }\n\n // Deduplicate plans by sourcePath: duplicate derived_from entries for\n // the same source would cause the second wx-flagged write to fail with\n // EEXIST after the first succeeds. The first plan for each source wins;\n // subsequent duplicates are recorded as skipped. Applied before dry-run\n // so the preview accurately reflects what execution would do.\n const seenSourcePaths = new Set<string>();\n const dedupedPlans: RestorePlan[] = [];\n for (const p of plans) {\n if (p.kind === \"write\" || p.kind === \"recovered_existing\") {\n if (seenSourcePaths.has(p.sourcePath)) {\n dedupedPlans.push({\n kind: \"skip\",\n restore: {\n entry: p.kind === \"write\" ? p.entry : p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_file_exists\",\n detail: \"duplicate derived_from entry — source already processed\",\n },\n });\n continue;\n }\n seenSourcePaths.add(p.sourcePath);\n }\n dedupedPlans.push(p);\n }\n\n // Dry-run: report what each plan would do.\n if (dryRun) {\n for (const p of dedupedPlans) {\n if (p.kind === \"write\") {\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_dry_run\",\n detail: \"would restore from snapshot\",\n });\n } else if (p.kind === \"recovered_existing\") {\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_file_exists\",\n detail: \"source file already exists; no restore needed\",\n });\n } else if (p.kind === \"skip\" && p.restore) {\n result.restores.push(p.restore);\n }\n }\n return result;\n }\n\n // All validations passed — execute writes. A write failure here\n // is a filesystem problem rather than a provenance problem, but\n // any failure still aborts the archive.\n\n let writeFailed = false;\n for (const p of dedupedPlans) {\n if (p.kind === \"skip\") {\n // Dedup-generated skip entries and other pre-write skips.\n if (p.restore) result.restores.push(p.restore);\n continue;\n }\n if (p.kind === \"recovered_existing\") {\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_file_exists\",\n detail: \"source file already exists; no restore needed\",\n });\n continue;\n }\n if (p.kind === \"write\") {\n if (writeFailed) {\n // All-or-nothing: once a write fails, skip all remaining writes\n // so the target is not archived with partial source coverage.\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_blocked_by_other_failures\",\n detail: \"a prior source write failed; skipping remaining writes to honor all-or-nothing contract\",\n });\n continue;\n }\n try {\n await mkdir(path.dirname(p.sourcePath), { recursive: true });\n // Use exclusive create (wx / O_EXCL) so that if another process\n // recreates the source file between planning and execution, this\n // write fails with EEXIST instead of silently overwriting the new\n // file (PR #637 round-11 review, codex P1).\n await writeFile(p.sourcePath, p.content, { encoding: \"utf-8\", flag: \"wx\" });\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"restored\",\n });\n } catch (err) {\n writeFailed = true;\n result.restores.push({\n entry: p.entry,\n sourcePath: p.sourcePath,\n outcome: \"skipped_write_failed\",\n detail: `write failed: ${err instanceof Error ? err.message : String(err)}`,\n });\n }\n }\n }\n\n if (writeFailed) {\n result.error =\n \"one or more source writes failed mid-restore; target not archived to preserve data\";\n return result;\n }\n\n // Archive the target memory. archiveMemory returns null on\n // failure — surface that as a fatal error (PR #637 round-5 review,\n // codex P2) so automation doesn't mistake a half-undo for a clean\n // run. The already-completed restores still roll forward; the\n // result.restores list records what was written.\n const archivedAt = await storage.archiveMemory(target, {\n actor: \"consolidate-undo\",\n reasonCode: \"consolidation-undo\",\n });\n result.targetArchived = archivedAt !== null;\n if (!result.targetArchived) {\n result.error =\n \"sources restored successfully but archiving the consolidated target failed; inspect storage for manual cleanup\";\n }\n return result;\n}\n\n/**\n * Render a consolidation-undo result as a human-readable multi-line\n * string for the CLI. Extracted so tests can snapshot the formatting\n * without parsing stdout.\n */\nexport function formatConsolidationUndoResult(result: ConsolidationUndoResult): string {\n const lines: string[] = [];\n lines.push(`consolidate undo ${result.dryRun ? \"(dry run) \" : \"\"}→ ${result.targetPath}`);\n // Emit per-restore details BEFORE the error (PR #637 review, cursor\n // Medium): the \"no sources could be recovered\" error is set after\n // the restore loop ran, so operators need the per-source skip\n // reasons to diagnose which snapshots were missing / outside\n // memoryDir / malformed. Early-bail errors (unloadable target,\n // target outside memoryDir, no derived_from) run before the loop,\n // so `result.restores` is empty in those cases and this block is a\n // no-op.\n for (const r of result.restores) {\n lines.push(` - ${r.entry} → ${r.outcome}${r.detail ? ` (${r.detail})` : \"\"}`);\n }\n if (result.error) {\n lines.push(` ERROR: ${result.error}`);\n return lines.join(\"\\n\");\n }\n lines.push(\n result.dryRun\n ? \" (dry run — no files were modified, target not archived)\"\n : ` target archived: ${result.targetArchived ? \"yes\" : \"no\"}`,\n );\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;AAsBA,OAAO,UAAU;AACjB,SAAS,OAAO,WAAW,QAAQ,UAAU,aAAa;AAC1D,SAAS,aAAa,mBAAmB;AA8CzC,IAAM,wBAAwB;AAE9B,SAAS,WAAW,OAAgE;AAGlF,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,QAAM,QAAQ,MAAM,MAAM,qBAAqB;AAC/C,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,EAAE,UAAU,MAAM,CAAC,GAAG,WAAW,MAAM,CAAC,EAAE;AACnD;AAUO,SAAS,kBAAkB,WAAmB,MAAuB;AAC1E,QAAM,WAAW,KAAK,QAAQ,IAAI;AAClC,QAAM,gBAAgB,KAAK,QAAQ,SAAS;AAC5C,QAAM,MAAM,KAAK,SAAS,UAAU,aAAa;AACjD,MAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,MAAI,IAAI,WAAW,IAAI,EAAG,QAAO;AACjC,MAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AACjC,SAAO;AACT;AAkBA,eAAsB,0BACpB,WACA,MACkB;AAClB,MAAI,CAAC,kBAAkB,WAAW,IAAI,EAAG,QAAO;AAIhD,QAAM,cAAc,UAAU,QAAQ,OAAO,GAAG,EAAE,MAAM,GAAG;AAC3D,MAAI,YAAY,KAAK,CAAC,MAAM,MAAM,IAAI,EAAG,QAAO;AAChD,MAAI;AACJ,MAAI;AACF,mBAAe,MAAM,SAAS,KAAK,QAAQ,IAAI,CAAC;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,gBAAgB,KAAK,QAAQ,SAAS;AAS5C,QAAM,WAAW,KAAK,QAAQ,IAAI;AAClC,QAAM,cAAc,KAAK,SAAS,UAAU,aAAa;AACzD,QAAM,WAAW,YAAY,SAAS,IAAI,YAAY,MAAM,KAAK,GAAG,IAAI,CAAC;AACzE,WAAS,IAAI,GAAG,KAAK,SAAS,QAAQ,KAAK;AACzC,UAAM,QAAQ,MAAM,IAAI,WAAW,KAAK,KAAK,UAAU,GAAG,SAAS,MAAM,GAAG,CAAC,CAAC;AAC9E,QAAI;AACF,YAAM,KAAK,MAAM,MAAM,KAAK;AAC5B,UAAI,GAAG,eAAe,KAAK,UAAU,UAAU;AAG7C,YAAI;AACJ,YAAI;AACF,mBAAS,MAAM,SAAS,KAAK;AAAA,QAC/B,QAAQ;AAEN,iBAAO;AAAA,QACT;AACA,cAAM,MAAM,KAAK,SAAS,cAAc,MAAM;AAC9C,YAAI,IAAI,WAAW,EAAG;AACtB,YAAI,IAAI,WAAW,IAAI,KAAK,KAAK,WAAW,GAAG,EAAG,QAAO;AAAA,MAC3D;AAAA,IACF,QAAQ;AAAA,IAGR;AAAA,EACF;AAIA,QAAM,QAAQ,cAAc,MAAM,KAAK,GAAG;AAC1C,WAAS,IAAI,MAAM,QAAQ,IAAI,GAAG,KAAK;AACrC,UAAM,QAAQ,MAAM,MAAM,GAAG,CAAC,EAAE,KAAK,KAAK,GAAG,KAAK,KAAK;AACvD,QAAI;AACF,YAAM,WAAW,MAAM,SAAS,KAAK;AAErC,YAAM,WAAW,MAAM,MAAM,CAAC,EAAE,KAAK,KAAK,GAAG;AAC7C,YAAM,QAAQ,SAAS,SAAS,IAAI,KAAK,KAAK,UAAU,QAAQ,IAAI;AAGpE,YAAM,MAAM,KAAK,SAAS,cAAc,KAAK;AAC7C,UAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,UAAI,IAAI,WAAW,IAAI,EAAG,QAAO;AACjC,UAAI,KAAK,WAAW,GAAG,EAAG,QAAO;AACjC,aAAO;AAAA,IACT,QAAQ;AACN;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AASA,IAAM,sBAAsB,CAAC,YAAY,QAAQ;AAOjD,SAAS,sBAAsB,GAAmB;AAEhD,QAAM,QAAQ,EAAE,QAAQ,OAAO,GAAG,EAAE,MAAM,GAAG;AAC7C,QAAM,WAAqB,CAAC;AAC5B,aAAW,OAAO,OAAO;AACvB,QAAI,QAAQ,MAAM,QAAQ,IAAK;AAC/B,QAAI,QAAQ,MAAM;AAChB,UAAI,SAAS,SAAS,EAAG,UAAS,IAAI;AAAA,IAGxC,OAAO;AACL,eAAS,KAAK,GAAG;AAAA,IACnB;AAAA,EACF;AACA,SAAO,SAAS,KAAK,GAAG;AAC1B;AAcO,SAAS,2BACd,UACA,YACS;AACT,QAAM,aAAa,sBAAsB,QAAQ;AACjD,QAAM,WAAW,CAAC,GAAG,mBAAmB;AACxC,MAAI,YAAY;AACd,UAAM,cAAc,sBAAsB,UAAU;AACpD,aAAS,KAAK,cAAc,GAAG;AAAA,EACjC;AACA,aAAW,UAAU,UAAU;AAC7B,QAAI,eAAe,OAAO,MAAM,GAAG,EAAE,KAAK,WAAW,WAAW,MAAM,GAAG;AACvE,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,eAAe,cAAc,GAA6B;AACxD,MAAI;AACF,UAAM,KAAK,MAAM,MAAM,CAAC;AACxB,WAAO,GAAG,OAAO;AAAA,EACnB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,eAAe,WAAW,GAA6B;AACrD,MAAI;AACF,UAAM,OAAO,GAAG,YAAY,IAAI;AAChC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAcA,eAAsB,qBAAqB,SAMN;AACnC,QAAM,EAAE,SAAS,WAAW,YAAY,WAAW,IAAI;AACvD,QAAM,SAAS,QAAQ,WAAW;AAElC,QAAM,SAAkC;AAAA,IACtC;AAAA,IACA,gBAAgB;AAAA,IAChB,UAAU,CAAC;AAAA,IACX;AAAA,EACF;AAQA,MAAI,CAAE,MAAM,0BAA0B,YAAY,SAAS,GAAI;AAC7D,WAAO,QAAQ,eAAe,UAAU,gCAAgC,SAAS;AACjF,WAAO;AAAA,EACT;AAOA,QAAM,YAAY,KAAK,SAAS,WAAW,UAAU;AACrD,MAAI,CAAC,2BAA2B,WAAW,WAAW,UAAU,GAAG;AACjE,WAAO,QAAQ,gBAAgB,SAAS;AACxC,WAAO;AAAA,EACT;AAKA,QAAM,SAAS,MAAM,QAAQ,iBAAiB,UAAU;AACxD,MAAI,CAAC,QAAQ;AACX,WAAO,QAAQ,mCAAmC,UAAU;AAC5D,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,OAAO,YAAY;AACvC,MAAI,CAAC,MAAM,QAAQ,WAAW,KAAK,YAAY,WAAW,GAAG;AAC3D,WAAO,QAAQ;AACf,WAAO;AAAA,EACT;AAcA,QAAM,QAAuB,CAAC;AAC9B,aAAW,YAAY,aAAa;AAClC,UAAM,QAAQ,OAAO,aAAa,WAAW,WAAW,OAAO,QAAQ;AACvE,UAAM,SAAS,WAAW,QAAQ;AAClC,QAAI,CAAC,QAAQ;AACX,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA,YAAY;AAAA,UACZ,SAAS;AAAA,UACT,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAKA,QAAI,KAAK,WAAW,OAAO,QAAQ,GAAG;AACpC,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA,YAAY,OAAO;AAAA,UACnB,SAAS;AAAA,UACT,QAAQ,sDAAsD,OAAO,QAAQ;AAAA,QAC/E;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,UAAM,aAAa,KAAK,KAAK,WAAW,OAAO,QAAQ;AAEvD,QAAI,CAAE,MAAM,0BAA0B,YAAY,SAAS,GAAI;AAC7D,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,QAAQ,0CAA0C,SAAS;AAAA,QAC7D;AAAA,MACF,CAAC;AACD;AAAA,IACF;AASA,QAAI,mBAAmB,OAAO;AAC9B,QAAI;AACF,YAAM,WAAW,MAAM,SAAS,SAAS;AACzC,UAAI;AACF,cAAM,aAAa,MAAM,SAAS,UAAU;AAC5C,cAAM,MAAM,KAAK,SAAS,UAAU,UAAU;AAC9C,YAAI,CAAC,IAAI,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,GAAG,GAAG;AAClD,6BAAmB,IAAI,QAAQ,OAAO,GAAG;AAAA,QAC3C;AAAA,MACF,QAAQ;AAKN,cAAM,YAAY,KAAK,QAAQ,UAAU;AACzC,YAAI;AACF,gBAAM,aAAa,MAAM,SAAS,SAAS;AAC3C,gBAAM,YAAY,KAAK,SAAS,UAAU,UAAU;AACpD,cAAI,CAAC,UAAU,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,SAAS,GAAG;AAC9D,kBAAM,WAAW,KAAK,SAAS,UAAU;AACzC,+BAAmB,KAAK,KAAK,WAAW,QAAQ,EAAE,QAAQ,OAAO,GAAG;AAAA,UACtE;AAAA,QACF,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF,QAAQ;AAAA,IAER;AACA,QAAI,CAAC,2BAA2B,OAAO,UAAU,WAAW,UAAU,KAClE,CAAC,2BAA2B,kBAAkB,WAAW,UAAU,GAAG;AACxE,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,QAAQ,gBAAgB,OAAO,QAAQ;AAAA,QACzC;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAOA,QAAI,KAAK,QAAQ,UAAU,MAAM,KAAK,QAAQ,UAAU,GAAG;AACzD,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,QAAQ,uBAAuB,KAAK;AAAA,QACtC;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,QAAI,MAAM,cAAc,UAAU,GAAG;AAOnC,YAAM,KAAK,EAAE,MAAM,sBAAsB,OAAO,WAAW,CAAC;AAC5D;AAAA,IACF;AACA,QAAI,MAAM,WAAW,UAAU,GAAG;AAMhC,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,QAAQ;AAAA,QACV;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,QAAI;AACJ,QAAI;AACF,wBAAkB,MAAM;AAAA,QACtB;AAAA,QACA,OAAO;AAAA,QACP;AAAA,QACA;AAAA,MACF;AAAA,IACF,QAAQ;AACN,YAAM,KAAK;AAAA,QACT,MAAM;AAAA,QACN,SAAS;AAAA,UACP;AAAA,UACA;AAAA,UACA,SAAS;AAAA,UACT,QAAQ,2BAA2B,OAAO,SAAS;AAAA,QACrD;AAAA,MACF,CAAC;AACD;AAAA,IACF;AAEA,UAAM,KAAK,EAAE,MAAM,SAAS,OAAO,YAAY,SAAS,gBAAgB,CAAC;AAAA,EAC3E;AAMA,QAAM,UAAU,MAAM,OAAO,CAAC,MAAM,EAAE,SAAS,MAAM;AACrD,MAAI,QAAQ,SAAS,GAAG;AACtB,eAAW,KAAK,OAAO;AACrB,UAAI,EAAE,SAAS,QAAQ;AACrB,eAAO,SAAS,KAAK,EAAE,OAAO;AAAA,MAChC,WAAW,EAAE,SAAS,SAAS;AAI7B,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS,SAAS,oBAAoB;AAAA,UACtC,QAAQ,SACJ,4DACA;AAAA,QACN,CAAC;AAAA,MACH,OAAO;AACL,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,UACT,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AAAA,IACF;AACA,UAAM,YAAY,OAAO,SAAS;AAAA,MAChC,CAAC,MAAM,EAAE,YAAY,cAAc,EAAE,YAAY;AAAA,IACnD,EAAE;AACF,QAAI,cAAc,GAAG;AACnB,aAAO,QACL;AAAA,IACJ,OAAO;AACL,aAAO,QAAQ,GAAG,QAAQ,MAAM,OAAO,MAAM,MAAM;AAAA,IACrD;AACA,WAAO;AAAA,EACT;AAOA,QAAM,kBAAkB,oBAAI,IAAY;AACxC,QAAM,eAA8B,CAAC;AACrC,aAAW,KAAK,OAAO;AACrB,QAAI,EAAE,SAAS,WAAW,EAAE,SAAS,sBAAsB;AACzD,UAAI,gBAAgB,IAAI,EAAE,UAAU,GAAG;AACrC,qBAAa,KAAK;AAAA,UAChB,MAAM;AAAA,UACN,SAAS;AAAA,YACP,OAAO,EAAE,SAAS,UAAU,EAAE,QAAQ,EAAE;AAAA,YACxC,YAAY,EAAE;AAAA,YACd,SAAS;AAAA,YACT,QAAQ;AAAA,UACV;AAAA,QACF,CAAC;AACD;AAAA,MACF;AACA,sBAAgB,IAAI,EAAE,UAAU;AAAA,IAClC;AACA,iBAAa,KAAK,CAAC;AAAA,EACrB;AAGA,MAAI,QAAQ;AACV,eAAW,KAAK,cAAc;AAC5B,UAAI,EAAE,SAAS,SAAS;AACtB,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,UACT,QAAQ;AAAA,QACV,CAAC;AAAA,MACH,WAAW,EAAE,SAAS,sBAAsB;AAC1C,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,UACT,QAAQ;AAAA,QACV,CAAC;AAAA,MACH,WAAW,EAAE,SAAS,UAAU,EAAE,SAAS;AACzC,eAAO,SAAS,KAAK,EAAE,OAAO;AAAA,MAChC;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAMA,MAAI,cAAc;AAClB,aAAW,KAAK,cAAc;AAC5B,QAAI,EAAE,SAAS,QAAQ;AAErB,UAAI,EAAE,QAAS,QAAO,SAAS,KAAK,EAAE,OAAO;AAC7C;AAAA,IACF;AACA,QAAI,EAAE,SAAS,sBAAsB;AACnC,aAAO,SAAS,KAAK;AAAA,QACnB,OAAO,EAAE;AAAA,QACT,YAAY,EAAE;AAAA,QACd,SAAS;AAAA,QACT,QAAQ;AAAA,MACV,CAAC;AACD;AAAA,IACF;AACA,QAAI,EAAE,SAAS,SAAS;AACtB,UAAI,aAAa;AAGf,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,UACT,QAAQ;AAAA,QACV,CAAC;AACD;AAAA,MACF;AACA,UAAI;AACF,cAAM,MAAM,KAAK,QAAQ,EAAE,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAK3D,cAAM,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,UAAU,SAAS,MAAM,KAAK,CAAC;AAC1E,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,QACX,CAAC;AAAA,MACH,SAAS,KAAK;AACZ,sBAAc;AACd,eAAO,SAAS,KAAK;AAAA,UACnB,OAAO,EAAE;AAAA,UACT,YAAY,EAAE;AAAA,UACd,SAAS;AAAA,UACT,QAAQ,iBAAiB,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,QAC3E,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAEA,MAAI,aAAa;AACf,WAAO,QACL;AACF,WAAO;AAAA,EACT;AAOA,QAAM,aAAa,MAAM,QAAQ,cAAc,QAAQ;AAAA,IACrD,OAAO;AAAA,IACP,YAAY;AAAA,EACd,CAAC;AACD,SAAO,iBAAiB,eAAe;AACvC,MAAI,CAAC,OAAO,gBAAgB;AAC1B,WAAO,QACL;AAAA,EACJ;AACA,SAAO;AACT;AAOO,SAAS,8BAA8B,QAAyC;AACrF,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,oBAAoB,OAAO,SAAS,eAAe,EAAE,UAAK,OAAO,UAAU,EAAE;AASxF,aAAW,KAAK,OAAO,UAAU;AAC/B,UAAM,KAAK,OAAO,EAAE,KAAK,WAAM,EAAE,OAAO,GAAG,EAAE,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE,EAAE;AAAA,EAC/E;AACA,MAAI,OAAO,OAAO;AAChB,UAAM,KAAK,YAAY,OAAO,KAAK,EAAE;AACrC,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AACA,QAAM;AAAA,IACJ,OAAO,SACH,mEACA,sBAAsB,OAAO,iBAAiB,QAAQ,IAAI;AAAA,EAChE;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
|
|
@@ -32,7 +32,7 @@ IMPORTANT:
|
|
|
32
32
|
- Two memories about the same entity/topic are NOT necessarily contradictory.
|
|
33
33
|
- Temporal changes ("Joshua uses pnpm" vs "Joshua switched to npm") ARE contradictions.
|
|
34
34
|
- Different aspects of the same entity ("Joshua uses pnpm" vs "Joshua works on Remnic") are "independent".`;
|
|
35
|
-
var
|
|
35
|
+
var defaultVerdictCache = /* @__PURE__ */ new Map();
|
|
36
36
|
var CACHE_MAX = 1e4;
|
|
37
37
|
function pairKey(idA, idB) {
|
|
38
38
|
const sorted = [idA, idB].sort();
|
|
@@ -49,7 +49,7 @@ function contentHash(a) {
|
|
|
49
49
|
async function judgeContradictionPairs(pairs, config, localLlm, fallbackLlm, cache) {
|
|
50
50
|
const startTime = Date.now();
|
|
51
51
|
const results = /* @__PURE__ */ new Map();
|
|
52
|
-
const activeCache = cache ??
|
|
52
|
+
const activeCache = cache ?? defaultVerdictCache;
|
|
53
53
|
let cached = 0;
|
|
54
54
|
let judged = 0;
|
|
55
55
|
const toJudge = [];
|
|
@@ -207,8 +207,9 @@ var SCAN_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
207
207
|
]);
|
|
208
208
|
async function runContradictionScan(deps) {
|
|
209
209
|
const startTime = Date.now();
|
|
210
|
-
const { storage, config, memoryDir, embeddingLookup, localLlm, fallbackLlm, namespace } = deps;
|
|
210
|
+
const { storage, config, memoryDir, embeddingLookup, embeddingLookupFactory, localLlm, fallbackLlm, namespace } = deps;
|
|
211
211
|
const scanConfig = config.contradictionScan;
|
|
212
|
+
const scopedEmbeddingLookup = embeddingLookupFactory ? embeddingLookupFactory(storage) : embeddingLookup;
|
|
212
213
|
if (!scanConfig.enabled) {
|
|
213
214
|
log.info("[contradiction-scan] disabled by config");
|
|
214
215
|
return { scanned: 0, candidates: 0, judged: 0, queued: 0, cooledDown: 0, elapsedMs: 0 };
|
|
@@ -223,7 +224,7 @@ async function runContradictionScan(deps) {
|
|
|
223
224
|
for (const p of existingPairs) {
|
|
224
225
|
existingMap.set(p.pairId, p);
|
|
225
226
|
}
|
|
226
|
-
const candidates = generatePairs(memories, existingMap, scanConfig,
|
|
227
|
+
const candidates = await generatePairs(memories, existingMap, scanConfig, scopedEmbeddingLookup);
|
|
227
228
|
const cooledDown = candidates.skipped;
|
|
228
229
|
log.info("[contradiction-scan] generated %d candidates (%d cooled down)", candidates.pairs.length, cooledDown);
|
|
229
230
|
if (candidates.pairs.length === 0) {
|
|
@@ -245,7 +246,8 @@ async function runContradictionScan(deps) {
|
|
|
245
246
|
categoryA: pair.categoryA,
|
|
246
247
|
categoryB: pair.categoryB
|
|
247
248
|
}));
|
|
248
|
-
const
|
|
249
|
+
const scanCache = /* @__PURE__ */ new Map();
|
|
250
|
+
const judgeResult = await judgeContradictionPairs(judgeInputs, config, localLlm, fallbackLlm, scanCache);
|
|
249
251
|
log.info("[contradiction-scan] judge completed: %d judged, %d cached in %dms", judgeResult.judged, judgeResult.cached, judgeResult.elapsed);
|
|
250
252
|
const queueEntries = [];
|
|
251
253
|
for (const [key, result] of judgeResult.results) {
|
|
@@ -272,8 +274,9 @@ async function runContradictionScan(deps) {
|
|
|
272
274
|
elapsedMs: elapsed
|
|
273
275
|
};
|
|
274
276
|
}
|
|
275
|
-
function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
277
|
+
async function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
276
278
|
const pairs = [];
|
|
279
|
+
const embeddingPairs = [];
|
|
277
280
|
let skipped = 0;
|
|
278
281
|
const seen = /* @__PURE__ */ new Set();
|
|
279
282
|
const byEntity = /* @__PURE__ */ new Map();
|
|
@@ -338,6 +341,39 @@ function generatePairs(memories, existingPairs, scanConfig, embeddingLookup) {
|
|
|
338
341
|
});
|
|
339
342
|
}
|
|
340
343
|
}
|
|
344
|
+
if (embeddingLookup) {
|
|
345
|
+
const memoryById = new Map(memories.map((m) => [m.frontmatter.id, m]));
|
|
346
|
+
for (const mem of memories) {
|
|
347
|
+
const id = mem.frontmatter.id;
|
|
348
|
+
try {
|
|
349
|
+
const hits = await embeddingLookup(mem.content, 20);
|
|
350
|
+
for (const hit of hits) {
|
|
351
|
+
if (hit.score < scanConfig.similarityFloor) continue;
|
|
352
|
+
if (hit.id === id) continue;
|
|
353
|
+
const peer = memoryById.get(hit.id);
|
|
354
|
+
if (!peer) continue;
|
|
355
|
+
const pairId = computePairId(id, hit.id);
|
|
356
|
+
if (seen.has(pairId)) continue;
|
|
357
|
+
seen.add(pairId);
|
|
358
|
+
const existing = existingPairs.get(pairId);
|
|
359
|
+
if (existing && isCoolingDown(existing, scanConfig.cooldownDays)) {
|
|
360
|
+
skipped++;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
embeddingPairs.push({
|
|
364
|
+
idA: id,
|
|
365
|
+
idB: hit.id,
|
|
366
|
+
textA: mem.content,
|
|
367
|
+
textB: peer.content,
|
|
368
|
+
categoryA: mem.frontmatter.category,
|
|
369
|
+
categoryB: peer.frontmatter.category
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
pairs.push(...embeddingPairs);
|
|
341
377
|
return { pairs, skipped };
|
|
342
378
|
}
|
|
343
379
|
async function loadEligibleMemories(storage, namespace) {
|
|
@@ -373,4 +409,4 @@ export {
|
|
|
373
409
|
ACTIVE_STATUSES,
|
|
374
410
|
runContradictionScan
|
|
375
411
|
};
|
|
376
|
-
//# sourceMappingURL=contradiction-scan-
|
|
412
|
+
//# sourceMappingURL=contradiction-scan-E3GJTI4F.js.map
|