@theglitchking/semantic-memory 1.6.1 → 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 +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/README.md +19 -2
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/commands/jev-mode.md +18 -0
- package/commands/skill-loop.md +23 -0
- package/dist/{chunk-TMMTOFHI.js → chunk-34VN47IW.js} +2 -2
- package/dist/chunk-3FPUBAEW.js +113 -0
- package/dist/chunk-3FPUBAEW.js.map +1 -0
- package/dist/{chunk-ZLIHQZ4C.js → chunk-6TIKWOY4.js} +5 -5
- package/dist/{chunk-KSH2QO6V.js → chunk-7UAVDOI6.js} +5 -5
- package/dist/{chunk-PGVIBUL3.js → chunk-AIXQFTSB.js} +5 -5
- 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-DBFD3MTT.js +130 -0
- package/dist/chunk-DBFD3MTT.js.map +1 -0
- package/dist/{chunk-HZ7B5KHY.js → chunk-EXAUVIGW.js} +4 -4
- package/dist/chunk-GYR7U6KA.js +80 -0
- package/dist/chunk-GYR7U6KA.js.map +1 -0
- package/dist/chunk-IEZVFKUT.js +157 -0
- package/dist/chunk-IEZVFKUT.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-TKHWVISV.js +71 -0
- package/dist/chunk-TKHWVISV.js.map +1 -0
- package/dist/{chunk-GYBUBJYM.js → chunk-VBLCUWT4.js} +17 -8
- 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-HUG7E25D.js → chunk-Y7H76Q7F.js} +1 -1
- 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 +381 -52
- 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 -21
- 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/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-EN26WDKC.js → dossier-RXBLQOXC.js} +3 -3
- package/dist/failure-eval-IUJO47UC.js +18 -0
- package/dist/failure-eval-IUJO47UC.js.map +1 -0
- package/dist/{healthcheck-53OVXUQQ.js → healthcheck-S7D6YNTA.js} +7 -7
- package/dist/healthcheck-S7D6YNTA.js.map +1 -0
- package/dist/{lexicon-XNA5IFIA.js → lexicon-SEJSLBHO.js} +4 -4
- package/dist/lexicon-SEJSLBHO.js.map +1 -0
- package/dist/{lint-O4RQ47NX.js → lint-3JNLRM5U.js} +6 -6
- package/dist/lint-3JNLRM5U.js.map +1 -0
- package/dist/mcp/server.js +19 -17
- package/dist/mcp/server.js.map +1 -1
- package/dist/{profile-SIWAVD7O.js → profile-SNULTJ27.js} +4 -4
- package/dist/profile-SNULTJ27.js.map +1 -0
- package/dist/{schema-UJNYYRZZ.js → schema-RDYUK6CC.js} +2 -2
- package/dist/schema-RDYUK6CC.js.map +1 -0
- 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/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-5F5ZIAW2.js +0 -79
- package/dist/chunk-5F5ZIAW2.js.map +0 -1
- package/dist/chunk-GYBUBJYM.js.map +0 -1
- package/dist/chunk-HUG7E25D.js.map +0 -1
- package/dist/failure-eval-37JRNTIW.js +0 -10
- /package/dist/{chunk-TMMTOFHI.js.map → chunk-34VN47IW.js.map} +0 -0
- /package/dist/{chunk-ZLIHQZ4C.js.map → chunk-6TIKWOY4.js.map} +0 -0
- /package/dist/{chunk-KSH2QO6V.js.map → chunk-7UAVDOI6.js.map} +0 -0
- /package/dist/{chunk-PGVIBUL3.js.map → chunk-AIXQFTSB.js.map} +0 -0
- /package/dist/{chunk-HZ7B5KHY.js.map → chunk-EXAUVIGW.js.map} +0 -0
- /package/dist/{dossier-EN26WDKC.js.map → core/features.js.map} +0 -0
- /package/dist/{failure-eval-37JRNTIW.js.map → core/jev.js.map} +0 -0
- /package/dist/{healthcheck-53OVXUQQ.js.map → core/skill-curator.js.map} +0 -0
- /package/dist/{lexicon-XNA5IFIA.js.map → core/skill-evidence.js.map} +0 -0
- /package/dist/{lint-O4RQ47NX.js.map → core/skill-queue.js.map} +0 -0
- /package/dist/{profile-SIWAVD7O.js.map → core/skill-review.js.map} +0 -0
- /package/dist/{schema-UJNYYRZZ.js.map → dossier-RXBLQOXC.js.map} +0 -0
package/hooks/vault-context.js
CHANGED
|
@@ -939,7 +939,32 @@ async function handleStop(projectRoot, sessionId) {
|
|
|
939
939
|
// --- PostToolUseFailure: look up a knowledge-base fact for a failed tool call (v1.6, opt-in) ---
|
|
940
940
|
// Measured on 553 real failures (scripts/eval-failures): facts-only at score >= 0.50, about 6 in 10
|
|
941
941
|
// injections were on target, firing on ~4% of failures. Below the plan's 95% bar, hence off by default.
|
|
942
|
-
|
|
942
|
+
// v1.7 jev mode: Jev can veto the candidate fact. Measured on 1,171 Claude-labelled failure/doc pairs, Jev's
|
|
943
|
+
// confident no's (<= 0.05) were 727 of 729 right, but it never said yes at >= 0.95 to a relevant pair (max 0.89):
|
|
944
|
+
// a "yes" gate would inject nothing. So a confident no suppresses, anything else keeps the plain hook's choice.
|
|
945
|
+
// Replayed, the veto removed only wrong facts (12 wrong, 0 right at score 0.30) but none at the shipped 0.50, and
|
|
946
|
+
// precision stays far from the 95% bar, so jev mode does not turn the hook on by itself.
|
|
947
|
+
const JEV_GATE = { adopted: false, budgetMs: Number(process.env.SEMANTIC_MEMORY_JEV_HOOK_BUDGET_MS) || 1500 };
|
|
948
|
+
const FAILURE_HOOK_ENV = () => process.env.SEMANTIC_MEMORY_FAILURE_HOOK;
|
|
949
|
+
const FAILURE_HOOK_ON = (projectRoot) =>
|
|
950
|
+
FAILURE_HOOK_ENV() === "1" || (FAILURE_HOOK_ENV() !== "0" && JEV_GATE.adopted && jevOn(projectRoot));
|
|
951
|
+
|
|
952
|
+
// cheap (env + a few stats); the hook-side copy of src/core/features.ts
|
|
953
|
+
let featuresMod;
|
|
954
|
+
async function loadFeatures() {
|
|
955
|
+
if (featuresMod === undefined) {
|
|
956
|
+
try {
|
|
957
|
+
featuresMod = await import(new URL("../dist/core/features.js", import.meta.url).href);
|
|
958
|
+
} catch (e) {
|
|
959
|
+
debug(`features unavailable: ${e.message}`);
|
|
960
|
+
featuresMod = null;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return featuresMod;
|
|
964
|
+
}
|
|
965
|
+
function jevOn(projectRoot) {
|
|
966
|
+
return Boolean(featuresMod && featuresMod.featureOn("jev", projectRoot));
|
|
967
|
+
}
|
|
943
968
|
const FAILURE_MIN_SCORE = Number(process.env.SEMANTIC_MEMORY_FAILURE_MIN_SCORE) || 0.5;
|
|
944
969
|
const USER_DENIED = /^(The user doesn't want to proceed|Permission to use .* has been denied)/;
|
|
945
970
|
const isFactPath = (p) => /(^|\/)facts\/[^/]+\.md$/.test(String(p));
|
|
@@ -964,7 +989,65 @@ async function redactText(text) {
|
|
|
964
989
|
}
|
|
965
990
|
}
|
|
966
991
|
|
|
967
|
-
|
|
992
|
+
// One live Jev call for the candidate: a confident no (<= 0.05) vetoes it, a yes or an unsure answer lets it
|
|
993
|
+
// through. An error, a timeout or the daily ceiling injects nothing: in jev mode an unchecked fact is not shown.
|
|
994
|
+
async function jevAllows(projectRoot, command, error, fact, started) {
|
|
995
|
+
try {
|
|
996
|
+
const jev = await import(new URL("../dist/core/jev.js", import.meta.url).href);
|
|
997
|
+
const title = String(fact.title ?? fact.path);
|
|
998
|
+
const excerpt = await redactText(String(fact.snippet ?? ""));
|
|
999
|
+
const left = JEV_GATE.budgetMs - (Date.now() - started);
|
|
1000
|
+
if (left < 300) {
|
|
1001
|
+
debug(`jev gate: ${left} ms left of the hook budget — skipped`);
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
const a = await jev.askJev(
|
|
1005
|
+
{
|
|
1006
|
+
cwd: projectRoot,
|
|
1007
|
+
caller: "semantic-memory-hook",
|
|
1008
|
+
state: jev.failureState(command, error),
|
|
1009
|
+
questions: [{ key: "fits", instructions: jev.relevanceQuestion(title, excerpt) }],
|
|
1010
|
+
},
|
|
1011
|
+
{ timeoutMs: left - 50 },
|
|
1012
|
+
);
|
|
1013
|
+
if (a.ceiling) process.stderr.write(`[semantic-memory] jev mode: ${a.error}; the failure hook injects nothing until tomorrow\n`);
|
|
1014
|
+
else if (a.error) debug(`jev gate: ${a.error}`);
|
|
1015
|
+
if (a.error || typeof a.scores.fits !== "number") return false;
|
|
1016
|
+
return jev.jevVerdict(a.scores.fits) !== false;
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
debug(`jev gate failed: ${e.message}`);
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// v1.7 skill loop: failures, corrections and /skill uses become evidence for the between-session review.
|
|
1024
|
+
// Never blocks or changes this hook's output; any error is swallowed.
|
|
1025
|
+
async function recordSkillEvidence(eventName, projectRoot, input) {
|
|
1026
|
+
try {
|
|
1027
|
+
const ev = await import(new URL("../dist/core/skill-evidence.js", import.meta.url).href);
|
|
1028
|
+
const root = ev.repoRoot(projectRoot);
|
|
1029
|
+
const session_id = typeof input.session_id === "string" ? input.session_id : null;
|
|
1030
|
+
if (eventName === "PostToolUseFailure") {
|
|
1031
|
+
const name = String(input.tool_name || "");
|
|
1032
|
+
const error = String(input.error || "");
|
|
1033
|
+
if ((name === "Bash" || name.startsWith("mcp__")) && !input.is_interrupt && error && !USER_DENIED.test(error.trim())) {
|
|
1034
|
+
ev.appendEvidence(root, "failures", ev.failureRecord(input));
|
|
1035
|
+
}
|
|
1036
|
+
} else if (eventName === "UserPromptSubmit" && typeof input.prompt === "string") {
|
|
1037
|
+
const cue = ev.detectCorrection(input.prompt);
|
|
1038
|
+
if (cue) ev.appendEvidence(root, "corrections", { session_id, cue, excerpt: input.prompt.slice(0, 300) });
|
|
1039
|
+
const slash = ev.slashSkill(input.prompt);
|
|
1040
|
+
if (slash) {
|
|
1041
|
+
const file = join(root, ".claude", "skills", slash, "SKILL.md");
|
|
1042
|
+
if (existsSync(file) && /^skill_loop:/m.test(readFileSync(file, "utf8"))) ev.appendEvidence(root, "usage", { session_id, skill: slash, via: "slash" });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
} catch (e) {
|
|
1046
|
+
debug(`skill evidence failed: ${e.message}`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
async function handleFailure(projectRoot, sources, cliBin, input, sessionId, started = Date.now()) {
|
|
968
1051
|
const name = String(input.tool_name || "");
|
|
969
1052
|
const error = String(input.error || "");
|
|
970
1053
|
if (!(name === "Bash" || name.startsWith("mcp__")) || input.is_interrupt || !error || USER_DENIED.test(error.trim())) {
|
|
@@ -979,11 +1062,16 @@ async function handleFailure(projectRoot, sources, cliBin, input, sessionId) {
|
|
|
979
1062
|
const command = typeof args.command === "string" ? args.command : `${name} ${JSON.stringify(args)}`.slice(0, 300);
|
|
980
1063
|
const query = await failureQueryFor(error, command);
|
|
981
1064
|
const hits = await searchSources(cliBin, sources, 8, query);
|
|
1065
|
+
const gated = jevOn(projectRoot);
|
|
982
1066
|
const fact = hits.find((h) => isFactPath(h.path) && typeof h.score === "number" && h.score >= FAILURE_MIN_SCORE);
|
|
983
1067
|
if (!fact) {
|
|
984
1068
|
emit("PostToolUseFailure", "");
|
|
985
1069
|
return;
|
|
986
1070
|
}
|
|
1071
|
+
if (gated && !(await jevAllows(projectRoot, command, error, fact, started))) {
|
|
1072
|
+
emit("PostToolUseFailure", "");
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
987
1075
|
|
|
988
1076
|
// the same fact at most once per session: a repeat failure the agent already saw the fact for adds only noise
|
|
989
1077
|
const { resolvedRead, writePath } = statePath(projectRoot, "failure-shown");
|
|
@@ -1006,8 +1094,10 @@ async function handleFailure(projectRoot, sources, cliBin, input, sessionId) {
|
|
|
1006
1094
|
}
|
|
1007
1095
|
|
|
1008
1096
|
const lines = [
|
|
1009
|
-
`<vault-fact source="tool-failure" score="${fact.score.toFixed(2)}">`,
|
|
1010
|
-
|
|
1097
|
+
`<vault-fact source="tool-failure" score="${fact.score.toFixed(2)}"${gated ? ` checked="jev"` : ""}>`,
|
|
1098
|
+
gated
|
|
1099
|
+
? `A knowledge-base fact may explain this failure (checked: not ruled out). Check it applies before acting on it.`
|
|
1100
|
+
: `A knowledge-base fact may explain this failure. Check it applies before acting on it (measured: about 6 in 10 of these are on target).`,
|
|
1011
1101
|
`- \`${fact.path}\`${fact.source ? ` (${fact.source})` : ""}`,
|
|
1012
1102
|
];
|
|
1013
1103
|
if (fact.snippet) lines.push(` > ${(await redactText(String(fact.snippet))).replace(/\s+/g, " ").slice(0, 240)}`);
|
|
@@ -1066,8 +1156,16 @@ async function main() {
|
|
|
1066
1156
|
const projectRoot = input.cwd || process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
1067
1157
|
const sessionId = typeof input.session_id === "string" ? input.session_id : undefined;
|
|
1068
1158
|
debug(`event=${eventName} projectRoot=${projectRoot}`);
|
|
1159
|
+
const started = Date.now();
|
|
1160
|
+
// recursion guard: inside a skill-loop claude run every semantic-memory hook leaves at once
|
|
1161
|
+
if (process.env.SEMANTIC_MEMORY_REVIEWER === "1") {
|
|
1162
|
+
emitNoop(eventName);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
await loadFeatures();
|
|
1166
|
+
if (featuresMod?.featureOn("skill_loop", projectRoot)) await recordSkillEvidence(eventName, projectRoot, input);
|
|
1069
1167
|
// opt-in, and registered for every failed tool call: leave before any lookup when off
|
|
1070
|
-
if (eventName === "PostToolUseFailure" && !FAILURE_HOOK_ON()) {
|
|
1168
|
+
if (eventName === "PostToolUseFailure" && !FAILURE_HOOK_ON(projectRoot)) {
|
|
1071
1169
|
emitNoop(eventName);
|
|
1072
1170
|
return;
|
|
1073
1171
|
}
|
|
@@ -1091,7 +1189,7 @@ async function main() {
|
|
|
1091
1189
|
if (eventName === "UserPromptSubmit") {
|
|
1092
1190
|
await handlePrompt(projectRoot, sources, cliBin, input);
|
|
1093
1191
|
} else if (eventName === "PostToolUseFailure") {
|
|
1094
|
-
await handleFailure(projectRoot, sources, cliBin, input, sessionId);
|
|
1192
|
+
await handleFailure(projectRoot, sources, cliBin, input, sessionId, started);
|
|
1095
1193
|
} else if (eventName === "Stop") {
|
|
1096
1194
|
await handleStop(projectRoot, sessionId);
|
|
1097
1195
|
} else {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theglitchking/semantic-memory",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Unified memory-layer plugin for Claude Code. One MCP server, multiple typed corpora (vault, code, plans, docs, research, project-map), unified cross-corpus knowledge graph, conditional tool registration, in-process drift detection, and human↔code translation. Rebrand of @theglitchking/semantic-sidekick at v1.0 — see CHANGELOG for migration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/core/index.js",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"README.md",
|
|
30
30
|
"CHANGELOG.md",
|
|
31
31
|
"ROADMAP.md",
|
|
32
|
-
"LICENSE"
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"THIRD_PARTY_NOTICES.md",
|
|
34
|
+
"LICENSES"
|
|
33
35
|
],
|
|
34
36
|
"scripts": {
|
|
35
37
|
"build": "tsup",
|
package/dist/chunk-5F5ZIAW2.js
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
// src/core/failure-eval.ts
|
|
2
|
-
var key = (c, p) => `${c}\0${p}`;
|
|
3
|
-
function evaluateFailures(results, judgments, opts = {}) {
|
|
4
|
-
const target = opts.targetPrecision ?? 0.95;
|
|
5
|
-
const minJudged = opts.minJudged ?? 20;
|
|
6
|
-
const thresholds = opts.thresholds ?? Array.from({ length: 17 }, (_, i) => Math.round((0.2 + i * 0.05) * 100) / 100);
|
|
7
|
-
const label = /* @__PURE__ */ new Map();
|
|
8
|
-
const relevantFor = /* @__PURE__ */ new Map();
|
|
9
|
-
for (const j of judgments) {
|
|
10
|
-
label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
11
|
-
if (j.relevant) {
|
|
12
|
-
if (!relevantFor.has(j.case)) relevantFor.set(j.case, /* @__PURE__ */ new Set());
|
|
13
|
-
relevantFor.get(j.case).add(j.path);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
let answerable = 0, rank1 = 0, recallAtK = 0;
|
|
17
|
-
const unjudged = [];
|
|
18
|
-
for (const r of results) {
|
|
19
|
-
const relevant = relevantFor.get(r.case);
|
|
20
|
-
if (relevant && relevant.size > 0) {
|
|
21
|
-
answerable++;
|
|
22
|
-
if (r.hits[0] && relevant.has(r.hits[0].path)) rank1++;
|
|
23
|
-
if (r.hits.some((h) => relevant.has(h.path))) recallAtK++;
|
|
24
|
-
}
|
|
25
|
-
const top = r.hits[0];
|
|
26
|
-
if (top && !label.has(key(r.case, top.path))) unjudged.push({ case: r.case, path: top.path, score: top.score });
|
|
27
|
-
}
|
|
28
|
-
const rows = thresholds.map((threshold) => {
|
|
29
|
-
let injected = 0, judged = 0, correct = 0;
|
|
30
|
-
for (const r of results) {
|
|
31
|
-
const top = r.hits[0];
|
|
32
|
-
if (!top || top.score < threshold) continue;
|
|
33
|
-
injected++;
|
|
34
|
-
const l = label.get(key(r.case, top.path));
|
|
35
|
-
if (l === void 0) continue;
|
|
36
|
-
judged++;
|
|
37
|
-
if (l) correct++;
|
|
38
|
-
}
|
|
39
|
-
return {
|
|
40
|
-
threshold,
|
|
41
|
-
injected,
|
|
42
|
-
judged,
|
|
43
|
-
correct,
|
|
44
|
-
precision: judged > 0 ? correct / judged : null,
|
|
45
|
-
coverage: results.length > 0 ? injected / results.length : 0
|
|
46
|
-
};
|
|
47
|
-
});
|
|
48
|
-
const meets = (row) => row.precision !== null && row.precision >= target;
|
|
49
|
-
const recommended = rows.find(
|
|
50
|
-
(row, i) => row.judged >= minJudged && meets(row) && rows.slice(i + 1).every((r) => r.judged < minJudged || meets(r))
|
|
51
|
-
)?.threshold ?? null;
|
|
52
|
-
return { cases: results.length, answerable, rank1, recallAtK, rows, recommended, unjudged };
|
|
53
|
-
}
|
|
54
|
-
function formatFailureEval(report, label = "") {
|
|
55
|
-
const pct = (n) => n === null ? " - " : `${(n * 100).toFixed(1)}%`.padStart(6);
|
|
56
|
-
const lines = [
|
|
57
|
-
`Failure-lookup eval${label ? ` (${label})` : ""} \u2014 ${report.cases} cases, ${report.answerable} with a known answer`,
|
|
58
|
-
` answer ranked first: ${report.rank1}/${report.answerable} answer anywhere in hits: ${report.recallAtK}/${report.answerable}`,
|
|
59
|
-
"",
|
|
60
|
-
" threshold injected judged correct precision coverage"
|
|
61
|
-
];
|
|
62
|
-
for (const r of report.rows) {
|
|
63
|
-
lines.push(
|
|
64
|
-
` ${r.threshold.toFixed(2).padStart(9)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)}`
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
lines.push("");
|
|
68
|
-
lines.push(
|
|
69
|
-
report.recommended === null ? " recommended threshold: none reaches the target precision with enough judged injections" : ` recommended threshold: ${report.recommended.toFixed(2)}`
|
|
70
|
-
);
|
|
71
|
-
lines.push(` unjudged top hits: ${report.unjudged.length}`);
|
|
72
|
-
return lines.join("\n");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export {
|
|
76
|
-
evaluateFailures,
|
|
77
|
-
formatFailureEval
|
|
78
|
-
};
|
|
79
|
-
//# sourceMappingURL=chunk-5F5ZIAW2.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/failure-eval.ts"],"sourcesContent":["/**\n * Failure-lookup eval (v1.6): given failed tool calls, the hits a search returned for each, and human\n * (or judge) relevance labels, measure how well the top hit answers the failure and pick the score\n * threshold above which injecting it is right often enough. Pure — no I/O, no search.\n */\n\nexport interface FailureCase {\n id: string;\n /** notes folder this failure's repo searches (its docs) */\n notes: string;\n error: string;\n command: string;\n}\n\nexport interface Judgment {\n case: string;\n path: string;\n relevant: boolean;\n}\n\nexport interface CaseResult {\n case: string;\n hits: { path: string; score: number }[];\n}\n\nexport interface ThresholdRow {\n threshold: number;\n /** cases whose top hit scored >= threshold (would be injected) */\n injected: number;\n /** injected cases whose top hit has a label */\n judged: number;\n correct: number;\n /** correct / judged; null when nothing judged */\n precision: number | null;\n /** injected / all cases */\n coverage: number;\n}\n\nexport interface FailureEvalReport {\n cases: number;\n /** cases with at least one relevant path among their labels */\n answerable: number;\n /** answerable cases whose top hit is relevant */\n rank1: number;\n /** answerable cases with a relevant path anywhere in the hits */\n recallAtK: number;\n rows: ThresholdRow[];\n /** lowest threshold reaching the target precision on >= minJudged judged injections; null if none */\n recommended: number | null;\n /** top hits nobody has labelled yet: label these, then re-run */\n unjudged: { case: string; path: string; score: number }[];\n}\n\nconst key = (c: string, p: string) => `${c}\\u0000${p}`;\n\nexport function evaluateFailures(\n results: CaseResult[],\n judgments: Judgment[],\n opts: { targetPrecision?: number; minJudged?: number; thresholds?: number[] } = {}\n): FailureEvalReport {\n const target = opts.targetPrecision ?? 0.95;\n const minJudged = opts.minJudged ?? 20;\n const thresholds = opts.thresholds ?? Array.from({ length: 17 }, (_, i) => Math.round((0.2 + i * 0.05) * 100) / 100);\n\n const label = new Map<string, boolean>();\n const relevantFor = new Map<string, Set<string>>();\n for (const j of judgments) {\n // a positive label wins over a negative one for the same pair\n label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n if (j.relevant) {\n if (!relevantFor.has(j.case)) relevantFor.set(j.case, new Set());\n relevantFor.get(j.case)!.add(j.path);\n }\n }\n\n let answerable = 0, rank1 = 0, recallAtK = 0;\n const unjudged: FailureEvalReport[\"unjudged\"] = [];\n for (const r of results) {\n const relevant = relevantFor.get(r.case);\n if (relevant && relevant.size > 0) {\n answerable++;\n if (r.hits[0] && relevant.has(r.hits[0].path)) rank1++;\n if (r.hits.some((h) => relevant.has(h.path))) recallAtK++;\n }\n const top = r.hits[0];\n if (top && !label.has(key(r.case, top.path))) unjudged.push({ case: r.case, path: top.path, score: top.score });\n }\n\n const rows: ThresholdRow[] = thresholds.map((threshold) => {\n let injected = 0, judged = 0, correct = 0;\n for (const r of results) {\n const top = r.hits[0];\n if (!top || top.score < threshold) continue;\n injected++;\n const l = label.get(key(r.case, top.path));\n if (l === undefined) continue;\n judged++;\n if (l) correct++;\n }\n return {\n threshold,\n injected,\n judged,\n correct,\n precision: judged > 0 ? correct / judged : null,\n coverage: results.length > 0 ? injected / results.length : 0,\n };\n });\n\n // lowest threshold where this row and every higher row with enough judged injections reach the target\n const meets = (row: ThresholdRow) => row.precision !== null && row.precision >= target;\n const recommended =\n rows.find(\n (row, i) => row.judged >= minJudged && meets(row) && rows.slice(i + 1).every((r) => r.judged < minJudged || meets(r))\n )?.threshold ?? null;\n\n return { cases: results.length, answerable, rank1, recallAtK, rows, recommended, unjudged };\n}\n\nexport function formatFailureEval(report: FailureEvalReport, label = \"\"): string {\n const pct = (n: number | null) => (n === null ? \" - \" : `${(n * 100).toFixed(1)}%`.padStart(6));\n const lines = [\n `Failure-lookup eval${label ? ` (${label})` : \"\"} — ${report.cases} cases, ${report.answerable} with a known answer`,\n ` answer ranked first: ${report.rank1}/${report.answerable} answer anywhere in hits: ${report.recallAtK}/${report.answerable}`,\n \"\",\n \" threshold injected judged correct precision coverage\",\n ];\n for (const r of report.rows) {\n lines.push(\n ` ${r.threshold.toFixed(2).padStart(9)} ${String(r.injected).padStart(8)} ${String(r.judged).padStart(6)} ${String(r.correct).padStart(7)} ${pct(r.precision).padStart(9)} ${pct(r.coverage).padStart(8)}`\n );\n }\n lines.push(\"\");\n lines.push(\n report.recommended === null\n ? \" recommended threshold: none reaches the target precision with enough judged injections\"\n : ` recommended threshold: ${report.recommended.toFixed(2)}`\n );\n lines.push(` unjudged top hits: ${report.unjudged.length}`);\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/lint.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { glob } from \"glob\";\nimport matter from \"./matter.js\";\nimport { loadSchema, validateNote, type LintFinding, type VaultSchema } from \"./schema.js\";\nimport { deriveProjectRoot } from \"./session.js\";\nimport { computeDecay, loadDecayConfig, normalizeVerifiedDate } from \"./decay.js\";\nimport { readSelectionLog } from \"./telemetry.js\";\nimport { compileLexicon, findAliasConflicts } from \"./lexicon.js\";\nimport { findSecrets } from \"./redact.js\";\n\nexport interface LintReport {\n schemaPath: string;\n findings: LintFinding[];\n byRule: {\n schema_violations: LintFinding[];\n missing_provenance: LintFinding[];\n stale: LintFinding[];\n broken_links: LintFinding[];\n code_symbols: LintFinding[];\n decay_candidates: LintFinding[];\n alias_conflicts: LintFinding[];\n decoys: LintFinding[];\n /** only present when the opt-in secrets check ran */\n secrets?: LintFinding[];\n };\n counts: {\n errors: number;\n warnings: number;\n filesLinted: number;\n };\n}\n\nexport async function lintVault(\n notesPath: string,\n opts: {\n pathGlob?: string;\n schemaOverride?: VaultSchema;\n todayIso?: string;\n /**\n * When true, run the code-symbol drift check: flag inline-code file-path\n * references in notes that no longer exist under the project root. Opt-in\n * (off by default) so the standard report and the healthcheck slow tier stay\n * quiet and byte-stable. Fails open when the project root is not a code repo.\n */\n checkCodeSymbols?: boolean;\n /** Override the project root for code-symbol resolution (default: derived from notesPath). */\n projectRoot?: string;\n /**\n * When true, run the decay-candidates check: cross-reference the selection\n * log (notes that appeared in recent search results) against each note's\n * current decay multiplier, and flag notes that are frequently retrieved yet\n * heavily decayed (\"relevant but going stale — verify or revise\"). Opt-in;\n * a silent no-op when no selection log exists.\n */\n checkDecayCandidates?: boolean;\n /**\n * When true, run the alias-conflict check: flag lexicon phrases that map to\n * more than one canonical target (a wrong binding misroutes queries). Opt-in;\n * no-op when the vault has no lexicon.\n */\n checkAliasConflicts?: boolean;\n /**\n * When true, run the decoys check: flag notes that were retrieved often (per\n * the selection log) but NEVER cited in an answer. Surfaced for a human to\n * judge — a decoy might be genuine noise OR a note the answers were wrong to\n * skip; usage-feedback ranking (v1.5) never auto-down-ranks on this ambiguous\n * signal (decision Q5). Opt-in; a no-op with no selection log.\n */\n checkDecoys?: boolean;\n /**\n * When true, run the secrets check (v1.5.3): flag notes that contain a secret\n * (API key, token, password, private key — the redactor's precise set). Reports\n * path, line and kind, never the value; nothing is rewritten. Opt-in, so the\n * default report stays byte-stable. A real secret must be rotated: it is in\n * git history if the note was ever committed.\n */\n checkSecrets?: boolean;\n } = {}\n): Promise<LintReport> {\n const schema = opts.schemaOverride ?? (await loadSchema(notesPath));\n const files = await glob(opts.pathGlob ?? \"**/*.md\", {\n cwd: notesPath,\n ignore: [\"node_modules/**\", \".semantic-sidekick-index/**\", \"vault.schema.yml\"],\n });\n\n const findings: LintFinding[] = [];\n // Build known-note set first, to make broken-link detection O(1) per link.\n const knownNames = new Set<string>();\n const rawByPath = new Map<string, string>();\n for (const rel of files) {\n try {\n const raw = await readFile(join(notesPath, rel), \"utf-8\");\n rawByPath.set(rel, raw);\n // Wikilink names match on basename-without-ext (obsidian-style)\n knownNames.add(basename(rel).replace(/\\.md$/, \"\"));\n } catch {\n /* skip */\n }\n }\n\n // Code-symbol drift is opt-in and only meaningful against a real code repo.\n const codeRoot = opts.checkCodeSymbols\n ? resolveCodeRoot(opts.projectRoot ?? deriveProjectRoot(notesPath))\n : null;\n\n for (const [rel, raw] of rawByPath) {\n findings.push(...validateNote(rel, raw, schema, { todayIso: opts.todayIso }));\n findings.push(...findBrokenLinks(rel, raw, knownNames));\n if (codeRoot) findings.push(...findCodeSymbolDrift(rel, raw, codeRoot));\n if (opts.checkSecrets) {\n for (const s of findSecrets(raw)) {\n findings.push({\n path: rel,\n rule: \"secrets\",\n severity: \"error\",\n message: `line ${s.line}: ${s.kind} — rotate it if it is real, then remove it from the note (redaction does not rewrite git history)`,\n });\n }\n }\n }\n\n if (opts.checkDecayCandidates) {\n findings.push(...(await findDecayCandidates(notesPath, rawByPath, opts.todayIso)));\n }\n\n if (opts.checkAliasConflicts) {\n for (const c of findAliasConflicts(await compileLexicon(notesPath))) {\n findings.push({\n path: \"lexicon\",\n rule: \"alias_conflicts\",\n severity: \"warn\",\n message: `phrase \"${c.phrase}\" maps to ${c.canonicals.length} targets: ${c.canonicals.join(\", \")} — pick one`,\n });\n }\n }\n\n if (opts.checkDecoys) {\n findings.push(...(await findDecoys(notesPath, rawByPath)));\n }\n\n const byRule = {\n schema_violations: findings.filter((f) => f.rule === \"schema_violations\"),\n missing_provenance: findings.filter((f) => f.rule === \"missing_provenance\"),\n stale: findings.filter((f) => f.rule === \"stale\"),\n broken_links: findings.filter((f) => f.rule === \"broken_links\"),\n code_symbols: findings.filter((f) => f.rule === \"code_symbols\"),\n decay_candidates: findings.filter((f) => f.rule === \"decay_candidates\"),\n alias_conflicts: findings.filter((f) => f.rule === \"alias_conflicts\"),\n decoys: findings.filter((f) => f.rule === \"decoys\"),\n ...(opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === \"secrets\") } : {}),\n };\n const errors = findings.filter((f) => f.severity === \"error\").length;\n const warnings = findings.filter((f) => f.severity === \"warn\").length;\n\n return {\n schemaPath: join(notesPath, \"vault.schema.yml\"),\n findings,\n byRule,\n counts: { errors, warnings, filesLinted: files.length },\n };\n}\n\nfunction findBrokenLinks(\n relPath: string,\n rawFileContent: string,\n knownNames: Set<string>\n): LintFinding[] {\n const { content } = matter(rawFileContent);\n const findings: LintFinding[] = [];\n // Match [[target]] and [[target|alias]] — skip fenced code and inline code.\n const stripped = content\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`[^`]+`/g, \"\");\n const re = /\\[\\[([^\\]\\|]+)(?:\\|[^\\]]+)?\\]\\]/g;\n const seen = new Set<string>();\n let m: RegExpExecArray | null;\n while ((m = re.exec(stripped)) !== null) {\n const target = m[1].trim().split(\"#\")[0].split(\"/\").pop()!.replace(/\\.md$/, \"\");\n if (seen.has(target)) continue;\n seen.add(target);\n if (!knownNames.has(target)) {\n findings.push({\n path: relPath,\n rule: \"broken_links\",\n severity: \"warn\",\n message: `wikilink [[${target}]] has no matching note`,\n });\n }\n }\n return findings;\n}\n\n/** File extensions we treat as \"this looks like a source/doc file reference\". */\nconst CODE_EXTENSIONS = new Set([\n \"ts\", \"tsx\", \"js\", \"jsx\", \"mjs\", \"cjs\", \"py\", \"rb\", \"go\", \"rs\", \"java\", \"kt\",\n \"c\", \"h\", \"cpp\", \"hpp\", \"cs\", \"php\", \"swift\", \"scala\", \"sh\", \"bash\", \"sql\",\n \"json\", \"yaml\", \"yml\", \"toml\", \"css\", \"scss\", \"html\", \"vue\", \"svelte\", \"md\",\n]);\n\n/**\n * Decide whether `root` is a real code repository worth checking paths against.\n * Fails open (returns null) for non-code roots so the check is a silent no-op\n * outside a project — e.g. a standalone vault, or when babel-fish isn't present.\n */\nfunction resolveCodeRoot(root: string): string | null {\n if (!root || !existsSync(root)) return null;\n const markers = [\"package.json\", \".git\", \"src\", \"lib\", \"pyproject.toml\", \"go.mod\", \"Cargo.toml\"];\n return markers.some((m) => existsSync(join(root, m))) ? root : null;\n}\n\n/**\n * Code-symbol drift: scan a note's inline-code spans for repo-relative file-path\n * references and flag ones whose first path segment IS a real directory in the repo\n * but whose full path no longer exists — i.e. a stale reference to a moved/deleted\n * file. Anchoring on an existing first segment keeps false positives low: paths\n * belonging to other repos (whose top-level dir isn't present here) are skipped.\n *\n * Scope note: this validates *path* references. Fine-grained symbol-name checking\n * (function/class identifiers) needs a real symbol index and is planned for the\n * v1.4 lexicon arc, which will extend this same `code_symbols` rule.\n */\nfunction findCodeSymbolDrift(relPath: string, rawFileContent: string, codeRoot: string): LintFinding[] {\n const { content } = matter(rawFileContent);\n // Inline-code spans only (single backtick). Fenced blocks are illustrative and\n // over-match. Strip fenced blocks first so their backticks don't confuse the span regex.\n const withoutFences = content.replace(/```[\\s\\S]*?```/g, \"\");\n const findings: LintFinding[] = [];\n const seen = new Set<string>();\n const re = /`([^`\\n]+)`/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(withoutFences)) !== null) {\n let token = m[1].trim();\n if (!looksLikeRepoPath(token)) continue;\n token = token.replace(/^\\.\\//, \"\");\n if (seen.has(token)) continue;\n seen.add(token);\n const firstSeg = token.split(\"/\")[0];\n // Only judge paths anchored in a directory that actually exists in this repo.\n if (!existsSync(join(codeRoot, firstSeg))) continue;\n if (!existsSync(join(codeRoot, token))) {\n findings.push({\n path: relPath,\n rule: \"code_symbols\",\n severity: \"warn\",\n message: `inline code path \\`${token}\\` does not exist under the project root (stale reference?)`,\n });\n }\n }\n return findings;\n}\n\nfunction looksLikeRepoPath(token: string): boolean {\n if (!token.includes(\"/\")) return false;\n if (/\\s/.test(token)) return false; // commands, prose\n if (token.includes(\"://\")) return false; // URLs\n if (/[*?\\[\\]{}()<>|]/.test(token)) return false; // globs / shell / placeholders\n if (token.startsWith(\"/\") || token.startsWith(\"~\")) return false; // absolute / home\n const normalized = token.replace(/^\\.\\//, \"\");\n if (normalized.startsWith(\"..\")) return false; // escapes the repo\n const last = normalized.split(\"/\").pop() ?? \"\";\n const dot = last.lastIndexOf(\".\");\n if (dot <= 0) return false; // no extension → likely a dir or a symbol like a/b\n const ext = last.slice(dot + 1).toLowerCase();\n return CODE_EXTENSIONS.has(ext);\n}\n\n/** Multiplier at or below which a frequently-retrieved note is worth surfacing. */\nconst DECAY_CANDIDATE_THRESHOLD = 0.5;\n\n/**\n * Decay-candidates: notes that keep showing up in search results but have decayed.\n * Reads the selection log (search events record the paths each query returned),\n * computes each retrieved note's current decay multiplier from its frontmatter,\n * and flags those at/below the threshold — \"this keeps being relevant but is\n * going stale; verify or revise.\" Index-free (no embedder); a no-op when there's\n * no selection log yet.\n */\nasync function findDecayCandidates(\n notesPath: string,\n rawByPath: Map<string, string>,\n todayIso?: string\n): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n // Count how often each path was retrieved (appeared in a search's results).\n const retrieved = new Map<string, number>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n }\n }\n if (retrieved.size === 0) return [];\n\n const config = loadDecayConfig(notesPath);\n if (!config.enabled) return [];\n const nowMs = todayIso ? Date.parse(todayIso) : undefined;\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n const raw = rawByPath.get(rel);\n if (!raw) continue; // retrieved path no longer in the vault — skip\n const { data: fm } = matter(raw);\n const decay = computeDecay({\n type: typeof fm.type === \"string\" ? fm.type : undefined,\n last_verified: normalizeVerifiedDate(fm.last_verified),\n evergreen: fm.evergreen === true,\n config,\n nowMs,\n });\n if (decay.multiplier <= DECAY_CANDIDATE_THRESHOLD) {\n findings.push({\n path: rel,\n rule: \"decay_candidates\",\n severity: \"warn\",\n message: `retrieved ${count}× recently but decayed to ${decay.multiplier.toFixed(2)} (age ${Math.round(decay.age_days)}d) — verify_note or revise`,\n });\n }\n }\n // Most-retrieved first — the ones worth re-verifying soonest.\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\n/** A note must be retrieved at least this many times before \"never cited\" is signal. */\nconst DECOY_MIN_RETRIEVALS = 3;\n\n/**\n * Decoys (v1.5 Phase 9): notes retrieved ≥ DECOY_MIN_RETRIEVALS times that were\n * NEVER cited. Surfaced only — usage-feedback ranking never auto-down-ranks on\n * this ambiguous signal (a decoy might be genuine noise, or a note the answers\n * were wrong to skip). A human decides; the lint just points.\n */\nasync function findDecoys(notesPath: string, rawByPath: Map<string, string>): Promise<LintFinding[]> {\n const events = await readSelectionLog(notesPath);\n if (events.length === 0) return [];\n\n const retrieved = new Map<string, number>();\n const cited = new Set<string>();\n for (const e of events) {\n if (e.kind === \"search\") {\n for (const r of e.results) retrieved.set(r.path, (retrieved.get(r.path) ?? 0) + 1);\n } else if (e.kind === \"selection\") {\n cited.add(e.note_path);\n }\n }\n\n const findings: LintFinding[] = [];\n for (const [rel, count] of retrieved) {\n if (count < DECOY_MIN_RETRIEVALS) continue;\n if (cited.has(rel)) continue;\n if (!rawByPath.has(rel)) continue; // retrieved path no longer in the vault — skip\n findings.push({\n path: rel,\n rule: \"decoys\",\n severity: \"warn\",\n message: `retrieved ${count}× but never cited — decoy noise, or a note answers wrongly skip? (surfaced, not auto-down-ranked)`,\n });\n }\n findings.sort((a, b) => (retrieved.get(b.path) ?? 0) - (retrieved.get(a.path) ?? 0));\n return findings;\n}\n\nexport function formatLintReport(report: LintReport, opts: { rule?: keyof LintReport[\"byRule\"] } = {}): string {\n const target = (opts.rule ? report.byRule[opts.rule] : report.findings) ?? [];\n if (target.length === 0) {\n return `clean: 0 findings across ${report.counts.filesLinted} files`;\n }\n const lines = [\n `${target.length} finding${target.length === 1 ? \"\" : \"s\"} (errors=${report.counts.errors}, warnings=${report.counts.warnings}, files=${report.counts.filesLinted})`,\n \"\",\n ];\n for (const f of target) {\n lines.push(`[${f.severity}] ${f.rule}: ${f.path} — ${f.message}`);\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/schema.ts","../src/core/schema-default.ts"],"sourcesContent":["import { readFile, writeFile, access } from \"node:fs/promises\";\nimport { constants } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { parse as parseYaml } from \"yaml\";\nimport matter from \"./matter.js\";\nimport { DEFAULT_SCHEMA_YAML } from \"./schema-default.js\";\n\nexport interface VaultSchema {\n version: number;\n provenance_fields: string[];\n status_enum: string[];\n confidence_enum: string[];\n severity_enum: string[];\n types: Record<string, { description?: string; required: string[] }>;\n lint: {\n missing_provenance: { applies_to: string[]; severity: \"warn\" | \"error\" };\n stale: { max_age_days: number; severity: \"warn\" | \"error\" };\n schema_violations: { severity: \"warn\" | \"error\" };\n };\n}\n\nexport type Severity = \"warn\" | \"error\";\n\nexport interface LintFinding {\n path: string;\n rule: \"missing_provenance\" | \"stale\" | \"schema_violations\" | \"broken_links\" | \"code_symbols\" | \"decay_candidates\" | \"alias_conflicts\" | \"decoys\" | \"secrets\";\n severity: Severity;\n message: string;\n}\n\nexport async function loadSchema(vaultPath: string): Promise<VaultSchema> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n let raw: string;\n try {\n await access(schemaPath, constants.R_OK);\n raw = await readFile(schemaPath, \"utf-8\");\n } catch {\n raw = DEFAULT_SCHEMA_YAML;\n }\n const parsed = parseYaml(raw) as VaultSchema;\n return parsed;\n}\n\n/**\n * Bootstrap the schema file in a vault by writing the default if none exists.\n * Used by the `--install-schema` CLI flag. Returns the path that was (or would be) written.\n */\nexport async function installDefaultSchema(vaultPath: string, force = false): Promise<{ written: boolean; path: string }> {\n const schemaPath = join(vaultPath, \"vault.schema.yml\");\n if (!force) {\n try {\n await access(schemaPath, constants.F_OK);\n return { written: false, path: schemaPath };\n } catch {\n // not present → write it\n }\n }\n await writeFile(schemaPath, DEFAULT_SCHEMA_YAML, \"utf-8\");\n return { written: true, path: schemaPath };\n}\n\n/**\n * Validate a single note's frontmatter + content against the schema.\n * Returns findings (possibly empty). Does NOT throw — lint never throws.\n */\nexport function validateNote(\n relativePath: string,\n rawFileContent: string,\n schema: VaultSchema,\n opts: { todayIso?: string } = {}\n): LintFinding[] {\n const findings: LintFinding[] = [];\n const { data: fm } = matter(rawFileContent);\n\n const typeName = typeof fm.type === \"string\" ? fm.type : \"note\";\n const typeDef = schema.types[typeName];\n\n if (!typeDef) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `unknown type \"${typeName}\" — allowed: ${Object.keys(schema.types).join(\", \")}`,\n });\n return findings; // can't validate required fields if type is unknown\n }\n\n // Required field check\n for (const field of typeDef.required) {\n if (fm[field] === undefined || fm[field] === null || fm[field] === \"\") {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `missing required field \"${field}\" for type \"${typeName}\"`,\n });\n }\n }\n\n // Enum checks\n if (fm.status !== undefined && !schema.status_enum.includes(String(fm.status))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `status \"${fm.status}\" not in enum [${schema.status_enum.join(\", \")}]`,\n });\n }\n if (fm.confidence !== undefined && !schema.confidence_enum.includes(String(fm.confidence))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `confidence \"${fm.confidence}\" not in enum [${schema.confidence_enum.join(\", \")}]`,\n });\n }\n if (typeName === \"gotcha\" && fm.severity !== undefined && !schema.severity_enum.includes(String(fm.severity))) {\n findings.push({\n path: relativePath,\n rule: \"schema_violations\",\n severity: schema.lint.schema_violations.severity,\n message: `severity \"${fm.severity}\" not in enum [${schema.severity_enum.join(\", \")}]`,\n });\n }\n\n // Missing provenance\n if (schema.lint.missing_provenance.applies_to.includes(typeName)) {\n const hasSources = Array.isArray(fm.sources) ? fm.sources.length > 0 : typeof fm.sources === \"string\" && fm.sources.length > 0;\n const hasDerived = Array.isArray(fm.derived_from) ? fm.derived_from.length > 0 : typeof fm.derived_from === \"string\" && fm.derived_from.length > 0;\n if (!hasSources && !hasDerived) {\n findings.push({\n path: relativePath,\n rule: \"missing_provenance\",\n severity: schema.lint.missing_provenance.severity,\n message: `no sources or derived_from on ${typeName}`,\n });\n }\n }\n\n // Stale\n if (schema.lint.stale.max_age_days > 0 && fm.last_verified) {\n const today = opts.todayIso ?? new Date().toISOString().slice(0, 10);\n const verified = String(fm.last_verified);\n const ageDays = dayDiff(today, verified);\n if (ageDays !== null && ageDays > schema.lint.stale.max_age_days) {\n findings.push({\n path: relativePath,\n rule: \"stale\",\n severity: schema.lint.stale.severity,\n message: `last_verified ${verified} is ${ageDays} days old (limit ${schema.lint.stale.max_age_days})`,\n });\n }\n }\n\n return findings;\n}\n\nfunction dayDiff(todayIso: string, thenIso: string): number | null {\n const a = Date.parse(todayIso);\n const b = Date.parse(thenIso);\n if (Number.isNaN(a) || Number.isNaN(b)) return null;\n return Math.round((a - b) / 86400000);\n}\n","// Default vault schema shipped with semantic-sidekick.\n// Intentionally minimal — the plan is to grow from pain, not preempt it.\n// Users can override with a vault.schema.yml at the vault root.\n\nexport const DEFAULT_SCHEMA_YAML = `# semantic-sidekick vault schema (minimal starter).\n# Override by placing a vault.schema.yml at the vault root.\n\nversion: 1\n\n# Provenance fields — apply to all types, enforced by lint.find_missing_provenance.\n# sources: URLs or filesystem paths the note was derived from\n# derived_from: wikilinks to other notes this one synthesizes\n# last_verified: ISO date this note was last confirmed true (YYYY-MM-DD).\n# Set at creation and by verify_note; NOT bumped on ordinary edits.\n# Drives confidence decay (see the decay: block below).\n# evergreen: true → the note is pinned against decay while last_verified is\n# within 365 days; after that it decays normally (re-affirm to reset).\n# status: one of: draft | active | stale | deprecated | archived\n# confidence: one of: high | medium | low\nprovenance_fields:\n - sources\n - derived_from\n - last_verified\n - status\n - confidence\n\n# Allowed status values (enforced by schema_violations lint).\nstatus_enum: [draft, active, stale, deprecated, archived]\nconfidence_enum: [high, medium, low]\n\n# Note types. Each type declares its required frontmatter.\n# Any field not listed as required is allowed but not enforced.\ntypes:\n note:\n description: Generic note — default type when \\`type\\` frontmatter is absent\n required: [title, status]\n\n decision:\n description: Architectural or process decision with dated rationale\n required: [title, status, decided_on, decision_maker]\n\n gotcha:\n description: Compressed lesson from a past incident/bug/surprise\n required: [title, status, last_verified, severity]\n\n source:\n description: External reference (paper, blog post, doc) used as provenance\n required: [title, status, source]\n\n alias:\n description: Learned human→artifact vocabulary entry (v1.4 lexicon). Maps the human's phrasing to a concrete path/symbol.\n required: [canonical]\n\n dossier:\n description: Entity-centric living note (v1.5). One per critical component — purpose, failure modes, knobs, incident log, current state. Accretes in place.\n required: [title, status, entity]\n\n profile:\n description: Speaker profile (v1.5). Models how THIS human communicates — severity calibration, chronic omissions, verbosity, shorthand. One human, one profile.\n required: [title, status]\n\n# Severity values for gotcha type.\nseverity_enum: [low, medium, high, critical]\n\n# Lint policy. Each rule has a default severity that pre-commit/CI consumes.\nlint:\n missing_provenance:\n # Triggers when a note type in this list has no sources AND no derived_from.\n applies_to: [note, decision, gotcha]\n severity: warn\n stale:\n # Triggers when last_verified is older than this many days (0 = disabled).\n max_age_days: 180\n severity: warn\n schema_violations:\n # Triggers on missing required fields, unknown type, or enum mismatch.\n severity: error\n\n# Confidence decay (v1.3). Down-weights search results by time since last_verified.\n# Composes multiplicatively with load_priority — it never removes notes, only ranks\n# them lower (floored). Set enabled: false for byte-identical pre-v1.3 ranking.\ndecay:\n enabled: true\n default_half_life_days: 365 # multiplier = 0.5 ^ (age_days / half_life)\n per_type:\n decision: 365\n note: 365\n gotcha: 180\n source: null # null = never decays\n proposal: 14\n floor: 0.1 # notes never drop below this multiplier\n hotness_boost:\n enabled: false # extend half-life for heavily-backlinked notes (opt-in)\n cap: 2.0\n\n# Path-class ranking (v1.4). Down-weights whole regions of the vault by path glob\n# (composes multiplicatively with decay + load_priority). First matching rule wins.\n# The default down-weights archived notes so retired docs stop dominating results.\npath_class:\n enabled: true\n rules:\n - glob: \"archive/**\"\n multiplier: 0.3\n\n# Usage-feedback ranking (v1.5). Boosts notes that get CITED after retrieval (per\n# the local selection log) so load-bearing knowledge floats up. Bounded by cap to\n# stop a feedback runaway; composes multiplicatively with decay + path_class +\n# load_priority. A no-op until the selection log has citations. The complementary\n# signal (retrieved-but-never-cited \"decoys\") is surfaced by lint_vault, NEVER\n# auto-down-ranked. Set enabled: false for byte-identical pre-v1.5 ranking.\nusage_boost:\n enabled: true\n cap: 1.5 # max multiplier a note can earn from citations\n per_citation: 0.1 # multiplier += per_citation × citation_count, then capped\n\n# Token-frugal tool surface (v1.4). When enabled, the server registers a reduced\n# tool set in modes that don't need the full surface (outage-silence → search/read\n# only). DEFAULT OFF — mid-session mode switches don't re-register, so opt in and\n# dogfood before relying on it.\ntools:\n conditional: false\n\n# Selection-logging telemetry (v1.3.1). Records what search returned and which\n# note the answer used, to a LOCAL append-only JSONL at\n# .claude/.semantic-memory/selection.jsonl (gitignored). Never leaves the machine.\n# Feeds the decay_candidates lint and (later) usage-feedback ranking. Set\n# enabled: false to disable all logging.\ntelemetry:\n enabled: true\n`;\n"],"mappings":";;;;;AAAA,SAAS,UAAU,WAAW,cAAc;AAC5C,SAAS,iBAAiB;AAC1B,SAAS,YAAY;AACrB,SAAS,SAAS,iBAAiB;;;ACC5B,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;AD0BnC,eAAsB,WAAW,WAAyC;AACxE,QAAM,aAAa,KAAK,WAAW,kBAAkB;AACrD,MAAI;AACJ,MAAI;AACF,UAAM,OAAO,YAAY,UAAU,IAAI;AACvC,UAAM,MAAM,SAAS,YAAY,OAAO;AAAA,EAC1C,QAAQ;AACN,UAAM;AAAA,EACR;AACA,QAAM,SAAS,UAAU,GAAG;AAC5B,SAAO;AACT;AAMA,eAAsB,qBAAqB,WAAmB,QAAQ,OAAoD;AACxH,QAAM,aAAa,KAAK,WAAW,kBAAkB;AACrD,MAAI,CAAC,OAAO;AACV,QAAI;AACF,YAAM,OAAO,YAAY,UAAU,IAAI;AACvC,aAAO,EAAE,SAAS,OAAO,MAAM,WAAW;AAAA,IAC5C,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,UAAU,YAAY,qBAAqB,OAAO;AACxD,SAAO,EAAE,SAAS,MAAM,MAAM,WAAW;AAC3C;AAMO,SAAS,aACd,cACA,gBACA,QACA,OAA8B,CAAC,GAChB;AACf,QAAM,WAA0B,CAAC;AACjC,QAAM,EAAE,MAAM,GAAG,IAAI,eAAO,cAAc;AAE1C,QAAM,WAAW,OAAO,GAAG,SAAS,WAAW,GAAG,OAAO;AACzD,QAAM,UAAU,OAAO,MAAM,QAAQ;AAErC,MAAI,CAAC,SAAS;AACZ,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,iBAAiB,QAAQ,qBAAgB,OAAO,KAAK,OAAO,KAAK,EAAE,KAAK,IAAI,CAAC;AAAA,IACxF,CAAC;AACD,WAAO;AAAA,EACT;AAGA,aAAW,SAAS,QAAQ,UAAU;AACpC,QAAI,GAAG,KAAK,MAAM,UAAa,GAAG,KAAK,MAAM,QAAQ,GAAG,KAAK,MAAM,IAAI;AACrE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,QACxC,SAAS,2BAA2B,KAAK,eAAe,QAAQ;AAAA,MAClE,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,GAAG,WAAW,UAAa,CAAC,OAAO,YAAY,SAAS,OAAO,GAAG,MAAM,CAAC,GAAG;AAC9E,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,WAAW,GAAG,MAAM,kBAAkB,OAAO,YAAY,KAAK,IAAI,CAAC;AAAA,IAC9E,CAAC;AAAA,EACH;AACA,MAAI,GAAG,eAAe,UAAa,CAAC,OAAO,gBAAgB,SAAS,OAAO,GAAG,UAAU,CAAC,GAAG;AAC1F,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,eAAe,GAAG,UAAU,kBAAkB,OAAO,gBAAgB,KAAK,IAAI,CAAC;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,MAAI,aAAa,YAAY,GAAG,aAAa,UAAa,CAAC,OAAO,cAAc,SAAS,OAAO,GAAG,QAAQ,CAAC,GAAG;AAC7G,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,MAAM;AAAA,MACN,UAAU,OAAO,KAAK,kBAAkB;AAAA,MACxC,SAAS,aAAa,GAAG,QAAQ,kBAAkB,OAAO,cAAc,KAAK,IAAI,CAAC;AAAA,IACpF,CAAC;AAAA,EACH;AAGA,MAAI,OAAO,KAAK,mBAAmB,WAAW,SAAS,QAAQ,GAAG;AAChE,UAAM,aAAa,MAAM,QAAQ,GAAG,OAAO,IAAI,GAAG,QAAQ,SAAS,IAAI,OAAO,GAAG,YAAY,YAAY,GAAG,QAAQ,SAAS;AAC7H,UAAM,aAAa,MAAM,QAAQ,GAAG,YAAY,IAAI,GAAG,aAAa,SAAS,IAAI,OAAO,GAAG,iBAAiB,YAAY,GAAG,aAAa,SAAS;AACjJ,QAAI,CAAC,cAAc,CAAC,YAAY;AAC9B,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,mBAAmB;AAAA,QACzC,SAAS,iCAAiC,QAAQ;AAAA,MACpD,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,OAAO,KAAK,MAAM,eAAe,KAAK,GAAG,eAAe;AAC1D,UAAM,QAAQ,KAAK,aAAY,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE;AACnE,UAAM,WAAW,OAAO,GAAG,aAAa;AACxC,UAAM,UAAU,QAAQ,OAAO,QAAQ;AACvC,QAAI,YAAY,QAAQ,UAAU,OAAO,KAAK,MAAM,cAAc;AAChE,eAAS,KAAK;AAAA,QACZ,MAAM;AAAA,QACN,MAAM;AAAA,QACN,UAAU,OAAO,KAAK,MAAM;AAAA,QAC5B,SAAS,iBAAiB,QAAQ,OAAO,OAAO,oBAAoB,OAAO,KAAK,MAAM,YAAY;AAAA,MACpG,CAAC;AAAA,IACH;AAAA,EACF;AAEA,SAAO;AACT;AAEA,SAAS,QAAQ,UAAkB,SAAgC;AACjE,QAAM,IAAI,KAAK,MAAM,QAAQ;AAC7B,QAAM,IAAI,KAAK,MAAM,OAAO;AAC5B,MAAI,OAAO,MAAM,CAAC,KAAK,OAAO,MAAM,CAAC,EAAG,QAAO;AAC/C,SAAO,KAAK,OAAO,IAAI,KAAK,KAAQ;AACtC;","names":[]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|