@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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-review.ts","../src/core/skill-review-prompt.ts"],"sourcesContent":["/**\n * The skill loop's reviewer (v1.7): one finished session in, zero to two skill drafts out.\n *\n * 1. A brief: the session's prompts, tool calls and errors (from the transcript, read fail-soft: its format\n * is internal to Claude Code), plus the evidence logs, the skills this repo loads and its KB fact ids.\n * Redacted (broad) and capped before the model sees it.\n * 2. A fenced `claude -p` (Read / Grep / Glob on the repo only; see claude-run.ts) answers JSON.\n * 3. Each draft is rendered, redacted, linted (skill-schema.ts) and written to\n * .claude/.semantic-memory/skill-drafts/<id>/ — or rejected with its reasons. Nothing is written anywhere\n * else; landing a draft is a person's call (`skill-loop review`).\n */\nimport { createHash } from \"node:crypto\";\nimport { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from \"node:fs\";\nimport { basename, join } from \"node:path\";\nimport { redact } from \"./redact.js\";\nimport { readEvidence, stateDir } from \"./skill-evidence.js\";\nimport { factIds, findSkills, lintSkill, renderSkill, type SkillFile, type SkillLoopMeta, type SkillScope } from \"./skill-schema.js\";\nimport { runClaude } from \"./claude-run.js\";\nimport { REVIEW_PROMPT } from \"./skill-review-prompt.js\";\nimport type { ReviewJob } from \"./skill-queue.js\";\nimport type { ReviewOutcome } from \"./skill-worker.js\";\nimport { decideScope, indexSkill, promotionPeers, type ScopeDecision } from \"./skill-scope.js\";\n\nexport const draftsDir = (repo: string) => join(stateDir(repo), \"skill-drafts\");\nconst BRIEF_MAX = 40_000;\nconst TIMELINE_MAX = 120;\n\nexport interface DraftRecord {\n id: string;\n kind: \"new\" | \"patch\" | \"merge\";\n name: string;\n scope: SkillScope;\n /** patch: the SKILL.md it replaces, and that file's sha256 when the draft was made */\n target?: string;\n base_sha256?: string;\n session_id: string;\n created: string;\n warnings: string[];\n /** directory scope: where it lands (repo-relative subdirectory) */\n directory?: string;\n scope_why?: string;\n /** Jev thinks it holds in any repo */\n global_candidate?: boolean;\n /** other repos with the same lesson: review offers one global skill replacing the copies */\n promotion?: { repo: string; path: string }[];\n /** merge: the SKILL.md files the merged skill replaces (archived when it lands) */\n supersedes?: string[];\n /** filled by skill evals (Phase 14) when they run at approval time */\n evals?: Record<string, unknown>;\n}\n\n// --- the brief ---\n\n/** The session's timeline from its transcript: prompts, tool calls, and the errors they returned. Fail-soft. */\nexport function transcriptTimeline(path: string | null, max = TIMELINE_MAX): string[] {\n if (!path) return [];\n let raw = \"\";\n try {\n if (statSync(path).size > 200 * 1024 * 1024) return [\"(transcript too large to read)\"];\n raw = readFileSync(path, \"utf8\");\n } catch {\n return [];\n }\n const out: string[] = [];\n const calls = new Map<string, string>();\n for (const line of raw.split(\"\\n\")) {\n let e: any;\n try {\n e = JSON.parse(line);\n } catch {\n continue;\n }\n const content = e?.message?.content;\n if (e?.type === \"user\" && typeof content === \"string\" && !e.isMeta) {\n // wrappers (slash-command echoes, task notifications, local command output) are not the user talking\n if (!content.trimStart().startsWith(\"<\")) out.push(`USER: ${content.slice(0, 600)}`);\n } else if (e?.type === \"assistant\" && Array.isArray(content)) {\n for (const c of content) {\n if (c?.type !== \"tool_use\") continue;\n const input = c.input ?? {};\n const what = typeof input.command === \"string\" ? input.command : typeof input.file_path === \"string\" ? input.file_path : typeof input.skill === \"string\" ? input.skill : JSON.stringify(input);\n calls.set(c.id, `${c.name}: ${String(what).slice(0, 300)}`);\n }\n } else if (e?.type === \"user\" && Array.isArray(content)) {\n for (const c of content) {\n if (c?.type !== \"tool_result\") continue;\n const call = calls.get(c.tool_use_id) ?? \"tool\";\n const text = typeof c.content === \"string\" ? c.content : Array.isArray(c.content) ? c.content.map((x: any) => x?.text ?? \"\").join(\" \") : \"\";\n out.push(c.is_error ? `TOOL ${call}\\n → ERROR (${c.tool_use_id}): ${text.slice(0, 500)}` : `TOOL ${call} → ok`);\n }\n }\n }\n // keep the end of long sessions (where the fix landed) and every error\n if (out.length <= max) return out;\n const errors = out.filter((l) => l.includes(\"→ ERROR\")).slice(-Math.floor(max / 2));\n const tail = out.slice(-(max - errors.length)).filter((l) => !errors.includes(l));\n return [`(${out.length - errors.length - tail.length} earlier entries omitted)`, ...errors, ...tail];\n}\n\nfunction topLevel(repo: string): string[] {\n try {\n return readdirSync(repo).filter((f) => !f.startsWith(\".\") || f === \".claude\" || f === \".documentation\").slice(0, 60);\n } catch {\n return [];\n }\n}\n\nexport function buildBrief(job: ReviewJob, skills: SkillFile[] = findSkills(job.repo)): string {\n const repo = job.repo;\n const failures = readEvidence(repo, \"failures\").filter((f) => f.session_id === job.session_id);\n const corrections = readEvidence(repo, \"corrections\").filter((c) => c.session_id === job.session_id);\n const used = new Set(job.evidence.skills_used);\n // free text is redacted (broad); the ids the reviewer must cite back (session, failure ids) are not, or broad\n // redaction would mask them too. The evidence logs were redacted when written.\n const r = (t: string) => redact(t, \"broad\").text;\n const lines = [\n `# Session ${job.session_id}`,\n `Repository: ${basename(repo)} — top level: ${topLevel(repo).join(\", \")}`,\n `Why reviewed: ${job.reason}`,\n \"\",\n \"## Failures (from the evidence log)\",\n ...(failures.length ? failures.map((f) => `- id ${f.id}: \\`${r(String(f.command))}\\`\\n error: ${r(String(f.error).slice(0, 500))}`) : [\"(none)\"]),\n \"\",\n \"## Corrections the user gave\",\n ...(corrections.length ? corrections.map((c, i) => `- correction ${i + 1} (${c.cue}): ${r(String(c.excerpt))}`) : [\"(none)\"]),\n \"\",\n \"## Skills this repository loads\",\n ...(skills.length\n ? skills.map((s) => `- ${s.skill.name || basename(s.dir)} (${s.scope}; learned: ${s.learned}${s.skill.meta?.pinned ? \"; pinned\" : \"\"}${used.has(s.skill.name) ? \"; USED this session\" : \"\"}): ${r(s.skill.description.slice(0, 200))}`)\n : [\"(none)\"]),\n ...skills\n .filter((s) => s.learned && used.has(s.skill.name))\n .flatMap((s) => [\"\", `### Current text of the learned skill ${s.skill.name} (used this session)`, r(s.skill.body.slice(0, 4000))]),\n \"\",\n \"## Knowledge-base fact ids (for cites)\",\n [...factIds(repo)].slice(0, 200).join(\", \") || \"(none)\",\n \"\",\n \"## Timeline (prompts, tool calls, errors)\",\n ...transcriptTimeline(job.transcript_path).map(r),\n ];\n let brief = lines.join(\"\\n\");\n if (brief.length > BRIEF_MAX) brief = brief.slice(0, BRIEF_MAX) + \"\\n(brief cut at its size cap)\";\n return brief;\n}\n\n// --- the answer ---\n\nexport interface ReviewerDraft {\n kind: \"new\" | \"patch\";\n target?: string;\n name: string;\n description: string;\n scope: string;\n applies_to?: { paths?: string[]; commands?: string[] };\n triggers?: string[];\n provenance?: { sessions?: string[]; failures?: string[]; corrections?: string[] };\n cites?: string[];\n body: string;\n}\n\n/** The JSON object in the reviewer's answer (bare, or inside a ```json fence). */\nexport function parseReviewerJson(text: string): { drafts: ReviewerDraft[]; none_reason?: string } | null {\n const fenced = text.match(/```(?:json)?\\s*([\\s\\S]*?)```/);\n const candidates = [fenced?.[1], text, text.slice(text.indexOf(\"{\"), text.lastIndexOf(\"}\") + 1)].filter(Boolean) as string[];\n for (const c of candidates) {\n try {\n const v = JSON.parse(c.trim());\n if (v && Array.isArray(v.drafts)) return v;\n } catch {}\n }\n return null;\n}\n\n// a placeholder the model copied from redacted text is not evidence\nconst strs = (v: unknown) => (Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\" && !x.includes(\"[REDACTED]\")) : []);\nconst sha256 = (s: string) => createHash(\"sha256\").update(s).digest(\"hex\");\n\nexport type ScopeResolver = (d: ReviewerDraft) => Promise<ScopeDecision>;\n\n/**\n * Turn one reviewer draft into a draft folder, or say why not. `resolveScope` decides the final scope\n * (skill-scope.ts: deterministic rules, then Jev); the reviewer's own choice is never trusted for it.\n */\nexport async function materializeDraft(\n repo: string,\n job: ReviewJob,\n d: ReviewerDraft,\n skills: SkillFile[],\n resolveScope: ScopeResolver = (x) => decideScope(repo, x),\n env = process.env,\n merge?: { supersedes: string[] },\n): Promise<{ id?: string; rejected?: string }> {\n if (!d || typeof d.name !== \"string\" || typeof d.body !== \"string\" || typeof d.description !== \"string\") return { rejected: \"malformed draft\" };\n let target: SkillFile | undefined;\n if (d.kind === \"patch\") {\n target = skills.find((s) => s.skill.name === d.target);\n if (!target) return { rejected: `patch target \"${d.target}\" not found` };\n if (!target.learned) return { rejected: `\"${d.target}\" was not written by the skill loop: never patched` };\n if (target.skill.meta?.pinned) return { rejected: `\"${d.target}\" is pinned` };\n }\n const prov = {\n sessions: [...new Set([job.session_id, ...strs(d.provenance?.sessions)])],\n failures: strs(d.provenance?.failures),\n corrections: strs(d.provenance?.corrections),\n };\n if (prov.failures.length + prov.corrections.length === 0 && job.evidence.failures.length + job.evidence.corrections === 0) {\n return { rejected: \"no failure or correction behind it\" };\n }\n // a patch keeps its skill's scope; a new skill's comes from the resolver\n const decision: ScopeDecision = target\n ? { scope: target.scope === \"directory\" ? \"directory\" : \"project\", globalCandidate: false, why: \"patch: the skill's own scope\" }\n : await resolveScope(d);\n const scope: SkillScope = target?.scope ?? decision.scope;\n const paths = strs(d.applies_to?.paths);\n if (decision.directory && !paths.includes(decision.directory)) paths.unshift(decision.directory);\n const name = d.kind === \"patch\" ? target!.skill.name : d.name;\n const meta: SkillLoopMeta = {\n schema: 1,\n scope,\n status: \"draft\",\n pinned: false,\n triggers: strs(d.triggers),\n applies_to: { paths, commands: strs(d.applies_to?.commands) },\n provenance: prov,\n cites: strs(d.cites),\n supersedes: merge ? merge.supersedes.map((p) => basename(join(p, \"..\"))) : (target?.skill.meta?.supersedes ?? []),\n };\n // precise: a skill is prose and code, broad would mangle ordinary identifiers\n const text = redact(renderSkill({ name, description: d.description, meta, body: d.body }), \"precise\").text;\n const findings = lintSkill(join(draftsDir(repo), name, \"SKILL.md\"), text, { projectRoot: repo, others: skills.filter((s) => s.path !== target?.path) }, name);\n const errors = findings.filter((f) => f.severity === \"error\");\n if (errors.length) return { rejected: `lint: ${errors.map((f) => `${f.rule}: ${f.message}`).join(\"; \")}` };\n const id = `${name}-${sha256(text).slice(0, 8)}`;\n const dir = join(draftsDir(repo), id);\n mkdirSync(dir, { recursive: true });\n writeFileSync(join(dir, \"SKILL.md\"), text);\n const record: DraftRecord = {\n id,\n kind: merge ? \"merge\" : d.kind === \"patch\" ? \"patch\" : \"new\",\n name,\n scope,\n ...(target ? { target: target.path, base_sha256: sha256(readFileSync(target.path, \"utf8\")) } : {}),\n session_id: job.session_id,\n created: new Date().toISOString(),\n warnings: findings.filter((f) => f.severity === \"warn\").map((f) => `${f.rule}: ${f.message}`),\n ...(decision.directory ? { directory: decision.directory } : {}),\n scope_why: decision.why,\n ...(decision.globalCandidate ? { global_candidate: true } : {}),\n ...(merge ? { supersedes: merge.supersedes } : {}),\n };\n if (!target && !merge) {\n const peers = promotionPeers(repo, name, env);\n if (peers.length) record.promotion = peers.map((p) => ({ repo: p.repo, path: p.path }));\n indexSkill({ repo, name, path: join(dir, \"SKILL.md\"), status: \"draft\", ...(decision.globalCandidate ? { global_candidate: true } : {}) }, env);\n }\n writeFileSync(join(dir, \"draft.json\"), JSON.stringify(record, null, 1));\n return { id };\n}\n\nexport function listDrafts(repo: string): { dir: string; record: DraftRecord }[] {\n let ids: string[] = [];\n try {\n ids = readdirSync(draftsDir(repo));\n } catch {\n return [];\n }\n const out: { dir: string; record: DraftRecord }[] = [];\n for (const id of ids) {\n const dir = join(draftsDir(repo), id);\n try {\n if (existsSync(join(dir, \"SKILL.md\"))) out.push({ dir, record: JSON.parse(readFileSync(join(dir, \"draft.json\"), \"utf8\")) });\n } catch {}\n }\n return out.sort((a, b) => a.record.created.localeCompare(b.record.created));\n}\n\nexport interface ReviewOptions {\n env?: NodeJS.ProcessEnv;\n resolveScope?: ScopeResolver;\n maxTurns?: number;\n}\n\n/** Review one queued session (the worker's review function). */\nexport async function reviewSession(job: ReviewJob, opts: ReviewOptions = {}): Promise<ReviewOutcome> {\n const skills = findSkills(job.repo);\n const brief = buildBrief(job, skills);\n const run = await runClaude({\n prompt: `${REVIEW_PROMPT}\\n\\n---\\n\\n${brief}`,\n cwd: job.repo,\n tools: [\"Read\", \"Grep\", \"Glob\"],\n maxTurns: opts.maxTurns ?? 8,\n output: \"json\",\n timeoutMs: 10 * 60_000,\n env: opts.env,\n });\n if (!run.ok && !run.result) throw new Error(run.error ?? \"reviewer run failed\");\n const answer = parseReviewerJson(run.result);\n if (!answer) return { drafts: [], rejected: [{ reason: \"reviewer answer was not JSON\" }], costUsd: run.costUsd };\n const drafts: string[] = [];\n const rejected: { reason: string }[] = [];\n for (const d of answer.drafts.slice(0, 2)) {\n const env = opts.env ?? process.env;\n const r = await materializeDraft(job.repo, job, d, skills, opts.resolveScope ?? ((x) => decideScope(job.repo, x, env)), env);\n if (r.id) drafts.push(r.id);\n else rejected.push({ reason: `${d?.name ?? \"?\"}: ${r.rejected}` });\n }\n return { drafts, rejected, costUsd: run.costUsd, note: answer.drafts.length ? undefined : answer.none_reason?.slice(0, 200) };\n}\n","/**\n * The skill loop reviewer's fixed instructions (v1.7). The brief (what happened in one session) follows\n * them on stdin. The reviewer can read the repo (Read / Grep / Glob) and nothing else; it answers with\n * JSON, and the worker — not the model — writes drafts, after redaction and lint.\n */\nexport const REVIEW_PROMPT = `You review one finished Claude Code session in this repository and decide whether it taught a\nreusable procedure worth saving as a skill — a short, step-by-step playbook a future session should follow.\n\nDraft only from the evidence in the brief below: a command that failed and the fix that worked, a correction\nthe user gave, or a learned skill that was used and then failed or was corrected. Never invent steps, commands,\npaths or facts that the brief or the repository does not show. Most sessions teach nothing reusable: then\nanswer with no drafts. A draft is worth it only when the same situation is likely to come up again here.\n\nKinds of draft:\n- \"new\": a procedure no existing skill covers. Check the existing skills in the brief first.\n- \"patch\": an existing learned skill (listed with \"learned: true\") was used in this session and then failed or\n was corrected: give its full revised body. Never patch a skill that is pinned or not learned.\n\nEach draft:\n- name: kebab-case, specific (\"restart-vite-dev-server\", not \"dev-helper\").\n- description: what it does and when to use it, one or two sentences, starting with the action; include\n \"Use when ...\" with the literal error, command or request that should load it. Under 300 characters.\n- scope: \"project\" (about this repository) or \"directory\" (only about one subdirectory; give it in applies_to.paths).\n- applies_to: { paths: [repo-relative paths that exist], commands: [program names the steps run] }.\n- triggers: the literal error strings, commands or user phrases from the brief that this skill answers.\n- provenance: { sessions: [the session id], failures: [failure ids from the brief it came from], corrections: [correction\n numbers from the brief, as strings] } — at least one failure or correction.\n- cites: knowledge-base fact ids from the brief's list that the steps rely on (only ids that are listed).\n- body: markdown with exactly these sections, in order:\n ## When to use — the situation, in two or three lines.\n ## Steps — numbered, concrete commands or actions as the session finally did them.\n ## Pitfalls — what went wrong in the session and why; each line names the failure id or fact id it comes from.\n ## Verify — one command whose exit code proves it worked, in a fenced code block; or, if none exists, one\n sentence a reviewer can check.\n\nHard rules:\n- No secrets, tokens, passwords, keys, personal data, customer data, names of people or money amounts, even if\n the brief shows them. Use placeholders like <token>.\n- At most 2 drafts. No duplicates of existing skills.\n- Do not write or change any file. You may read files in this repository to get paths and commands right.\n\nAnswer with one JSON object and nothing else:\n{\"drafts\": [{\"kind\": \"new\" | \"patch\", \"target\": \"<existing skill name, patch only>\", \"name\": \"...\",\n \"description\": \"...\", \"scope\": \"project\" | \"directory\", \"applies_to\": {\"paths\": [], \"commands\": []},\n \"triggers\": [], \"provenance\": {\"sessions\": [], \"failures\": [], \"corrections\": []}, \"cites\": [], \"body\": \"...\"}],\n \"none_reason\": \"<when drafts is empty: one line on why>\"}\n`;\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,177 @@
|
|
|
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
|
+
var meetsBar = (picks, wrong) => picks >= 40 && wrong <= 2 && (picks - wrong) / picks >= 0.95;
|
|
75
|
+
function calibrateJudge(labels, jev, bar = 0.95) {
|
|
76
|
+
const label = /* @__PURE__ */ new Map();
|
|
77
|
+
for (const j of labels) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
78
|
+
const relevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };
|
|
79
|
+
const notRelevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };
|
|
80
|
+
const seen = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const r of jev) {
|
|
82
|
+
const k = key(r.case, r.path);
|
|
83
|
+
const l = label.get(k);
|
|
84
|
+
if (l === void 0 || seen.has(k)) continue;
|
|
85
|
+
seen.add(k);
|
|
86
|
+
const side = l ? relevant : notRelevant;
|
|
87
|
+
side.pairs++;
|
|
88
|
+
if (r.p !== null && r.p >= bar) side.yes++;
|
|
89
|
+
else if (r.p !== null && r.p <= 1 - bar + 1e-9) side.no++;
|
|
90
|
+
else side.unsure++;
|
|
91
|
+
}
|
|
92
|
+
const pairs = relevant.pairs + notRelevant.pairs;
|
|
93
|
+
const picks = relevant.yes + relevant.no + notRelevant.yes + notRelevant.no;
|
|
94
|
+
const wrong = relevant.no + notRelevant.yes;
|
|
95
|
+
return {
|
|
96
|
+
pairs,
|
|
97
|
+
relevant,
|
|
98
|
+
notRelevant,
|
|
99
|
+
picks,
|
|
100
|
+
wrong,
|
|
101
|
+
agreement: picks ? (picks - wrong) / picks : null,
|
|
102
|
+
unsureRate: pairs ? (relevant.unsure + notRelevant.unsure) / pairs : 0,
|
|
103
|
+
yesPicks: relevant.yes + notRelevant.yes,
|
|
104
|
+
yesWrong: notRelevant.yes,
|
|
105
|
+
noPicks: relevant.no + notRelevant.no,
|
|
106
|
+
noWrong: relevant.no,
|
|
107
|
+
// both sides separately: a judge that only ever says no would pass a pooled check and still find nothing
|
|
108
|
+
verdict: meetsBar(relevant.yes + notRelevant.yes, notRelevant.yes) && meetsBar(relevant.no + notRelevant.no, relevant.no) ? "jev" : meetsBar(relevant.no + notRelevant.no, relevant.no) ? "hybrid" : "claude"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function evaluateGate(results, judgments, jev, opts = {}) {
|
|
112
|
+
const mode = opts.mode ?? "confirm";
|
|
113
|
+
const thresholds = opts.thresholds ?? [0.3, 0.35, 0.4, 0.5];
|
|
114
|
+
const topK = opts.topK ?? 5;
|
|
115
|
+
const bar = opts.bar ?? 0.95;
|
|
116
|
+
const isCandidate = opts.isCandidate ?? ((p) => /(^|\/)facts\/[^/]+\.md$/.test(p));
|
|
117
|
+
const label = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const j of judgments) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);
|
|
119
|
+
const score = /* @__PURE__ */ new Map();
|
|
120
|
+
for (const r of jev) score.set(key(r.case, r.path), r.p);
|
|
121
|
+
const missing = /* @__PURE__ */ new Map();
|
|
122
|
+
const rows = thresholds.map((threshold) => {
|
|
123
|
+
let candidates = 0, miss = 0, injected = 0, judged = 0, correct = 0;
|
|
124
|
+
for (const r of results) {
|
|
125
|
+
const hit = r.hits.slice(0, topK).find((h) => isCandidate(h.path) && h.score >= threshold);
|
|
126
|
+
if (!hit) continue;
|
|
127
|
+
candidates++;
|
|
128
|
+
const k = key(r.case, hit.path);
|
|
129
|
+
if (!score.has(k)) {
|
|
130
|
+
miss++;
|
|
131
|
+
missing.set(k, { case: r.case, path: hit.path });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const p = score.get(k);
|
|
135
|
+
const inject = mode === "confirm" ? p !== null && p !== void 0 && p >= bar : !(p !== null && p !== void 0 && p <= 1 - bar + 1e-9);
|
|
136
|
+
if (!inject) continue;
|
|
137
|
+
injected++;
|
|
138
|
+
const l = label.get(k);
|
|
139
|
+
if (l === void 0) continue;
|
|
140
|
+
judged++;
|
|
141
|
+
if (l) correct++;
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
threshold,
|
|
145
|
+
candidates,
|
|
146
|
+
missing: miss,
|
|
147
|
+
injected,
|
|
148
|
+
judged,
|
|
149
|
+
correct,
|
|
150
|
+
precision: judged ? correct / judged : null,
|
|
151
|
+
coverage: results.length ? injected / results.length : 0,
|
|
152
|
+
meetsBar: meetsBar(judged, judged - correct)
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
return { rows, missing: [...missing.values()] };
|
|
156
|
+
}
|
|
157
|
+
function formatGate(rows, mode = "confirm") {
|
|
158
|
+
const pct = (n) => n === null ? " - " : `${(n * 100).toFixed(1)}%`;
|
|
159
|
+
const what = mode === "confirm" ? "only when Jev says yes at >= 0.95" : "unless Jev says no at <= 0.05";
|
|
160
|
+
const lines = [`Jev gate, ${mode} (inject the first fact >= T ${what})`, "", " T candidates missing injected judged correct precision coverage bar"];
|
|
161
|
+
for (const r of rows) {
|
|
162
|
+
lines.push(
|
|
163
|
+
` ${r.threshold.toFixed(2)} ${String(r.candidates).padStart(10)} ${String(r.missing).padStart(7)} ${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)} ${r.meetsBar ? "yes" : "no"}`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export {
|
|
170
|
+
evaluateFailures,
|
|
171
|
+
formatFailureEval,
|
|
172
|
+
meetsBar,
|
|
173
|
+
calibrateJudge,
|
|
174
|
+
evaluateGate,
|
|
175
|
+
formatGate
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=chunk-PJXYCCI2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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\n// --- v1.7 jev mode: the judge's agreement with labels, and the failure hook's Jev gate ---\n\n/** jev-loop's adoption bar: right >= 95% of the time over >= 40 picks, with at most 2 wrong. */\nexport const meetsBar = (picks: number, wrong: number) => picks >= 40 && wrong <= 2 && (picks - wrong) / picks >= 0.95;\n\nexport interface JevJudgment {\n case: string;\n path: string;\n p: number | null;\n}\n\nexport interface JudgeCalibration {\n /** labelled pairs Jev answered */\n pairs: number;\n /** label relevant / not relevant: picks Jev made (>= bar yes, <= 1-bar no), how many disagree, unsure */\n relevant: { pairs: number; yes: number; no: number; unsure: number };\n notRelevant: { pairs: number; yes: number; no: number; unsure: number };\n /** every pick (yes or no): agreement with the labels */\n picks: number;\n wrong: number;\n agreement: number | null;\n unsureRate: number;\n /** yes picks alone (the side that would inject) */\n yesPicks: number;\n yesWrong: number;\n /** no picks alone (the side that would filter) */\n noPicks: number;\n noWrong: number;\n /**\n * \"jev\": Jev alone may judge (its yes picks and its no picks each meet the bar);\n * \"hybrid\": its no picks meet the bar — Jev filters, the rest (yes and unsure) go to Claude;\n * \"claude\": neither side does.\n */\n verdict: \"jev\" | \"hybrid\" | \"claude\";\n}\n\nexport function calibrateJudge(labels: Judgment[], jev: JevJudgment[], bar = 0.95): JudgeCalibration {\n const label = new Map<string, boolean>();\n for (const j of labels) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n const relevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };\n const notRelevant = { pairs: 0, yes: 0, no: 0, unsure: 0 };\n const seen = new Set<string>();\n for (const r of jev) {\n const k = key(r.case, r.path);\n const l = label.get(k);\n if (l === undefined || seen.has(k)) continue;\n seen.add(k);\n const side = l ? relevant : notRelevant;\n side.pairs++;\n if (r.p !== null && r.p >= bar) side.yes++;\n else if (r.p !== null && r.p <= 1 - bar + 1e-9) side.no++;\n else side.unsure++;\n }\n const pairs = relevant.pairs + notRelevant.pairs;\n const picks = relevant.yes + relevant.no + notRelevant.yes + notRelevant.no;\n const wrong = relevant.no + notRelevant.yes;\n return {\n pairs,\n relevant,\n notRelevant,\n picks,\n wrong,\n agreement: picks ? (picks - wrong) / picks : null,\n unsureRate: pairs ? (relevant.unsure + notRelevant.unsure) / pairs : 0,\n yesPicks: relevant.yes + notRelevant.yes,\n yesWrong: notRelevant.yes,\n noPicks: relevant.no + notRelevant.no,\n noWrong: relevant.no,\n // both sides separately: a judge that only ever says no would pass a pooled check and still find nothing\n verdict:\n meetsBar(relevant.yes + notRelevant.yes, notRelevant.yes) && meetsBar(relevant.no + notRelevant.no, relevant.no)\n ? \"jev\"\n : meetsBar(relevant.no + notRelevant.no, relevant.no)\n ? \"hybrid\"\n : \"claude\",\n };\n}\n\nexport interface GateRow {\n threshold: number;\n /** cases with a candidate fact (first fact in the top hits scoring >= threshold) */\n candidates: number;\n /** candidates Jev has not answered yet (ask, then re-run) */\n missing: number;\n /** candidates Jev said yes to at >= the bar: would be injected */\n injected: number;\n judged: number;\n correct: number;\n precision: number | null;\n coverage: number;\n meetsBar: boolean;\n}\n\n/**\n * The failure hook's Jev gate, replayed: per threshold T, the candidate is the first fact among the top\n * `topK` hits scoring >= T. `confirm` injects it only when Jev scores it >= the bar; `veto` injects it unless\n * Jev scores it <= 1 - bar (a confident no). Precision is measured on the injected candidates that carry a label.\n */\nexport function evaluateGate(\n results: CaseResult[],\n judgments: Judgment[],\n jev: JevJudgment[],\n opts: { thresholds?: number[]; topK?: number; bar?: number; isCandidate?: (path: string) => boolean; mode?: \"confirm\" | \"veto\" } = {},\n): { rows: GateRow[]; missing: { case: string; path: string }[] } {\n const mode = opts.mode ?? \"confirm\";\n const thresholds = opts.thresholds ?? [0.3, 0.35, 0.4, 0.5];\n const topK = opts.topK ?? 5;\n const bar = opts.bar ?? 0.95;\n const isCandidate = opts.isCandidate ?? ((p: string) => /(^|\\/)facts\\/[^/]+\\.md$/.test(p));\n const label = new Map<string, boolean>();\n for (const j of judgments) label.set(key(j.case, j.path), (label.get(key(j.case, j.path)) ?? false) || j.relevant);\n const score = new Map<string, number | null>();\n for (const r of jev) score.set(key(r.case, r.path), r.p);\n const missing = new Map<string, { case: string; path: string }>();\n const rows = thresholds.map((threshold) => {\n let candidates = 0, miss = 0, injected = 0, judged = 0, correct = 0;\n for (const r of results) {\n const hit = r.hits.slice(0, topK).find((h) => isCandidate(h.path) && h.score >= threshold);\n if (!hit) continue;\n candidates++;\n const k = key(r.case, hit.path);\n if (!score.has(k)) {\n miss++;\n missing.set(k, { case: r.case, path: hit.path });\n continue;\n }\n const p = score.get(k);\n const inject = mode === \"confirm\" ? p !== null && p !== undefined && p >= bar : !(p !== null && p !== undefined && p <= 1 - bar + 1e-9);\n if (!inject) continue;\n injected++;\n const l = label.get(k);\n if (l === undefined) continue;\n judged++;\n if (l) correct++;\n }\n return {\n threshold,\n candidates,\n missing: miss,\n injected,\n judged,\n correct,\n precision: judged ? correct / judged : null,\n coverage: results.length ? injected / results.length : 0,\n meetsBar: meetsBar(judged, judged - correct),\n };\n });\n return { rows, missing: [...missing.values()] };\n}\n\nexport function formatGate(rows: GateRow[], mode: \"confirm\" | \"veto\" = \"confirm\"): string {\n const pct = (n: number | null) => (n === null ? \" - \" : `${(n * 100).toFixed(1)}%`);\n const what = mode === \"confirm\" ? \"only when Jev says yes at >= 0.95\" : \"unless Jev says no at <= 0.05\";\n const lines = [`Jev gate, ${mode} (inject the first fact >= T ${what})`, \"\", \" T candidates missing injected judged correct precision coverage bar\"];\n for (const r of rows) {\n lines.push(\n ` ${r.threshold.toFixed(2)} ${String(r.candidates).padStart(10)} ${String(r.missing).padStart(7)} ${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)} ${r.meetsBar ? \"yes\" : \"no\"}`,\n );\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
redact
|
|
3
|
+
} from "./chunk-H2BES6FU.js";
|
|
4
|
+
|
|
5
|
+
// src/core/skill-evidence.ts
|
|
6
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync } from "fs";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
var FILES = { usage: "skill-usage.jsonl", failures: "failures.jsonl", corrections: "corrections.jsonl" };
|
|
9
|
+
var stateDir = (repoRoot2) => join(repoRoot2, ".claude", ".semantic-memory");
|
|
10
|
+
var evidencePath = (repoRoot2, kind) => join(stateDir(repoRoot2), FILES[kind]);
|
|
11
|
+
function repoRoot(cwd) {
|
|
12
|
+
for (let d = cwd; ; d = dirname(d)) {
|
|
13
|
+
if (existsSync(join(d, ".git"))) return d;
|
|
14
|
+
if (dirname(d) === d) return cwd;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
var TEXT_FIELDS = ["command", "error", "excerpt", "prompt"];
|
|
18
|
+
var maxBytes = (env = process.env) => (Number(env.SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB) || 5) * 1024 * 1024;
|
|
19
|
+
function appendEvidence(root, kind, record, env = process.env) {
|
|
20
|
+
const path = evidencePath(root, kind);
|
|
21
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
22
|
+
try {
|
|
23
|
+
if (statSync(path).size > maxBytes(env)) renameSync(path, `${path}.1`);
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
const out = { ts: (/* @__PURE__ */ new Date()).toISOString(), ...record };
|
|
27
|
+
for (const k of TEXT_FIELDS) if (typeof out[k] === "string") out[k] = redact(out[k], "broad").text;
|
|
28
|
+
appendFileSync(path, JSON.stringify(out) + "\n");
|
|
29
|
+
}
|
|
30
|
+
function readEvidence(root, kind) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const p of [`${evidencePath(root, kind)}.1`, evidencePath(root, kind)]) {
|
|
33
|
+
let raw = "";
|
|
34
|
+
try {
|
|
35
|
+
raw = readFileSync(p, "utf8");
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const line of raw.split("\n")) {
|
|
40
|
+
if (!line.trim()) continue;
|
|
41
|
+
try {
|
|
42
|
+
out.push(JSON.parse(line));
|
|
43
|
+
} catch {
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
var cap = (s, n) => String(s ?? "").slice(0, n);
|
|
50
|
+
var failureRecord = (input) => ({
|
|
51
|
+
session_id: input.session_id ?? null,
|
|
52
|
+
id: input.tool_use_id ?? null,
|
|
53
|
+
tool: input.tool_name ?? null,
|
|
54
|
+
command: cap(typeof input.tool_input?.command === "string" ? input.tool_input.command : JSON.stringify(input.tool_input ?? {}), 500),
|
|
55
|
+
error: cap(input.error, 1e3)
|
|
56
|
+
});
|
|
57
|
+
var CORRECTION_CUES = [
|
|
58
|
+
["no", /^\s*(no|nope|nah)\s*([,.!:;]|-|$)/i],
|
|
59
|
+
["wrong", /^\s*(wrong|incorrect)\b|\bthat'?s (wrong|not right|incorrect|not what)\b/i],
|
|
60
|
+
["not-what-i", /\bnot what i (asked|meant|wanted|said)\b/i],
|
|
61
|
+
["instead", /\b(use|do|run|try|call)\b[^.?!]{0,40}\binstead\b|^\s*instead\b/i],
|
|
62
|
+
["dont", /^\s*don'?t\b|\bdon'?t (do|use|run|touch|change|add|delete|remove|edit|call|push|commit)\b/i],
|
|
63
|
+
["you-broke", /\byou (broke|forgot|missed|skipped|ignored)\b/i],
|
|
64
|
+
["stop", /^\s*stop\b/i]
|
|
65
|
+
];
|
|
66
|
+
var stripQuoted = (p) => p.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^>.*$/gm, " ");
|
|
67
|
+
function detectCorrection(prompt) {
|
|
68
|
+
const text = stripQuoted(prompt);
|
|
69
|
+
for (const [cue, re] of CORRECTION_CUES) if (re.test(text)) return cue;
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
function slashSkill(prompt) {
|
|
73
|
+
const m = prompt.match(/^\s*\/([a-z0-9][a-z0-9-]*)(\s|$)/);
|
|
74
|
+
return m ? m[1] : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export {
|
|
78
|
+
stateDir,
|
|
79
|
+
evidencePath,
|
|
80
|
+
repoRoot,
|
|
81
|
+
appendEvidence,
|
|
82
|
+
readEvidence,
|
|
83
|
+
failureRecord,
|
|
84
|
+
detectCorrection,
|
|
85
|
+
slashSkill
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=chunk-R6GNP7DS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-evidence.ts"],"sourcesContent":["/**\n * The skill loop's evidence (v1.7): what the between-session review learns from. Hooks append one line\n * per event when the skill loop is on; nothing here decides anything.\n *\n * .claude/.semantic-memory/skill-usage.jsonl {ts, session_id, skill, via: \"tool\" | \"slash\"}\n * .claude/.semantic-memory/failures.jsonl {ts, session_id, id, tool, command, error}\n * .claude/.semantic-memory/corrections.jsonl {ts, session_id, cue, excerpt}\n *\n * The free-text fields (command, error, excerpt) are redacted (broad: these are logs) before anything is\n * written — ids and names are left alone so records still join up. Each record is capped in size, and each file is\n * rotated to `<file>.1` past SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB (default 5). Standalone for hooks.\n */\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { redact } from \"./redact.js\";\n\nexport type EvidenceKind = \"usage\" | \"failures\" | \"corrections\";\nconst FILES: Record<EvidenceKind, string> = { usage: \"skill-usage.jsonl\", failures: \"failures.jsonl\", corrections: \"corrections.jsonl\" };\n\nexport const stateDir = (repoRoot: string) => join(repoRoot, \".claude\", \".semantic-memory\");\nexport const evidencePath = (repoRoot: string, kind: EvidenceKind) => join(stateDir(repoRoot), FILES[kind]);\n\n/** The repo a hook's cwd belongs to: the nearest ancestor holding .git, else the cwd itself. */\nexport function repoRoot(cwd: string): string {\n for (let d = cwd; ; d = dirname(d)) {\n if (existsSync(join(d, \".git\"))) return d;\n if (dirname(d) === d) return cwd;\n }\n}\n\n// broad redaction would also mask session and tool-use ids (long mixed tokens): only free text is redacted\nconst TEXT_FIELDS = [\"command\", \"error\", \"excerpt\", \"prompt\"];\nconst maxBytes = (env = process.env) => (Number(env.SEMANTIC_MEMORY_SKILL_LOOP_EVIDENCE_MB) || 5) * 1024 * 1024;\n\nexport function appendEvidence(root: string, kind: EvidenceKind, record: Record<string, unknown>, env = process.env): void {\n const path = evidencePath(root, kind);\n mkdirSync(dirname(path), { recursive: true });\n try {\n if (statSync(path).size > maxBytes(env)) renameSync(path, `${path}.1`);\n } catch {}\n const out: Record<string, unknown> = { ts: new Date().toISOString(), ...record };\n for (const k of TEXT_FIELDS) if (typeof out[k] === \"string\") out[k] = redact(out[k] as string, \"broad\").text;\n appendFileSync(path, JSON.stringify(out) + \"\\n\");\n}\n\nexport function readEvidence<T = Record<string, any>>(root: string, kind: EvidenceKind): T[] {\n const out: T[] = [];\n for (const p of [`${evidencePath(root, kind)}.1`, evidencePath(root, kind)]) {\n let raw = \"\";\n try {\n raw = readFileSync(p, \"utf8\");\n } catch {\n continue;\n }\n for (const line of raw.split(\"\\n\")) {\n if (!line.trim()) continue;\n try {\n out.push(JSON.parse(line));\n } catch {}\n }\n }\n return out;\n}\n\nconst cap = (s: unknown, n: number) => String(s ?? \"\").slice(0, n);\n\nexport const failureRecord = (input: { session_id?: string; tool_use_id?: string; tool_name?: string; tool_input?: any; error?: string }) => ({\n session_id: input.session_id ?? null,\n id: input.tool_use_id ?? null,\n tool: input.tool_name ?? null,\n command: cap(typeof input.tool_input?.command === \"string\" ? input.tool_input.command : JSON.stringify(input.tool_input ?? {}), 500),\n error: cap(input.error, 1000),\n});\n\n// A correction: the user telling the agent it got something wrong. Only these cues, so ordinary\n// prompts (\"I don't know why…\", \"no rush\") do not count.\nconst CORRECTION_CUES: [string, RegExp][] = [\n [\"no\", /^\\s*(no|nope|nah)\\s*([,.!:;]|-|$)/i],\n [\"wrong\", /^\\s*(wrong|incorrect)\\b|\\bthat'?s (wrong|not right|incorrect|not what)\\b/i],\n [\"not-what-i\", /\\bnot what i (asked|meant|wanted|said)\\b/i],\n [\"instead\", /\\b(use|do|run|try|call)\\b[^.?!]{0,40}\\binstead\\b|^\\s*instead\\b/i],\n [\"dont\", /^\\s*don'?t\\b|\\bdon'?t (do|use|run|touch|change|add|delete|remove|edit|call|push|commit)\\b/i],\n [\"you-broke\", /\\byou (broke|forgot|missed|skipped|ignored)\\b/i],\n [\"stop\", /^\\s*stop\\b/i],\n];\n\n/** Fenced code, inline code and quoted lines are machinery, not the user talking. */\nconst stripQuoted = (p: string) => p.replace(/```[\\s\\S]*?```/g, \" \").replace(/`[^`]*`/g, \" \").replace(/^>.*$/gm, \" \");\n\nexport function detectCorrection(prompt: string): string | null {\n const text = stripQuoted(prompt);\n for (const [cue, re] of CORRECTION_CUES) if (re.test(text)) return cue;\n return null;\n}\n\n/** `/name …` typed by the user: Claude Code runs the skill without a Skill tool call, so PostToolUse never sees it. */\nexport function slashSkill(prompt: string): string | null {\n const m = prompt.match(/^\\s*\\/([a-z0-9][a-z0-9-]*)(\\s|$)/);\n return m ? m[1] : null;\n}\n"],"mappings":"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","names":["repoRoot"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
stateDir
|
|
3
|
+
} from "./chunk-R6GNP7DS.js";
|
|
4
|
+
|
|
5
|
+
// src/core/skill-ledger.ts
|
|
6
|
+
import { createHash, randomBytes } from "crypto";
|
|
7
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
var ledgerPath = (repo) => join(stateDir(repo), "skill-ledger.jsonl");
|
|
10
|
+
var blobDir = (repo) => join(stateDir(repo), "skill-ledger-blobs");
|
|
11
|
+
var archiveDir = (repo) => join(stateDir(repo), "skills-archive");
|
|
12
|
+
var sha256 = (b) => createHash("sha256").update(b).digest("hex");
|
|
13
|
+
function putBlob(repo, bytes) {
|
|
14
|
+
const h = sha256(bytes);
|
|
15
|
+
const p = join(blobDir(repo), h);
|
|
16
|
+
if (!existsSync(p)) {
|
|
17
|
+
mkdirSync(blobDir(repo), { recursive: true });
|
|
18
|
+
writeFileSync(p, bytes);
|
|
19
|
+
}
|
|
20
|
+
return h;
|
|
21
|
+
}
|
|
22
|
+
var snapshot = (repo, path) => existsSync(path) ? putBlob(repo, readFileSync(path)) : null;
|
|
23
|
+
function applyChange(repo, meta, ops) {
|
|
24
|
+
const files = [];
|
|
25
|
+
for (const op of ops) {
|
|
26
|
+
const before = snapshot(repo, op.path);
|
|
27
|
+
if (op.content === null) {
|
|
28
|
+
if (before) {
|
|
29
|
+
unlinkSync(op.path);
|
|
30
|
+
try {
|
|
31
|
+
rmdirSync(dirname(op.path));
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} else {
|
|
36
|
+
mkdirSync(dirname(op.path), { recursive: true });
|
|
37
|
+
writeFileSync(op.path, op.content);
|
|
38
|
+
}
|
|
39
|
+
files.push({ path: op.path, before, after: snapshot(repo, op.path) });
|
|
40
|
+
}
|
|
41
|
+
const entry = { id: `${Date.now().toString(36)}-${randomBytes(3).toString("hex")}`, ts: (/* @__PURE__ */ new Date()).toISOString(), ...meta, files };
|
|
42
|
+
mkdirSync(stateDir(repo), { recursive: true });
|
|
43
|
+
appendFileSync(ledgerPath(repo), JSON.stringify(entry) + "\n");
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
function readLedger(repo) {
|
|
47
|
+
try {
|
|
48
|
+
return readFileSync(ledgerPath(repo), "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function rollback(repo, id) {
|
|
54
|
+
const entry = readLedger(repo).find((e) => e.id === id);
|
|
55
|
+
if (!entry) throw new Error(`no ledger entry ${id}`);
|
|
56
|
+
for (const f of entry.files) {
|
|
57
|
+
const now = existsSync(f.path) ? sha256(readFileSync(f.path)) : null;
|
|
58
|
+
if (now !== f.after) throw new Error(`${f.path} changed since ${id}: roll back the later change first`);
|
|
59
|
+
}
|
|
60
|
+
const ops = entry.files.map((f) => ({ path: f.path, content: f.before ? readFileSync(join(blobDir(repo), f.before)) : null }));
|
|
61
|
+
return applyChange(repo, { action: "rollback", actor: "user", skill: entry.skill, why: `rollback of ${entry.action} ${id}`, reverts: id }, ops);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export {
|
|
65
|
+
ledgerPath,
|
|
66
|
+
archiveDir,
|
|
67
|
+
applyChange,
|
|
68
|
+
readLedger,
|
|
69
|
+
rollback
|
|
70
|
+
};
|
|
71
|
+
//# sourceMappingURL=chunk-TKHWVISV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/skill-ledger.ts"],"sourcesContent":["/**\n * The skill loop's ledger (v1.7): every change the loop or a person makes to a learned skill — land, patch,\n * stale, archive, restore, promote, merge, reject — as one line with who, why, and each file's bytes before\n * and after (stored once by sha256 in skill-ledger-blobs/). That is what makes a single change reversible:\n * `rollback <id>` puts every file it touched back exactly as it was. Nothing is lost: a file the loop removes\n * (an archive moves a skill out of .claude/skills/) keeps its bytes in the blob store first.\n */\nimport { createHash, randomBytes } from \"node:crypto\";\nimport { appendFileSync, existsSync, mkdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { stateDir } from \"./skill-evidence.js\";\n\nexport type LedgerAction = \"land\" | \"patch\" | \"stale\" | \"archive\" | \"restore\" | \"promote\" | \"merge\" | \"reject\" | \"rollback\";\n\nexport interface FileChange {\n path: string;\n /** sha256 of the bytes before (null: the file did not exist) */\n before: string | null;\n after: string | null;\n}\n\nexport interface LedgerEntry {\n id: string;\n ts: string;\n action: LedgerAction;\n actor: \"user\" | \"curator\" | \"reviewer\";\n skill: string;\n why: string;\n files: FileChange[];\n /** rollback: the entry it reversed */\n reverts?: string;\n}\n\nexport const ledgerPath = (repo: string) => join(stateDir(repo), \"skill-ledger.jsonl\");\nconst blobDir = (repo: string) => join(stateDir(repo), \"skill-ledger-blobs\");\nexport const archiveDir = (repo: string) => join(stateDir(repo), \"skills-archive\");\nconst sha256 = (b: Buffer | string) => createHash(\"sha256\").update(b).digest(\"hex\");\n\nfunction putBlob(repo: string, bytes: Buffer): string {\n const h = sha256(bytes);\n const p = join(blobDir(repo), h);\n if (!existsSync(p)) {\n mkdirSync(blobDir(repo), { recursive: true });\n writeFileSync(p, bytes);\n }\n return h;\n}\n\nconst snapshot = (repo: string, path: string): string | null => (existsSync(path) ? putBlob(repo, readFileSync(path)) : null);\n\n/**\n * Apply file writes and removals as one ledgered change. `ops`: write `content` to `path`; null content removes\n * the file (its bytes are in the blob store already, so rollback restores them) and its folder if now empty.\n */\nexport function applyChange(\n repo: string,\n meta: { action: LedgerAction; actor: LedgerEntry[\"actor\"]; skill: string; why: string; reverts?: string },\n ops: { path: string; content: string | Buffer | null }[],\n): LedgerEntry {\n const files: FileChange[] = [];\n for (const op of ops) {\n const before = snapshot(repo, op.path);\n if (op.content === null) {\n if (before) {\n unlinkSync(op.path);\n try {\n rmdirSync(dirname(op.path)); // only when empty: a skill folder with nothing left would still be listed\n } catch {}\n }\n } else {\n mkdirSync(dirname(op.path), { recursive: true });\n writeFileSync(op.path, op.content);\n }\n files.push({ path: op.path, before, after: snapshot(repo, op.path) });\n }\n const entry: LedgerEntry = { id: `${Date.now().toString(36)}-${randomBytes(3).toString(\"hex\")}`, ts: new Date().toISOString(), ...meta, files };\n mkdirSync(stateDir(repo), { recursive: true });\n appendFileSync(ledgerPath(repo), JSON.stringify(entry) + \"\\n\");\n return entry;\n}\n\nexport function readLedger(repo: string): LedgerEntry[] {\n try {\n return readFileSync(ledgerPath(repo), \"utf8\").split(\"\\n\").filter(Boolean).map((l) => JSON.parse(l));\n } catch {\n return [];\n }\n}\n\n/** Undo one entry: every file it touched goes back to its exact bytes before (or aside, if it did not exist). */\nexport function rollback(repo: string, id: string): LedgerEntry {\n const entry = readLedger(repo).find((e) => e.id === id);\n if (!entry) throw new Error(`no ledger entry ${id}`);\n for (const f of entry.files) {\n const now = existsSync(f.path) ? sha256(readFileSync(f.path)) : null;\n if (now !== f.after) throw new Error(`${f.path} changed since ${id}: roll back the later change first`);\n }\n const ops = entry.files.map((f) => ({ path: f.path, content: f.before ? readFileSync(join(blobDir(repo), f.before)) : null }));\n return applyChange(repo, { action: \"rollback\", actor: \"user\", skill: entry.skill, why: `rollback of ${entry.action} ${id}`, reverts: id }, ops);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
loadSchema,
|
|
3
3
|
validateNote
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Y7H76Q7F.js";
|
|
5
5
|
import {
|
|
6
6
|
computeDecay,
|
|
7
7
|
loadDecayConfig,
|
|
@@ -13,21 +13,21 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
compileLexicon,
|
|
15
15
|
findAliasConflicts
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-7UAVDOI6.js";
|
|
17
17
|
import {
|
|
18
18
|
deriveProjectRoot
|
|
19
19
|
} from "./chunk-EYVFC745.js";
|
|
20
|
-
import {
|
|
21
|
-
findSecrets
|
|
22
|
-
} from "./chunk-H2BES6FU.js";
|
|
23
20
|
import {
|
|
24
21
|
matter_default
|
|
25
22
|
} from "./chunk-J7LYDRML.js";
|
|
23
|
+
import {
|
|
24
|
+
findSecrets
|
|
25
|
+
} from "./chunk-H2BES6FU.js";
|
|
26
26
|
|
|
27
27
|
// src/core/lint.ts
|
|
28
28
|
import { readFile } from "fs/promises";
|
|
29
29
|
import { existsSync } from "fs";
|
|
30
|
-
import { basename, join } from "path";
|
|
30
|
+
import { basename, dirname, join, relative } from "path";
|
|
31
31
|
import { glob } from "glob";
|
|
32
32
|
async function lintVault(notesPath, opts = {}) {
|
|
33
33
|
const schema = opts.schemaOverride ?? await loadSchema(notesPath);
|
|
@@ -78,6 +78,14 @@ async function lintVault(notesPath, opts = {}) {
|
|
|
78
78
|
if (opts.checkDecoys) {
|
|
79
79
|
findings.push(...await findDecoys(notesPath, rawByPath));
|
|
80
80
|
}
|
|
81
|
+
if (opts.checkSkills) {
|
|
82
|
+
const { lintSkills } = await import("./skill-schema-RVB3EHEF.js");
|
|
83
|
+
const derived = deriveProjectRoot(notesPath);
|
|
84
|
+
const root = opts.projectRoot ?? (basename(derived) === ".documentation" ? dirname(derived) : derived);
|
|
85
|
+
for (const f of lintSkills(root).findings) {
|
|
86
|
+
findings.push({ path: relative(root, f.path), rule: "skills", severity: f.severity, message: `${f.rule}: ${f.message}` });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
81
89
|
const byRule = {
|
|
82
90
|
schema_violations: findings.filter((f) => f.rule === "schema_violations"),
|
|
83
91
|
missing_provenance: findings.filter((f) => f.rule === "missing_provenance"),
|
|
@@ -87,7 +95,8 @@ async function lintVault(notesPath, opts = {}) {
|
|
|
87
95
|
decay_candidates: findings.filter((f) => f.rule === "decay_candidates"),
|
|
88
96
|
alias_conflicts: findings.filter((f) => f.rule === "alias_conflicts"),
|
|
89
97
|
decoys: findings.filter((f) => f.rule === "decoys"),
|
|
90
|
-
...opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === "secrets") } : {}
|
|
98
|
+
...opts.checkSecrets ? { secrets: findings.filter((f) => f.rule === "secrets") } : {},
|
|
99
|
+
...opts.checkSkills ? { skills: findings.filter((f) => f.rule === "skills") } : {}
|
|
91
100
|
};
|
|
92
101
|
const errors = findings.filter((f) => f.severity === "error").length;
|
|
93
102
|
const warnings = findings.filter((f) => f.severity === "warn").length;
|
|
@@ -285,4 +294,4 @@ export {
|
|
|
285
294
|
lintVault,
|
|
286
295
|
formatLintReport
|
|
287
296
|
};
|
|
288
|
-
//# sourceMappingURL=chunk-
|
|
297
|
+
//# sourceMappingURL=chunk-VBLCUWT4.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/lint.ts"],"sourcesContent":["import { readFile } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { basename, dirname, join, relative } 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 /** only present when the opt-in skills check ran */\n skills?: 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 /** When true, lint the skill loop's learned skills this repo loads (v1.7): format, evidence, cites, clashes. Opt-in. */\n checkSkills?: 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 if (opts.checkSkills) {\n const { lintSkills } = await import(\"./skill-schema.js\");\n // .claude/.vault → the repo; a docs corpus (.documentation) sits at the repo root too\n const derived = deriveProjectRoot(notesPath);\n const root = opts.projectRoot ?? (basename(derived) === \".documentation\" ? dirname(derived) : derived);\n for (const f of lintSkills(root).findings) {\n findings.push({ path: relative(root, f.path), rule: \"skills\", severity: f.severity, message: `${f.rule}: ${f.message}` });\n }\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 ...(opts.checkSkills ? { skills: findings.filter((f) => f.rule === \"skills\") } : {}),\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":[]}
|