@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +90 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +49 -29
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/{chunk-7PNF3AP2.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.js.map +1 -0
- package/dist/{chunk-WZEKIUYG.js → chunk-6TIKWOY4.js} +10 -8
- package/dist/{chunk-WZEKIUYG.js.map → chunk-6TIKWOY4.js.map} +1 -1
- package/dist/{chunk-NBY4TRP7.js → chunk-7M34JDTH.js} +3 -3
- package/dist/{chunk-FTRXH2WC.js → chunk-7UAVDOI6.js} +15 -12
- package/dist/chunk-7UAVDOI6.js.map +1 -0
- package/dist/{chunk-BBP3DG2U.js → chunk-AIXQFTSB.js} +25 -26
- package/dist/{chunk-BBP3DG2U.js.map → chunk-AIXQFTSB.js.map} +1 -1
- package/dist/chunk-BEACL25M.js +224 -0
- package/dist/chunk-BEACL25M.js.map +1 -0
- package/dist/chunk-CATPGINC.js +186 -0
- package/dist/chunk-CATPGINC.js.map +1 -0
- package/dist/{chunk-IGHCVAAL.js → chunk-CJUY55GG.js} +6 -3
- package/dist/{chunk-IGHCVAAL.js.map → chunk-CJUY55GG.js.map} +1 -1
- package/dist/chunk-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.js.map +1 -0
- package/dist/{chunk-XAXK7NM4.js → chunk-EXAUVIGW.js} +9 -7
- package/dist/{chunk-XAXK7NM4.js.map → chunk-EXAUVIGW.js.map} +1 -1
- package/dist/{chunk-LGQPP5FQ.js → chunk-EYVFC745.js} +2 -2
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/{chunk-OEF5U5GV.js → chunk-H2BES6FU.js} +10 -2
- package/dist/{chunk-OEF5U5GV.js.map → chunk-H2BES6FU.js.map} +1 -1
- package/dist/chunk-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.js.map +1 -0
- package/dist/chunk-J7LYDRML.js +9 -0
- package/dist/chunk-J7LYDRML.js.map +1 -0
- package/dist/chunk-MQS7JEBD.js +137 -0
- package/dist/chunk-MQS7JEBD.js.map +1 -0
- package/dist/chunk-NCGM36NX.js +118 -0
- package/dist/chunk-NCGM36NX.js.map +1 -0
- package/dist/chunk-NIZLBNH4.js +165 -0
- package/dist/chunk-NIZLBNH4.js.map +1 -0
- package/dist/chunk-OT3MV7XF.js +123 -0
- package/dist/chunk-OT3MV7XF.js.map +1 -0
- package/dist/chunk-P3GLY42T.js +291 -0
- package/dist/chunk-P3GLY42T.js.map +1 -0
- package/dist/chunk-PJXYCCI2.js +177 -0
- package/dist/chunk-PJXYCCI2.js.map +1 -0
- package/dist/chunk-R6GNP7DS.js +87 -0
- package/dist/chunk-R6GNP7DS.js.map +1 -0
- package/dist/{chunk-44BQDRXD.js → chunk-SHSOCFJH.js} +4 -1
- package/dist/chunk-SHSOCFJH.js.map +1 -0
- package/dist/chunk-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/{chunk-CAX46XLY.js → chunk-VBLCUWT4.js} +23 -12
- package/dist/chunk-VBLCUWT4.js.map +1 -0
- package/dist/chunk-VQCNWRTL.js +121 -0
- package/dist/chunk-VQCNWRTL.js.map +1 -0
- package/dist/{chunk-SX427M2R.js → chunk-XKGK643R.js} +2 -2
- package/dist/{chunk-FURCIJBO.js → chunk-Y7H76Q7F.js} +6 -3
- package/dist/chunk-Y7H76Q7F.js.map +1 -0
- package/dist/chunk-ZSWI3Y4Y.js +232 -0
- package/dist/chunk-ZSWI3Y4Y.js.map +1 -0
- package/dist/cli/index.js +389 -60
- package/dist/cli/index.js.map +1 -1
- package/dist/core/features.d.ts +38 -0
- package/dist/core/features.js +28 -0
- package/dist/core/index.d.ts +285 -2
- package/dist/core/index.js +170 -20
- package/dist/core/index.js.map +1 -1
- package/dist/core/jev.d.ts +35 -0
- package/dist/core/jev.js +20 -0
- package/dist/core/redact.js +1 -1
- package/dist/core/skill-curator.d.ts +1 -0
- package/dist/core/skill-curator.js +41 -0
- package/dist/core/skill-evidence.d.ts +25 -0
- package/dist/core/skill-evidence.js +23 -0
- package/dist/core/skill-queue.d.ts +40 -0
- package/dist/core/skill-queue.js +38 -0
- package/dist/core/skill-review.d.ts +3 -0
- package/dist/core/skill-review.js +29 -0
- package/dist/core/skill-worker.d.ts +21 -0
- package/dist/core/skill-worker.js +70 -0
- package/dist/core/skill-worker.js.map +1 -0
- package/dist/{dossier-RWTIMGZN.js → dossier-RXBLQOXC.js} +5 -4
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/healthcheck-S7D6YNTA.js +21 -0
- package/dist/indexer-RZPH2PVL.js +9 -0
- package/dist/{lexicon-IVNJTMWE.js → lexicon-SEJSLBHO.js} +6 -5
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/lint-3JNLRM5U.js +18 -0
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/log-RM2NDNPH.js +11 -0
- package/dist/log-RM2NDNPH.js.map +1 -0
- package/dist/matter-SNJ2WDL6.js +8 -0
- package/dist/matter-SNJ2WDL6.js.map +1 -0
- package/dist/mcp/server.js +43 -38
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-UE4YVTDC.js → profile-SNULTJ27.js} +6 -5
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-LY6533NJ.js → schema-RDYUK6CC.js} +3 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- package/dist/{search-MZC4DC7H.js → search-325ULY57.js} +4 -3
- package/dist/{search-MZC4DC7H.js.map → search-325ULY57.js.map} +1 -1
- package/dist/skill-curator-D7HHZaUI.d.ts +71 -0
- package/dist/skill-land-XU2APPBJ.js +31 -0
- package/dist/skill-land-XU2APPBJ.js.map +1 -0
- package/dist/skill-ledger-AYXVNFX7.js +18 -0
- package/dist/skill-ledger-AYXVNFX7.js.map +1 -0
- package/dist/skill-merge-GXBQLBZA.js +24 -0
- package/dist/skill-merge-GXBQLBZA.js.map +1 -0
- package/dist/skill-review-BaW5n0Zg.d.ts +179 -0
- package/dist/skill-schema-RVB3EHEF.js +30 -0
- package/dist/skill-schema-RVB3EHEF.js.map +1 -0
- package/dist/{telemetry-B4KP3UVE.js → telemetry-YAG3VA4Y.js} +4 -4
- package/dist/telemetry-YAG3VA4Y.js.map +1 -0
- package/docs/regression-snapshot.md +5 -5
- package/hooks/hooks.json +26 -1
- package/hooks/jev-bridge.py +109 -0
- package/hooks/session-start.js +5 -0
- package/hooks/skill-loop.js +65 -0
- package/hooks/vault-context.js +104 -6
- package/package.json +4 -2
- package/dist/chunk-44BQDRXD.js.map +0 -1
- package/dist/chunk-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-CAX46XLY.js.map +0 -1
- package/dist/chunk-FTRXH2WC.js.map +0 -1
- package/dist/chunk-FURCIJBO.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- package/dist/healthcheck-3FSNL2U7.js +0 -20
- package/dist/indexer-RAKJ7JR3.js +0 -8
- package/dist/lint-36WTW762.js +0 -17
- package/dist/log-YIXF6CYM.js +0 -11
- /package/dist/{chunk-7PNF3AP2.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-NBY4TRP7.js.map → chunk-7M34JDTH.js.map} +0 -0
- /package/dist/{chunk-LGQPP5FQ.js.map → chunk-EYVFC745.js.map} +0 -0
- /package/dist/{chunk-SX427M2R.js.map → chunk-XKGK643R.js.map} +0 -0
- /package/dist/{dossier-RWTIMGZN.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-3FSNL2U7.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{indexer-RAKJ7JR3.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lexicon-IVNJTMWE.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{lint-36WTW762.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{log-YIXF6CYM.js.map → dossier-RXBLQOXC.js.map} +0 -0
- /package/dist/{profile-UE4YVTDC.js.map → failure-eval-IUJO47UC.js.map} +0 -0
- /package/dist/{schema-LY6533NJ.js.map → healthcheck-S7D6YNTA.js.map} +0 -0
- /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-
|
|
4
|
+
} from "./chunk-EXAUVIGW.js";
|
|
5
|
+
import {
|
|
6
|
+
matter_default
|
|
7
|
+
} from "./chunk-J7LYDRML.js";
|
|
5
8
|
import {
|
|
6
9
|
redact
|
|
7
|
-
} from "./chunk-
|
|
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 } =
|
|
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,
|
|
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 =
|
|
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,
|
|
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-
|
|
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 \"
|
|
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-
|
|
3
|
+
} from "./chunk-EYVFC745.js";
|
|
4
4
|
import {
|
|
5
5
|
redact
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
95
|
+
//# sourceMappingURL=chunk-7M34JDTH.js.map
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
listDossiers
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EXAUVIGW.js";
|
|
4
4
|
import {
|
|
5
5
|
deriveSessionDir
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EYVFC745.js";
|
|
7
|
+
import {
|
|
8
|
+
matter_default
|
|
9
|
+
} from "./chunk-J7LYDRML.js";
|
|
7
10
|
import {
|
|
8
11
|
redactDeep
|
|
9
|
-
} from "./chunk-
|
|
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 } =
|
|
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")
|
|
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,
|
|
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-
|
|
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":"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","names":["evidence_count","confidence","redacted"]}
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadSchema,
|
|
3
3
|
validateNote
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
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 =
|
|
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 } =
|
|
72
|
+
const { data, content: body2 } = matter_default(existing);
|
|
71
73
|
const newBody = content + "\n" + body2;
|
|
72
|
-
updated = Object.keys(data).length > 0 ?
|
|
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 } =
|
|
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,
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
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 =
|
|
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 } =
|
|
663
|
+
const { data, content: body } = matter_default(existing);
|
|
665
664
|
const newBody = u.content + "\n" + body;
|
|
666
|
-
return Object.keys(data).length > 0 ?
|
|
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-
|
|
946
|
+
//# sourceMappingURL=chunk-AIXQFTSB.js.map
|