@remnic/core 9.3.663 → 9.3.664
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-cli.js +25 -23
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +20 -18
- package/dist/access-mcp.js +19 -17
- package/dist/access-schema.js +4 -3
- package/dist/access-service.js +17 -15
- package/dist/briefing.js +5 -4
- package/dist/{capsule-merge-T2JRE46P.js → capsule-merge-GK5E647P.js} +3 -2
- package/dist/{capsule-merge-T2JRE46P.js.map → capsule-merge-GK5E647P.js.map} +1 -1
- package/dist/causal-consolidation.js +6 -5
- package/dist/causal-consolidation.js.map +1 -1
- package/dist/{chunk-2KDQI363.js → chunk-2HEZXPYU.js} +4 -4
- package/dist/chunk-5GPPACXK.js +16 -0
- package/dist/chunk-5GPPACXK.js.map +1 -0
- package/dist/{chunk-F6O7IOS3.js → chunk-6JBKHTQD.js} +2 -2
- package/dist/{chunk-TBLGI2LT.js → chunk-7ILWCUWH.js} +5 -3
- package/dist/{chunk-TBLGI2LT.js.map → chunk-7ILWCUWH.js.map} +1 -1
- package/dist/{chunk-AL4RAJL5.js → chunk-7XH7VJN4.js} +6 -4
- package/dist/chunk-7XH7VJN4.js.map +1 -0
- package/dist/{chunk-Q4CAQGKQ.js → chunk-AER6MT24.js} +12 -21
- package/dist/chunk-AER6MT24.js.map +1 -0
- package/dist/{chunk-DHGSZ3UD.js → chunk-ARV3AUOM.js} +2 -2
- package/dist/{chunk-PXVFMQLD.js → chunk-BZG2CWOQ.js} +3 -3
- package/dist/{chunk-ANJOULTP.js → chunk-C7AF236A.js} +2 -2
- package/dist/{chunk-FZC2WSDB.js → chunk-DOCTITOP.js} +2 -2
- package/dist/{chunk-WOQIHC67.js → chunk-DQY7NJ5L.js} +2 -2
- package/dist/{chunk-NMPEJV5M.js → chunk-DSLUOQDY.js} +2 -2
- package/dist/{chunk-A7EF2XRO.js → chunk-EXXBA5OM.js} +30 -8
- package/dist/chunk-EXXBA5OM.js.map +1 -0
- package/dist/{chunk-QXHBWFR3.js → chunk-IHG6CC7T.js} +2 -2
- package/dist/{chunk-4KDLCMLK.js → chunk-IROWLAWG.js} +5 -5
- package/dist/{chunk-ILXTATKK.js → chunk-J2HSAU72.js} +5 -5
- package/dist/chunk-J2HSAU72.js.map +1 -0
- package/dist/{chunk-DFAXGZKI.js → chunk-JIX3ZL2J.js} +8 -8
- package/dist/{chunk-GY3V3SUI.js → chunk-KHGE6PMF.js} +2 -2
- package/dist/{chunk-TGOOJCGA.js → chunk-LIERUFPO.js} +76 -54
- package/dist/chunk-LIERUFPO.js.map +1 -0
- package/dist/{chunk-HSCJYHYV.js → chunk-NLF54XMD.js} +49 -19
- package/dist/chunk-NLF54XMD.js.map +1 -0
- package/dist/{chunk-TWAJICBN.js → chunk-OHJFJ4HI.js} +2 -2
- package/dist/{chunk-WSQG37DV.js → chunk-OUWAQVDJ.js} +2 -2
- package/dist/{chunk-ZLDUQWT2.js → chunk-PWWWLD7D.js} +2 -2
- package/dist/{chunk-ZJH723NM.js → chunk-Q5ZU3RNY.js} +2 -2
- package/dist/{chunk-35HP3TGR.js → chunk-ROHLEUTH.js} +4 -4
- package/dist/{chunk-5RIRL3XL.js → chunk-RS25QOKZ.js} +2 -2
- package/dist/{chunk-RQGR3ETH.js → chunk-T2AN3BSP.js} +2 -2
- package/dist/{chunk-UAU5U5ML.js → chunk-UDJLF3BO.js} +2 -2
- package/dist/{chunk-ALEPI75L.js → chunk-VF4XKTX3.js} +6 -4
- package/dist/{chunk-ALEPI75L.js.map → chunk-VF4XKTX3.js.map} +1 -1
- package/dist/{chunk-AX5O25EF.js → chunk-VH6EIKVS.js} +152 -190
- package/dist/chunk-VH6EIKVS.js.map +1 -0
- package/dist/chunk-VS2IYZRU.js +43 -0
- package/dist/chunk-VS2IYZRU.js.map +1 -0
- package/dist/{chunk-YYQRVNSV.js → chunk-XB5P5P2L.js} +6 -6
- package/dist/{chunk-D2EFNQMY.js → chunk-XW3W4PV4.js} +2 -2
- package/dist/{chunk-5AYAZN45.js → chunk-YKX63GBK.js} +5 -5
- package/dist/{chunk-TYIXG4VR.js → chunk-YW52BQSU.js} +2 -2
- package/dist/{cli-C6twwe84.d.ts → cli-BQRqR9N-.d.ts} +12 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +32 -28
- package/dist/compounding/engine.js +5 -4
- package/dist/connectors/codex-materialize-runner.js +5 -4
- package/dist/connectors/index.js +5 -4
- package/dist/consolidation-provenance-check.js +3 -2
- package/dist/consolidation-undo.js +2 -1
- package/dist/consolidation-undo.js.map +1 -1
- package/dist/entity-retrieval.js +5 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +39 -36
- package/dist/index.js.map +1 -1
- package/dist/maintenance/memory-governance.js +6 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -4
- package/dist/maintenance/rebuild-memory-projection.js +7 -5
- package/dist/namespaces/migrate.js +13 -11
- package/dist/namespaces/search.js +8 -6
- package/dist/namespaces/storage.js +5 -4
- package/dist/offline-sync.js +3 -2
- package/dist/operator-toolkit.js +16 -14
- package/dist/orchestrator.js +21 -19
- package/dist/page-versioning.js +2 -1
- package/dist/schemas.d.ts +22 -22
- package/dist/search/document-scanner.d.ts +11 -7
- package/dist/search/document-scanner.js +3 -1
- package/dist/search/factory.js +7 -5
- package/dist/search/index.js +7 -5
- package/dist/search/lancedb-backend.js +4 -2
- package/dist/search/meilisearch-backend.js +4 -2
- package/dist/search/orama-backend.js +4 -2
- package/dist/secure-store/index.js +3 -2
- package/dist/semantic-consolidation.js +6 -5
- package/dist/semantic-rule-promotion.js +5 -4
- package/dist/semantic-rule-verifier.js +5 -4
- package/dist/storage.d.ts +17 -3
- package/dist/storage.js +4 -3
- package/dist/transfer/capsule-import.js +3 -2
- package/dist/transfer/types.d.ts +12 -12
- package/dist/verified-recall.js +5 -4
- package/package.json +1 -1
- package/src/cli.ts +62 -23
- package/src/consolidation-provenance-check.ts +7 -6
- package/src/maintenance/memory-governance.ts +47 -7
- package/src/orchestrator.ts +84 -58
- package/src/page-versioning.ts +7 -4
- package/src/search/document-scanner.test.ts +29 -0
- package/src/search/document-scanner.ts +17 -29
- package/src/secure-store/secure-fs.ts +19 -5
- package/src/secure-store/secure-store.test.ts +28 -0
- package/src/storage.ts +42 -43
- package/src/training-export/converter.test.ts +19 -0
- package/src/training-export/converter.ts +8 -5
- package/src/utils/category-dir.ts +10 -4
- package/src/utils/path-containment.ts +40 -0
- package/dist/chunk-A7EF2XRO.js.map +0 -1
- package/dist/chunk-AL4RAJL5.js.map +0 -1
- package/dist/chunk-AX5O25EF.js.map +0 -1
- package/dist/chunk-HSCJYHYV.js.map +0 -1
- package/dist/chunk-ILXTATKK.js.map +0 -1
- package/dist/chunk-Q4CAQGKQ.js.map +0 -1
- package/dist/chunk-TGOOJCGA.js.map +0 -1
- /package/dist/{chunk-2KDQI363.js.map → chunk-2HEZXPYU.js.map} +0 -0
- /package/dist/{chunk-F6O7IOS3.js.map → chunk-6JBKHTQD.js.map} +0 -0
- /package/dist/{chunk-DHGSZ3UD.js.map → chunk-ARV3AUOM.js.map} +0 -0
- /package/dist/{chunk-PXVFMQLD.js.map → chunk-BZG2CWOQ.js.map} +0 -0
- /package/dist/{chunk-ANJOULTP.js.map → chunk-C7AF236A.js.map} +0 -0
- /package/dist/{chunk-FZC2WSDB.js.map → chunk-DOCTITOP.js.map} +0 -0
- /package/dist/{chunk-WOQIHC67.js.map → chunk-DQY7NJ5L.js.map} +0 -0
- /package/dist/{chunk-NMPEJV5M.js.map → chunk-DSLUOQDY.js.map} +0 -0
- /package/dist/{chunk-QXHBWFR3.js.map → chunk-IHG6CC7T.js.map} +0 -0
- /package/dist/{chunk-4KDLCMLK.js.map → chunk-IROWLAWG.js.map} +0 -0
- /package/dist/{chunk-DFAXGZKI.js.map → chunk-JIX3ZL2J.js.map} +0 -0
- /package/dist/{chunk-GY3V3SUI.js.map → chunk-KHGE6PMF.js.map} +0 -0
- /package/dist/{chunk-TWAJICBN.js.map → chunk-OHJFJ4HI.js.map} +0 -0
- /package/dist/{chunk-WSQG37DV.js.map → chunk-OUWAQVDJ.js.map} +0 -0
- /package/dist/{chunk-ZLDUQWT2.js.map → chunk-PWWWLD7D.js.map} +0 -0
- /package/dist/{chunk-ZJH723NM.js.map → chunk-Q5ZU3RNY.js.map} +0 -0
- /package/dist/{chunk-35HP3TGR.js.map → chunk-ROHLEUTH.js.map} +0 -0
- /package/dist/{chunk-5RIRL3XL.js.map → chunk-RS25QOKZ.js.map} +0 -0
- /package/dist/{chunk-RQGR3ETH.js.map → chunk-T2AN3BSP.js.map} +0 -0
- /package/dist/{chunk-UAU5U5ML.js.map → chunk-UDJLF3BO.js.map} +0 -0
- /package/dist/{chunk-YYQRVNSV.js.map → chunk-XB5P5P2L.js.map} +0 -0
- /package/dist/{chunk-D2EFNQMY.js.map → chunk-XW3W4PV4.js.map} +0 -0
- /package/dist/{chunk-5AYAZN45.js.map → chunk-YKX63GBK.js.map} +0 -0
- /package/dist/{chunk-TYIXG4VR.js.map → chunk-YW52BQSU.js.map} +0 -0
|
@@ -1 +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":"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","names":[]}
|
|
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":"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","names":[]}
|
package/dist/entity-retrieval.js
CHANGED
|
@@ -3,9 +3,9 @@ import {
|
|
|
3
3
|
entityIndexVersion,
|
|
4
4
|
entityRecentTranscriptLookbackHours,
|
|
5
5
|
readRecentEntityTranscriptEntries
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-T2AN3BSP.js";
|
|
7
7
|
import "./chunk-BEUDU7Y4.js";
|
|
8
|
-
import "./chunk-
|
|
8
|
+
import "./chunk-VH6EIKVS.js";
|
|
9
9
|
import "./chunk-M7XQSUBB.js";
|
|
10
10
|
import "./chunk-5UZXUTVO.js";
|
|
11
11
|
import "./chunk-J6A3CX5N.js";
|
|
@@ -20,12 +20,13 @@ import "./chunk-DM2T26WE.js";
|
|
|
20
20
|
import "./chunk-LDXUBPMO.js";
|
|
21
21
|
import "./chunk-FVQJYWH7.js";
|
|
22
22
|
import "./chunk-G7D6GZ5J.js";
|
|
23
|
-
import "./chunk-
|
|
23
|
+
import "./chunk-VF4XKTX3.js";
|
|
24
24
|
import "./chunk-4DJQYKMN.js";
|
|
25
25
|
import "./chunk-2ODBA7MQ.js";
|
|
26
|
-
import "./chunk-
|
|
26
|
+
import "./chunk-J2HSAU72.js";
|
|
27
27
|
import "./chunk-A6XUJE5D.js";
|
|
28
28
|
import "./chunk-P7FMDTKL.js";
|
|
29
|
+
import "./chunk-VS2IYZRU.js";
|
|
29
30
|
import "./chunk-PZ5AY32C.js";
|
|
30
31
|
export {
|
|
31
32
|
buildEntityRecallSection,
|
package/dist/index.d.ts
CHANGED
|
@@ -78,7 +78,7 @@ export { EnrichmentAuditEntry, EnrichmentCandidate, EnrichmentCostTier, Enrichme
|
|
|
78
78
|
export { d as BulkImportError, B as BulkImportOptions, b as BulkImportResult, a as BulkImportSource, e as BulkImportSourceAdapter, f as ImportSourceRole, I as ImportTurn, g as ImportTurnValidationIssue, i as isImportRole, p as parseIsoTimestamp, v as validateImportTurn } from './types-ByK7T3L6.js';
|
|
79
79
|
export { clearBulkImportSources, getBulkImportSource, listBulkImportSources, registerBulkImportSource } from './bulk-import/index.js';
|
|
80
80
|
export { a as ProcessBatchContext, P as ProcessBatchFn, b as ProcessBatchResult, f as formatBatchTranscript, r as resolveBulkImportContext, c as runBulkImportPipeline, v as validateBatchSize } from './pipeline-BSTuml0d.js';
|
|
81
|
-
export { B as BulkImportCliCommandOptions, p as parseStrictCliDate, r as runBulkImportCliCommand } from './cli-
|
|
81
|
+
export { B as BulkImportCliCommandOptions, p as parseStrictCliDate, r as runBulkImportCliCommand } from './cli-BQRqR9N-.js';
|
|
82
82
|
export { DEFAULT_IMPORT_BATCH_SIZE, ImportProgress, ImportedMemory, ImporterAdapter, ImporterParseOptions, ImporterTransformOptions, ImporterWriteResult, ImporterWriteTarget, RunImportOptions, RunImporterResult, defaultWriteMemoriesToOrchestrator, importedMemoryToTurn, runImporter, validateImportBatchSize, validateImportRateLimit } from './importers/index.js';
|
|
83
83
|
export { FallbackLlmClient, FallbackLlmOptions, FallbackLlmResponse, FallbackLlmRuntimeContext } from './fallback-llm.js';
|
|
84
84
|
export { ComputeMemoryWorthInput, MemoryWorthResult, computeMemoryWorth } from './memory-worth.js';
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
registerTrainingExportAdapter,
|
|
95
95
|
runBulkImportCliCommand,
|
|
96
96
|
runWearablesCliCommand
|
|
97
|
-
} from "./chunk-
|
|
97
|
+
} from "./chunk-NLF54XMD.js";
|
|
98
98
|
import "./chunk-MC4FJXPA.js";
|
|
99
99
|
import "./chunk-LQHDIS7L.js";
|
|
100
100
|
import "./chunk-7F7Z6MOS.js";
|
|
@@ -112,8 +112,8 @@ import "./chunk-765K3SAT.js";
|
|
|
112
112
|
import "./chunk-EEC4PCG5.js";
|
|
113
113
|
import "./chunk-PYIFUBRK.js";
|
|
114
114
|
import "./chunk-HOJZMQ4J.js";
|
|
115
|
-
import "./chunk-
|
|
116
|
-
import "./chunk-
|
|
115
|
+
import "./chunk-6JBKHTQD.js";
|
|
116
|
+
import "./chunk-BZG2CWOQ.js";
|
|
117
117
|
import "./chunk-LZZNTPLR.js";
|
|
118
118
|
import "./chunk-WMWVO45V.js";
|
|
119
119
|
import "./chunk-KJTKLXTH.js";
|
|
@@ -136,7 +136,7 @@ import {
|
|
|
136
136
|
} from "./chunk-OUELRE5E.js";
|
|
137
137
|
import "./chunk-GQL52GQ5.js";
|
|
138
138
|
import "./chunk-EVWIEEKZ.js";
|
|
139
|
-
import "./chunk-
|
|
139
|
+
import "./chunk-IHG6CC7T.js";
|
|
140
140
|
import {
|
|
141
141
|
clearAuthTokenSecretCache,
|
|
142
142
|
isAgentAccessSecretRef,
|
|
@@ -147,8 +147,8 @@ import {
|
|
|
147
147
|
parseXrayBudgetFlag,
|
|
148
148
|
parseXrayCliOptions
|
|
149
149
|
} from "./chunk-SXYCVRLK.js";
|
|
150
|
-
import "./chunk-
|
|
151
|
-
import "./chunk-
|
|
150
|
+
import "./chunk-YKX63GBK.js";
|
|
151
|
+
import "./chunk-XB5P5P2L.js";
|
|
152
152
|
import {
|
|
153
153
|
parseStrictCliDate
|
|
154
154
|
} from "./chunk-3T74IZB3.js";
|
|
@@ -157,7 +157,7 @@ import "./chunk-YNDLCWXS.js";
|
|
|
157
157
|
import "./chunk-AUDJPF4N.js";
|
|
158
158
|
import "./chunk-44442YCH.js";
|
|
159
159
|
import "./chunk-Y4Z4I6WK.js";
|
|
160
|
-
import "./chunk-
|
|
160
|
+
import "./chunk-7XH7VJN4.js";
|
|
161
161
|
import {
|
|
162
162
|
reportBufferSurpriseDistribution
|
|
163
163
|
} from "./chunk-YBPYIAA5.js";
|
|
@@ -192,7 +192,7 @@ import {
|
|
|
192
192
|
saveTaxonomy,
|
|
193
193
|
validateSlug,
|
|
194
194
|
validateTaxonomy
|
|
195
|
-
} from "./chunk-
|
|
195
|
+
} from "./chunk-LIERUFPO.js";
|
|
196
196
|
import {
|
|
197
197
|
WEARABLE_SOURCE_PREFIX,
|
|
198
198
|
buildExtractionTurns,
|
|
@@ -236,7 +236,7 @@ import "./chunk-TPDBFYEG.js";
|
|
|
236
236
|
import "./chunk-KCYE2MZM.js";
|
|
237
237
|
import "./chunk-TECVW3JP.js";
|
|
238
238
|
import "./chunk-T2PO5MUF.js";
|
|
239
|
-
import "./chunk-
|
|
239
|
+
import "./chunk-OHJFJ4HI.js";
|
|
240
240
|
import "./chunk-DRD2Q7HQ.js";
|
|
241
241
|
import "./chunk-LXOM6IQU.js";
|
|
242
242
|
import "./chunk-VOUOLGIP.js";
|
|
@@ -323,7 +323,7 @@ import {
|
|
|
323
323
|
} from "./chunk-LZTFCAKE.js";
|
|
324
324
|
import {
|
|
325
325
|
buildEntityRecallSection
|
|
326
|
-
} from "./chunk-
|
|
326
|
+
} from "./chunk-T2AN3BSP.js";
|
|
327
327
|
import {
|
|
328
328
|
buildEventOrderRecallSection,
|
|
329
329
|
shouldRecallEventOrderEvidence
|
|
@@ -363,11 +363,11 @@ import {
|
|
|
363
363
|
import "./chunk-OD4FM2U7.js";
|
|
364
364
|
import {
|
|
365
365
|
buildExtensionsBlockForConsolidation
|
|
366
|
-
} from "./chunk-
|
|
366
|
+
} from "./chunk-XW3W4PV4.js";
|
|
367
367
|
import {
|
|
368
368
|
runCodexMaterialize,
|
|
369
369
|
runPostConsolidationMaterialize
|
|
370
|
-
} from "./chunk-
|
|
370
|
+
} from "./chunk-C7AF236A.js";
|
|
371
371
|
import {
|
|
372
372
|
MATERIALIZE_VERSION,
|
|
373
373
|
SENTINEL_FILE,
|
|
@@ -386,9 +386,9 @@ import "./chunk-2PRQG7PV.js";
|
|
|
386
386
|
import "./chunk-X6IRLNOO.js";
|
|
387
387
|
import "./chunk-2F6NP3NT.js";
|
|
388
388
|
import "./chunk-TERNBNJB.js";
|
|
389
|
-
import "./chunk-
|
|
389
|
+
import "./chunk-YW52BQSU.js";
|
|
390
390
|
import "./chunk-W4RVMTHR.js";
|
|
391
|
-
import "./chunk-
|
|
391
|
+
import "./chunk-DSLUOQDY.js";
|
|
392
392
|
import "./chunk-ZRWB5D4H.js";
|
|
393
393
|
import "./chunk-AGRPGAKR.js";
|
|
394
394
|
import {
|
|
@@ -399,7 +399,7 @@ import {
|
|
|
399
399
|
} from "./chunk-S4DDLTPX.js";
|
|
400
400
|
import "./chunk-SFQ6QNL7.js";
|
|
401
401
|
import "./chunk-33JBK2XP.js";
|
|
402
|
-
import "./chunk-
|
|
402
|
+
import "./chunk-7ILWCUWH.js";
|
|
403
403
|
import "./chunk-KFY3SGN7.js";
|
|
404
404
|
import {
|
|
405
405
|
resolvePluginEntry
|
|
@@ -496,7 +496,7 @@ import "./chunk-HQ6NIBL6.js";
|
|
|
496
496
|
import "./chunk-OADWQ5CR.js";
|
|
497
497
|
import {
|
|
498
498
|
EngramAccessHttpServer
|
|
499
|
-
} from "./chunk-
|
|
499
|
+
} from "./chunk-IROWLAWG.js";
|
|
500
500
|
import "./chunk-SEDEKFYQ.js";
|
|
501
501
|
import "./chunk-RKNJBZ55.js";
|
|
502
502
|
import "./chunk-J64TK33U.js";
|
|
@@ -510,7 +510,7 @@ import {
|
|
|
510
510
|
} from "./chunk-7WV3F5DQ.js";
|
|
511
511
|
import {
|
|
512
512
|
EngramMcpServer
|
|
513
|
-
} from "./chunk-
|
|
513
|
+
} from "./chunk-2HEZXPYU.js";
|
|
514
514
|
import {
|
|
515
515
|
REMNIC_CHATGPT_MEMORY_INSPECTOR_CANONICAL_TOOL,
|
|
516
516
|
REMNIC_CHATGPT_MEMORY_INSPECTOR_MIME_TYPE,
|
|
@@ -531,12 +531,12 @@ import {
|
|
|
531
531
|
EngramAccessService,
|
|
532
532
|
computeProcedureStats,
|
|
533
533
|
formatProcedureStatsText
|
|
534
|
-
} from "./chunk-
|
|
534
|
+
} from "./chunk-JIX3ZL2J.js";
|
|
535
535
|
import "./chunk-GDASG7NC.js";
|
|
536
536
|
import "./chunk-GDB4J2H3.js";
|
|
537
537
|
import {
|
|
538
538
|
importCapsule
|
|
539
|
-
} from "./chunk-
|
|
539
|
+
} from "./chunk-ARV3AUOM.js";
|
|
540
540
|
import "./chunk-H7XKCNR6.js";
|
|
541
541
|
import {
|
|
542
542
|
RECALL_XRAY_FORMATS,
|
|
@@ -548,7 +548,7 @@ import {
|
|
|
548
548
|
} from "./chunk-TIJYQXDI.js";
|
|
549
549
|
import "./chunk-SOBJ6NEY.js";
|
|
550
550
|
import "./chunk-BT7NVCML.js";
|
|
551
|
-
import "./chunk-
|
|
551
|
+
import "./chunk-EXXBA5OM.js";
|
|
552
552
|
import {
|
|
553
553
|
branchNamespaceName,
|
|
554
554
|
describeCodingScope,
|
|
@@ -558,7 +558,7 @@ import {
|
|
|
558
558
|
resolveGitContext,
|
|
559
559
|
stableHash
|
|
560
560
|
} from "./chunk-GYSYLGNE.js";
|
|
561
|
-
import "./chunk-
|
|
561
|
+
import "./chunk-RS25QOKZ.js";
|
|
562
562
|
import {
|
|
563
563
|
isTrustZoneName
|
|
564
564
|
} from "./chunk-JGSKJHF7.js";
|
|
@@ -566,23 +566,24 @@ import {
|
|
|
566
566
|
storagePathHash
|
|
567
567
|
} from "./chunk-FF4KLI5W.js";
|
|
568
568
|
import "./chunk-2MXEVL75.js";
|
|
569
|
-
import "./chunk-
|
|
570
|
-
import "./chunk-
|
|
569
|
+
import "./chunk-DQY7NJ5L.js";
|
|
570
|
+
import "./chunk-ROHLEUTH.js";
|
|
571
571
|
import {
|
|
572
572
|
MeilisearchBackend
|
|
573
|
-
} from "./chunk-
|
|
573
|
+
} from "./chunk-DOCTITOP.js";
|
|
574
574
|
import "./chunk-CYEPCZN5.js";
|
|
575
575
|
import {
|
|
576
576
|
OramaBackend
|
|
577
|
-
} from "./chunk-
|
|
577
|
+
} from "./chunk-Q5ZU3RNY.js";
|
|
578
578
|
import "./chunk-JOASJWQR.js";
|
|
579
579
|
import "./chunk-RN7MUWON.js";
|
|
580
580
|
import {
|
|
581
581
|
LanceDbBackend
|
|
582
|
-
} from "./chunk-
|
|
583
|
-
import "./chunk-
|
|
582
|
+
} from "./chunk-OUWAQVDJ.js";
|
|
583
|
+
import "./chunk-AER6MT24.js";
|
|
584
584
|
import "./chunk-CINZGPSJ.js";
|
|
585
585
|
import "./chunk-ZFXCQPNO.js";
|
|
586
|
+
import "./chunk-5GPPACXK.js";
|
|
586
587
|
import "./chunk-7OGJQP7T.js";
|
|
587
588
|
import "./chunk-E6ZDCOHM.js";
|
|
588
589
|
import "./chunk-OIF36KGD.js";
|
|
@@ -641,15 +642,12 @@ import {
|
|
|
641
642
|
renderBriefingMarkdown,
|
|
642
643
|
resolveBriefingSaveDir,
|
|
643
644
|
validateBriefingFormat
|
|
644
|
-
} from "./chunk-
|
|
645
|
+
} from "./chunk-KHGE6PMF.js";
|
|
645
646
|
import {
|
|
646
|
-
ALL_CATEGORY_DIRS,
|
|
647
|
-
ALL_CATEGORY_KEYS,
|
|
648
647
|
StorageManager,
|
|
649
|
-
getCategoryDir,
|
|
650
648
|
parseEntityFile,
|
|
651
649
|
serializeEntityFile
|
|
652
|
-
} from "./chunk-
|
|
650
|
+
} from "./chunk-VH6EIKVS.js";
|
|
653
651
|
import {
|
|
654
652
|
DEFAULT_SELF_NAME,
|
|
655
653
|
WEARABLES_DIR_NAME,
|
|
@@ -703,7 +701,7 @@ import {
|
|
|
703
701
|
getVersion,
|
|
704
702
|
listVersions,
|
|
705
703
|
revertToVersion
|
|
706
|
-
} from "./chunk-
|
|
704
|
+
} from "./chunk-VF4XKTX3.js";
|
|
707
705
|
import "./chunk-RGMVMVMF.js";
|
|
708
706
|
import "./chunk-ZY2MNJR6.js";
|
|
709
707
|
import "./chunk-SSOMTUCA.js";
|
|
@@ -745,7 +743,7 @@ import {
|
|
|
745
743
|
recallRequestSchema,
|
|
746
744
|
suggestionSubmitRequestSchema,
|
|
747
745
|
validateRequest
|
|
748
|
-
} from "./chunk-
|
|
746
|
+
} from "./chunk-UDJLF3BO.js";
|
|
749
747
|
import "./chunk-KQAFEZQX.js";
|
|
750
748
|
import {
|
|
751
749
|
CAPSULE_ID_PATTERN
|
|
@@ -784,19 +782,24 @@ import {
|
|
|
784
782
|
summarizeOfflineSyncPendingChanges,
|
|
785
783
|
summarizeOfflineSyncPendingFiles,
|
|
786
784
|
writeOfflineSyncState
|
|
787
|
-
} from "./chunk-
|
|
785
|
+
} from "./chunk-PWWWLD7D.js";
|
|
788
786
|
import "./chunk-UI3NYK34.js";
|
|
789
787
|
import {
|
|
790
788
|
assertIsDirectoryNotSymlink,
|
|
791
789
|
assertRealpathInsideRoot
|
|
792
790
|
} from "./chunk-GCGJW34D.js";
|
|
793
|
-
import "./chunk-
|
|
791
|
+
import "./chunk-J2HSAU72.js";
|
|
794
792
|
import "./chunk-A6XUJE5D.js";
|
|
795
793
|
import {
|
|
796
794
|
parseFlexibleIsoTimestamp,
|
|
797
795
|
parseIsoOffsetTimestamp,
|
|
798
796
|
parseIsoUtcTimestamp
|
|
799
797
|
} from "./chunk-P7FMDTKL.js";
|
|
798
|
+
import {
|
|
799
|
+
ALL_CATEGORY_DIRS,
|
|
800
|
+
ALL_CATEGORY_KEYS,
|
|
801
|
+
getCategoryDir
|
|
802
|
+
} from "./chunk-VS2IYZRU.js";
|
|
800
803
|
import {
|
|
801
804
|
ACTION_CONFIDENCE_CONTEXT_READINESS,
|
|
802
805
|
ACTION_CONFIDENCE_DECISIONS,
|