@jmtrin/kevin-core 1.3.0 → 1.5.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.
@@ -50,6 +50,14 @@ export declare const CONTRACT_METRIC_ADDITIONS: readonly {
50
50
  name: string;
51
51
  since: string;
52
52
  }[];
53
+ /**
54
+ * v1.4.0 (K14-006 / plan §4.3) — config keys added after the freeze,
55
+ * each carrying the `since` the deprecation policy requires (C-04).
56
+ */
57
+ export declare const CONTRACT_CONFIG_ADDITIONS: readonly {
58
+ name: string;
59
+ since: string;
60
+ }[];
53
61
  /**
54
62
  * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
55
63
  * not trusted: anything reaching an artifact or a prompt is escaped at
package/dist/contract.js CHANGED
@@ -97,6 +97,30 @@ export const CONTRACT_METRIC_ADDITIONS = [
97
97
  // v1.2.0 (K12-001 / plan §4, D12-??) — surface metrics (no migration this release)
98
98
  { name: "tui_snapshots_flushed", since: "1.2.0" },
99
99
  { name: "tui_actions_invoked", since: "1.2.0" },
100
+ // v1.4.0 (K14-006 / plan §4.3) — MCP bridge metrics (seeded by 013)
101
+ { name: "mcp_requests_total", since: "1.4.0" },
102
+ { name: "mcp_reads_served", since: "1.4.0" },
103
+ { name: "mcp_writes_accepted", since: "1.4.0" },
104
+ { name: "mcp_writes_refused", since: "1.4.0" },
105
+ { name: "mcp_errors_total", since: "1.4.0" },
106
+ // v1.5.0 (K15-001 / plan §4) — Diaspora metrics; lazy-incr.
107
+ { name: "mif_exports_total", since: "1.5.0" },
108
+ { name: "mif_imports_total", since: "1.5.0" },
109
+ { name: "skills_emitted_total", since: "1.5.0" },
110
+ ];
111
+ /**
112
+ * v1.4.0 (K14-006 / plan §4.3) — config keys added after the freeze,
113
+ * each carrying the `since` the deprecation policy requires (C-04).
114
+ */
115
+ export const CONTRACT_CONFIG_ADDITIONS = [
116
+ { name: "mcp_approve_enabled", since: "1.4.0" },
117
+ { name: "mcp_repo_override", since: "1.4.0" },
118
+ { name: "mcp_write_enabled", since: "1.4.0" },
119
+ // v1.5.0 (K15-001 / plan §4) — Diaspora settings; since 1.5.0.
120
+ { name: "import_host_memory", since: "1.5.0" },
121
+ { name: "skills_canonical_dir", since: "1.5.0" },
122
+ { name: "skills_mirror_claude", since: "1.5.0" },
123
+ { name: "skills_mirror_cursor", since: "1.5.0" },
100
124
  ];
101
125
  /**
102
126
  * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
@@ -137,7 +161,12 @@ export function describeContract(_input) {
137
161
  const toolValue = {
138
162
  tools: [[...CONTRACT_TOOL_NAMES].sort(), toolAdditions].flat(),
139
163
  };
140
- const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
164
+ // v1.4.0 config keys added after freeze carry `since` (C-04)
165
+ const configAdditions = [...CONTRACT_CONFIG_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
166
+ const baseConfigKeys = [...KEVIN_CONFIG_KEYS]
167
+ .filter((k) => !configAdditions.some((a) => a.name === k))
168
+ .sort();
169
+ const settingValue = { keys: [...baseConfigKeys, ...configAdditions].flat() };
141
170
  // v1.1.0 — metric keys added after freeze carry `since` (C-05)
142
171
  const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
143
172
  const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
@@ -211,7 +240,7 @@ export function describeContract(_input) {
211
240
  stability: "forward-only",
212
241
  since: "0.1.0",
213
242
  value: {
214
- schema_version: "012",
243
+ schema_version: "013",
215
244
  migrations_forward_only: true,
216
245
  },
217
246
  },
@@ -0,0 +1,41 @@
1
+ import { type KevinEnv } from "./env.js";
2
+ import type { MemoryService } from "./MemoryService.js";
3
+ import type { Store } from "./Store.js";
4
+ import type { Metrics } from "./metrics.js";
5
+ export interface HostImportReport {
6
+ files_scanned: number;
7
+ candidates: number;
8
+ saved: number;
9
+ duplicates: number;
10
+ skipped_weak: number;
11
+ error?: string;
12
+ hint?: string;
13
+ truncated?: boolean;
14
+ skipped_files?: number;
15
+ }
16
+ export declare function parseClaudeMemory(dataRoot: string, env?: KevinEnv): {
17
+ candidates: {
18
+ content: string;
19
+ type: string;
20
+ }[];
21
+ files_scanned: number;
22
+ skipped_files: number;
23
+ truncated: boolean;
24
+ };
25
+ export declare function parseCodexMemories(dataRoot: string, env?: KevinEnv): {
26
+ candidates: {
27
+ content: string;
28
+ type: string;
29
+ }[];
30
+ files_scanned: number;
31
+ skipped_files: number;
32
+ truncated: boolean;
33
+ };
34
+ export declare function importHostMemories(opts: {
35
+ store: Store;
36
+ memoryService: MemoryService;
37
+ metrics?: Metrics;
38
+ env?: KevinEnv;
39
+ dataRoot?: string;
40
+ source: "claude-memory" | "codex-memories";
41
+ }): HostImportReport;
@@ -0,0 +1,286 @@
1
+ // K15-011/012/013 — Host importers (plan §4.5)
2
+ // Defensive markdown parsers, no YAML lib, same naive parser as validator.
3
+ import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { resolveEnv } from "./env.js";
6
+ import { fingerprint as computeFingerprint } from "./fingerprint.js";
7
+ import { QualityGate } from "./QualityGate.js";
8
+ const MAX_FILE_BYTES = 1 * 1024 * 1024;
9
+ const MAX_CANDIDATES = 5000;
10
+ const CLAUDE_TYPE_MAP = {
11
+ user_preference: "context",
12
+ project_context: "context",
13
+ correction: "rule",
14
+ code_pattern: "pattern",
15
+ };
16
+ // naive frontmatter parse for Claude topic files: extract type field
17
+ function parseFrontmatterType(content) {
18
+ const lines = content.replace(/\r\n/g, "\n").split("\n");
19
+ if (lines[0]?.trim() !== "---")
20
+ return null;
21
+ let type = null;
22
+ for (let i = 1; i < lines.length; i++) {
23
+ if (lines[i].trim() === "---")
24
+ break;
25
+ const m = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
26
+ if (m && m[1].toLowerCase() === "type") {
27
+ type = m[2].trim().replace(/^["']|["']$/g, "");
28
+ }
29
+ }
30
+ return type;
31
+ }
32
+ function extractBullets(content) {
33
+ // remove frontmatter block first
34
+ const withoutFm = content.replace(/^---\n[\s\S]*?\n---\n?/, "");
35
+ const lines = withoutFm.split("\n");
36
+ const out = [];
37
+ for (const line of lines) {
38
+ const m = line.match(/^\s*[-*]\s+(.*)$/);
39
+ if (m) {
40
+ const trimmed = m[1].trim();
41
+ if (trimmed !== "")
42
+ out.push(trimmed);
43
+ }
44
+ else {
45
+ // also handle heading/bullet? codex may have headings
46
+ const hm = line.match(/^\s*#{1,6}\s+(.*)$/);
47
+ if (hm && hm[1].trim() !== "") {
48
+ // treat heading text as candidate? For codex, headings+bulles both
49
+ // but we will extract bullets only for claude; codex extractor handles headings separately
50
+ }
51
+ }
52
+ }
53
+ return out;
54
+ }
55
+ function safeRead(filePath) {
56
+ try {
57
+ const st = lstatSync(filePath);
58
+ if (st.isSymbolicLink())
59
+ return null;
60
+ if (!st.isFile())
61
+ return null;
62
+ if (st.size > MAX_FILE_BYTES)
63
+ return null;
64
+ return readFileSync(filePath, "utf8");
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ export function parseClaudeMemory(dataRoot, env) {
71
+ const root = join(resolveEnv(env).dataRoot ?? dataRoot, "claude", "projects");
72
+ // but dataRoot is already resolveEnv(...).dataRoot; we accept direct
73
+ const scanRoot = join(dataRoot, "claude", "projects");
74
+ const candidates = [];
75
+ let files_scanned = 0;
76
+ let skipped_files = 0;
77
+ if (!existsSync(scanRoot))
78
+ return { candidates, files_scanned, skipped_files, truncated: false };
79
+ let projectDirs = [];
80
+ try {
81
+ projectDirs = readdirSync(scanRoot);
82
+ }
83
+ catch {
84
+ return { candidates, files_scanned, skipped_files, truncated: false };
85
+ }
86
+ for (const proj of projectDirs) {
87
+ const memDir = join(scanRoot, proj, "memory");
88
+ if (!existsSync(memDir))
89
+ continue;
90
+ let files = [];
91
+ try {
92
+ files = readdirSync(memDir);
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ for (const f of files) {
98
+ if (!f.endsWith(".md"))
99
+ continue;
100
+ const full = join(memDir, f);
101
+ files_scanned++;
102
+ if (f === "MEMORY.md") {
103
+ // index only, skip harvesting
104
+ continue;
105
+ }
106
+ const content = safeRead(full);
107
+ if (content === null) {
108
+ skipped_files++;
109
+ continue;
110
+ }
111
+ // M-04: frontmatter without closing --- should skip
112
+ if (content.trimStart().startsWith("---")) {
113
+ const fmLines = content.replace(/\r\n/g, "\n").split("\n");
114
+ let hasClosing = false;
115
+ for (let i = 1; i < fmLines.length; i++) {
116
+ if (fmLines[i].trim() === "---") {
117
+ hasClosing = true;
118
+ break;
119
+ }
120
+ }
121
+ if (!hasClosing) {
122
+ skipped_files++;
123
+ continue;
124
+ }
125
+ }
126
+ const rawType = parseFrontmatterType(content);
127
+ if (rawType === null) {
128
+ // check if frontmatter missing or malformed: count as skipped but continue
129
+ // if file has no frontmatter, we still skip but not throw
130
+ // For test: one malformed topic file should be counted as skipped
131
+ if (!content.trim().startsWith("---")) {
132
+ skipped_files++;
133
+ continue;
134
+ }
135
+ // if frontmatter exists but type missing, map to context
136
+ }
137
+ const mapped = rawType ? (CLAUDE_TYPE_MAP[rawType.toLowerCase()] ?? "context") : "context";
138
+ // bullets
139
+ const bullets = extractBullets(content);
140
+ if (bullets.length === 0) {
141
+ // no bullets -> skip but not error
142
+ continue;
143
+ }
144
+ for (const b of bullets) {
145
+ if (candidates.length >= MAX_CANDIDATES)
146
+ break;
147
+ candidates.push({ content: b, type: mapped });
148
+ }
149
+ }
150
+ }
151
+ const truncated = candidates.length >= MAX_CANDIDATES;
152
+ return { candidates, files_scanned, skipped_files, truncated };
153
+ }
154
+ export function parseCodexMemories(dataRoot, env) {
155
+ const scanRoot = join(dataRoot, "codex", "memories");
156
+ const candidates = [];
157
+ let files_scanned = 0;
158
+ let skipped_files = 0;
159
+ if (!existsSync(scanRoot))
160
+ return { candidates, files_scanned, skipped_files, truncated: false };
161
+ let files = [];
162
+ try {
163
+ files = readdirSync(scanRoot);
164
+ }
165
+ catch {
166
+ return { candidates, files_scanned, skipped_files, truncated: false };
167
+ }
168
+ const targets = ["memory_summary.md", "MEMORY.md"];
169
+ for (const t of targets) {
170
+ if (!files.includes(t))
171
+ continue;
172
+ const full = join(scanRoot, t);
173
+ files_scanned++;
174
+ const content = safeRead(full);
175
+ if (content === null) {
176
+ skipped_files++;
177
+ continue;
178
+ }
179
+ // extract headings and bullets: both become candidates type context
180
+ const lines = content.split("\n");
181
+ for (const line of lines) {
182
+ const bullet = line.match(/^\s*[-*]\s+(.*)$/);
183
+ if (bullet && bullet[1].trim() !== "") {
184
+ if (candidates.length >= MAX_CANDIDATES)
185
+ break;
186
+ candidates.push({ content: bullet[1].trim(), type: "context" });
187
+ continue;
188
+ }
189
+ const heading = line.match(/^\s*#{1,6}\s+(.*)$/);
190
+ if (heading && heading[1].trim() !== "") {
191
+ // heading text as candidate (avoid duplicates where heading is also bullet)
192
+ // we treat heading as candidate as well
193
+ if (candidates.length >= MAX_CANDIDATES)
194
+ break;
195
+ candidates.push({ content: heading[1].trim(), type: "context" });
196
+ }
197
+ }
198
+ }
199
+ const truncated2 = candidates.length >= MAX_CANDIDATES;
200
+ return { candidates, files_scanned, skipped_files, truncated: truncated2 };
201
+ }
202
+ export function importHostMemories(opts) {
203
+ const dataRoot = opts.dataRoot ?? resolveEnv(opts.env).dataRoot;
204
+ // gate
205
+ const gate = opts.memoryService.getSetting("import_host_memory", "0");
206
+ if (gate !== "1") {
207
+ return {
208
+ files_scanned: 0,
209
+ candidates: 0,
210
+ saved: 0,
211
+ duplicates: 0,
212
+ skipped_weak: 0,
213
+ error: "disabled",
214
+ hint: "Enable with kevin_config set import_host_memory 1",
215
+ };
216
+ }
217
+ let parsed;
218
+ if (opts.source === "claude-memory") {
219
+ parsed = parseClaudeMemory(dataRoot, opts.env);
220
+ }
221
+ else {
222
+ parsed = parseCodexMemories(dataRoot, opts.env);
223
+ }
224
+ let candidates = parsed.candidates;
225
+ // M-05: propagate truncated correctly from parsers
226
+ const truncated = (parsed.truncated ?? false) || candidates.length >= MAX_CANDIDATES;
227
+ if (candidates.length > MAX_CANDIDATES)
228
+ candidates = candidates.slice(0, MAX_CANDIDATES);
229
+ // pipeline per candidate: redact -> fingerprint dedup (existing rows) -> quality-gate classification -> save
230
+ let saved = 0;
231
+ let duplicates = 0;
232
+ let skipped_weak = 0;
233
+ // dedup intra-run via fingerprint set
234
+ const seenFingerprints = new Set();
235
+ // existing fingerprints from DB
236
+ const existingRows = opts.store.prepare("SELECT fingerprint FROM memories WHERE fingerprint IS NOT NULL").all();
237
+ for (const r of existingRows)
238
+ seenFingerprints.add(r.fingerprint);
239
+ for (const c of candidates) {
240
+ // redact (paths and private)
241
+ let content = c.content;
242
+ // simple redact paths: reuse import? For now strip private blocks
243
+ content = content.replace(/<private\b[^>]*>[\s\S]*?<\/private>/gi, (m) => `<private: redacted ${m.length} chars>`);
244
+ // fingerprint dedup
245
+ const fp = computeFingerprint(content);
246
+ if (seenFingerprints.has(fp)) {
247
+ duplicates++;
248
+ continue;
249
+ }
250
+ seenFingerprints.add(fp);
251
+ // quality-gate classification: weak stored-not-injected naturally
252
+ // We simulate: isActionable and strength via QualityGate.evaluate
253
+ // For host import, we consider type context is weak if generic? But we will use evaluate to decide
254
+ const q = QualityGate.evaluate({ errorType: "unknown", suggestion: content }, null, "unknown");
255
+ // If weak and not actionable, count as skipped_weak but still store? Plan says weak entries stored-but-NOT-injected
256
+ // So we still save but count as weak
257
+ const isWeak = q.strength === "weak" && !q.isActionable;
258
+ if (isWeak)
259
+ skipped_weak++;
260
+ // save
261
+ try {
262
+ opts.memoryService.save({
263
+ type: c.type,
264
+ content,
265
+ scope: "project",
266
+ origin: "imported",
267
+ fingerprint: fp,
268
+ metadata: { source: opts.source },
269
+ // evidence_count 0 => confidence low naturally, stored but not injected
270
+ });
271
+ saved++;
272
+ }
273
+ catch {
274
+ // skip on error
275
+ }
276
+ }
277
+ return {
278
+ files_scanned: parsed.files_scanned,
279
+ candidates: parsed.candidates.length,
280
+ saved,
281
+ duplicates,
282
+ skipped_weak,
283
+ truncated: truncated || undefined,
284
+ skipped_files: parsed.skipped_files || undefined,
285
+ };
286
+ }
package/dist/index.d.ts CHANGED
@@ -66,7 +66,11 @@ export * from "./kevin_propose.js";
66
66
  export { kevinPublish } from "./kevin_publish.js";
67
67
  export type { PublishResult } from "./kevin_publish.js";
68
68
  export * from "./kevin_why.js";
69
+ export * from "./skills-validate.js";
70
+ export * from "./skills-emit.js";
71
+ export * from "./mif.js";
72
+ export * from "./import-host.js";
69
73
  export * from "./env.js";
70
- export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled"];
74
+ export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "import_host_memory"];
71
75
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
72
- export declare const KEVIN_VERSION = "1.3.0";
76
+ export declare const KEVIN_VERSION = "1.5.0";
package/dist/index.js CHANGED
@@ -68,6 +68,10 @@ export { handleNative } from "./kevin_native.js";
68
68
  export * from "./kevin_propose.js";
69
69
  export { kevinPublish } from "./kevin_publish.js";
70
70
  export * from "./kevin_why.js";
71
+ export * from "./skills-validate.js";
72
+ export * from "./skills-emit.js";
73
+ export * from "./mif.js";
74
+ export * from "./import-host.js";
71
75
  // KEVIN_CONFIG_KEYS and related constants — moved from adapter index so core owns the source of truth (C-04).
72
76
  // Duplicated here to allow adapter to import from core; adapter will re-export them.
73
77
  export * from "./env.js";
@@ -104,6 +108,13 @@ export const KEVIN_CONFIG_KEYS = [
104
108
  "perf_flush_on_idle",
105
109
  "contract_report_enabled",
106
110
  "tui_snapshots_enabled",
111
+ "mcp_write_enabled",
112
+ "mcp_approve_enabled",
113
+ "mcp_repo_override",
114
+ "skills_canonical_dir",
115
+ "skills_mirror_claude",
116
+ "skills_mirror_cursor",
117
+ "import_host_memory",
107
118
  ];
108
119
  export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
109
- export const KEVIN_VERSION = "1.3.0";
120
+ export const KEVIN_VERSION = "1.5.0";
@@ -170,6 +170,37 @@ export interface AuditReport {
170
170
  dashboard_last_write_age_s: number | null;
171
171
  bridge_interceptions: number;
172
172
  };
173
+ /**
174
+ * v1.4.0 (K14-016) — MCP bridge block. Reads the five MCP counters and
175
+ * channel split. Omitted on pre-013 DBs (partial:true).
176
+ */
177
+ mcp?: {
178
+ requests: number;
179
+ reads: number;
180
+ writes_accepted: number;
181
+ writes_refused: number;
182
+ errors: number;
183
+ channel_split: {
184
+ plugin: number;
185
+ mcp: number;
186
+ };
187
+ };
188
+ channels_v2?: {
189
+ push: {
190
+ injections_total: number;
191
+ precision_rate: number;
192
+ coverage_rate: number;
193
+ };
194
+ mcp: {
195
+ injections_total: number;
196
+ precision_rate?: number;
197
+ coverage_rate?: number;
198
+ };
199
+ pull: {
200
+ registered_surfaces: number;
201
+ note: string;
202
+ };
203
+ };
173
204
  partial: boolean;
174
205
  }
175
206
  /**
@@ -609,6 +609,98 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
609
609
  catch {
610
610
  tui = undefined;
611
611
  }
612
+ // v1.4.0 (K14-016) — MCP block, gated on channel column
613
+ let mcp;
614
+ try {
615
+ const hasChannel = (() => {
616
+ try {
617
+ store.prepare("SELECT channel FROM kevin_injections LIMIT 1").get();
618
+ return true;
619
+ }
620
+ catch {
621
+ return false;
622
+ }
623
+ })();
624
+ if (!hasChannel) {
625
+ partial = true;
626
+ mcp = undefined;
627
+ }
628
+ else {
629
+ const getMetric = (k) => {
630
+ try {
631
+ const row = store.prepare("SELECT value FROM kevin_metrics WHERE key = ?").get(k);
632
+ return row?.value ?? metrics.get(k) ?? 0;
633
+ }
634
+ catch {
635
+ return metrics.get(k) ?? 0;
636
+ }
637
+ };
638
+ const plugin = (() => {
639
+ try {
640
+ return store.prepare("SELECT COUNT(*) as c FROM kevin_injections WHERE channel = 'plugin'").get().c;
641
+ }
642
+ catch {
643
+ return 0;
644
+ }
645
+ })();
646
+ const mcpCount = (() => {
647
+ try {
648
+ return store.prepare("SELECT COUNT(*) as c FROM kevin_injections WHERE channel = 'mcp'").get().c;
649
+ }
650
+ catch {
651
+ return 0;
652
+ }
653
+ })();
654
+ mcp = {
655
+ requests: getMetric("mcp_requests_total"),
656
+ reads: getMetric("mcp_reads_served"),
657
+ writes_accepted: getMetric("mcp_writes_accepted"),
658
+ writes_refused: getMetric("mcp_writes_refused"),
659
+ errors: getMetric("mcp_errors_total"),
660
+ channel_split: { plugin, mcp: mcpCount },
661
+ };
662
+ }
663
+ }
664
+ catch {
665
+ partial = true;
666
+ mcp = undefined;
667
+ }
668
+ // v1.5.0 (K15-015) — channels_v2 honest scope (D15-06)
669
+ let channels_v2;
670
+ try {
671
+ const push_total = scalar(store, "SELECT COUNT(*) AS n FROM kevin_injections WHERE channel = 'plugin' OR channel IS NULL");
672
+ const mcp_total = (() => { try {
673
+ return scalar(store, "SELECT COUNT(*) AS n FROM kevin_injections WHERE channel = 'mcp'");
674
+ }
675
+ catch {
676
+ return 0;
677
+ } })();
678
+ const hasChannel = (() => { try {
679
+ store.prepare("SELECT channel FROM kevin_injections LIMIT 1").get();
680
+ return true;
681
+ }
682
+ catch {
683
+ return false;
684
+ } })();
685
+ const registered_surfaces = (() => {
686
+ try {
687
+ const r1 = store.prepare("SELECT value FROM kevin_metrics WHERE key = 'skills_registered'").get();
688
+ const r2 = store.prepare("SELECT value FROM kevin_metrics WHERE key = 'references_registered'").get();
689
+ return (r1?.value ?? 0) + (r2?.value ?? 0);
690
+ }
691
+ catch {
692
+ return 0;
693
+ }
694
+ })();
695
+ channels_v2 = {
696
+ push: { injections_total: push_total, precision_rate: metrics.precisionRate(), coverage_rate: metrics.coverageRate() },
697
+ mcp: { injections_total: hasChannel ? mcp_total : 0 },
698
+ pull: { registered_surfaces, note: "pull-effectiveness telemetry unavailable pre-contract-v2 — qualitative" },
699
+ };
700
+ }
701
+ catch {
702
+ channels_v2 = undefined;
703
+ }
612
704
  return {
613
705
  memories,
614
706
  injections,
@@ -626,6 +718,8 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
626
718
  perf,
627
719
  contract,
628
720
  tui,
721
+ mcp,
722
+ channels_v2,
629
723
  partial,
630
724
  };
631
725
  }
package/dist/metrics.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { Store } from "./Store.js";
6
6
  * underlying table is empty (e.g., before 003 is applied, on a fresh
7
7
  * :memory: test DB, or after a manual wipe).
8
8
  */
9
- export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked"];
9
+ export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total", "skills_emitted_total", "mif_exports_total", "mif_imports_total"];
10
10
  export type MetricKey = (typeof METRIC_KEYS)[number];
11
11
  /**
12
12
  * Cheap token estimate used when bumping the `tokens_injected_*` counters.
package/dist/metrics.js CHANGED
@@ -65,6 +65,16 @@ export const METRIC_KEYS = [
65
65
  // release — rows are created on first incr via upsert (K12-001).
66
66
  "tui_snapshots_flushed",
67
67
  "tui_actions_invoked",
68
+ // v1.4.0 (K14-003 / plan §4.3, D14-??) — MCP bridge metrics; seeds via 013.
69
+ "mcp_requests_total",
70
+ "mcp_reads_served",
71
+ "mcp_writes_accepted",
72
+ "mcp_writes_refused",
73
+ "mcp_errors_total",
74
+ // v1.5.0 (K15-001 / plan §4) — Diaspora metrics; lazy-incr, no migration.
75
+ "skills_emitted_total",
76
+ "mif_exports_total",
77
+ "mif_imports_total",
68
78
  ];
69
79
  const DEFAULT_FLUSH_MS = 1000;
70
80
  function zeroCache() {
package/dist/mif.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { Memory } from "./MemoryService.js";
2
+ export interface MifEnvelope {
3
+ format: "mif";
4
+ version: 1;
5
+ memories: MifMemory[];
6
+ vendorExtensions?: Record<string, unknown>;
7
+ }
8
+ export interface MifMemory {
9
+ id: string;
10
+ content: string;
11
+ type: string;
12
+ timestamp: string;
13
+ source: string;
14
+ metadata: Record<string, string>;
15
+ [k: string]: unknown;
16
+ }
17
+ export declare function toMif(rows: Memory[], opts: {
18
+ redactPii: boolean;
19
+ }): MifEnvelope;
20
+ export interface ImportCandidate {
21
+ id: string;
22
+ content: string;
23
+ type: string;
24
+ timestamp: string;
25
+ source: string;
26
+ metadata: Record<string, string>;
27
+ unknownFields: Record<string, unknown>;
28
+ }
29
+ export declare function fromMif(env: MifEnvelope): {
30
+ candidates: ImportCandidate[];
31
+ unknownFieldsPreserved: string[];
32
+ };