@remnic/core 9.3.663 → 9.3.665

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.
Files changed (149) hide show
  1. package/dist/access-cli.js +25 -23
  2. package/dist/access-cli.js.map +1 -1
  3. package/dist/access-http.js +20 -18
  4. package/dist/access-mcp.js +19 -17
  5. package/dist/access-schema.d.ts +36 -36
  6. package/dist/access-schema.js +4 -3
  7. package/dist/access-service.js +17 -15
  8. package/dist/briefing.js +5 -4
  9. package/dist/{capsule-merge-T2JRE46P.js → capsule-merge-GK5E647P.js} +3 -2
  10. package/dist/{capsule-merge-T2JRE46P.js.map → capsule-merge-GK5E647P.js.map} +1 -1
  11. package/dist/causal-consolidation.js +6 -5
  12. package/dist/causal-consolidation.js.map +1 -1
  13. package/dist/{chunk-2KDQI363.js → chunk-2HEZXPYU.js} +4 -4
  14. package/dist/{chunk-HSCJYHYV.js → chunk-2OPARZ4B.js} +49 -19
  15. package/dist/chunk-2OPARZ4B.js.map +1 -0
  16. package/dist/chunk-5GPPACXK.js +16 -0
  17. package/dist/chunk-5GPPACXK.js.map +1 -0
  18. package/dist/{chunk-F6O7IOS3.js → chunk-6JBKHTQD.js} +2 -2
  19. package/dist/{chunk-YYQRVNSV.js → chunk-7C4MPEPE.js} +6 -6
  20. package/dist/{chunk-AL4RAJL5.js → chunk-7XH7VJN4.js} +6 -4
  21. package/dist/chunk-7XH7VJN4.js.map +1 -0
  22. package/dist/{chunk-Q4CAQGKQ.js → chunk-AER6MT24.js} +12 -21
  23. package/dist/chunk-AER6MT24.js.map +1 -0
  24. package/dist/{chunk-DHGSZ3UD.js → chunk-ARV3AUOM.js} +2 -2
  25. package/dist/{chunk-PXVFMQLD.js → chunk-BZG2CWOQ.js} +3 -3
  26. package/dist/{chunk-ANJOULTP.js → chunk-C7AF236A.js} +2 -2
  27. package/dist/{chunk-TBLGI2LT.js → chunk-D7IXTY5E.js} +31 -4
  28. package/dist/chunk-D7IXTY5E.js.map +1 -0
  29. package/dist/{chunk-FZC2WSDB.js → chunk-DOCTITOP.js} +2 -2
  30. package/dist/{chunk-WOQIHC67.js → chunk-DQY7NJ5L.js} +2 -2
  31. package/dist/{chunk-NMPEJV5M.js → chunk-DSLUOQDY.js} +2 -2
  32. package/dist/{chunk-A7EF2XRO.js → chunk-EXXBA5OM.js} +30 -8
  33. package/dist/chunk-EXXBA5OM.js.map +1 -0
  34. package/dist/{chunk-QXHBWFR3.js → chunk-IHG6CC7T.js} +2 -2
  35. package/dist/{chunk-4KDLCMLK.js → chunk-IROWLAWG.js} +5 -5
  36. package/dist/{chunk-ILXTATKK.js → chunk-J2HSAU72.js} +5 -5
  37. package/dist/chunk-J2HSAU72.js.map +1 -0
  38. package/dist/{chunk-DFAXGZKI.js → chunk-JIX3ZL2J.js} +8 -8
  39. package/dist/{chunk-GY3V3SUI.js → chunk-KHGE6PMF.js} +2 -2
  40. package/dist/{chunk-TWAJICBN.js → chunk-OHJFJ4HI.js} +2 -2
  41. package/dist/{chunk-WSQG37DV.js → chunk-OUWAQVDJ.js} +2 -2
  42. package/dist/{chunk-ZLDUQWT2.js → chunk-PWWWLD7D.js} +2 -2
  43. package/dist/{chunk-ZJH723NM.js → chunk-Q5ZU3RNY.js} +2 -2
  44. package/dist/{chunk-35HP3TGR.js → chunk-ROHLEUTH.js} +4 -4
  45. package/dist/{chunk-5RIRL3XL.js → chunk-RS25QOKZ.js} +2 -2
  46. package/dist/{chunk-RQGR3ETH.js → chunk-T2AN3BSP.js} +2 -2
  47. package/dist/{chunk-UAU5U5ML.js → chunk-UDJLF3BO.js} +2 -2
  48. package/dist/{chunk-ALEPI75L.js → chunk-VF4XKTX3.js} +6 -4
  49. package/dist/{chunk-ALEPI75L.js.map → chunk-VF4XKTX3.js.map} +1 -1
  50. package/dist/{chunk-AX5O25EF.js → chunk-VH6EIKVS.js} +152 -190
  51. package/dist/chunk-VH6EIKVS.js.map +1 -0
  52. package/dist/chunk-VS2IYZRU.js +43 -0
  53. package/dist/chunk-VS2IYZRU.js.map +1 -0
  54. package/dist/{chunk-TGOOJCGA.js → chunk-WH4SKYPX.js} +76 -54
  55. package/dist/chunk-WH4SKYPX.js.map +1 -0
  56. package/dist/{chunk-5AYAZN45.js → chunk-XRSIGVTS.js} +5 -5
  57. package/dist/{chunk-D2EFNQMY.js → chunk-XW3W4PV4.js} +2 -2
  58. package/dist/{chunk-TYIXG4VR.js → chunk-YW52BQSU.js} +2 -2
  59. package/dist/{cli-C6twwe84.d.ts → cli-BQRqR9N-.d.ts} +12 -1
  60. package/dist/cli.d.ts +1 -1
  61. package/dist/cli.js +32 -28
  62. package/dist/compounding/engine.js +5 -4
  63. package/dist/connectors/codex-materialize-runner.js +5 -4
  64. package/dist/connectors/index.js +5 -4
  65. package/dist/consolidation-provenance-check.js +3 -2
  66. package/dist/consolidation-undo.js +2 -1
  67. package/dist/consolidation-undo.js.map +1 -1
  68. package/dist/entity-retrieval.js +5 -4
  69. package/dist/index.d.ts +1 -1
  70. package/dist/index.js +39 -36
  71. package/dist/index.js.map +1 -1
  72. package/dist/maintenance/memory-governance.js +6 -4
  73. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -4
  74. package/dist/maintenance/rebuild-memory-projection.js +7 -5
  75. package/dist/namespaces/migrate.js +13 -11
  76. package/dist/namespaces/search.js +8 -6
  77. package/dist/namespaces/storage.d.ts +13 -0
  78. package/dist/namespaces/storage.js +5 -4
  79. package/dist/offline-sync.js +3 -2
  80. package/dist/operator-toolkit.js +16 -14
  81. package/dist/orchestrator.js +21 -19
  82. package/dist/page-versioning.js +2 -1
  83. package/dist/schemas.d.ts +64 -64
  84. package/dist/search/document-scanner.d.ts +11 -7
  85. package/dist/search/document-scanner.js +3 -1
  86. package/dist/search/factory.js +7 -5
  87. package/dist/search/index.js +7 -5
  88. package/dist/search/lancedb-backend.js +4 -2
  89. package/dist/search/meilisearch-backend.js +4 -2
  90. package/dist/search/orama-backend.js +4 -2
  91. package/dist/secure-store/index.js +3 -2
  92. package/dist/semantic-consolidation.js +6 -5
  93. package/dist/semantic-rule-promotion.js +5 -4
  94. package/dist/semantic-rule-verifier.js +5 -4
  95. package/dist/shared-context/manager.d.ts +2 -2
  96. package/dist/storage.d.ts +17 -3
  97. package/dist/storage.js +4 -3
  98. package/dist/transfer/capsule-import.js +3 -2
  99. package/dist/transfer/types.d.ts +12 -12
  100. package/dist/verified-recall.js +5 -4
  101. package/package.json +1 -1
  102. package/src/cli.ts +62 -23
  103. package/src/consolidation-provenance-check.ts +7 -6
  104. package/src/maintenance/memory-governance.ts +47 -7
  105. package/src/namespaces/catalog.test.ts +12 -12
  106. package/src/namespaces/storage.ts +28 -1
  107. package/src/orchestrator.ts +84 -58
  108. package/src/page-versioning.ts +7 -4
  109. package/src/search/document-scanner.test.ts +29 -0
  110. package/src/search/document-scanner.ts +17 -29
  111. package/src/secure-store/secure-fs.ts +19 -5
  112. package/src/secure-store/secure-store.test.ts +28 -0
  113. package/src/storage.ts +42 -43
  114. package/src/training-export/converter.test.ts +19 -0
  115. package/src/training-export/converter.ts +8 -5
  116. package/src/utils/category-dir.ts +10 -4
  117. package/src/utils/path-containment.ts +40 -0
  118. package/dist/chunk-A7EF2XRO.js.map +0 -1
  119. package/dist/chunk-AL4RAJL5.js.map +0 -1
  120. package/dist/chunk-AX5O25EF.js.map +0 -1
  121. package/dist/chunk-HSCJYHYV.js.map +0 -1
  122. package/dist/chunk-ILXTATKK.js.map +0 -1
  123. package/dist/chunk-Q4CAQGKQ.js.map +0 -1
  124. package/dist/chunk-TBLGI2LT.js.map +0 -1
  125. package/dist/chunk-TGOOJCGA.js.map +0 -1
  126. /package/dist/{chunk-2KDQI363.js.map → chunk-2HEZXPYU.js.map} +0 -0
  127. /package/dist/{chunk-F6O7IOS3.js.map → chunk-6JBKHTQD.js.map} +0 -0
  128. /package/dist/{chunk-YYQRVNSV.js.map → chunk-7C4MPEPE.js.map} +0 -0
  129. /package/dist/{chunk-DHGSZ3UD.js.map → chunk-ARV3AUOM.js.map} +0 -0
  130. /package/dist/{chunk-PXVFMQLD.js.map → chunk-BZG2CWOQ.js.map} +0 -0
  131. /package/dist/{chunk-ANJOULTP.js.map → chunk-C7AF236A.js.map} +0 -0
  132. /package/dist/{chunk-FZC2WSDB.js.map → chunk-DOCTITOP.js.map} +0 -0
  133. /package/dist/{chunk-WOQIHC67.js.map → chunk-DQY7NJ5L.js.map} +0 -0
  134. /package/dist/{chunk-NMPEJV5M.js.map → chunk-DSLUOQDY.js.map} +0 -0
  135. /package/dist/{chunk-QXHBWFR3.js.map → chunk-IHG6CC7T.js.map} +0 -0
  136. /package/dist/{chunk-4KDLCMLK.js.map → chunk-IROWLAWG.js.map} +0 -0
  137. /package/dist/{chunk-DFAXGZKI.js.map → chunk-JIX3ZL2J.js.map} +0 -0
  138. /package/dist/{chunk-GY3V3SUI.js.map → chunk-KHGE6PMF.js.map} +0 -0
  139. /package/dist/{chunk-TWAJICBN.js.map → chunk-OHJFJ4HI.js.map} +0 -0
  140. /package/dist/{chunk-WSQG37DV.js.map → chunk-OUWAQVDJ.js.map} +0 -0
  141. /package/dist/{chunk-ZLDUQWT2.js.map → chunk-PWWWLD7D.js.map} +0 -0
  142. /package/dist/{chunk-ZJH723NM.js.map → chunk-Q5ZU3RNY.js.map} +0 -0
  143. /package/dist/{chunk-35HP3TGR.js.map → chunk-ROHLEUTH.js.map} +0 -0
  144. /package/dist/{chunk-5RIRL3XL.js.map → chunk-RS25QOKZ.js.map} +0 -0
  145. /package/dist/{chunk-RQGR3ETH.js.map → chunk-T2AN3BSP.js.map} +0 -0
  146. /package/dist/{chunk-UAU5U5ML.js.map → chunk-UDJLF3BO.js.map} +0 -0
  147. /package/dist/{chunk-5AYAZN45.js.map → chunk-XRSIGVTS.js.map} +0 -0
  148. /package/dist/{chunk-D2EFNQMY.js.map → chunk-XW3W4PV4.js.map} +0 -0
  149. /package/dist/{chunk-TYIXG4VR.js.map → chunk-YW52BQSU.js.map} +0 -0
@@ -0,0 +1,16 @@
1
+ // src/utils/path-containment.ts
2
+ import path from "path";
3
+ function pathIsInside(parent, child) {
4
+ const relative = path.relative(parent, child);
5
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
6
+ }
7
+ function assertPathInsideRoot(rootReal, candidateReal, originalPath) {
8
+ if (!pathIsInside(rootReal, candidateReal)) {
9
+ throw new Error(`Refusing to scan memory path outside memoryDir: ${originalPath}`);
10
+ }
11
+ }
12
+
13
+ export {
14
+ assertPathInsideRoot
15
+ };
16
+ //# sourceMappingURL=chunk-5GPPACXK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/path-containment.ts"],"sourcesContent":["/**\n * @remnic/core — Path Containment Guards\n *\n * Shared symlink/traversal containment helpers for filesystem walkers that\n * scan the memory store. Extracted from search/document-scanner.ts so every\n * walker (the search index scanner, the CLI dedupe walker, ...) enforces the\n * SAME containment semantics instead of forking the check (CLAUDE.md rule 22).\n *\n * Both helpers operate on realpath()-resolved absolute paths: callers resolve\n * symlinks first, then assert the resolved target is still inside the (also\n * realpath-resolved) memory root. This blocks a symlinked category directory\n * (e.g. decisions/ → /etc) from redirecting a scan — or a destructive dedupe\n * unlink — outside the memory store.\n */\n\nimport path from \"node:path\";\n\n/**\n * True when `child` is `parent` itself or nested underneath it. Both arguments\n * must already be resolved (realpath) absolute paths.\n */\nexport function pathIsInside(parent: string, child: string): boolean {\n const relative = path.relative(parent, child);\n return relative === \"\" || (!relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n\n/**\n * Throw if the realpath-resolved `candidateReal` escapes `rootReal`. The\n * message references the original (pre-realpath) path so operators can locate\n * the offending symlink/entry.\n */\nexport function assertPathInsideRoot(\n rootReal: string,\n candidateReal: string,\n originalPath: string,\n): void {\n if (!pathIsInside(rootReal, candidateReal)) {\n throw new Error(`Refusing to scan memory path outside memoryDir: ${originalPath}`);\n }\n}\n"],"mappings":";AAeA,OAAO,UAAU;AAMV,SAAS,aAAa,QAAgB,OAAwB;AACnE,QAAM,WAAW,KAAK,SAAS,QAAQ,KAAK;AAC5C,SAAO,aAAa,MAAO,CAAC,SAAS,WAAW,IAAI,KAAK,CAAC,KAAK,WAAW,QAAQ;AACpF;AAOO,SAAS,qBACd,UACA,eACA,cACM;AACN,MAAI,CAAC,aAAa,UAAU,aAAa,GAAG;AAC1C,UAAM,IAAI,MAAM,mDAAmD,YAAY,EAAE;AAAA,EACnF;AACF;","names":[]}
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-SFQ6QNL7.js";
7
7
  import {
8
8
  StorageManager
9
- } from "./chunk-AX5O25EF.js";
9
+ } from "./chunk-VH6EIKVS.js";
10
10
  import {
11
11
  buildLifecycleEventsForMemory,
12
12
  sortMemoryLifecycleEvents
@@ -74,4 +74,4 @@ export {
74
74
  backupExistingLedger,
75
75
  rebuildMemoryLifecycleLedger
76
76
  };
77
- //# sourceMappingURL=chunk-F6O7IOS3.js.map
77
+ //# sourceMappingURL=chunk-6JBKHTQD.js.map
@@ -1,19 +1,19 @@
1
1
  import {
2
2
  NamespaceStorageRouter
3
- } from "./chunk-TBLGI2LT.js";
3
+ } from "./chunk-D7IXTY5E.js";
4
4
  import {
5
5
  namespaceCollectionName
6
- } from "./chunk-WOQIHC67.js";
6
+ } from "./chunk-DQY7NJ5L.js";
7
7
  import {
8
8
  namespaceIdentityFromToken,
9
9
  namespaceIdentityToken
10
10
  } from "./chunk-ZFXCQPNO.js";
11
- import {
12
- ALL_CATEGORY_DIRS
13
- } from "./chunk-AX5O25EF.js";
14
11
  import {
15
12
  isSafeRouteNamespace
16
13
  } from "./chunk-U3PN77QT.js";
14
+ import {
15
+ ALL_CATEGORY_DIRS
16
+ } from "./chunk-VS2IYZRU.js";
17
17
 
18
18
  // src/namespaces/migrate.ts
19
19
  import path from "path";
@@ -204,4 +204,4 @@ export {
204
204
  verifyNamespaces,
205
205
  runNamespaceMigration
206
206
  };
207
- //# sourceMappingURL=chunk-YYQRVNSV.js.map
207
+ //# sourceMappingURL=chunk-7C4MPEPE.js.map
@@ -4,7 +4,10 @@ import {
4
4
  } from "./chunk-G7D6GZ5J.js";
5
5
  import {
6
6
  sidecarKey
7
- } from "./chunk-ALEPI75L.js";
7
+ } from "./chunk-VF4XKTX3.js";
8
+ import {
9
+ RECALL_FALLBACK_DIRS
10
+ } from "./chunk-VS2IYZRU.js";
8
11
 
9
12
  // src/consolidation-provenance-check.ts
10
13
  import path from "path";
@@ -279,8 +282,7 @@ async function runConsolidationProvenanceCheck(options) {
279
282
  }
280
283
  try {
281
284
  const seenPaths = new Set(memories.map((m) => m.path));
282
- const scanRoots = ["facts", "corrections", "procedures", "reasoning-traces"];
283
- for (const rootName of scanRoots) {
285
+ for (const rootName of RECALL_FALLBACK_DIRS) {
284
286
  const rootPath = path.join(memoryDir, rootName);
285
287
  for await (const file of walkMarkdownFiles(rootPath, memoryDir)) {
286
288
  if (seenPaths.has(file)) continue;
@@ -340,4 +342,4 @@ function isPathWithin(candidate, root) {
340
342
  export {
341
343
  runConsolidationProvenanceCheck
342
344
  };
343
- //# sourceMappingURL=chunk-AL4RAJL5.js.map
345
+ //# sourceMappingURL=chunk-7XH7VJN4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/consolidation-provenance-check.ts"],"sourcesContent":["/**\n * Consolidation provenance integrity check (issue #561 PR 4).\n *\n * Validates that every memory carrying consolidation provenance frontmatter\n * (`derived_from`, `derived_via`) resolves to real data:\n *\n * - Each `derived_from` entry `\"<path>:<version>\"` must name a\n * page-version snapshot that exists on disk (via the sidecar layout\n * documented in `page-versioning.ts`).\n * - Each `derived_via` must be one of the known\n * `ConsolidationOperator` values — malformed values are surfaced as\n * warnings rather than crashes so legacy or future operators survive a\n * rollback.\n *\n * Non-fatal: every failure renders a warning with the offending file path\n * and a human-readable reason. Integrity problems are informational for\n * now — we do not auto-heal or archive broken memories.\n */\n\nimport path from \"node:path\";\nimport { lstat, readdir, readFile, realpath, stat } from \"node:fs/promises\";\nimport { constants as fsConstants } from \"node:fs\";\nimport type { StorageManager } from \"./storage.js\";\nimport {\n DERIVED_FROM_MEMORY_ID_RE,\n isConsolidationOperator,\n} from \"./consolidation-operator.js\";\n// Import the canonical `sidecarKey` from page-versioning (PR #634\n// review, cursor Medium) so a future key-format change stays in\n// lock-step with the doctor scan.\nimport { sidecarKey } from \"./page-versioning.js\";\nimport { RECALL_FALLBACK_DIRS } from \"./utils/category-dir.js\";\n\n/**\n * Regex to spot a `derived_via: <value>` line in the raw YAML frontmatter\n * between the opening and first closing `---` delimiters. We use the raw\n * text rather than the parsed `frontmatter.derived_via` because the\n * read-path parser coerces unknown values back to `undefined` — that\n * would silently hide corrupted-or-future operators from the doctor scan\n * (PR #634 review feedback, codex P2).\n */\n// Allow empty capture groups so truncated/blank `derived_via:` and\n// `derived_from:` lines (key present, no value) are distinguishable\n// from \"key missing entirely\" (regex returns null). Optional\n// leading whitespace accepts indented keys which `parseFrontmatter`\n// also accepts (PR #634 round-6 review, codex P2).\nconst DERIVED_VIA_RAW_RE = /^[\\t ]*derived_via:[\\t ]*(.*)$/mu;\nconst DERIVED_FROM_RAW_RE = /^[\\t ]*derived_from:[\\t ]*(.*)$/mu;\n\n/**\n * Tokenize a YAML-block-style list under `key:` in the given\n * frontmatter slice. Looks for lines matching `^ - <value>` after a\n * `key:` line and before the next non-list line. Returns `null` when\n * the key is missing or the value is a scalar / flow list (no block\n * entries found).\n *\n * Only used for the mixed-list malformed-entry detection — it does\n * not try to decode YAML escape sequences since we only need the\n * entry count + raw token text to compare against the parsed array.\n */\nfunction tokenizeRawBlockList(fmSlice: string, key: string): string[] | null {\n const lines = fmSlice.split(\"\\n\");\n // Accept indented keys too — parseFrontmatter does (PR #634 round-7\n // review, codex P2 / cursor Low).\n const keyRe = new RegExp(`^[\\\\t ]*${key}:[\\\\t ]*(.*)$`, \"u\");\n let startIdx = -1;\n for (let i = 0; i < lines.length; i++) {\n const m = lines[i].match(keyRe);\n if (m) {\n if (m[1].trim().length === 0) {\n startIdx = i + 1;\n }\n break;\n }\n }\n if (startIdx < 0) return null;\n const items: string[] = [];\n for (let i = startIdx; i < lines.length; i++) {\n const line = lines[i];\n if (!/^\\s+-/.test(line)) break; // not a block-list entry\n const m = line.match(/^\\s+-\\s*(.*)$/u);\n if (!m) break;\n let tok = m[1].trim();\n if (\n (tok.startsWith('\"') && tok.endsWith('\"') && tok.length >= 2) ||\n (tok.startsWith(\"'\") && tok.endsWith(\"'\") && tok.length >= 2)\n ) {\n tok = tok.slice(1, -1);\n }\n items.push(tok);\n }\n return items.length > 0 ? items : null;\n}\n\n/**\n * Tokenize a YAML-flow-style list (`[\"a\", \"b\", ...]`) into a flat\n * string array. Returns `null` when the input isn't a flow list.\n * Best-effort — we don't implement a full YAML parser, just enough to\n * detect mixed valid/invalid entries for the doctor integrity check.\n */\nfunction tokenizeRawFlowList(raw: string): string[] | null {\n const trimmed = raw.trim();\n if (!trimmed.startsWith(\"[\") || !trimmed.endsWith(\"]\")) return null;\n const inner = trimmed.slice(1, -1);\n const parts: string[] = [];\n let current = \"\";\n let inSingle = false;\n let inDouble = false;\n for (let i = 0; i < inner.length; i++) {\n const ch = inner[i];\n if (inDouble) {\n if (ch === \"\\\\\" && i + 1 < inner.length) {\n current += inner[++i];\n continue;\n }\n if (ch === '\"') {\n inDouble = false;\n continue;\n }\n current += ch;\n } else if (inSingle) {\n if (ch === \"'\" && inner[i + 1] === \"'\") {\n current += \"'\";\n i++;\n continue;\n }\n if (ch === \"'\") {\n inSingle = false;\n continue;\n }\n current += ch;\n } else if (ch === '\"') {\n inDouble = true;\n } else if (ch === \"'\") {\n inSingle = true;\n } else if (ch === \",\") {\n parts.push(current.trim());\n current = \"\";\n } else {\n current += ch;\n }\n }\n if (current.trim().length > 0 || parts.length > 0) {\n parts.push(current.trim());\n }\n return parts;\n}\n\n/**\n * One integrity warning attached to a specific memory.\n */\nexport interface ConsolidationProvenanceIssue {\n /** Absolute path to the memory markdown file. */\n memoryPath: string;\n /** Memory id from frontmatter. */\n memoryId: string;\n /** Type of integrity issue. */\n kind:\n | \"derived_from_missing_snapshot\"\n | \"derived_from_malformed_entry\"\n | \"derived_via_unknown_operator\";\n /** Human-readable detail — includes the offending value when relevant. */\n detail: string;\n}\n\n/**\n * Summary of a provenance-integrity scan. Used by the operator-doctor\n * report and surfaced in the CLI output.\n */\nexport interface ConsolidationProvenanceReport {\n /** Total memories inspected. */\n scanned: number;\n /** Memories that carry `derived_from` and/or `derived_via`. */\n withProvenance: number;\n /** One entry per problem detected (may be empty). */\n issues: ConsolidationProvenanceIssue[];\n}\n\nconst DERIVED_FROM_ENTRY_RE = /^(.+):(\\d+)$/;\n\n/**\n * Build the on-disk snapshot path for a `\"<relpath>:<version>\"` entry,\n * relative to the given memory directory. Mirrors the layout documented\n * in `page-versioning.ts`:\n *\n * memoryDir/<sidecarDir>/<sidecarKey>/<version><ext>\n */\nfunction resolveSnapshotPath(\n memoryDir: string,\n sidecarDir: string,\n entry: string,\n): { ok: true; snapshotPath: string } | { ok: false; reason: string } {\n const match = entry.match(DERIVED_FROM_ENTRY_RE);\n if (!match) {\n return { ok: false, reason: `malformed entry (expected \"<path>:<version>\")` };\n }\n const pagePath = match[1];\n const versionId = match[2];\n const ext = path.extname(pagePath) || \".md\";\n const key = sidecarKey(pagePath);\n const snapshotPath = path.join(memoryDir, sidecarDir, key, `${versionId}${ext}`);\n return { ok: true, snapshotPath };\n}\n\n/**\n * Scan every memory under `storage` and flag consolidation-provenance\n * problems. Does not throw on individual failures — collects them in the\n * returned report.\n */\nexport async function runConsolidationProvenanceCheck(options: {\n storage: StorageManager;\n memoryDir: string;\n /**\n * Page-versioning sidecar directory name. Defaults to `.versions` —\n * matches the baked-in default used by `setVersioningConfig` when\n * versioning is enabled via config.\n */\n sidecarDir?: string;\n}): Promise<ConsolidationProvenanceReport> {\n const { storage, memoryDir } = options;\n const sidecarDir = options.sidecarDir ?? \".versions\";\n\n const report: ConsolidationProvenanceReport = {\n scanned: 0,\n withProvenance: 0,\n issues: [],\n };\n\n let memories;\n try {\n memories = await storage.readAllMemories();\n } catch {\n // If we can't enumerate memories at all, surface a single synthetic\n // issue rather than throwing — the doctor wrapper treats an empty\n // issues list as \"ok\" and we don't want a filesystem hiccup to crash\n // the whole diagnostic.\n return {\n scanned: 0,\n withProvenance: 0,\n issues: [\n {\n memoryPath: memoryDir,\n memoryId: \"(unreadable)\",\n kind: \"derived_from_malformed_entry\",\n detail: \"Could not enumerate memory directory to scan provenance.\",\n },\n ],\n };\n }\n\n for (const memory of memories) {\n report.scanned += 1;\n const fm = memory.frontmatter;\n const derivedFrom = fm.derived_from;\n const derivedVia = fm.derived_via;\n\n // Raw frontmatter values from disk — the read-path parser coerces\n // malformed `derived_from` and unknown `derived_via` back to\n // `undefined`, which would silently hide on-disk corruption from\n // the doctor scan (PR #634 review feedback, codex P2). We\n // re-extract both via regex so integrity issues are reported even\n // when the parser normalized them away. `rawDerivedVia` /\n // `rawDerivedFrom` being `\"\"` (empty string) represents a\n // corrupted file with the key present but the value truncated —\n // that's distinct from \"key missing entirely\" (undefined).\n let rawDerivedVia: string | undefined;\n let rawDerivedFrom: string | undefined;\n let rawDerivedViaKeyPresent = false;\n let rawDerivedFromKeyPresent = false;\n let duplicateViaKeys = false;\n let duplicateFromKeys = false;\n let viaMatchCount = 0;\n let fromMatchCount = 0;\n let fmSlice = \"\";\n try {\n const raw = await readFile(memory.path, \"utf-8\");\n const frontmatterEnd = raw.indexOf(\"\\n---\", raw.indexOf(\"---\") + 3);\n fmSlice = frontmatterEnd > 0 ? raw.slice(0, frontmatterEnd) : raw;\n // Use matchAll to find ALL occurrences of `derived_via` / `derived_from`\n // in the raw YAML. `parseFrontmatter` keeps the LAST assignment when\n // duplicate keys appear, so the doctor must read the last occurrence\n // to match what the storage reader actually uses (PR #634 review,\n // codex P2 — duplicate `derived_via` keys caused false-clean or\n // false-unknown-operator warnings depending on order).\n const viaMatches = [...fmSlice.matchAll(new RegExp(DERIVED_VIA_RAW_RE.source, DERIVED_VIA_RAW_RE.flags + \"g\"))];\n viaMatchCount = viaMatches.length;\n duplicateViaKeys = viaMatches.length > 1;\n if (viaMatches.length > 0) {\n rawDerivedViaKeyPresent = true;\n // Use the last occurrence — `parseFrontmatter` keeps the last\n // assignment when duplicate keys appear, so the doctor must\n // match that behavior to produce accurate warnings (PR #634\n // review, codex P2).\n const lastVia = viaMatches[viaMatches.length - 1];\n let val = lastVia[1].trim();\n if (\n (val.startsWith('\"') && val.endsWith('\"')) ||\n (val.startsWith(\"'\") && val.endsWith(\"'\"))\n ) {\n val = val.slice(1, -1);\n }\n rawDerivedVia = val;\n }\n const fromMatches = [...fmSlice.matchAll(new RegExp(DERIVED_FROM_RAW_RE.source, DERIVED_FROM_RAW_RE.flags + \"g\"))];\n fromMatchCount = fromMatches.length;\n duplicateFromKeys = fromMatches.length > 1;\n if (fromMatches.length > 0) {\n rawDerivedFromKeyPresent = true;\n const lastFrom = fromMatches[fromMatches.length - 1];\n rawDerivedFrom = lastFrom[1].trim();\n }\n } catch {\n // Fall through to the parsed values.\n }\n\n const hasFrom = Array.isArray(derivedFrom) && derivedFrom.length > 0;\n const hasVia = derivedVia !== undefined && derivedVia !== null;\n const hasRawVia = rawDerivedVia !== undefined && rawDerivedVia.length > 0;\n // A raw `derived_from` that the parser dropped indicates on-disk\n // corruption we must surface. We detect this by: (a) the raw YAML\n // contains a `derived_from:` key, AND (b) the parsed frontmatter\n // has no valid array. A scalar like `derived_from: facts/a.md:7`\n // (list brackets omitted) or a blank `derived_from:` both hit this\n // branch.\n const hasRawMalformedFrom = rawDerivedFromKeyPresent && !hasFrom;\n // A blank `derived_via:` with no value is also corrupt — the\n // parser drops it to undefined, but the raw key is still present\n // on disk (PR #634 round-3 review, codex P2).\n const hasBlankRawVia =\n rawDerivedViaKeyPresent &&\n (rawDerivedVia === undefined || rawDerivedVia.length === 0) &&\n !hasVia;\n if (\n !hasFrom && !hasVia && !hasRawVia &&\n !hasRawMalformedFrom && !hasBlankRawVia\n ) continue;\n report.withProvenance += 1;\n\n // Duplicate-key detection (PR #634 review, codex P2): when the raw\n // YAML contains multiple `derived_via` or `derived_from` lines,\n // `parseFrontmatter` silently uses the last one. Flag this as a\n // malformed entry so operators can inspect and fix the file.\n if (duplicateViaKeys) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_via_unknown_operator\",\n detail: `raw YAML contains ${viaMatchCount} \"derived_via\" keys; parseFrontmatter uses the last occurrence`,\n });\n }\n if (duplicateFromKeys) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_malformed_entry\",\n detail: `raw YAML contains ${fromMatchCount} \"derived_from\" keys; parseFrontmatter uses the last occurrence`,\n });\n }\n\n if (hasRawMalformedFrom) {\n const display = rawDerivedFrom ?? \"(blank)\";\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_malformed_entry\",\n detail: `raw YAML \"derived_from: ${display}\" could not be parsed as a list`,\n });\n }\n\n // Mixed-list detection (PR #634 round-4 + round-5 review, codex\n // P2): when the parser DID return a valid list but the raw YAML\n // includes additional tokens that got dropped, flag those as\n // malformed. Handles both flow-style (`[\"a\", \"\", \"b\"]`) and\n // block-style (`\\n - a\\n - \\n - b`) YAML lists.\n if (hasFrom && rawDerivedFromKeyPresent) {\n let rawList: string[] | null = null;\n if (rawDerivedFrom && rawDerivedFrom.length > 0) {\n rawList = tokenizeRawFlowList(rawDerivedFrom);\n }\n if (rawList === null) {\n // Fall back to block-list tokenization by re-reading the full\n // frontmatter (already loaded above as `raw`) and scanning\n // the lines following `derived_from:`.\n rawList = tokenizeRawBlockList(fmSlice, \"derived_from\");\n }\n if (rawList !== null && rawList.length > derivedFrom!.length) {\n for (const tok of rawList) {\n if (tok.length === 0) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_malformed_entry\",\n detail: `raw YAML derived_from contains an empty entry (mixed list)`,\n });\n continue;\n }\n if (!derivedFrom!.includes(tok)) {\n // Accept either the snapshot format `<path>:<version>` or\n // a bare memory id (issue #687 PR 2/4 — pattern\n // reinforcement uses ID-shaped entries). PR #730\n // review feedback, Codex P2.\n if (\n !/^(.+):(\\d+)$/u.test(tok) &&\n !DERIVED_FROM_MEMORY_ID_RE.test(tok)\n ) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_malformed_entry\",\n detail: `raw YAML derived_from contains a malformed entry: ${JSON.stringify(tok)}`,\n });\n }\n }\n }\n }\n }\n if (hasBlankRawVia) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_via_unknown_operator\",\n detail: \"raw YAML has `derived_via:` key with empty value\",\n });\n }\n\n if (hasFrom) {\n for (const entry of derivedFrom!) {\n // Pattern-reinforcement (issue #687 PR 2/4) records source\n // memory IDs directly in `derived_from` rather than\n // page-versioning snapshot references. Memory IDs may\n // contain `:` for namespace-prefixed forms like\n // `global:fact-abc-123`, but never `/` or `.` — those remain\n // exclusive to snapshot paths (PR #730 review feedback,\n // Codex P1). For ID-shaped entries we skip the snapshot\n // file check entirely — but ONLY when the operator is\n // `pattern-reinforcement`, which is the sole operator that\n // legitimately stores IDs rather than snapshot references.\n // Allowing the bypass for split/merge/update would weaken\n // validation on those existing consolidation paths (PR #730\n // review, Codex P2).\n if (\n derivedVia === \"pattern-reinforcement\" &&\n DERIVED_FROM_MEMORY_ID_RE.test(entry)\n ) {\n continue;\n }\n const resolved = resolveSnapshotPath(memoryDir, sidecarDir, entry);\n if (!resolved.ok) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_malformed_entry\",\n detail: `${JSON.stringify(entry)}: ${resolved.reason}`,\n });\n continue;\n }\n // Require a regular file at the snapshot path (PR #634\n // round-8 review, codex P2) — a directory or device node at\n // that path means the sidecar was corrupted and the snapshot\n // is effectively missing.\n let snapshotOk = false;\n try {\n const st = await stat(resolved.snapshotPath);\n snapshotOk = st.isFile();\n } catch {\n snapshotOk = false;\n }\n if (!snapshotOk) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_from_missing_snapshot\",\n detail: `${entry} → ${resolved.snapshotPath} (not a regular file)`,\n });\n }\n }\n }\n\n // Check the RAW YAML value for unknown operators. The parsed value\n // (`fm.derived_via`) is always known-good because the read-path\n // normalizer dropped anything else to undefined.\n if (hasRawVia && !isConsolidationOperator(rawDerivedVia)) {\n report.issues.push({\n memoryPath: memory.path,\n memoryId: fm.id,\n kind: \"derived_via_unknown_operator\",\n detail: `unknown operator: ${JSON.stringify(rawDerivedVia)}`,\n });\n }\n }\n\n // Parse-failure detection (PR #634 round-4 review, codex P2):\n // `readAllMemories()` silently drops files whose frontmatter\n // doesn't parse. Walk every recall category directory for `.md` files\n // that DO reference provenance frontmatter but didn't come back from the\n // reader — those are the corruption cases the doctor is meant to surface.\n // Uses RECALL_FALLBACK_DIRS (the single source of truth) so newly-routed\n // categories (decisions/, preferences/, ...) are scanned too (#1546).\n try {\n const seenPaths = new Set(memories.map((m) => m.path));\n for (const rootName of RECALL_FALLBACK_DIRS) {\n const rootPath = path.join(memoryDir, rootName);\n for await (const file of walkMarkdownFiles(rootPath, memoryDir)) {\n if (seenPaths.has(file)) continue;\n try {\n const raw = await readFile(file, \"utf-8\");\n if (\n DERIVED_FROM_RAW_RE.test(raw) ||\n DERIVED_VIA_RAW_RE.test(raw)\n ) {\n report.withProvenance += 1;\n report.issues.push({\n memoryPath: file,\n memoryId: \"(parse failed)\",\n kind: \"derived_from_malformed_entry\",\n detail:\n \"frontmatter could not be parsed by storage reader; provenance fields visible in raw YAML\",\n });\n }\n } catch {\n // Unreadable file — skip.\n }\n }\n }\n } catch {\n // Best-effort; don't fail the whole scan on a filesystem hiccup.\n }\n\n return report;\n}\n\n/**\n * Recursively yield all `.md` file paths under `root`. Silent on\n * missing directories — the facts/corrections dirs may not exist in\n * fresh installs. Symlinked roots/directories are skipped so the\n * best-effort parse-failure pass cannot escape `memoryDir`.\n */\nasync function* walkMarkdownFiles(root: string, memoryDir: string): AsyncGenerator<string> {\n let entries;\n let memoryDirReal: string;\n try {\n const rootStat = await lstat(root);\n if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return;\n memoryDirReal = await realpath(memoryDir);\n const rootReal = await realpath(root);\n if (!isPathWithin(rootReal, memoryDirReal)) return;\n entries = await readdir(root, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n const full = path.join(root, entry.name);\n if (entry.isSymbolicLink()) continue;\n if (entry.isDirectory()) {\n yield* walkMarkdownFiles(full, memoryDirReal);\n } else if (entry.isFile() && entry.name.endsWith(\".md\")) {\n try {\n const fileReal = await realpath(full);\n if (!isPathWithin(fileReal, memoryDirReal)) continue;\n } catch {\n continue;\n }\n yield full;\n }\n }\n}\n\nfunction isPathWithin(candidate: string, root: string): boolean {\n const relative = path.relative(root, candidate);\n return relative === \"\" || (!!relative && !relative.startsWith(\"..\") && !path.isAbsolute(relative));\n}\n"],"mappings":"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","names":[]}
@@ -1,3 +1,10 @@
1
+ import {
2
+ assertPathInsideRoot
3
+ } from "./chunk-5GPPACXK.js";
4
+ import {
5
+ RECALL_FALLBACK_DIRS
6
+ } from "./chunk-VS2IYZRU.js";
7
+
1
8
  // src/search/document-scanner.ts
2
9
  import path from "path";
3
10
  import { lstat, readdir, readFile, realpath } from "fs/promises";
@@ -66,15 +73,6 @@ async function scanDir(dir, memoryRootReal) {
66
73
  function isNodeError(err) {
67
74
  return typeof err === "object" && err !== null && "code" in err;
68
75
  }
69
- function pathIsInside(parent, child) {
70
- const relative = path.relative(parent, child);
71
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
72
- }
73
- function assertPathInsideRoot(rootReal, candidateReal, originalPath) {
74
- if (!pathIsInside(rootReal, candidateReal)) {
75
- throw new Error(`Refusing to scan memory path outside memoryDir: ${originalPath}`);
76
- }
77
- }
78
76
  async function scanMemoryDir(memoryDir) {
79
77
  let memoryRootReal;
80
78
  try {
@@ -85,20 +83,13 @@ async function scanMemoryDir(memoryDir) {
85
83
  }
86
84
  throw err;
87
85
  }
88
- const factsDir = path.join(memoryDir, "facts");
89
- const correctionsDir = path.join(memoryDir, "corrections");
90
- const proceduresDir = path.join(memoryDir, "procedures");
91
- const reasoningTracesDir = path.join(memoryDir, "reasoning-traces");
92
- const [facts, corrections, procedures, reasoningTraces] = await Promise.all([
93
- scanDir(factsDir, memoryRootReal),
94
- scanDir(correctionsDir, memoryRootReal),
95
- scanDir(proceduresDir, memoryRootReal),
96
- scanDir(reasoningTracesDir, memoryRootReal)
97
- ]);
98
- return [...facts, ...corrections, ...procedures, ...reasoningTraces];
86
+ const perDir = await Promise.all(
87
+ RECALL_FALLBACK_DIRS.map((dir) => scanDir(path.join(memoryDir, dir), memoryRootReal))
88
+ );
89
+ return perDir.flat();
99
90
  }
100
91
 
101
92
  export {
102
93
  scanMemoryDir
103
94
  };
104
- //# sourceMappingURL=chunk-Q4CAQGKQ.js.map
95
+ //# sourceMappingURL=chunk-AER6MT24.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/search/document-scanner.ts"],"sourcesContent":["import path from \"node:path\";\nimport { lstat, readdir, readFile, realpath } from \"node:fs/promises\";\nimport { RECALL_FALLBACK_DIRS } from \"../utils/category-dir.js\";\nimport { assertPathInsideRoot } from \"../utils/path-containment.js\";\n\nexport interface IndexableDocument {\n /** Memory ID from frontmatter or filename stem */\n docid: string;\n /** Absolute file path */\n path: string;\n /** Markdown body (no YAML frontmatter) */\n content: string;\n /** First ~200 chars for display */\n snippet: string;\n}\n\n/**\n * Parse YAML frontmatter from a markdown string.\n * Returns the frontmatter key-value pairs and body, or null if no frontmatter block.\n */\nfunction parseFrontmatter(raw: string): { data: Record<string, string>; body: string } | null {\n // Support both LF and CRLF line endings\n const normalized = raw.replace(/\\r\\n/g, \"\\n\");\n const match = normalized.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);\n if (!match) return null;\n\n const fmBlock = match[1];\n const body = (match[2] ?? \"\").trim();\n const data: Record<string, string> = {};\n\n for (const line of fmBlock.split(\"\\n\")) {\n const colonIdx = line.indexOf(\":\");\n if (colonIdx === -1) continue;\n const key = line.slice(0, colonIdx).trim();\n const value = line.slice(colonIdx + 1).trim();\n data[key] = value;\n }\n\n return { data, body };\n}\n\n/**\n * Recursively scan a directory for `.md` files and return IndexableDocuments.\n */\nasync function scanDir(dir: string, memoryRootReal: string): Promise<IndexableDocument[]> {\n const docs: IndexableDocument[] = [];\n try {\n const dirStat = await lstat(dir);\n if (dirStat.isSymbolicLink()) {\n throw new Error(`Refusing to scan symlinked memory category directory: ${dir}`);\n }\n if (!dirStat.isDirectory()) {\n const error = new Error(`Memory category path is not a directory: ${dir}`) as NodeJS.ErrnoException;\n error.code = \"ENOTDIR\";\n throw error;\n }\n assertPathInsideRoot(memoryRootReal, await realpath(dir), dir);\n\n const entries = await readdir(dir, { withFileTypes: true });\n for (const entry of entries) {\n if (entry.isSymbolicLink()) continue;\n const fullPath = path.join(dir, entry.name);\n if (entry.isDirectory()) {\n assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);\n const sub = await scanDir(fullPath, memoryRootReal);\n docs.push(...sub);\n } else if (entry.name.endsWith(\".md\")) {\n try {\n assertPathInsideRoot(memoryRootReal, await realpath(fullPath), fullPath);\n const raw = await readFile(fullPath, \"utf-8\");\n const parsed = parseFrontmatter(raw);\n const body = parsed ? parsed.body : raw.trim();\n const docid = parsed?.data.id || path.basename(entry.name, \".md\");\n docs.push({\n docid,\n path: fullPath,\n content: body,\n snippet: body.slice(0, 200),\n });\n } catch {\n // Skip unreadable files\n }\n }\n }\n } catch (err) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n // Optional category directories may not exist yet.\n return docs;\n }\n throw err;\n }\n return docs;\n}\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return typeof err === \"object\" && err !== null && \"code\" in err;\n}\n\n/**\n * Scan every recall category subdir of memoryDir for indexable markdown\n * documents. The directory set is derived from `RECALL_FALLBACK_DIRS`\n * (utils/category-dir.ts → ALL_CATEGORY_DIRS minus non-recall queue dirs) —\n * the single source of truth — so adding a new category never requires\n * touching this scanner. Non-QMD backends (Orama / Meilisearch / LanceDB)\n * build their index through this helper; deriving from RECALL_FALLBACK_DIRS\n * keeps them in parity with writeMemory's category-dir routing (issue #1546)\n * and the QMD filesystem-fallback corpus. reasoning-traces/ and the other\n * category dirs are covered automatically (issue #564 PR 3 no longer needs a\n * hand-maintained list). scanDir tolerates missing dirs (ENOENT), so category\n * dirs that do not exist yet are skipped.\n */\nexport async function scanMemoryDir(memoryDir: string): Promise<IndexableDocument[]> {\n let memoryRootReal: string;\n try {\n memoryRootReal = await realpath(memoryDir);\n } catch (err) {\n if (isNodeError(err) && err.code === \"ENOENT\") {\n return [];\n }\n throw err;\n }\n const perDir = await Promise.all(\n RECALL_FALLBACK_DIRS.map((dir) => scanDir(path.join(memoryDir, dir), memoryRootReal)),\n );\n return perDir.flat();\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createVersion
3
- } from "./chunk-ALEPI75L.js";
3
+ } from "./chunk-VF4XKTX3.js";
4
4
  import {
5
5
  parseExportBundle
6
6
  } from "./chunk-WEHSQBFR.js";
@@ -227,4 +227,4 @@ function mintForkId(capsuleId, originalId, now) {
227
227
  export {
228
228
  importCapsule
229
229
  };
230
- //# sourceMappingURL=chunk-DHGSZ3UD.js.map
230
+ //# sourceMappingURL=chunk-ARV3AUOM.js.map
@@ -10,10 +10,10 @@ import {
10
10
  import {
11
11
  listMemoryGovernanceRuns,
12
12
  readMemoryGovernanceRunArtifact
13
- } from "./chunk-A7EF2XRO.js";
13
+ } from "./chunk-EXXBA5OM.js";
14
14
  import {
15
15
  StorageManager
16
- } from "./chunk-AX5O25EF.js";
16
+ } from "./chunk-VH6EIKVS.js";
17
17
  import {
18
18
  MEMORY_LIFECYCLE_EVENT_SORT_ORDER,
19
19
  buildLifecycleEventsForMemory,
@@ -929,4 +929,4 @@ export {
929
929
  verifyMemoryProjection,
930
930
  repairMemoryProjection
931
931
  };
932
- //# sourceMappingURL=chunk-PXVFMQLD.js.map
932
+ //# sourceMappingURL=chunk-BZG2CWOQ.js.map
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-3UXOZBHV.js";
8
8
  import {
9
9
  StorageManager
10
- } from "./chunk-AX5O25EF.js";
10
+ } from "./chunk-VH6EIKVS.js";
11
11
  import {
12
12
  isSafeRouteNamespace
13
13
  } from "./chunk-U3PN77QT.js";
@@ -118,4 +118,4 @@ export {
118
118
  runCodexMaterialize,
119
119
  runPostConsolidationMaterialize
120
120
  };
121
- //# sourceMappingURL=chunk-ANJOULTP.js.map
121
+ //# sourceMappingURL=chunk-C7AF236A.js.map
@@ -3,12 +3,14 @@ import {
3
3
  normalizeNamespaceIdentity
4
4
  } from "./chunk-ZFXCQPNO.js";
5
5
  import {
6
- ALL_CATEGORY_DIRS,
7
6
  StorageManager
8
- } from "./chunk-AX5O25EF.js";
7
+ } from "./chunk-VH6EIKVS.js";
9
8
  import {
10
9
  isSafeRouteNamespace
11
10
  } from "./chunk-U3PN77QT.js";
11
+ import {
12
+ ALL_CATEGORY_DIRS
13
+ } from "./chunk-VS2IYZRU.js";
12
14
 
13
15
  // src/namespaces/storage.ts
14
16
  import path from "path";
@@ -124,6 +126,11 @@ var NamespaceStorageRouter = class {
124
126
  // entry is always removed when the promise settles, so the map cannot grow
125
127
  // unbounded (one transient entry per concurrently-resolving namespace).
126
128
  inFlightResolved = /* @__PURE__ */ new Map();
129
+ // Tracks every in-flight resolve-hook promise so callers can deterministically
130
+ // await the fire-and-forget registrations that `storageFor()` kicks off (see
131
+ // `whenResolveHooksSettled`). Entries are removed as each hook settles, so the
132
+ // set holds at most one promise per concurrently-resolving namespace.
133
+ pendingResolveHooks = /* @__PURE__ */ new Set();
127
134
  // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`
128
135
  // normalizes its input, so default-namespace branches must compare against the
129
136
  // normalized config default too — otherwise a whitespace-padded configured
@@ -178,7 +185,9 @@ var NamespaceStorageRouter = class {
178
185
  if (this.inFlightResolved.get(namespace) === storageDir) return;
179
186
  try {
180
187
  this.inFlightResolved.set(namespace, storageDir);
181
- Promise.resolve(hook(namespace, storageDir)).then(
188
+ const hookResult = Promise.resolve(hook(namespace, storageDir));
189
+ this.pendingResolveHooks.add(hookResult);
190
+ hookResult.then(
182
191
  (persisted) => {
183
192
  if (this.inFlightResolved.get(namespace) === storageDir) {
184
193
  this.inFlightResolved.delete(namespace);
@@ -186,6 +195,7 @@ var NamespaceStorageRouter = class {
186
195
  if (persisted !== false) {
187
196
  this.notifiedResolved.set(namespace, storageDir);
188
197
  }
198
+ this.pendingResolveHooks.delete(hookResult);
189
199
  },
190
200
  () => {
191
201
  if (this.inFlightResolved.get(namespace) === storageDir) {
@@ -194,6 +204,7 @@ var NamespaceStorageRouter = class {
194
204
  if (this.notifiedResolved.get(namespace) === storageDir) {
195
205
  this.notifiedResolved.delete(namespace);
196
206
  }
207
+ this.pendingResolveHooks.delete(hookResult);
197
208
  }
198
209
  );
199
210
  } catch {
@@ -202,6 +213,22 @@ var NamespaceStorageRouter = class {
202
213
  }
203
214
  }
204
215
  }
216
+ /**
217
+ * Resolve once every in-flight `onResolve` registration has settled.
218
+ *
219
+ * `storageFor()` fires the resolve hook fire-and-forget, so its catalog side
220
+ * effect (e.g. `registerResolved(...)`) is not observable the moment
221
+ * `storageFor()` returns. Callers that must act on that side effect — notably
222
+ * tests asserting the catalog was updated — should await this instead of
223
+ * racing a timer. Resolves immediately when no hook is registered or nothing
224
+ * is in flight. The loop re-checks because a settling hook could, in
225
+ * principle, trigger a follow-on resolution.
226
+ */
227
+ async whenResolveHooksSettled() {
228
+ while (this.pendingResolveHooks.size > 0) {
229
+ await Promise.allSettled([...this.pendingResolveHooks]);
230
+ }
231
+ }
205
232
  };
206
233
 
207
234
  export {
@@ -209,4 +236,4 @@ export {
209
236
  resolveNamespaceStorageRoot,
210
237
  NamespaceStorageRouter
211
238
  };
212
- //# sourceMappingURL=chunk-TBLGI2LT.js.map
239
+ //# sourceMappingURL=chunk-D7IXTY5E.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/namespaces/storage.ts"],"sourcesContent":["import path from \"node:path\";\nimport { access, lstat, readdir } from \"node:fs/promises\";\nimport { isSafeRouteNamespace } from \"../routing/engine.js\";\nimport { StorageManager } from \"../storage.js\";\nimport type { PluginConfig } from \"../types.js\";\nimport { ALL_CATEGORY_DIRS } from \"../utils/category-dir.js\";\nimport { namespaceIdentityToken, normalizeNamespaceIdentity } from \"./identity.js\";\n\nasync function exists(p: string): Promise<boolean> {\n try {\n await access(p);\n return true;\n } catch {\n return false;\n }\n}\n\nasync function hasStoredEntries(p: string): Promise<boolean> {\n try {\n const entry = await lstat(p);\n if (entry.isSymbolicLink()) return true;\n if (!entry.isDirectory()) return true;\n const children = await readdir(p, { withFileTypes: true });\n for (const child of children) {\n const childPath = path.join(p, child.name);\n if (child.isSymbolicLink() || child.isFile()) return true;\n if (child.isDirectory() && (await hasStoredEntries(childPath))) return true;\n }\n return false;\n } catch {\n return false;\n }\n}\n\n// Build a per-namespace directory under `<memoryDir>/namespaces` and assert the\n// resolved path stays inside that base. Namespace identifiers can originate from\n// operator config (config.defaultNamespace) and request-derived routing, so this\n// containment check prevents directory traversal (CodeQL js/path-injection).\n// For safe segments this returns exactly `path.join(base, segment)`, so there is\n// no behavioral change for valid namespaces.\nfunction resolveNamespaceDir(memoryDir: string, segment: string): string {\n // Mirror isSafeRouteNamespace's separator/parent-ref rejection (without its\n // 64-char cap, so identity tokens still pass). Rejecting separators and \"..\"\n // up front keeps the value a single contained child of <memoryDir>/namespaces.\n if (\n segment.length === 0 ||\n segment.includes(\"/\") ||\n segment.includes(\"\\\\\") ||\n segment.includes(\"..\") ||\n path.isAbsolute(segment)\n ) {\n throw new Error(`unsafe namespace path segment: ${segment}`);\n }\n return path.join(memoryDir, \"namespaces\", segment);\n}\n\nconst LEGACY_NAMESPACE_CONTENT_CHILDREN = [\n ...ALL_CATEGORY_DIRS,\n \"entities\",\n \"artifacts\",\n \"identity\",\n \"config\",\n \"summaries\",\n \"profile.md\",\n] as const;\n\nconst LEGACY_NAMESPACE_RUNTIME_CHILDREN = [\"state\"] as const;\n\nasync function hasAnyLegacyData(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await hasStoredEntries(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\nasync function hasAnyNamespaceStorageMarker(\n rootDir: string,\n options: { includeRuntimeState?: boolean } = {},\n): Promise<boolean> {\n const children = options.includeRuntimeState === true\n ? [...LEGACY_NAMESPACE_CONTENT_CHILDREN, ...LEGACY_NAMESPACE_RUNTIME_CHILDREN]\n : LEGACY_NAMESPACE_CONTENT_CHILDREN;\n for (const child of children) {\n if (await exists(path.join(rootDir, child))) return true;\n }\n return false;\n}\n\n/**\n * Storage routing for namespaces.\n *\n * Compatibility note:\n * - When namespaces are enabled, existing raw namespace roots are preserved.\n * New namespace roots use tokenized names under `memoryDir/namespaces/<token>`.\n * - The default namespace continues to use the legacy `memoryDir` root unless the caller\n * has created `memoryDir/namespaces/<defaultNamespace>` (in which case we use that).\n *\n * This avoids surprising \"lost memories\" when an install flips namespaces on without\n * migrating existing data.\n */\n/**\n * Optional hooks for the storage router. `onResolve` fires whenever a namespace's\n * storage is resolved/created, so a downstream consumer (e.g. the namespace\n * catalog, issue #1499) can register the namespace. The hook MUST NOT throw into\n * the router; the router invokes it defensively and a hook failure never affects\n * storage resolution.\n *\n * The hook MAY return (or resolve to) a boolean indicating whether the\n * registration actually PERSISTED (round 6, codex P2 — NEFoX). When it resolves\n * to `false` (a dropped/no-op registration), the router does NOT mark the\n * (namespace, storageDir) pair as notified, so the next resolve RETRIES it\n * instead of suppressing it forever. A `void`/`undefined` result is treated as\n * success (legacy hooks).\n */\nexport interface NamespaceStorageRouterHooks {\n onResolve?: (\n namespace: string,\n storageDir: string,\n ) => void | boolean | Promise<void | boolean>;\n}\n\n/**\n * Resolve the runtime storage root for the configured DEFAULT namespace.\n *\n * Shared between the live router (`NamespaceStorageRouter.defaultNamespaceRoot`)\n * and the rebuildable catalog (`NamespaceCatalog.rebuildFromDisk`) so the two\n * can never diverge (CLAUDE.md rule #22/#42 — read & write paths resolve through\n * the same logic). The contract is: while legacy memory data still lives\n * directly under `memoryDir`, the default root stays `memoryDir`; only once the\n * legacy root is empty and a `namespaces/<default|token>` dir holds data does\n * the default migrate into that tokenized/legacy-named dir.\n */\nexport async function resolveDefaultNamespaceRoot(config: PluginConfig): Promise<string> {\n if (!config.namespacesEnabled) {\n return config.memoryDir;\n }\n\n // Build the legacy default root from the NORMALIZED (trimmed) name so a\n // whitespace-padded `defaultNamespace` still finds the live `namespaces/default`\n // root (NIabe). `storageFor()` classifies the trimmed value as the default, and\n // the on-disk legacy dir is created under the trimmed name; using the raw spaced\n // name here would look for `namespaces/<spaced>` and miss the real root, falling\n // back to memoryDir/tokenized. `namespaceIdentityToken` already normalizes\n // internally, so the tokenized path is unaffected.\n const defaultIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n const legacyNsDir = resolveNamespaceDir(config.memoryDir, defaultIdentity);\n const tokenizedNsDir = resolveNamespaceDir(\n config.memoryDir,\n namespaceIdentityToken(config.defaultNamespace),\n );\n const tokenizedHasData =\n (await exists(tokenizedNsDir)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedNsDir, { includeRuntimeState: true }));\n const nsDir = tokenizedHasData\n ? tokenizedNsDir\n : (await exists(legacyNsDir))\n ? legacyNsDir\n : tokenizedNsDir;\n return (await exists(nsDir)) && !(await hasAnyLegacyData(config.memoryDir))\n ? nsDir\n : config.memoryDir;\n}\n\n/**\n * Resolve the runtime storage root for ANY namespace exactly as the live router\n * would (`NamespaceStorageRouter.namespaceRoot`). Shared so the rebuildable\n * catalog records the SAME on-disk root the router routes to — a recall/read\n * touch must not guess `namespaces/<token>` when the router actually serves a\n * legacy raw-name dir or a migrated default root (CLAUDE.md rule #22/#42; round\n * 4, cursor Medium). The default namespace delegates to `resolveDefaultNamespaceRoot`;\n * every other namespace prefers the tokenized root when it has a storage marker,\n * else a legacy raw-name dir when present, else the tokenized root.\n */\nexport async function resolveNamespaceStorageRoot(\n config: PluginConfig,\n namespace: string,\n): Promise<string> {\n if (!config.namespacesEnabled) return config.memoryDir;\n // Compare on NORMALIZED identity so a whitespace-padded configured default name\n // still routes to the default root rather than a tokenized non-default dir\n // (NH-FH). The catalog keys records by the same normalized identity.\n if (normalizeNamespaceIdentity(namespace) === normalizeNamespaceIdentity(config.defaultNamespace)) {\n return resolveDefaultNamespaceRoot(config);\n }\n const legacyRoot = resolveNamespaceDir(config.memoryDir, namespace);\n const tokenizedRoot = resolveNamespaceDir(config.memoryDir, namespaceIdentityToken(namespace));\n if (\n (await exists(tokenizedRoot)) &&\n (await hasAnyNamespaceStorageMarker(tokenizedRoot, { includeRuntimeState: true }))\n ) {\n return tokenizedRoot;\n }\n return (await exists(legacyRoot)) ? legacyRoot : tokenizedRoot;\n}\n\nexport class NamespaceStorageRouter {\n private readonly cache = new Map<string, StorageManager>();\n private defaultNsRootResolved: string | null = null;\n // Dedup the resolve hook (round 6, cursor Medium — NCNL2). Recall/extraction\n // call `storageFor` repeatedly; firing `onResolve` (→ catalog loadCompacted +\n // append) on every cache hit grows `namespaces.jsonl` without bound between\n // rebuilds. We fire the hook only when the (namespace, storageDir) pair is new\n // or its dir changed, so a steady-state cache hit is a no-op for the catalog.\n private readonly notifiedResolved = new Map<string, string>();\n // In-flight resolve-hook dedup (NFJV-, codex P2). The catalog's `onResolve`\n // hook is ASYNC (it returns `registerResolved(...)`), so `notifiedResolved` is\n // only set after the hook's promise SETTLES. Without tracking the in-flight\n // window, a burst of `storageFor()` cache hits for the SAME namespace before\n // the first registration finishes would each pass the `notifiedResolved` guard\n // and fire their OWN `onResolve` — queueing N duplicate catalog touches + lock\n // acquisitions despite the once-per-namespace intent. We therefore record the\n // (namespace → storageDir) being registered BEFORE awaiting the hook so a\n // concurrent call for the same pair skips firing. On SUCCESS the pair is\n // promoted to `notifiedResolved` (future calls skip permanently); on `false`\n // (dropped touch — e.g. rebuild-lock timeout) OR rejection the in-flight marker\n // is CLEARED so a later `storageFor()` can RETRY the dropped registration. The\n // entry is always removed when the promise settles, so the map cannot grow\n // unbounded (one transient entry per concurrently-resolving namespace).\n private readonly inFlightResolved = new Map<string, string>();\n // Tracks every in-flight resolve-hook promise so callers can deterministically\n // await the fire-and-forget registrations that `storageFor()` kicks off (see\n // `whenResolveHooksSettled`). Entries are removed as each hook settles, so the\n // set holds at most one promise per concurrently-resolving namespace.\n private readonly pendingResolveHooks = new Set<Promise<unknown>>();\n\n // Normalized (trimmed) default namespace identity (NH-FH). `storageFor`\n // normalizes its input, so default-namespace branches must compare against the\n // normalized config default too — otherwise a whitespace-padded configured\n // default name routes the default namespace to a tokenized non-default root.\n private readonly defaultNamespaceIdentity: string;\n\n constructor(\n private readonly config: PluginConfig,\n private readonly hooks: NamespaceStorageRouterHooks = {},\n ) {\n this.defaultNamespaceIdentity = normalizeNamespaceIdentity(config.defaultNamespace);\n }\n\n private async defaultNamespaceRoot(): Promise<string> {\n this.defaultNsRootResolved = await resolveDefaultNamespaceRoot(this.config);\n return this.defaultNsRootResolved;\n }\n\n private async namespaceRoot(namespace: string): Promise<string> {\n // NOTE: only used after defaultNamespaceRoot() resolution.\n if (!this.config.namespacesEnabled) return this.config.memoryDir;\n if (normalizeNamespaceIdentity(namespace) === this.defaultNamespaceIdentity) {\n return this.defaultNsRootResolved ?? this.config.memoryDir;\n }\n return resolveNamespaceStorageRoot(this.config, namespace);\n }\n\n async storageFor(namespace: string): Promise<StorageManager> {\n const ns = normalizeNamespaceIdentity(namespace || this.config.defaultNamespace);\n if (ns !== this.defaultNamespaceIdentity && !isSafeRouteNamespace(ns)) {\n throw new Error(`unsafe namespace: ${ns}`);\n }\n // Even when the default namespace is exempt from the check above, every\n // on-disk path is built through resolveNamespaceDir(), which rejects\n // traversal segments — so an unsafe configured default still cannot escape\n // <memoryDir>/namespaces (CodeQL js/path-injection).\n\n let root: string;\n if (ns === this.defaultNamespaceIdentity) {\n root = await this.defaultNamespaceRoot();\n const cached = this.cache.get(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n } else {\n const cached = this.cache.get(ns);\n root = await this.namespaceRoot(ns);\n if (cached && cached.dir === root) {\n this.notifyResolved(ns, root);\n return cached;\n }\n }\n\n const sm = new StorageManager(root, this.config.entitySchemas);\n // Propagate the inline-attribution template so that router-created storages\n // (used by extraction and shared-promotion paths) strip citations consistently,\n // matching the behaviour of the primary this.storage instance in the orchestrator.\n sm.citationTemplate = this.config.inlineSourceAttributionFormat;\n this.cache.set(ns, sm);\n this.notifyResolved(ns, root);\n return sm;\n }\n\n /**\n * Fire the resolve hook defensively. A hook failure (e.g. a catalog write\n * error) MUST NOT crash storage resolution — see CLAUDE.md gotcha #13.\n */\n private notifyResolved(namespace: string, storageDir: string): void {\n const hook = this.hooks.onResolve;\n if (!hook) return;\n // Skip when we've already SUCCESSFULLY notified this exact (namespace,\n // storageDir) — a steady-state cache hit must not re-append to the catalog\n // log (NCNL2). A changed dir (rare: migration/realignment) still re-fires\n // once. We mark the pair as notified ONLY AFTER the hook succeeds, and CLEAR\n // it on failure, so a dropped registration (e.g. rebuild-lock timeout) is\n // RETRIED on the next cache hit instead of being suppressed forever (round 6,\n // cursor Medium — ND3EJ).\n if (this.notifiedResolved.get(namespace) === storageDir) return;\n // In-flight dedup (NFJV-, codex P2): if a registration for this exact\n // (namespace, storageDir) is already AWAITING its async hook, do not fire a\n // second one. Without this, concurrent cache-hit bursts before the first\n // append settles each pass the `notifiedResolved` guard above and queue\n // duplicate catalog touches/lock acquisitions. A pair with a DIFFERENT\n // in-flight dir (rare mid-migration realignment) still fires once.\n if (this.inFlightResolved.get(namespace) === storageDir) return;\n try {\n // Handle BOTH synchronous throws and asynchronous rejections (round 6,\n // codex P2 — NDo8C). The hook may be `async`; its rejected promise would\n // bypass this try/catch and, where unhandled rejections are fatal, crash\n // storage resolution. Mark the dedup pair as notified ONLY when the hook\n // resolves to a PERSISTED result (round 6, codex P2 — NEFoX): a result of\n // `false` means the registration was dropped/no-op (e.g. rebuild-lock\n // timeout), so we must NOT suppress its retry. `void`/`undefined` is treated\n // as success for legacy hooks. On rejection we leave it un-notified to retry.\n //\n // Record the in-flight marker BEFORE awaiting so concurrent calls for the\n // same pair skip (NFJV-). It is always cleared once the promise settles, so\n // the map holds at most one transient entry per concurrently-resolving\n // namespace and cannot grow unbounded.\n this.inFlightResolved.set(namespace, storageDir);\n const hookResult = Promise.resolve(hook(namespace, storageDir));\n // Track the in-flight promise so `whenResolveHooksSettled()` can await it.\n this.pendingResolveHooks.add(hookResult);\n hookResult.then(\n (persisted) => {\n // Clear the in-flight marker ONLY if it is still ours (a newer resolve\n // for a different dir may have replaced it).\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (persisted !== false) {\n this.notifiedResolved.set(namespace, storageDir);\n }\n // On `false` (dropped touch) we intentionally do NOT mark notified, so\n // a later `storageFor()` retries the registration. Clearing the\n // in-flight marker above is what re-enables that retry.\n this.pendingResolveHooks.delete(hookResult);\n },\n () => {\n // Registration failed — clear in-flight AND do NOT mark as notified, so\n // it is retried on the next cache hit.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n if (this.notifiedResolved.get(namespace) === storageDir) {\n this.notifiedResolved.delete(namespace);\n }\n this.pendingResolveHooks.delete(hookResult);\n },\n );\n } catch {\n // Synchronous throw: clear any in-flight marker we just set and leave the\n // pair un-notified so a later resolve retries.\n if (this.inFlightResolved.get(namespace) === storageDir) {\n this.inFlightResolved.delete(namespace);\n }\n }\n }\n\n /**\n * Resolve once every in-flight `onResolve` registration has settled.\n *\n * `storageFor()` fires the resolve hook fire-and-forget, so its catalog side\n * effect (e.g. `registerResolved(...)`) is not observable the moment\n * `storageFor()` returns. Callers that must act on that side effect — notably\n * tests asserting the catalog was updated — should await this instead of\n * racing a timer. Resolves immediately when no hook is registered or nothing\n * is in flight. The loop re-checks because a settling hook could, in\n * principle, trigger a follow-on resolution.\n */\n async whenResolveHooksSettled(): Promise<void> {\n while (this.pendingResolveHooks.size > 0) {\n await Promise.allSettled([...this.pendingResolveHooks]);\n }\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  scanMemoryDir
3
- } from "./chunk-Q4CAQGKQ.js";
3
+ } from "./chunk-AER6MT24.js";
4
4
  import {
5
5
  isSearchAborted,
6
6
  throwIfSearchAborted
@@ -245,4 +245,4 @@ function isMeilisearchIndexNotFoundError(err) {
245
245
  export {
246
246
  MeilisearchBackend
247
247
  };
248
- //# sourceMappingURL=chunk-FZC2WSDB.js.map
248
+ //# sourceMappingURL=chunk-DOCTITOP.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createSearchBackend
3
- } from "./chunk-35HP3TGR.js";
3
+ } from "./chunk-ROHLEUTH.js";
4
4
  import {
5
5
  namespaceIdentityToken,
6
6
  normalizeNamespaceIdentity
@@ -401,4 +401,4 @@ export {
401
401
  namespaceCollectionName,
402
402
  NamespaceSearchRouter
403
403
  };
404
- //# sourceMappingURL=chunk-WOQIHC67.js.map
404
+ //# sourceMappingURL=chunk-DQY7NJ5L.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  StorageManager
3
- } from "./chunk-AX5O25EF.js";
3
+ } from "./chunk-VH6EIKVS.js";
4
4
  import {
5
5
  getCachedRuleMemories,
6
6
  setCachedRuleMemories
@@ -117,4 +117,4 @@ export {
117
117
  compareVerifiedSemanticRuleResults,
118
118
  searchVerifiedSemanticRules
119
119
  };
120
- //# sourceMappingURL=chunk-NMPEJV5M.js.map
120
+ //# sourceMappingURL=chunk-DSLUOQDY.js.map
@@ -1,13 +1,19 @@
1
+ import {
2
+ assertPathInsideRoot
3
+ } from "./chunk-5GPPACXK.js";
1
4
  import {
2
5
  StorageManager
3
- } from "./chunk-AX5O25EF.js";
6
+ } from "./chunk-VH6EIKVS.js";
4
7
  import {
5
8
  decideLifecycleTransition
6
9
  } from "./chunk-TBBDFYXW.js";
10
+ import {
11
+ RECALL_FALLBACK_DIRS
12
+ } from "./chunk-VS2IYZRU.js";
7
13
 
8
14
  // src/maintenance/memory-governance.ts
9
15
  import path from "path";
10
- import { mkdir, readFile, readdir, rm, writeFile } from "fs/promises";
16
+ import { lstat, mkdir, readFile, readdir, realpath, rm, writeFile } from "fs/promises";
11
17
  var RULE_VERSION = "memory-governance.v2";
12
18
  var SEMANTIC_DUPLICATE_MIN_TOKENS = 6;
13
19
  var SEMANTIC_DUPLICATE_MIN_JACCARD = 0.66;
@@ -168,18 +174,27 @@ function buildExplicitCaptureReviewEntries(memories, lifecycleEvents) {
168
174
  relatedMemoryIds: []
169
175
  }));
170
176
  }
171
- async function listMarkdownFiles(root) {
177
+ async function listMarkdownFiles(root, containmentRoot) {
172
178
  const files = [];
173
179
  const walk = async (dir) => {
174
180
  try {
181
+ const dirStat = await lstat(dir);
182
+ if (dirStat.isSymbolicLink() || !dirStat.isDirectory()) return;
183
+ assertPathInsideRoot(containmentRoot, await realpath(dir), dir);
175
184
  const entries = await readdir(dir, { withFileTypes: true });
176
185
  for (const entry of entries) {
186
+ if (entry.isSymbolicLink()) continue;
177
187
  const fullPath = path.join(dir, entry.name);
178
188
  if (entry.isDirectory()) {
179
189
  await walk(fullPath);
180
190
  continue;
181
191
  }
182
192
  if (entry.isFile() && entry.name.endsWith(".md")) {
193
+ try {
194
+ assertPathInsideRoot(containmentRoot, await realpath(fullPath), fullPath);
195
+ } catch {
196
+ continue;
197
+ }
183
198
  files.push(fullPath);
184
199
  }
185
200
  }
@@ -194,10 +209,17 @@ function malformedMemoryId(memoryDir, filePath) {
194
209
  }
195
210
  async function buildMalformedImportEntries(memoryDir, storage, parsedMemories, candidateFiles) {
196
211
  const parsedPaths = new Set(parsedMemories.map((memory) => memory.path));
197
- const filesToInspect = candidateFiles ?? [
198
- ...await listMarkdownFiles(path.join(memoryDir, "facts")),
199
- ...await listMarkdownFiles(path.join(memoryDir, "corrections"))
200
- ];
212
+ let containmentRoot = null;
213
+ try {
214
+ containmentRoot = await realpath(memoryDir);
215
+ } catch {
216
+ containmentRoot = null;
217
+ }
218
+ const filesToInspect = candidateFiles ?? (containmentRoot === null ? [] : (await Promise.all(
219
+ RECALL_FALLBACK_DIRS.map(
220
+ (dir) => listMarkdownFiles(path.join(memoryDir, dir), containmentRoot)
221
+ )
222
+ )).flat());
201
223
  const entries = [];
202
224
  for (const filePath of filesToInspect) {
203
225
  if (parsedPaths.has(filePath)) continue;
@@ -729,4 +751,4 @@ export {
729
751
  listMemoryGovernanceRuns,
730
752
  readMemoryGovernanceRunArtifact
731
753
  };
732
- //# sourceMappingURL=chunk-A7EF2XRO.js.map
754
+ //# sourceMappingURL=chunk-EXXBA5OM.js.map