@theglitchking/semantic-memory 1.6.0 → 1.7.0

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/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +90 -0
  4. package/LICENSES/Apache-2.0.txt +202 -0
  5. package/README.md +49 -29
  6. package/THIRD_PARTY_NOTICES.md +17 -0
  7. package/commands/jev-mode.md +18 -0
  8. package/commands/skill-loop.md +23 -0
  9. package/dist/{chunk-7PNF3AP2.js → chunk-34VN47IW.js} +2 -2
  10. package/dist/chunk-3FPUBAEW.js +113 -0
  11. package/dist/chunk-3FPUBAEW.js.map +1 -0
  12. package/dist/{chunk-WZEKIUYG.js → chunk-6TIKWOY4.js} +10 -8
  13. package/dist/{chunk-WZEKIUYG.js.map → chunk-6TIKWOY4.js.map} +1 -1
  14. package/dist/{chunk-NBY4TRP7.js → chunk-7M34JDTH.js} +3 -3
  15. package/dist/{chunk-FTRXH2WC.js → chunk-7UAVDOI6.js} +15 -12
  16. package/dist/chunk-7UAVDOI6.js.map +1 -0
  17. package/dist/{chunk-BBP3DG2U.js → chunk-AIXQFTSB.js} +25 -26
  18. package/dist/{chunk-BBP3DG2U.js.map → chunk-AIXQFTSB.js.map} +1 -1
  19. package/dist/chunk-BEACL25M.js +224 -0
  20. package/dist/chunk-BEACL25M.js.map +1 -0
  21. package/dist/chunk-CATPGINC.js +186 -0
  22. package/dist/chunk-CATPGINC.js.map +1 -0
  23. package/dist/{chunk-IGHCVAAL.js → chunk-CJUY55GG.js} +6 -3
  24. package/dist/{chunk-IGHCVAAL.js.map → chunk-CJUY55GG.js.map} +1 -1
  25. package/dist/chunk-DBFD3MTT.js +130 -0
  26. package/dist/chunk-DBFD3MTT.js.map +1 -0
  27. package/dist/{chunk-XAXK7NM4.js → chunk-EXAUVIGW.js} +9 -7
  28. package/dist/{chunk-XAXK7NM4.js.map → chunk-EXAUVIGW.js.map} +1 -1
  29. package/dist/{chunk-LGQPP5FQ.js → chunk-EYVFC745.js} +2 -2
  30. package/dist/chunk-GYR7U6KA.js +80 -0
  31. package/dist/chunk-GYR7U6KA.js.map +1 -0
  32. package/dist/{chunk-OEF5U5GV.js → chunk-H2BES6FU.js} +10 -2
  33. package/dist/{chunk-OEF5U5GV.js.map → chunk-H2BES6FU.js.map} +1 -1
  34. package/dist/chunk-IEZVFKUT.js +157 -0
  35. package/dist/chunk-IEZVFKUT.js.map +1 -0
  36. package/dist/chunk-J7LYDRML.js +9 -0
  37. package/dist/chunk-J7LYDRML.js.map +1 -0
  38. package/dist/chunk-MQS7JEBD.js +137 -0
  39. package/dist/chunk-MQS7JEBD.js.map +1 -0
  40. package/dist/chunk-NCGM36NX.js +118 -0
  41. package/dist/chunk-NCGM36NX.js.map +1 -0
  42. package/dist/chunk-NIZLBNH4.js +165 -0
  43. package/dist/chunk-NIZLBNH4.js.map +1 -0
  44. package/dist/chunk-OT3MV7XF.js +123 -0
  45. package/dist/chunk-OT3MV7XF.js.map +1 -0
  46. package/dist/chunk-P3GLY42T.js +291 -0
  47. package/dist/chunk-P3GLY42T.js.map +1 -0
  48. package/dist/chunk-PJXYCCI2.js +177 -0
  49. package/dist/chunk-PJXYCCI2.js.map +1 -0
  50. package/dist/chunk-R6GNP7DS.js +87 -0
  51. package/dist/chunk-R6GNP7DS.js.map +1 -0
  52. package/dist/{chunk-44BQDRXD.js → chunk-SHSOCFJH.js} +4 -1
  53. package/dist/chunk-SHSOCFJH.js.map +1 -0
  54. package/dist/chunk-TKHWVISV.js +71 -0
  55. package/dist/chunk-TKHWVISV.js.map +1 -0
  56. package/dist/{chunk-CAX46XLY.js → chunk-VBLCUWT4.js} +23 -12
  57. package/dist/chunk-VBLCUWT4.js.map +1 -0
  58. package/dist/chunk-VQCNWRTL.js +121 -0
  59. package/dist/chunk-VQCNWRTL.js.map +1 -0
  60. package/dist/{chunk-SX427M2R.js → chunk-XKGK643R.js} +2 -2
  61. package/dist/{chunk-FURCIJBO.js → chunk-Y7H76Q7F.js} +6 -3
  62. package/dist/chunk-Y7H76Q7F.js.map +1 -0
  63. package/dist/chunk-ZSWI3Y4Y.js +232 -0
  64. package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
  65. package/dist/cli/index.js +389 -60
  66. package/dist/cli/index.js.map +1 -1
  67. package/dist/core/features.d.ts +38 -0
  68. package/dist/core/features.js +28 -0
  69. package/dist/core/index.d.ts +285 -2
  70. package/dist/core/index.js +170 -20
  71. package/dist/core/index.js.map +1 -1
  72. package/dist/core/jev.d.ts +35 -0
  73. package/dist/core/jev.js +20 -0
  74. package/dist/core/redact.js +1 -1
  75. package/dist/core/skill-curator.d.ts +1 -0
  76. package/dist/core/skill-curator.js +41 -0
  77. package/dist/core/skill-evidence.d.ts +25 -0
  78. package/dist/core/skill-evidence.js +23 -0
  79. package/dist/core/skill-queue.d.ts +40 -0
  80. package/dist/core/skill-queue.js +38 -0
  81. package/dist/core/skill-review.d.ts +3 -0
  82. package/dist/core/skill-review.js +29 -0
  83. package/dist/core/skill-worker.d.ts +21 -0
  84. package/dist/core/skill-worker.js +70 -0
  85. package/dist/core/skill-worker.js.map +1 -0
  86. package/dist/{dossier-RWTIMGZN.js → dossier-RXBLQOXC.js} +5 -4
  87. package/dist/failure-eval-IUJO47UC.js +18 -0
  88. package/dist/healthcheck-S7D6YNTA.js +21 -0
  89. package/dist/indexer-RZPH2PVL.js +9 -0
  90. package/dist/{lexicon-IVNJTMWE.js → lexicon-SEJSLBHO.js} +6 -5
  91. package/dist/lexicon-SEJSLBHO.js.map +1 -0
  92. package/dist/lint-3JNLRM5U.js +18 -0
  93. package/dist/lint-3JNLRM5U.js.map +1 -0
  94. package/dist/log-RM2NDNPH.js +11 -0
  95. package/dist/log-RM2NDNPH.js.map +1 -0
  96. package/dist/matter-SNJ2WDL6.js +8 -0
  97. package/dist/matter-SNJ2WDL6.js.map +1 -0
  98. package/dist/mcp/server.js +43 -38
  99. package/dist/mcp/server.js.map +1 -1
  100. package/dist/{profile-UE4YVTDC.js → profile-SNULTJ27.js} +6 -5
  101. package/dist/profile-SNULTJ27.js.map +1 -0
  102. package/dist/{schema-LY6533NJ.js → schema-RDYUK6CC.js} +3 -2
  103. package/dist/schema-RDYUK6CC.js.map +1 -0
  104. package/dist/{search-MZC4DC7H.js → search-325ULY57.js} +4 -3
  105. package/dist/{search-MZC4DC7H.js.map → search-325ULY57.js.map} +1 -1
  106. package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
  107. package/dist/skill-land-XU2APPBJ.js +31 -0
  108. package/dist/skill-land-XU2APPBJ.js.map +1 -0
  109. package/dist/skill-ledger-AYXVNFX7.js +18 -0
  110. package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
  111. package/dist/skill-merge-GXBQLBZA.js +24 -0
  112. package/dist/skill-merge-GXBQLBZA.js.map +1 -0
  113. package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
  114. package/dist/skill-schema-RVB3EHEF.js +30 -0
  115. package/dist/skill-schema-RVB3EHEF.js.map +1 -0
  116. package/dist/{telemetry-B4KP3UVE.js → telemetry-YAG3VA4Y.js} +4 -4
  117. package/dist/telemetry-YAG3VA4Y.js.map +1 -0
  118. package/docs/regression-snapshot.md +5 -5
  119. package/hooks/hooks.json +26 -1
  120. package/hooks/jev-bridge.py +109 -0
  121. package/hooks/session-start.js +5 -0
  122. package/hooks/skill-loop.js +65 -0
  123. package/hooks/vault-context.js +104 -6
  124. package/package.json +4 -2
  125. package/dist/chunk-44BQDRXD.js.map +0 -1
  126. package/dist/chunk-5F5ZIAW2.js +0 -79
  127. package/dist/chunk-5F5ZIAW2.js.map +0 -1
  128. package/dist/chunk-CAX46XLY.js.map +0 -1
  129. package/dist/chunk-FTRXH2WC.js.map +0 -1
  130. package/dist/chunk-FURCIJBO.js.map +0 -1
  131. package/dist/failure-eval-37JRNTIW.js +0 -10
  132. package/dist/healthcheck-3FSNL2U7.js +0 -20
  133. package/dist/indexer-RAKJ7JR3.js +0 -8
  134. package/dist/lint-36WTW762.js +0 -17
  135. package/dist/log-YIXF6CYM.js +0 -11
  136. /package/dist/{chunk-7PNF3AP2.js.map → chunk-34VN47IW.js.map} +0 -0
  137. /package/dist/{chunk-NBY4TRP7.js.map → chunk-7M34JDTH.js.map} +0 -0
  138. /package/dist/{chunk-LGQPP5FQ.js.map → chunk-EYVFC745.js.map} +0 -0
  139. /package/dist/{chunk-SX427M2R.js.map → chunk-XKGK643R.js.map} +0 -0
  140. /package/dist/{dossier-RWTIMGZN.js.map → core/features.js.map} +0 -0
  141. /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
  142. /package/dist/{healthcheck-3FSNL2U7.js.map → core/skill-curator.js.map} +0 -0
  143. /package/dist/{indexer-RAKJ7JR3.js.map → core/skill-evidence.js.map} +0 -0
  144. /package/dist/{lexicon-IVNJTMWE.js.map → core/skill-queue.js.map} +0 -0
  145. /package/dist/{lint-36WTW762.js.map → core/skill-review.js.map} +0 -0
  146. /package/dist/{log-YIXF6CYM.js.map → dossier-RXBLQOXC.js.map} +0 -0
  147. /package/dist/{profile-UE4YVTDC.js.map → failure-eval-IUJO47UC.js.map} +0 -0
  148. /package/dist/{schema-LY6533NJ.js.map → healthcheck-S7D6YNTA.js.map} +0 -0
  149. /package/dist/{telemetry-B4KP3UVE.js.map → indexer-RZPH2PVL.js.map} +0 -0
@@ -0,0 +1,113 @@
1
+ import {
2
+ featureOn
3
+ } from "./chunk-VQCNWRTL.js";
4
+ import {
5
+ askJev,
6
+ jevVerdict
7
+ } from "./chunk-GYR7U6KA.js";
8
+ import {
9
+ smHome
10
+ } from "./chunk-NIZLBNH4.js";
11
+
12
+ // src/core/skill-scope.ts
13
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { join, sep } from "path";
16
+ function privatePaths(env = process.env) {
17
+ const home = env.HOME || homedir();
18
+ return (env.SEMANTIC_MEMORY_PRIVATE_PATHS ?? "~/workspace/kentro").split(",").map((p) => p.trim().replace(/^~(?=$|\/)/, home)).filter(Boolean);
19
+ }
20
+ var isPrivateRepo = (repo, env = process.env) => privatePaths(env).some((p) => repo === p || repo.startsWith(p.endsWith(sep) ? p : p + sep));
21
+ function repoReferences(repo, text, paths = []) {
22
+ const tokens = new Set(paths);
23
+ for (const m of text.matchAll(/(?:^|[\s`'"(])((?:\.\/)?[\w.@-]+(?:\/[\w.@-]+)+\/?|[\w-]+\.(?:sh|js|mjs|ts|py|json|toml|ya?ml|md))(?=[\s`'"),:;]|$)/gm)) {
24
+ tokens.add(m[1].replace(/^\.\//, "").replace(/\/$/, ""));
25
+ }
26
+ return [...tokens].filter((p) => p && !p.startsWith("/") && !p.startsWith("~") && !p.includes("..") && existsSync(join(repo, p)));
27
+ }
28
+ function commonSubdir(repo, refs) {
29
+ const dirs = refs.map((r) => {
30
+ let isDir = false;
31
+ try {
32
+ isDir = statSync(join(repo, r)).isDirectory();
33
+ } catch {
34
+ }
35
+ const parts = r.split("/");
36
+ return (isDir ? parts : parts.slice(0, -1)).slice(0, 2).join("/");
37
+ });
38
+ if (!dirs.length || dirs.some((d) => !d)) return null;
39
+ const first = dirs[0];
40
+ return dirs.every((d) => d === first || d.startsWith(first + "/")) ? first : null;
41
+ }
42
+ var SCOPE_QUESTION = "Would these steps work unchanged in any software repository \u2014 they name no file, script, package, service or setting that belongs to one particular repository?";
43
+ async function decideScope(repo, draft, env = process.env) {
44
+ const refs = repoReferences(repo, `${draft.description}
45
+ ${draft.body}`, draft.applies_to?.paths ?? []);
46
+ if (refs.length) {
47
+ const sub = commonSubdir(repo, refs);
48
+ if (sub && (existsSync(join(repo, sub, "package.json")) || existsSync(join(repo, sub, ".claude")) || existsSync(join(repo, sub, "pyproject.toml")))) {
49
+ return { scope: "directory", directory: sub, globalCandidate: false, why: `every path it names is under ${sub}/` };
50
+ }
51
+ return { scope: "project", globalCandidate: false, why: `names this repo's ${refs.slice(0, 3).join(", ")}` };
52
+ }
53
+ if (isPrivateRepo(repo, env)) return { scope: "project", globalCandidate: false, why: "private repo: stays here" };
54
+ if (!featureOn("jev", repo, env)) return { scope: "project", globalCandidate: false, why: "names nothing in the repo; jev mode off \u2192 project" };
55
+ const a = await askJev(
56
+ {
57
+ cwd: repo,
58
+ caller: "semantic-memory-skill-loop",
59
+ state: `Skill ${draft.name}: ${draft.description}
60
+
61
+ ${draft.body}`.slice(0, 2e3),
62
+ questions: [{ key: "global", instructions: SCOPE_QUESTION }]
63
+ },
64
+ { env, timeoutMs: 3e4 }
65
+ );
66
+ const yes = jevVerdict(a.scores.global) === true;
67
+ return {
68
+ scope: "project",
69
+ globalCandidate: yes,
70
+ why: yes ? `Jev: holds in any repo (p=${a.scores.global?.toFixed(2)}); promotion needs a second repo` : `names nothing in the repo; Jev not sure it is general (${a.error ?? `p=${a.scores.global?.toFixed(2)}`}) \u2192 project`
71
+ };
72
+ }
73
+ var skillIndexPath = (env = process.env) => join(smHome(env), "skill-index.jsonl");
74
+ function indexSkill(e, env = process.env) {
75
+ if (isPrivateRepo(e.repo, env)) return;
76
+ mkdirSync(smHome(env), { recursive: true });
77
+ appendFileSync(skillIndexPath(env), JSON.stringify({ ...e, ts: (/* @__PURE__ */ new Date()).toISOString() }) + "\n");
78
+ }
79
+ function readIndex(env = process.env) {
80
+ const latest = /* @__PURE__ */ new Map();
81
+ let raw = "";
82
+ try {
83
+ raw = readFileSync(skillIndexPath(env), "utf8");
84
+ } catch {
85
+ return [];
86
+ }
87
+ for (const line of raw.split("\n")) {
88
+ try {
89
+ const e = JSON.parse(line);
90
+ latest.set(`${e.repo}\0${e.name}\0${e.path}`, e);
91
+ } catch {
92
+ }
93
+ }
94
+ return [...latest.values()];
95
+ }
96
+ var nameTokens = (n) => new Set(n.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1));
97
+ function sameLesson(a, b) {
98
+ if (a === b) return true;
99
+ const x = nameTokens(a), y = nameTokens(b);
100
+ const shared = [...x].filter((t) => y.has(t)).length;
101
+ return shared / Math.max(x.size, y.size, 1) >= 0.75;
102
+ }
103
+ function promotionPeers(repo, name, env = process.env) {
104
+ if (isPrivateRepo(repo, env)) return [];
105
+ return readIndex(env).filter((e) => e.repo !== repo && e.status !== "archived" && sameLesson(e.name, name) && !isPrivateRepo(e.repo, env));
106
+ }
107
+
108
+ export {
109
+ decideScope,
110
+ indexSkill,
111
+ promotionPeers
112
+ };
113
+ //# sourceMappingURL=chunk-3FPUBAEW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/skill-scope.ts"],"sourcesContent":["/**\n * Where a learned skill lives (v1.7): directory, project, or — only by promotion — global.\n *\n * 1. Deterministic first: a draft that names this repo's paths is project-scoped; one whose paths all sit\n * under a single subdirectory is directory-scoped there.\n * 2. Unresolved (names nothing in the repo): in jev mode one batch Jev question, \"is this true in any\n * repository?\"; a yes at >= 0.95 only marks the draft a global *candidate*. Otherwise: project.\n * 3. Global only by promotion: when the cross-repo index shows the same skill name drafted or landed in\n * >= 2 repos, review offers one global skill that replaces the project copies. A person approves it.\n *\n * Private repos (SEMANTIC_MEMORY_PRIVATE_PATHS, default ~/workspace/kentro: a Claude-only boundary) are\n * always project, are never written to the cross-repo index, and never reach Jev.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, statSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, sep } from \"node:path\";\nimport { featureOn } from \"./features.js\";\nimport { askJev, jevVerdict } from \"./jev.js\";\nimport { smHome } from \"./skill-queue.js\";\nimport type { SkillScope } from \"./skill-schema.js\";\n\nexport interface ScopeDecision {\n scope: Exclude<SkillScope, \"global\">;\n /** directory scope: the subdirectory (repo-relative) */\n directory?: string;\n /** Jev said this holds in any repo: offer promotion once another repo has it too */\n globalCandidate: boolean;\n why: string;\n}\n\nexport function privatePaths(env = process.env): string[] {\n const home = env.HOME || homedir();\n return (env.SEMANTIC_MEMORY_PRIVATE_PATHS ?? \"~/workspace/kentro\")\n .split(\",\")\n .map((p) => p.trim().replace(/^~(?=$|\\/)/, home))\n .filter(Boolean);\n}\n\nexport const isPrivateRepo = (repo: string, env = process.env) =>\n privatePaths(env).some((p) => repo === p || repo.startsWith(p.endsWith(sep) ? p : p + sep));\n\n/** Repo-relative paths a draft mentions that exist in the repo (from applies_to.paths and path-like tokens). */\nexport function repoReferences(repo: string, text: string, paths: string[] = []): string[] {\n const tokens = new Set(paths);\n for (const m of text.matchAll(/(?:^|[\\s`'\"(])((?:\\.\\/)?[\\w.@-]+(?:\\/[\\w.@-]+)+\\/?|[\\w-]+\\.(?:sh|js|mjs|ts|py|json|toml|ya?ml|md))(?=[\\s`'\"),:;]|$)/gm)) {\n tokens.add(m[1].replace(/^\\.\\//, \"\").replace(/\\/$/, \"\"));\n }\n return [...tokens].filter((p) => p && !p.startsWith(\"/\") && !p.startsWith(\"~\") && !p.includes(\"..\") && existsSync(join(repo, p)));\n}\n\n/** The one subdirectory every referenced path sits under (two levels at most), or null. */\nfunction commonSubdir(repo: string, refs: string[]): string | null {\n const dirs = refs.map((r) => {\n let isDir = false;\n try {\n isDir = statSync(join(repo, r)).isDirectory();\n } catch {}\n const parts = r.split(\"/\");\n return (isDir ? parts : parts.slice(0, -1)).slice(0, 2).join(\"/\");\n });\n if (!dirs.length || dirs.some((d) => !d)) return null;\n const first = dirs[0];\n return dirs.every((d) => d === first || d.startsWith(first + \"/\")) ? first : null;\n}\n\nexport const SCOPE_QUESTION =\n \"Would these steps work unchanged in any software repository — they name no file, script, package, service or \" +\n \"setting that belongs to one particular repository?\";\n\nexport async function decideScope(\n repo: string,\n draft: { name: string; description: string; body: string; applies_to?: { paths?: string[] } },\n env = process.env,\n): Promise<ScopeDecision> {\n const refs = repoReferences(repo, `${draft.description}\\n${draft.body}`, draft.applies_to?.paths ?? []);\n if (refs.length) {\n const sub = commonSubdir(repo, refs);\n // a subdir that holds its own package/skills is a real unit; a bare src/ or scripts/ is still the whole project\n if (sub && (existsSync(join(repo, sub, \"package.json\")) || existsSync(join(repo, sub, \".claude\")) || existsSync(join(repo, sub, \"pyproject.toml\")))) {\n return { scope: \"directory\", directory: sub, globalCandidate: false, why: `every path it names is under ${sub}/` };\n }\n return { scope: \"project\", globalCandidate: false, why: `names this repo's ${refs.slice(0, 3).join(\", \")}` };\n }\n if (isPrivateRepo(repo, env)) return { scope: \"project\", globalCandidate: false, why: \"private repo: stays here\" };\n if (!featureOn(\"jev\", repo, env)) return { scope: \"project\", globalCandidate: false, why: \"names nothing in the repo; jev mode off → project\" };\n const a = await askJev(\n {\n cwd: repo,\n caller: \"semantic-memory-skill-loop\",\n state: `Skill ${draft.name}: ${draft.description}\\n\\n${draft.body}`.slice(0, 2000),\n questions: [{ key: \"global\", instructions: SCOPE_QUESTION }],\n },\n { env, timeoutMs: 30_000 },\n );\n const yes = jevVerdict(a.scores.global) === true;\n return {\n scope: \"project\",\n globalCandidate: yes,\n why: yes ? `Jev: holds in any repo (p=${a.scores.global?.toFixed(2)}); promotion needs a second repo` : `names nothing in the repo; Jev not sure it is general (${a.error ?? `p=${a.scores.global?.toFixed(2)}`}) → project`,\n };\n}\n\n// --- the cross-repo index: paths and names only ---\n\nexport interface IndexEntry {\n repo: string;\n name: string;\n path: string;\n status: \"draft\" | \"active\" | \"archived\";\n global_candidate?: boolean;\n ts: string;\n}\n\nexport const skillIndexPath = (env = process.env) => join(smHome(env), \"skill-index.jsonl\");\n\nexport function indexSkill(e: Omit<IndexEntry, \"ts\">, env = process.env): void {\n if (isPrivateRepo(e.repo, env)) return;\n mkdirSync(smHome(env), { recursive: true });\n appendFileSync(skillIndexPath(env), JSON.stringify({ ...e, ts: new Date().toISOString() }) + \"\\n\");\n}\n\n/** Latest entry per (repo, name, path). */\nexport function readIndex(env = process.env): IndexEntry[] {\n const latest = new Map<string, IndexEntry>();\n let raw = \"\";\n try {\n raw = readFileSync(skillIndexPath(env), \"utf8\");\n } catch {\n return [];\n }\n for (const line of raw.split(\"\\n\")) {\n try {\n const e = JSON.parse(line) as IndexEntry;\n latest.set(`${e.repo}\\0${e.name}\\0${e.path}`, e);\n } catch {}\n }\n return [...latest.values()];\n}\n\nconst nameTokens = (n: string) => new Set(n.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1));\n\n/** Same lesson: the same name, or names sharing >= 3/4 of their words. */\nexport function sameLesson(a: string, b: string): boolean {\n if (a === b) return true;\n const x = nameTokens(a), y = nameTokens(b);\n const shared = [...x].filter((t) => y.has(t)).length;\n return shared / Math.max(x.size, y.size, 1) >= 0.75;\n}\n\n/** Other repos holding the same lesson (not archived). Promotion is offered when this returns >= 1 repo. */\nexport function promotionPeers(repo: string, name: string, env = process.env): IndexEntry[] {\n if (isPrivateRepo(repo, env)) return [];\n return readIndex(env).filter((e) => e.repo !== repo && e.status !== \"archived\" && sameLesson(e.name, name) && !isPrivateRepo(e.repo, env));\n}\n"],"mappings":"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","names":[]}
@@ -1,16 +1,18 @@
1
1
  import {
2
2
  appendToSection,
3
3
  replaceSectionBody
4
- } from "./chunk-XAXK7NM4.js";
4
+ } from "./chunk-EXAUVIGW.js";
5
+ import {
6
+ matter_default
7
+ } from "./chunk-J7LYDRML.js";
5
8
  import {
6
9
  redact
7
- } from "./chunk-OEF5U5GV.js";
10
+ } from "./chunk-H2BES6FU.js";
8
11
 
9
12
  // src/core/profile.ts
10
13
  import { readFile, writeFile, mkdir } from "fs/promises";
11
14
  import { existsSync } from "fs";
12
15
  import { join, dirname } from "path";
13
- import matter from "gray-matter";
14
16
  var SPEAKER_PROFILE_PATH = "profile/speaker.md";
15
17
  var PROFILE_SECTIONS = [
16
18
  "Severity calibration",
@@ -49,7 +51,7 @@ async function readSpeakerProfile(vaultPath, maxLines = 30) {
49
51
  if (!existsSync(abs)) return null;
50
52
  try {
51
53
  const raw = await readFile(abs, "utf-8");
52
- const { content } = matter(raw);
54
+ const { content } = matter_default(raw);
53
55
  const body = content.trim();
54
56
  const kept = [];
55
57
  let lastWasHeading = false;
@@ -84,17 +86,17 @@ async function initSpeakerProfile(vaultPath, today) {
84
86
  evergreen: true
85
87
  };
86
88
  await mkdir(dirname(abs), { recursive: true });
87
- await writeFile(abs, matter.stringify(speakerProfileTemplate(), fm), "utf-8");
89
+ await writeFile(abs, matter_default.stringify(speakerProfileTemplate(), fm), "utf-8");
88
90
  return { path: SPEAKER_PROFILE_PATH, created: true };
89
91
  }
90
92
  async function updateProfileSection(vaultPath, section, text, mode = "append") {
91
93
  await initSpeakerProfile(vaultPath);
92
94
  const abs = join(vaultPath, SPEAKER_PROFILE_PATH);
93
95
  const raw = await readFile(abs, "utf-8");
94
- const parsed = matter(raw);
96
+ const parsed = matter_default(raw);
95
97
  const incoming = redact(text.trim());
96
98
  const patchedBody = mode === "replace" ? replaceSectionBody(parsed.content, section, incoming.text) : appendToSection(parsed.content, section, `- ${incoming.text}`);
97
- await writeFile(abs, matter.stringify(patchedBody, parsed.data), "utf-8");
99
+ await writeFile(abs, matter_default.stringify(patchedBody, parsed.data), "utf-8");
98
100
  return { path: SPEAKER_PROFILE_PATH, ...incoming.count ? { redacted: incoming.count } : {} };
99
101
  }
100
102
 
@@ -106,4 +108,4 @@ export {
106
108
  initSpeakerProfile,
107
109
  updateProfileSection
108
110
  };
109
- //# sourceMappingURL=chunk-WZEKIUYG.js.map
111
+ //# sourceMappingURL=chunk-6TIKWOY4.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/core/profile.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport matter from \"gray-matter\";\nimport { redact } from \"./redact.js\";\nimport { replaceSectionBody, appendToSection } from \"./dossier.js\";\n\n/**\n * Speaker profile (v1.5 Phase 11) — the model of THIS human.\n *\n * The difference between an expert who knows the system and one who knows the\n * system *and knows you*: how this person calibrates severity (\"annoying\" vs\n * \"broken\" vs \"on fire\"), what they chronically leave unsaid, how much detail they\n * want back, and the shorthand they use that isn't an entity. One note, one human\n * (multi-speaker is an explicit non-goal). Injected at SessionStart alongside the\n * session digest; updated via capture cues when the user corrects an interpretation.\n */\n\nexport const SPEAKER_PROFILE_PATH = \"profile/speaker.md\";\n\n/** The fixed sections a speaker profile carries. */\nexport const PROFILE_SECTIONS = [\n \"Severity calibration\",\n \"Chronic omissions\",\n \"Verbosity preference\",\n \"Shorthand & terms\",\n] as const;\n\nexport type ProfileSection = (typeof PROFILE_SECTIONS)[number];\n\nexport interface SpeakerProfile {\n path: string;\n /** The whole note body (frontmatter stripped), trimmed. */\n body: string;\n /** A capped, injection-ready rendering (headings + non-placeholder lines). */\n head: string;\n}\n\nexport function speakerProfileTemplate(): string {\n return `# Speaker profile\n\n_How this human communicates — learned over time, corrected in place. One human, one profile._\n\n## Severity calibration\n\n_What their words map to. e.g. \"annoying\" = low, \"broken\" = ship-blocking, \"on fire\" = drop everything._\n\n## Chronic omissions\n\n_What they habitually leave unsaid — env, repro steps, which service — so you ask up front._\n\n## Verbosity preference\n\n_How much detail they want back: terse answer first, or full reasoning? Code or prose?_\n\n## Shorthand & terms\n\n_Non-entity shorthand: \"the usual\", \"ship it\", \"the thing from yesterday\" — and what each has meant._\n`;\n}\n\n/** True when a line is only a template placeholder (italic prompt) or blank. */\nfunction isPlaceholderLine(line: string): boolean {\n const t = line.trim();\n return t === \"\" || /^_.*_$/.test(t);\n}\n\n/**\n * Read the speaker profile, returning a capped injection head that drops the\n * template placeholders (so an unfilled profile injects nothing but its headings,\n * and a filled one injects only the real content). Returns null when absent.\n */\nexport async function readSpeakerProfile(vaultPath: string, maxLines = 30): Promise<SpeakerProfile | null> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { content } = matter(raw);\n const body = content.trim();\n // Build the head: keep headings and real (non-placeholder) content lines.\n const kept: string[] = [];\n let lastWasHeading = false;\n for (const line of body.split(\"\\n\")) {\n const isHeading = /^#{1,6}\\s/.test(line);\n if (isHeading) {\n // Drop a heading immediately followed by nothing real (avoid trailing empty sections at render).\n kept.push(line.replace(/^#+\\s*/, \"\").trim());\n lastWasHeading = true;\n continue;\n }\n if (isPlaceholderLine(line)) continue;\n kept.push(line.trim());\n lastWasHeading = false;\n }\n void lastWasHeading;\n const head = kept.slice(0, maxLines).join(\"\\n\").trim();\n return { path: SPEAKER_PROFILE_PATH, body, head };\n } catch {\n return null;\n }\n}\n\nexport interface InitProfileResult {\n path: string;\n created: boolean;\n}\n\n/** Scaffold the speaker profile note. No-op (created:false) if it already exists. */\nexport async function initSpeakerProfile(vaultPath: string, today?: string): Promise<InitProfileResult> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (existsSync(abs)) return { path: SPEAKER_PROFILE_PATH, created: false };\n const date = today ?? new Date().toISOString().slice(0, 10);\n const fm = {\n title: \"Speaker profile\",\n type: \"profile\",\n status: \"active\",\n last_verified: date,\n confidence: \"medium\",\n evergreen: true,\n };\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(speakerProfileTemplate(), fm), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH, created: true };\n}\n\n/**\n * Update one section of the speaker profile. `mode: \"replace\"` swaps the section\n * body; `mode: \"append\"` adds a line (accretion). Scaffolds the note first if\n * absent. Returns the resolved path.\n */\nexport async function updateProfileSection(\n vaultPath: string,\n section: string,\n text: string,\n mode: \"replace\" | \"append\" = \"append\"\n): Promise<{ path: string; redacted?: number }> {\n await initSpeakerProfile(vaultPath);\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n const raw = await readFile(abs, \"utf-8\");\n const parsed = matter(raw);\n const incoming = redact(text.trim());\n const patchedBody = mode === \"replace\"\n ? replaceSectionBody(parsed.content, section, incoming.text)\n : appendToSection(parsed.content, section, `- ${incoming.text}`);\n await writeFile(abs, matter.stringify(patchedBody, parsed.data), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH, ...(incoming.count ? { redacted: incoming.count } : {}) };\n}\n"],"mappings":";;;;;;;;;AAAA,SAAS,UAAU,WAAW,aAAa;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,MAAM,eAAe;AAC9B,OAAO,YAAY;AAeZ,IAAM,uBAAuB;AAG7B,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,yBAAiC;AAC/C,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBT;AAGA,SAAS,kBAAkB,MAAuB;AAChD,QAAM,IAAI,KAAK,KAAK;AACpB,SAAO,MAAM,MAAM,SAAS,KAAK,CAAC;AACpC;AAOA,eAAsB,mBAAmB,WAAmB,WAAW,IAAoC;AACzG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,UAAM,EAAE,QAAQ,IAAI,OAAO,GAAG;AAC9B,UAAM,OAAO,QAAQ,KAAK;AAE1B,UAAM,OAAiB,CAAC;AACxB,QAAI,iBAAiB;AACrB,eAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,YAAM,YAAY,YAAY,KAAK,IAAI;AACvC,UAAI,WAAW;AAEb,aAAK,KAAK,KAAK,QAAQ,UAAU,EAAE,EAAE,KAAK,CAAC;AAC3C,yBAAiB;AACjB;AAAA,MACF;AACA,UAAI,kBAAkB,IAAI,EAAG;AAC7B,WAAK,KAAK,KAAK,KAAK,CAAC;AACrB,uBAAiB;AAAA,IACnB;AACA,SAAK;AACL,UAAM,OAAO,KAAK,MAAM,GAAG,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK;AACrD,WAAO,EAAE,MAAM,sBAAsB,MAAM,KAAK;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,mBAAmB,WAAmB,OAA4C;AACtG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,WAAW,GAAG,EAAG,QAAO,EAAE,MAAM,sBAAsB,SAAS,MAAM;AACzE,QAAM,OAAO,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC1D,QAAM,KAAK;AAAA,IACT,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,WAAW;AAAA,EACb;AACA,QAAM,MAAM,QAAQ,GAAG,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,QAAM,UAAU,KAAK,OAAO,UAAU,uBAAuB,GAAG,EAAE,GAAG,OAAO;AAC5E,SAAO,EAAE,MAAM,sBAAsB,SAAS,KAAK;AACrD;AAOA,eAAsB,qBACpB,WACA,SACA,MACA,OAA6B,UACiB;AAC9C,QAAM,mBAAmB,SAAS;AAClC,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,QAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,QAAM,SAAS,OAAO,GAAG;AACzB,QAAM,WAAW,OAAO,KAAK,KAAK,CAAC;AACnC,QAAM,cAAc,SAAS,YACzB,mBAAmB,OAAO,SAAS,SAAS,SAAS,IAAI,IACzD,gBAAgB,OAAO,SAAS,SAAS,KAAK,SAAS,IAAI,EAAE;AACjE,QAAM,UAAU,KAAK,OAAO,UAAU,aAAa,OAAO,IAAI,GAAG,OAAO;AACxE,SAAO,EAAE,MAAM,sBAAsB,GAAI,SAAS,QAAQ,EAAE,UAAU,SAAS,MAAM,IAAI,CAAC,EAAG;AAC/F;","names":[]}
1
+ {"version":3,"sources":["../src/core/profile.ts"],"sourcesContent":["import { readFile, writeFile, mkdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport matter from \"./matter.js\";\nimport { redact } from \"./redact.js\";\nimport { replaceSectionBody, appendToSection } from \"./dossier.js\";\n\n/**\n * Speaker profile (v1.5 Phase 11) — the model of THIS human.\n *\n * The difference between an expert who knows the system and one who knows the\n * system *and knows you*: how this person calibrates severity (\"annoying\" vs\n * \"broken\" vs \"on fire\"), what they chronically leave unsaid, how much detail they\n * want back, and the shorthand they use that isn't an entity. One note, one human\n * (multi-speaker is an explicit non-goal). Injected at SessionStart alongside the\n * session digest; updated via capture cues when the user corrects an interpretation.\n */\n\nexport const SPEAKER_PROFILE_PATH = \"profile/speaker.md\";\n\n/** The fixed sections a speaker profile carries. */\nexport const PROFILE_SECTIONS = [\n \"Severity calibration\",\n \"Chronic omissions\",\n \"Verbosity preference\",\n \"Shorthand & terms\",\n] as const;\n\nexport type ProfileSection = (typeof PROFILE_SECTIONS)[number];\n\nexport interface SpeakerProfile {\n path: string;\n /** The whole note body (frontmatter stripped), trimmed. */\n body: string;\n /** A capped, injection-ready rendering (headings + non-placeholder lines). */\n head: string;\n}\n\nexport function speakerProfileTemplate(): string {\n return `# Speaker profile\n\n_How this human communicates — learned over time, corrected in place. One human, one profile._\n\n## Severity calibration\n\n_What their words map to. e.g. \"annoying\" = low, \"broken\" = ship-blocking, \"on fire\" = drop everything._\n\n## Chronic omissions\n\n_What they habitually leave unsaid — env, repro steps, which service — so you ask up front._\n\n## Verbosity preference\n\n_How much detail they want back: terse answer first, or full reasoning? Code or prose?_\n\n## Shorthand & terms\n\n_Non-entity shorthand: \"the usual\", \"ship it\", \"the thing from yesterday\" — and what each has meant._\n`;\n}\n\n/** True when a line is only a template placeholder (italic prompt) or blank. */\nfunction isPlaceholderLine(line: string): boolean {\n const t = line.trim();\n return t === \"\" || /^_.*_$/.test(t);\n}\n\n/**\n * Read the speaker profile, returning a capped injection head that drops the\n * template placeholders (so an unfilled profile injects nothing but its headings,\n * and a filled one injects only the real content). Returns null when absent.\n */\nexport async function readSpeakerProfile(vaultPath: string, maxLines = 30): Promise<SpeakerProfile | null> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (!existsSync(abs)) return null;\n try {\n const raw = await readFile(abs, \"utf-8\");\n const { content } = matter(raw);\n const body = content.trim();\n // Build the head: keep headings and real (non-placeholder) content lines.\n const kept: string[] = [];\n let lastWasHeading = false;\n for (const line of body.split(\"\\n\")) {\n const isHeading = /^#{1,6}\\s/.test(line);\n if (isHeading) {\n // Drop a heading immediately followed by nothing real (avoid trailing empty sections at render).\n kept.push(line.replace(/^#+\\s*/, \"\").trim());\n lastWasHeading = true;\n continue;\n }\n if (isPlaceholderLine(line)) continue;\n kept.push(line.trim());\n lastWasHeading = false;\n }\n void lastWasHeading;\n const head = kept.slice(0, maxLines).join(\"\\n\").trim();\n return { path: SPEAKER_PROFILE_PATH, body, head };\n } catch {\n return null;\n }\n}\n\nexport interface InitProfileResult {\n path: string;\n created: boolean;\n}\n\n/** Scaffold the speaker profile note. No-op (created:false) if it already exists. */\nexport async function initSpeakerProfile(vaultPath: string, today?: string): Promise<InitProfileResult> {\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n if (existsSync(abs)) return { path: SPEAKER_PROFILE_PATH, created: false };\n const date = today ?? new Date().toISOString().slice(0, 10);\n const fm = {\n title: \"Speaker profile\",\n type: \"profile\",\n status: \"active\",\n last_verified: date,\n confidence: \"medium\",\n evergreen: true,\n };\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(speakerProfileTemplate(), fm), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH, created: true };\n}\n\n/**\n * Update one section of the speaker profile. `mode: \"replace\"` swaps the section\n * body; `mode: \"append\"` adds a line (accretion). Scaffolds the note first if\n * absent. Returns the resolved path.\n */\nexport async function updateProfileSection(\n vaultPath: string,\n section: string,\n text: string,\n mode: \"replace\" | \"append\" = \"append\"\n): Promise<{ path: string; redacted?: number }> {\n await initSpeakerProfile(vaultPath);\n const abs = join(vaultPath, SPEAKER_PROFILE_PATH);\n const raw = await readFile(abs, \"utf-8\");\n const parsed = matter(raw);\n const incoming = redact(text.trim());\n const patchedBody = mode === \"replace\"\n ? replaceSectionBody(parsed.content, section, incoming.text)\n : appendToSection(parsed.content, section, `- ${incoming.text}`);\n await writeFile(abs, matter.stringify(patchedBody, parsed.data), \"utf-8\");\n return { path: SPEAKER_PROFILE_PATH, ...(incoming.count ? { redacted: incoming.count } : {}) };\n}\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,UAAU,WAAW,aAAa;AAC3C,SAAS,kBAAkB;AAC3B,SAAS,MAAM,eAAe;AAgBvB,IAAM,uBAAuB;AAG7B,IAAM,mBAAmB;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAYO,SAAS,yBAAiC;AAC/C,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBT;AAGA,SAAS,kBAAkB,MAAuB;AAChD,QAAM,IAAI,KAAK,KAAK;AACpB,SAAO,MAAM,MAAM,SAAS,KAAK,CAAC;AACpC;AAOA,eAAsB,mBAAmB,WAAmB,WAAW,IAAoC;AACzG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,CAAC,WAAW,GAAG,EAAG,QAAO;AAC7B,MAAI;AACF,UAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,UAAM,EAAE,QAAQ,IAAI,eAAO,GAAG;AAC9B,UAAM,OAAO,QAAQ,KAAK;AAE1B,UAAM,OAAiB,CAAC;AACxB,QAAI,iBAAiB;AACrB,eAAW,QAAQ,KAAK,MAAM,IAAI,GAAG;AACnC,YAAM,YAAY,YAAY,KAAK,IAAI;AACvC,UAAI,WAAW;AAEb,aAAK,KAAK,KAAK,QAAQ,UAAU,EAAE,EAAE,KAAK,CAAC;AAC3C,yBAAiB;AACjB;AAAA,MACF;AACA,UAAI,kBAAkB,IAAI,EAAG;AAC7B,WAAK,KAAK,KAAK,KAAK,CAAC;AACrB,uBAAiB;AAAA,IACnB;AACA,SAAK;AACL,UAAM,OAAO,KAAK,MAAM,GAAG,QAAQ,EAAE,KAAK,IAAI,EAAE,KAAK;AACrD,WAAO,EAAE,MAAM,sBAAsB,MAAM,KAAK;AAAA,EAClD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,eAAsB,mBAAmB,WAAmB,OAA4C;AACtG,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,MAAI,WAAW,GAAG,EAAG,QAAO,EAAE,MAAM,sBAAsB,SAAS,MAAM;AACzE,QAAM,OAAO,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AAC1D,QAAM,KAAK;AAAA,IACT,OAAO;AAAA,IACP,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,eAAe;AAAA,IACf,YAAY;AAAA,IACZ,WAAW;AAAA,EACb;AACA,QAAM,MAAM,QAAQ,GAAG,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,QAAM,UAAU,KAAK,eAAO,UAAU,uBAAuB,GAAG,EAAE,GAAG,OAAO;AAC5E,SAAO,EAAE,MAAM,sBAAsB,SAAS,KAAK;AACrD;AAOA,eAAsB,qBACpB,WACA,SACA,MACA,OAA6B,UACiB;AAC9C,QAAM,mBAAmB,SAAS;AAClC,QAAM,MAAM,KAAK,WAAW,oBAAoB;AAChD,QAAM,MAAM,MAAM,SAAS,KAAK,OAAO;AACvC,QAAM,SAAS,eAAO,GAAG;AACzB,QAAM,WAAW,OAAO,KAAK,KAAK,CAAC;AACnC,QAAM,cAAc,SAAS,YACzB,mBAAmB,OAAO,SAAS,SAAS,SAAS,IAAI,IACzD,gBAAgB,OAAO,SAAS,SAAS,KAAK,SAAS,IAAI,EAAE;AACjE,QAAM,UAAU,KAAK,eAAO,UAAU,aAAa,OAAO,IAAI,GAAG,OAAO;AACxE,SAAO,EAAE,MAAM,sBAAsB,GAAI,SAAS,QAAQ,EAAE,UAAU,SAAS,MAAM,IAAI,CAAC,EAAG;AAC/F;","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  deriveSessionDir
3
- } from "./chunk-LGQPP5FQ.js";
3
+ } from "./chunk-EYVFC745.js";
4
4
  import {
5
5
  redact
6
- } from "./chunk-OEF5U5GV.js";
6
+ } from "./chunk-H2BES6FU.js";
7
7
 
8
8
  // src/core/telemetry.ts
9
9
  import { appendFile, readFile, mkdir } from "fs/promises";
@@ -92,4 +92,4 @@ export {
92
92
  readSelectionLog,
93
93
  computeSelectionStats
94
94
  };
95
- //# sourceMappingURL=chunk-NBY4TRP7.js.map
95
+ //# sourceMappingURL=chunk-7M34JDTH.js.map
@@ -1,19 +1,21 @@
1
1
  import {
2
2
  listDossiers
3
- } from "./chunk-XAXK7NM4.js";
3
+ } from "./chunk-EXAUVIGW.js";
4
4
  import {
5
5
  deriveSessionDir
6
- } from "./chunk-LGQPP5FQ.js";
6
+ } from "./chunk-EYVFC745.js";
7
+ import {
8
+ matter_default
9
+ } from "./chunk-J7LYDRML.js";
7
10
  import {
8
11
  redactDeep
9
- } from "./chunk-OEF5U5GV.js";
12
+ } from "./chunk-H2BES6FU.js";
10
13
 
11
14
  // src/core/lexicon.ts
12
15
  import { readFile, writeFile, mkdir, unlink } from "fs/promises";
13
16
  import { existsSync } from "fs";
14
17
  import { join, dirname } from "path";
15
18
  import { glob } from "glob";
16
- import matter from "gray-matter";
17
19
  var CACHE_FILE = "lexicon-cache.json";
18
20
  var LEXICON_DIR = "lexicon";
19
21
  function normalizePhrase(s) {
@@ -30,7 +32,7 @@ async function compileLexicon(vaultPath) {
30
32
  const entries = [];
31
33
  for (const rel of files) {
32
34
  try {
33
- const { data } = matter(await readFile(join(vaultPath, rel), "utf-8"));
35
+ const { data } = matter_default(await readFile(join(vaultPath, rel), "utf-8"));
34
36
  if (data.type !== "alias" || typeof data.canonical !== "string") continue;
35
37
  const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];
36
38
  entries.push({
@@ -92,7 +94,7 @@ function expandQuery(aliases, query) {
92
94
  return { expanded: `${query} ${additions}`, matched };
93
95
  }
94
96
  async function writeAliasNote(vaultPath, rel, raw) {
95
- const entry = redactDeep(raw, "precise").value;
97
+ const { value: entry, count } = redactDeep(raw, "precise");
96
98
  const fm = {
97
99
  type: "alias",
98
100
  canonical: entry.canonical,
@@ -108,7 +110,8 @@ Human phrases that refer to \`${entry.canonical}\`.
108
110
  `;
109
111
  const abs = join(vaultPath, rel);
110
112
  await mkdir(dirname(abs), { recursive: true });
111
- await writeFile(abs, matter.stringify(body, fm), "utf-8");
113
+ await writeFile(abs, matter_default.stringify(body, fm), "utf-8");
114
+ return count;
112
115
  }
113
116
  async function addAlias(vaultPath, input) {
114
117
  const existing = await compileLexicon(vaultPath);
@@ -119,14 +122,14 @@ async function addAlias(vaultPath, input) {
119
122
  const phrases = [.../* @__PURE__ */ new Set([...match.phrases, ...cleanPhrases])];
120
123
  const evidence_count2 = match.evidence_count + 1;
121
124
  const confidence2 = confidenceFor(evidence_count2);
122
- await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count: evidence_count2, confidence: confidence2, last_used: today });
125
+ const redacted2 = await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count: evidence_count2, confidence: confidence2, last_used: today });
123
126
  await compileLexicon(vaultPath);
124
- return { path: match.path, created: false, evidence_count: evidence_count2, confidence: confidence2 };
127
+ return { path: match.path, created: false, evidence_count: evidence_count2, confidence: confidence2, ...redacted2 ? { redacted: redacted2 } : {} };
125
128
  }
126
129
  const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);
127
130
  const evidence_count = 1;
128
131
  const confidence = confidenceFor(evidence_count);
129
- await writeAliasNote(vaultPath, rel, {
132
+ const redacted = await writeAliasNote(vaultPath, rel, {
130
133
  canonical: input.canonical,
131
134
  phrases: cleanPhrases,
132
135
  confidence,
@@ -135,7 +138,7 @@ async function addAlias(vaultPath, input) {
135
138
  source: input.source ?? "learned"
136
139
  });
137
140
  await compileLexicon(vaultPath);
138
- return { path: rel, created: true, evidence_count, confidence };
141
+ return { path: rel, created: true, evidence_count, confidence, ...redacted ? { redacted } : {} };
139
142
  }
140
143
  function findAliasConflicts(aliases) {
141
144
  const byPhrase = /* @__PURE__ */ new Map();
@@ -177,4 +180,4 @@ export {
177
180
  findAliasConflicts,
178
181
  removeAlias
179
182
  };
180
- //# sourceMappingURL=chunk-FTRXH2WC.js.map
183
+ //# sourceMappingURL=chunk-7UAVDOI6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/lexicon.ts"],"sourcesContent":["import { readFile, writeFile, mkdir, unlink } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"./matter.js\";\nimport { redactDeep } from \"./redact.js\";\nimport { deriveSessionDir } from \"./session.js\";\nimport { listDossiers } from \"./dossier.js\";\n\n/**\n * Lexicon corpus (v1.4 Phase 3) — the learned human→artifact vocabulary.\n *\n * The heart of the human→LLM bridge: maps the human's own phrasing (\"the flaky\n * thing\", \"the deploy dance\") to concrete artifacts (paths / code symbols).\n * Canonical storage is markdown notes of type `alias` under <vault>/lexicon/;\n * a derived JSON cache under .claude/.semantic-memory/ is what the hot-path hook\n * reads for deterministic query expansion.\n */\n\nexport type Confidence = \"low\" | \"medium\" | \"high\";\n\nexport interface AliasEntry {\n /** The concrete target: a note path or a code symbol/path. */\n canonical: string;\n /** The human's actual words that refer to it (verbatim). */\n phrases: string[];\n confidence: Confidence;\n evidence_count: number;\n last_used?: string;\n source: \"learned\" | \"authored\";\n /** The lexicon note path this entry came from. */\n path: string;\n}\n\nconst CACHE_FILE = \"lexicon-cache.json\";\nconst LEXICON_DIR = \"lexicon\";\n\nexport function normalizePhrase(s: string): string {\n return s.toLowerCase().trim().replace(/\\s+/g, \" \");\n}\n\nexport function slugify(s: string): string {\n return s.toLowerCase().replace(/[^a-z0-9]+/g, \"-\").replace(/^-+|-+$/g, \"\").slice(0, 60) || \"alias\";\n}\n\nfunction confidenceFor(evidenceCount: number): Confidence {\n return evidenceCount >= 3 ? \"high\" : evidenceCount >= 2 ? \"medium\" : \"low\";\n}\n\n/** Read every lexicon note into entries and write the derived cache. */\nexport async function compileLexicon(vaultPath: string): Promise<AliasEntry[]> {\n const files = await glob(`${LEXICON_DIR}/**/*.md`, { cwd: vaultPath });\n const entries: AliasEntry[] = [];\n for (const rel of files) {\n try {\n const { data } = matter(await readFile(join(vaultPath, rel), \"utf-8\"));\n if (data.type !== \"alias\" || typeof data.canonical !== \"string\") continue;\n const phrases = Array.isArray(data.phrases) ? data.phrases.map(String).map((p) => p.trim()).filter(Boolean) : [];\n entries.push({\n canonical: data.canonical,\n phrases,\n confidence: ([\"low\", \"medium\", \"high\"] as const).includes(data.confidence) ? data.confidence : confidenceFor(1),\n evidence_count: typeof data.evidence_count === \"number\" ? data.evidence_count : phrases.length ? 1 : 0,\n last_used: typeof data.last_used === \"string\" ? data.last_used : undefined,\n source: data.source === \"authored\" ? \"authored\" : \"learned\",\n path: rel,\n });\n } catch {\n /* skip unreadable note */\n }\n }\n // Fold in dossier aliases (v1.5 Phase 8): a dossier's `aliases:` are the human's\n // phrases for that entity, so they become query-expansion bindings pointing at the\n // dossier note. Authored, not learned — the human named the entity deliberately.\n // A lexicon note for the same canonical wins (it carries real evidence_count).\n try {\n const dossiers = await listDossiers(vaultPath);\n for (const d of dossiers) {\n if (d.aliases.length === 0) continue;\n if (entries.some((e) => e.canonical === d.path)) continue;\n entries.push({\n canonical: d.path,\n phrases: d.aliases,\n confidence: \"high\",\n evidence_count: d.aliases.length,\n source: \"authored\",\n path: d.path,\n });\n }\n } catch {\n /* dossiers optional — lexicon compiles fine without them */\n }\n try {\n const dir = deriveSessionDir(vaultPath);\n await mkdir(dir, { recursive: true });\n await writeFile(join(dir, CACHE_FILE), JSON.stringify({ compiledAt: new Date().toISOString(), aliases: entries }, null, 2), \"utf-8\");\n } catch {\n /* cache write is best-effort */\n }\n return entries;\n}\n\n/** Fast read for the hook: use the cache if present, else compile. */\nexport async function loadLexiconCache(vaultPath: string): Promise<AliasEntry[]> {\n const cachePath = join(deriveSessionDir(vaultPath), CACHE_FILE);\n if (existsSync(cachePath)) {\n try {\n const data = JSON.parse(await readFile(cachePath, \"utf-8\"));\n if (Array.isArray(data?.aliases)) return data.aliases as AliasEntry[];\n } catch {\n /* fall through to recompile */\n }\n }\n return compileLexicon(vaultPath);\n}\n\nexport function lookupPhrase(aliases: AliasEntry[], phrase: string): AliasEntry[] {\n const n = normalizePhrase(phrase);\n return aliases.filter((a) => a.phrases.some((p) => normalizePhrase(p) === n));\n}\n\n/**\n * Expand a query by appending the canonical targets of any alias phrases that\n * appear in it. Deterministic, no LLM — this is the Tier-1 hook-time expansion.\n */\nexport function expandQuery(aliases: AliasEntry[], query: string): { expanded: string; matched: AliasEntry[] } {\n const nq = normalizePhrase(query);\n const matched = aliases.filter((a) => a.phrases.some((p) => p && nq.includes(normalizePhrase(p))));\n if (matched.length === 0) return { expanded: query, matched: [] };\n const additions = [...new Set(matched.map((a) => a.canonical))].join(\" \");\n return { expanded: `${query} ${additions}`, matched };\n}\n\nasync function writeAliasNote(vaultPath: string, rel: string, raw: Omit<AliasEntry, \"path\">): Promise<number> {\n // phrases are captured from user utterances\n const { value: entry, count } = redactDeep(raw, \"precise\");\n const fm = {\n type: \"alias\",\n canonical: entry.canonical,\n phrases: entry.phrases,\n confidence: entry.confidence,\n evidence_count: entry.evidence_count,\n last_used: entry.last_used,\n source: entry.source,\n };\n const body = `# Alias: ${entry.canonical}\\n\\nHuman phrases that refer to \\`${entry.canonical}\\`.\\n`;\n const abs = join(vaultPath, rel);\n await mkdir(dirname(abs), { recursive: true });\n await writeFile(abs, matter.stringify(body, fm), \"utf-8\");\n return count;\n}\n\n/**\n * Upsert an alias. New canonical → new note (evidence 1). Existing canonical →\n * merge phrases, bump evidence_count, recompute confidence. Recompiles the cache.\n */\nexport async function addAlias(\n vaultPath: string,\n input: { canonical: string; phrases: string[]; source?: \"learned\" | \"authored\"; today?: string }\n): Promise<{ path: string; created: boolean; evidence_count: number; confidence: Confidence; redacted?: number }> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === input.canonical);\n const today = input.today ?? new Date().toISOString().slice(0, 10);\n const cleanPhrases = input.phrases.map((p) => p.trim()).filter(Boolean);\n\n if (match) {\n const phrases = [...new Set([...match.phrases, ...cleanPhrases])];\n const evidence_count = match.evidence_count + 1;\n const confidence = confidenceFor(evidence_count);\n const redacted = await writeAliasNote(vaultPath, match.path, { ...match, phrases, evidence_count, confidence, last_used: today });\n await compileLexicon(vaultPath);\n return { path: match.path, created: false, evidence_count, confidence, ...(redacted ? { redacted } : {}) };\n }\n\n const rel = join(LEXICON_DIR, `${slugify(input.canonical)}.md`);\n const evidence_count = 1;\n const confidence = confidenceFor(evidence_count);\n const redacted = await writeAliasNote(vaultPath, rel, {\n canonical: input.canonical,\n phrases: cleanPhrases,\n confidence,\n evidence_count,\n last_used: today,\n source: input.source ?? \"learned\",\n });\n await compileLexicon(vaultPath);\n return { path: rel, created: true, evidence_count, confidence, ...(redacted ? { redacted } : {}) };\n}\n\nexport interface AliasConflict {\n phrase: string;\n canonicals: string[];\n}\n\n/**\n * Find conflicting bindings: the same (normalized) phrase mapping to more than one\n * canonical target. A wrong alias silently misroutes every future query with that\n * phrase, so conflicts are surfaced (never auto-resolved) for a human to fix.\n */\nexport function findAliasConflicts(aliases: AliasEntry[]): AliasConflict[] {\n const byPhrase = new Map<string, Set<string>>();\n for (const a of aliases) {\n for (const p of a.phrases) {\n const n = normalizePhrase(p);\n if (!n) continue;\n if (!byPhrase.has(n)) byPhrase.set(n, new Set());\n byPhrase.get(n)!.add(a.canonical);\n }\n }\n const conflicts: AliasConflict[] = [];\n for (const [phrase, canonicals] of byPhrase) {\n if (canonicals.size > 1) conflicts.push({ phrase, canonicals: [...canonicals].sort() });\n }\n return conflicts.sort((a, b) => a.phrase.localeCompare(b.phrase));\n}\n\n/** Remove an alias note by canonical. Returns whether one was removed. */\nexport async function removeAlias(vaultPath: string, canonical: string): Promise<boolean> {\n const existing = await compileLexicon(vaultPath);\n const match = existing.find((a) => a.canonical === canonical);\n if (!match) return false;\n try {\n await unlink(join(vaultPath, match.path));\n } catch {\n return false;\n }\n await compileLexicon(vaultPath);\n return true;\n}\n"],"mappings":";;;;;;;;;;;;;;AAAA,SAAS,UAAU,WAAW,OAAO,cAAc;AACnD,SAAS,kBAAkB;AAC3B,SAAS,MAAM,eAAe;AAC9B,SAAS,YAAY;AA+BrB,IAAM,aAAa;AACnB,IAAM,cAAc;AAEb,SAAS,gBAAgB,GAAmB;AACjD,SAAO,EAAE,YAAY,EAAE,KAAK,EAAE,QAAQ,QAAQ,GAAG;AACnD;AAEO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,YAAY,EAAE,QAAQ,eAAe,GAAG,EAAE,QAAQ,YAAY,EAAE,EAAE,MAAM,GAAG,EAAE,KAAK;AAC7F;AAEA,SAAS,cAAc,eAAmC;AACxD,SAAO,iBAAiB,IAAI,SAAS,iBAAiB,IAAI,WAAW;AACvE;AAGA,eAAsB,eAAe,WAA0C;AAC7E,QAAM,QAAQ,MAAM,KAAK,GAAG,WAAW,YAAY,EAAE,KAAK,UAAU,CAAC;AACrE,QAAM,UAAwB,CAAC;AAC/B,aAAW,OAAO,OAAO;AACvB,QAAI;AACF,YAAM,EAAE,KAAK,IAAI,eAAO,MAAM,SAAS,KAAK,WAAW,GAAG,GAAG,OAAO,CAAC;AACrE,UAAI,KAAK,SAAS,WAAW,OAAO,KAAK,cAAc,SAAU;AACjE,YAAM,UAAU,MAAM,QAAQ,KAAK,OAAO,IAAI,KAAK,QAAQ,IAAI,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO,IAAI,CAAC;AAC/G,cAAQ,KAAK;AAAA,QACX,WAAW,KAAK;AAAA,QAChB;AAAA,QACA,YAAa,CAAC,OAAO,UAAU,MAAM,EAAY,SAAS,KAAK,UAAU,IAAI,KAAK,aAAa,cAAc,CAAC;AAAA,QAC9G,gBAAgB,OAAO,KAAK,mBAAmB,WAAW,KAAK,iBAAiB,QAAQ,SAAS,IAAI;AAAA,QACrG,WAAW,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY;AAAA,QACjE,QAAQ,KAAK,WAAW,aAAa,aAAa;AAAA,QAClD,MAAM;AAAA,MACR,CAAC;AAAA,IACH,QAAQ;AAAA,IAER;AAAA,EACF;AAKA,MAAI;AACF,UAAM,WAAW,MAAM,aAAa,SAAS;AAC7C,eAAW,KAAK,UAAU;AACxB,UAAI,EAAE,QAAQ,WAAW,EAAG;AAC5B,UAAI,QAAQ,KAAK,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAG;AACjD,cAAQ,KAAK;AAAA,QACX,WAAW,EAAE;AAAA,QACb,SAAS,EAAE;AAAA,QACX,YAAY;AAAA,QACZ,gBAAgB,EAAE,QAAQ;AAAA,QAC1B,QAAQ;AAAA,QACR,MAAM,EAAE;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF,QAAQ;AAAA,EAER;AACA,MAAI;AACF,UAAM,MAAM,iBAAiB,SAAS;AACtC,UAAM,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC,UAAM,UAAU,KAAK,KAAK,UAAU,GAAG,KAAK,UAAU,EAAE,aAAY,oBAAI,KAAK,GAAE,YAAY,GAAG,SAAS,QAAQ,GAAG,MAAM,CAAC,GAAG,OAAO;AAAA,EACrI,QAAQ;AAAA,EAER;AACA,SAAO;AACT;AAGA,eAAsB,iBAAiB,WAA0C;AAC/E,QAAM,YAAY,KAAK,iBAAiB,SAAS,GAAG,UAAU;AAC9D,MAAI,WAAW,SAAS,GAAG;AACzB,QAAI;AACF,YAAM,OAAO,KAAK,MAAM,MAAM,SAAS,WAAW,OAAO,CAAC;AAC1D,UAAI,MAAM,QAAQ,MAAM,OAAO,EAAG,QAAO,KAAK;AAAA,IAChD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,SAAO,eAAe,SAAS;AACjC;AAEO,SAAS,aAAa,SAAuB,QAA8B;AAChF,QAAM,IAAI,gBAAgB,MAAM;AAChC,SAAO,QAAQ,OAAO,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,gBAAgB,CAAC,MAAM,CAAC,CAAC;AAC9E;AAMO,SAAS,YAAY,SAAuB,OAA4D;AAC7G,QAAM,KAAK,gBAAgB,KAAK;AAChC,QAAM,UAAU,QAAQ,OAAO,CAAC,MAAM,EAAE,QAAQ,KAAK,CAAC,MAAM,KAAK,GAAG,SAAS,gBAAgB,CAAC,CAAC,CAAC,CAAC;AACjG,MAAI,QAAQ,WAAW,EAAG,QAAO,EAAE,UAAU,OAAO,SAAS,CAAC,EAAE;AAChE,QAAM,YAAY,CAAC,GAAG,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,KAAK,GAAG;AACxE,SAAO,EAAE,UAAU,GAAG,KAAK,IAAI,SAAS,IAAI,QAAQ;AACtD;AAEA,eAAe,eAAe,WAAmB,KAAa,KAAgD;AAE5G,QAAM,EAAE,OAAO,OAAO,MAAM,IAAI,WAAW,KAAK,SAAS;AACzD,QAAM,KAAK;AAAA,IACT,MAAM;AAAA,IACN,WAAW,MAAM;AAAA,IACjB,SAAS,MAAM;AAAA,IACf,YAAY,MAAM;AAAA,IAClB,gBAAgB,MAAM;AAAA,IACtB,WAAW,MAAM;AAAA,IACjB,QAAQ,MAAM;AAAA,EAChB;AACA,QAAM,OAAO,YAAY,MAAM,SAAS;AAAA;AAAA,gCAAqC,MAAM,SAAS;AAAA;AAC5F,QAAM,MAAM,KAAK,WAAW,GAAG;AAC/B,QAAM,MAAM,QAAQ,GAAG,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7C,QAAM,UAAU,KAAK,eAAO,UAAU,MAAM,EAAE,GAAG,OAAO;AACxD,SAAO;AACT;AAMA,eAAsB,SACpB,WACA,OACgH;AAChH,QAAM,WAAW,MAAM,eAAe,SAAS;AAC/C,QAAM,QAAQ,SAAS,KAAK,CAAC,MAAM,EAAE,cAAc,MAAM,SAAS;AAClE,QAAM,QAAQ,MAAM,UAAS,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACjE,QAAM,eAAe,MAAM,QAAQ,IAAI,CAAC,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,OAAO;AAEtE,MAAI,OAAO;AACT,UAAM,UAAU,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,MAAM,SAAS,GAAG,YAAY,CAAC,CAAC;AAChE,UAAMA,kBAAiB,MAAM,iBAAiB;AAC9C,UAAMC,cAAa,cAAcD,eAAc;AAC/C,UAAME,YAAW,MAAM,eAAe,WAAW,MAAM,MAAM,EAAE,GAAG,OAAO,SAAS,gBAAAF,iBAAgB,YAAAC,aAAY,WAAW,MAAM,CAAC;AAChI,UAAM,eAAe,SAAS;AAC9B,WAAO,EAAE,MAAM,MAAM,MAAM,SAAS,OAAO,gBAAAD,iBAAgB,YAAAC,aAAY,GAAIC,YAAW,EAAE,UAAAA,UAAS,IAAI,CAAC,EAAG;AAAA,EAC3G;AAEA,QAAM,MAAM,KAAK,aAAa,GAAG,QAAQ,MAAM,SAAS,CAAC,KAAK;AAC9D,QAAM,iBAAiB;AACvB,QAAM,aAAa,cAAc,cAAc;AAC/C,QAAM,WAAW,MAAM,eAAe,WAAW,KAAK;AAAA,IACpD,WAAW,MAAM;AAAA,IACjB,SAAS;AAAA,IACT;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,QAAQ,MAAM,UAAU;AAAA,EAC1B,CAAC;AACD,QAAM,eAAe,SAAS;AAC9B,SAAO,EAAE,MAAM,KAAK,SAAS,MAAM,gBAAgB,YAAY,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC,EAAG;AACnG;AAYO,SAAS,mBAAmB,SAAwC;AACzE,QAAM,WAAW,oBAAI,IAAyB;AAC9C,aAAW,KAAK,SAAS;AACvB,eAAW,KAAK,EAAE,SAAS;AACzB,YAAM,IAAI,gBAAgB,CAAC;AAC3B,UAAI,CAAC,EAAG;AACR,UAAI,CAAC,SAAS,IAAI,CAAC,EAAG,UAAS,IAAI,GAAG,oBAAI,IAAI,CAAC;AAC/C,eAAS,IAAI,CAAC,EAAG,IAAI,EAAE,SAAS;AAAA,IAClC;AAAA,EACF;AACA,QAAM,YAA6B,CAAC;AACpC,aAAW,CAAC,QAAQ,UAAU,KAAK,UAAU;AAC3C,QAAI,WAAW,OAAO,EAAG,WAAU,KAAK,EAAE,QAAQ,YAAY,CAAC,GAAG,UAAU,EAAE,KAAK,EAAE,CAAC;AAAA,EACxF;AACA,SAAO,UAAU,KAAK,CAAC,GAAG,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAC;AAClE;AAGA,eAAsB,YAAY,WAAmB,WAAqC;AACxF,QAAM,WAAW,MAAM,eAAe,SAAS;AAC/C,QAAM,QAAQ,SAAS,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS;AAC5D,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,UAAM,OAAO,KAAK,WAAW,MAAM,IAAI,CAAC;AAAA,EAC1C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,QAAM,eAAe,SAAS;AAC9B,SAAO;AACT;","names":["evidence_count","confidence","redacted"]}
@@ -1,18 +1,20 @@
1
1
  import {
2
2
  loadSchema,
3
3
  validateNote
4
- } from "./chunk-FURCIJBO.js";
4
+ } from "./chunk-Y7H76Q7F.js";
5
+ import {
6
+ matter_default
7
+ } from "./chunk-J7LYDRML.js";
5
8
  import {
6
9
  redact,
7
10
  redactDeep
8
- } from "./chunk-OEF5U5GV.js";
11
+ } from "./chunk-H2BES6FU.js";
9
12
 
10
13
  // src/core/crud.ts
11
14
  import { readFile, writeFile, unlink, rename, mkdir } from "fs/promises";
12
15
  import { dirname, join } from "path";
13
16
  import { existsSync } from "fs";
14
17
  import { glob } from "glob";
15
- import matter from "gray-matter";
16
18
  var NoteCrud = class {
17
19
  notesPath;
18
20
  constructor(notesPath) {
@@ -28,7 +30,7 @@ var NoteCrud = class {
28
30
  const fm = redactDeep(frontmatter ?? {}, "precise");
29
31
  const stamps = computeStamps(body.text);
30
32
  const merged = { ...stamps, ...fm.value };
31
- const fileContent = matter.stringify(body.text, merged);
33
+ const fileContent = matter_default.stringify(body.text, merged);
32
34
  await writeFile(absPath, fileContent, "utf-8");
33
35
  return { path: relativePath, redacted: body.count + fm.count };
34
36
  }
@@ -67,9 +69,9 @@ var NoteCrud = class {
67
69
  updated = existing + "\n" + content;
68
70
  break;
69
71
  case "prepend": {
70
- const { data, content: body2 } = matter(existing);
72
+ const { data, content: body2 } = matter_default(existing);
71
73
  const newBody = content + "\n" + body2;
72
- updated = Object.keys(data).length > 0 ? matter.stringify(newBody, data) : newBody;
74
+ updated = Object.keys(data).length > 0 ? matter_default.stringify(newBody, data) : newBody;
73
75
  break;
74
76
  }
75
77
  case "patch-by-heading": {
@@ -78,10 +80,10 @@ var NoteCrud = class {
78
80
  break;
79
81
  }
80
82
  }
81
- const { data: existingFm, content: body } = matter(updated);
83
+ const { data: existingFm, content: body } = matter_default(updated);
82
84
  const stamps = computeStamps(body);
83
85
  const newFm = { ...existingFm, ...stamps };
84
- await writeFile(absPath, matter.stringify(body, newFm), "utf-8");
86
+ await writeFile(absPath, matter_default.stringify(body, newFm), "utf-8");
85
87
  return { path: relativePath, redacted: incoming.count };
86
88
  }
87
89
  async delete(relativePath) {
@@ -165,7 +167,6 @@ function computeStamps(body) {
165
167
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
166
168
  import { join as join2 } from "path";
167
169
  import { glob as glob2 } from "glob";
168
- import matter2 from "gray-matter";
169
170
  var FrontmatterManager = class {
170
171
  notesPath;
171
172
  constructor(notesPath) {
@@ -174,13 +175,13 @@ var FrontmatterManager = class {
174
175
  async get(relativePath) {
175
176
  const absPath = join2(this.notesPath, relativePath);
176
177
  const raw = await readFile2(absPath, "utf-8");
177
- const { data } = matter2(raw);
178
+ const { data } = matter_default(raw);
178
179
  return data;
179
180
  }
180
181
  async update(relativePath, fields) {
181
182
  const absPath = join2(this.notesPath, relativePath);
182
183
  const raw = await readFile2(absPath, "utf-8");
183
- const { data, content } = matter2(raw);
184
+ const { data, content } = matter_default(raw);
184
185
  const incoming = redactDeep(fields, "precise");
185
186
  for (const [key, value] of Object.entries(incoming.value)) {
186
187
  if (value === null || value === void 0) {
@@ -189,7 +190,7 @@ var FrontmatterManager = class {
189
190
  data[key] = value;
190
191
  }
191
192
  }
192
- const updated = matter2.stringify(content, data);
193
+ const updated = matter_default.stringify(content, data);
193
194
  await writeFile2(absPath, updated, "utf-8");
194
195
  return { redacted: incoming.count };
195
196
  }
@@ -202,7 +203,7 @@ var TagManager = class {
202
203
  async list(relativePath) {
203
204
  const absPath = join2(this.notesPath, relativePath);
204
205
  const raw = await readFile2(absPath, "utf-8");
205
- const { data, content } = matter2(raw);
206
+ const { data, content } = matter_default(raw);
206
207
  const fmTags = Array.isArray(data.tags) ? data.tags : [];
207
208
  const inlineTags = [...content.matchAll(/(?:^|\s)#([a-zA-Z][\w-/]*)/g)].map(
208
209
  (m) => m[1]
@@ -212,17 +213,17 @@ var TagManager = class {
212
213
  async add(relativePath, tags) {
213
214
  const absPath = join2(this.notesPath, relativePath);
214
215
  const raw = await readFile2(absPath, "utf-8");
215
- const { data, content } = matter2(raw);
216
+ const { data, content } = matter_default(raw);
216
217
  const existing = Array.isArray(data.tags) ? data.tags : [];
217
218
  const merged = [.../* @__PURE__ */ new Set([...existing, ...tags])];
218
219
  data.tags = merged;
219
- const updated = matter2.stringify(content, data);
220
+ const updated = matter_default.stringify(content, data);
220
221
  await writeFile2(absPath, updated, "utf-8");
221
222
  }
222
223
  async remove(relativePath, tags) {
223
224
  const absPath = join2(this.notesPath, relativePath);
224
225
  const raw = await readFile2(absPath, "utf-8");
225
- const { data, content } = matter2(raw);
226
+ const { data, content } = matter_default(raw);
226
227
  if (Array.isArray(data.tags)) {
227
228
  data.tags = data.tags.filter((t) => !tags.includes(t));
228
229
  }
@@ -231,7 +232,7 @@ var TagManager = class {
231
232
  const pattern = new RegExp(`(^|\\s)#${escapeRegex2(tag)}(?=\\s|$)`, "g");
232
233
  updatedContent = updatedContent.replace(pattern, "$1");
233
234
  }
234
- const updated = matter2.stringify(updatedContent, data);
235
+ const updated = matter_default.stringify(updatedContent, data);
235
236
  await writeFile2(absPath, updated, "utf-8");
236
237
  }
237
238
  async renameVaultWide(oldTag, newTag) {
@@ -240,7 +241,7 @@ var TagManager = class {
240
241
  for (const file of files) {
241
242
  const absPath = join2(this.notesPath, file);
242
243
  const raw = await readFile2(absPath, "utf-8");
243
- const { data, content } = matter2(raw);
244
+ const { data, content } = matter_default(raw);
244
245
  let changed = false;
245
246
  if (Array.isArray(data.tags)) {
246
247
  const idx = data.tags.indexOf(oldTag);
@@ -253,7 +254,7 @@ var TagManager = class {
253
254
  const updatedContent = content.replace(pattern, `$1#${newTag}`);
254
255
  if (updatedContent !== content) changed = true;
255
256
  if (changed) {
256
- const updated = matter2.stringify(updatedContent, data);
257
+ const updated = matter_default.stringify(updatedContent, data);
257
258
  await writeFile2(absPath, updated, "utf-8");
258
259
  count++;
259
260
  }
@@ -390,7 +391,6 @@ function slugify(s) {
390
391
  import { readdir, readFile as readFile3, writeFile as writeFile3, stat } from "fs/promises";
391
392
  import { existsSync as existsSync2 } from "fs";
392
393
  import { join as join3, dirname as dirname2, basename as basename2, sep } from "path";
393
- import matter3 from "gray-matter";
394
394
  var INDEX_FILE = "INDEX.md";
395
395
  var INDEX_OVERFLOW_THRESHOLD = 100;
396
396
  async function regenDirectoryIndex(vaultPath, dirRel) {
@@ -410,7 +410,7 @@ async function regenDirectoryIndex(vaultPath, dirRel) {
410
410
  const st = await stat(full);
411
411
  if (!st.isFile()) continue;
412
412
  const raw = await readFile3(full, "utf-8");
413
- const { data, content } = matter3(raw);
413
+ const { data, content } = matter_default(raw);
414
414
  const title = typeof data.title === "string" ? data.title : firstHeading(content) ?? basenameNoExt(f);
415
415
  const summary = typeof data.purpose === "string" ? data.purpose : firstParagraph(content);
416
416
  entries.push({ path: f, title, summary });
@@ -494,7 +494,6 @@ function truncate(s, n) {
494
494
  import { readFile as readFile4, writeFile as writeFile4, unlink as unlink2, rename as rename2, mkdir as mkdir2 } from "fs/promises";
495
495
  import { existsSync as existsSync3 } from "fs";
496
496
  import { dirname as dirname3, join as join4 } from "path";
497
- import matter4 from "gray-matter";
498
497
  async function applyPatch(notesPath, changeset, options = {}) {
499
498
  const validate = options.validate !== false;
500
499
  const dryRun = options.dryRun === true;
@@ -541,7 +540,7 @@ async function applyPatch(notesPath, changeset, options = {}) {
541
540
  }
542
541
  if (validate && schema) {
543
542
  for (const c of changeset.creates ?? []) {
544
- const proposed = matter4.stringify(c.content, c.frontmatter ?? {});
543
+ const proposed = matter_default.stringify(c.content, c.frontmatter ?? {});
545
544
  lint.push(...validateNote(c.path, proposed, schema));
546
545
  }
547
546
  for (const u of changeset.updates ?? []) {
@@ -661,9 +660,9 @@ function applyUpdateInMemory(existing, u) {
661
660
  case "append":
662
661
  return existing + "\n" + u.content;
663
662
  case "prepend": {
664
- const { data, content: body } = matter4(existing);
663
+ const { data, content: body } = matter_default(existing);
665
664
  const newBody = u.content + "\n" + body;
666
- return Object.keys(data).length > 0 ? matter4.stringify(newBody, data) : newBody;
665
+ return Object.keys(data).length > 0 ? matter_default.stringify(newBody, data) : newBody;
667
666
  }
668
667
  case "patch-by-heading": {
669
668
  if (!u.heading) throw new Error("patch-by-heading requires heading");
@@ -944,4 +943,4 @@ export {
944
943
  regenerateAgentsContract,
945
944
  inspectAgentsContract
946
945
  };
947
- //# sourceMappingURL=chunk-BBP3DG2U.js.map
946
+ //# sourceMappingURL=chunk-AIXQFTSB.js.map