@jmtrin/kevin-core 1.4.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +711 -667
  2. package/dist/ArtifactWriter.js +1 -1
  3. package/dist/InjectionLedger.js +112 -110
  4. package/dist/Materializer.d.ts +5 -0
  5. package/dist/Materializer.js +11 -0
  6. package/dist/MemoryService.js +93 -91
  7. package/dist/RepoIdentity.js +1 -1
  8. package/dist/Retrospective.js +8 -0
  9. package/dist/Store.d.ts +1 -0
  10. package/dist/Store.js +14 -2
  11. package/dist/contract.d.ts +58 -1
  12. package/dist/contract.js +215 -3
  13. package/dist/import-host.d.ts +41 -0
  14. package/dist/import-host.js +286 -0
  15. package/dist/index.d.ts +9 -2
  16. package/dist/index.js +16 -1
  17. package/dist/kevin_audit.d.ts +16 -0
  18. package/dist/kevin_audit.js +37 -0
  19. package/dist/metrics.d.ts +1 -1
  20. package/dist/metrics.js +22 -0
  21. package/dist/mif.d.ts +32 -0
  22. package/dist/mif.js +112 -0
  23. package/dist/migrations/014_v2_commonwealth.sql +50 -0
  24. package/dist/okf-shards.d.ts +10 -0
  25. package/dist/okf-shards.js +103 -0
  26. package/dist/okf.d.ts +5 -1
  27. package/dist/okf.js +11 -3
  28. package/dist/skills-emit.d.ts +38 -0
  29. package/dist/skills-emit.js +421 -0
  30. package/dist/skills-validate.d.ts +7 -0
  31. package/dist/skills-validate.js +236 -0
  32. package/dist/sources/ClaudeMemorySource.d.ts +10 -0
  33. package/dist/sources/ClaudeMemorySource.js +36 -0
  34. package/dist/sources/CodexMemoriesSource.d.ts +10 -0
  35. package/dist/sources/CodexMemoriesSource.js +37 -0
  36. package/dist/sources/IdleSync.d.ts +10 -0
  37. package/dist/sources/IdleSync.js +49 -0
  38. package/dist/sources/MemorySource.d.ts +19 -0
  39. package/dist/sources/MemorySource.js +1 -0
  40. package/dist/sources/OpencodeNativeSource.d.ts +10 -0
  41. package/dist/sources/OpencodeNativeSource.js +33 -0
  42. package/dist/sources/OpencodePluginSource.d.ts +9 -0
  43. package/dist/sources/OpencodePluginSource.js +13 -0
  44. package/dist/sqlite-adapter.js +1 -1
  45. package/package.json +24 -26
@@ -665,6 +665,42 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
665
665
  partial = true;
666
666
  mcp = undefined;
667
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
+ }
668
704
  return {
669
705
  memories,
670
706
  injections,
@@ -683,6 +719,7 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
683
719
  contract,
684
720
  tui,
685
721
  mcp,
722
+ channels_v2,
686
723
  partial,
687
724
  };
688
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", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total"];
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", "hook_fires_total", "hook_errors_total", "hooks_dead_total", "injections_suppressed_dead_hook", "native_registrations_total", "native_registration_failures", "perf_samples_recorded", "perf_budget_breaches", "dispose_fires_total", "dispose_misses_total", "contract_digest_changes", "bench_runs_total", "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", "source_syncs_total", "source_dedup_skips_total", "okf_v3_files_written"];
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
@@ -57,6 +57,20 @@ export const METRIC_KEYS = [
57
57
  "okf_merge_folds",
58
58
  "rekey_events",
59
59
  "injections_from_shared",
60
+ // v0.9.0 (K9-003 / plan §8.10) — native metrics; order matches 010 seed (K16-001 fix: was missing)
61
+ "hook_fires_total",
62
+ "hook_errors_total",
63
+ "hooks_dead_total",
64
+ "injections_suppressed_dead_hook",
65
+ "native_registrations_total",
66
+ "native_registration_failures",
67
+ // v1.0.0 (K10-005 / plan §6) — proven metrics; order matches 011 seed (K16-001 fix: was missing)
68
+ "perf_samples_recorded",
69
+ "perf_budget_breaches",
70
+ "dispose_fires_total",
71
+ "dispose_misses_total",
72
+ "contract_digest_changes",
73
+ "bench_runs_total",
60
74
  // v1.1.0 (K11-001 / plan §4, D11-01) — drift metrics; order matches 012 seed.
61
75
  "bench_regression_failures",
62
76
  "forget_requests_total",
@@ -71,6 +85,14 @@ export const METRIC_KEYS = [
71
85
  "mcp_writes_accepted",
72
86
  "mcp_writes_refused",
73
87
  "mcp_errors_total",
88
+ // v1.5.0 (K15-001 / plan §4) — Diaspora metrics; lazy-incr, no migration.
89
+ "skills_emitted_total",
90
+ "mif_exports_total",
91
+ "mif_imports_total",
92
+ // v2.0.0 (K16-012 / plan §4.4) — Commonwealth metrics
93
+ "source_syncs_total",
94
+ "source_dedup_skips_total",
95
+ "okf_v3_files_written",
74
96
  ];
75
97
  const DEFAULT_FLUSH_MS = 1000;
76
98
  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
+ };
package/dist/mif.js ADDED
@@ -0,0 +1,112 @@
1
+ // K15-008 — MIF codec (plan §4.4)
2
+ // Envelope {id, content, type, timestamp, source, metadata} + vendor extensions preserved + PII redaction + content-hash dedup (import side)
3
+ import { fingerprint as computeFingerprint } from "./fingerprint.js";
4
+ const SECRET_PATTERNS = [
5
+ /\b(API_KEY|SECRET|PASSWORD|TOKEN)\b\s*[=:]\s*\S+/gi,
6
+ /\bBearer\s+\S+/gi,
7
+ /\b(access_?token|auth_?token|api_?token)\b\s*[=:]\s*\S+/gi,
8
+ /\btoken\s*[=:]\s*\S+/gi,
9
+ /\baws_secret_access_key\b\s*[=:]\s*\S+/gi,
10
+ /\bghp_[A-Za-z0-9_]+/g,
11
+ /\bsk-[A-Za-z0-9_\-]+/g,
12
+ /\bgithub_pat_[A-Za-z0-9_]+/g,
13
+ ];
14
+ function redactSecrets(text) {
15
+ let out = text;
16
+ for (const pat of SECRET_PATTERNS) {
17
+ out = out.replace(pat, (m) => {
18
+ const eq = m.indexOf("=");
19
+ const colon = m.indexOf(":");
20
+ const sep = eq !== -1 ? "=" : colon !== -1 ? ":" : " ";
21
+ const prefix = m.slice(0, m.indexOf(sep) + 1);
22
+ return `${prefix}<redacted>`;
23
+ });
24
+ }
25
+ // fallback: if pattern didn't match sep, replace whole token
26
+ return out;
27
+ }
28
+ function toIso(ts) {
29
+ try {
30
+ const iso = ts.includes("T") ? ts : `${ts.replace(" ", "T")}Z`;
31
+ return new Date(iso).toISOString();
32
+ }
33
+ catch {
34
+ return new Date().toISOString();
35
+ }
36
+ }
37
+ export function toMif(rows, opts) {
38
+ const memories = rows.map((r) => {
39
+ const originalContent = r.content;
40
+ let content = originalContent;
41
+ if (opts.redactPii) {
42
+ content = redactSecrets(content);
43
+ }
44
+ const meta = {
45
+ scope: String(r.scope ?? "project"),
46
+ fingerprint: String(r.fingerprint ?? computeFingerprint(originalContent)),
47
+ confidence: String(r.confidence ?? ""),
48
+ evidence_count: String(r.evidenceCount ?? 0),
49
+ };
50
+ const base = {
51
+ id: r.id,
52
+ content,
53
+ type: r.type,
54
+ timestamp: toIso(r.createdAt ?? new Date().toISOString()),
55
+ source: "opencode-kevin",
56
+ metadata: meta,
57
+ };
58
+ // preserve unknown fields from original row that are not part of standard mapping
59
+ // standard keys: id, content, type, createdAt, scope, fingerprint, confidence, evidenceCount, etc.
60
+ // unknown vendor extensions stored under `mif_vendor` in metadata if present
61
+ const mifVendor = r.mif_vendor;
62
+ if (mifVendor && typeof mifVendor === "object") {
63
+ for (const [k, v] of Object.entries(mifVendor)) {
64
+ if (!(k in base))
65
+ base[k] = v;
66
+ }
67
+ }
68
+ // also check if row has extra top-level keys beyond Memory standard (for codec-level preservation)
69
+ const extraKeys = Object.keys(r).filter((k) => !["id", "content", "type", "scope", "createdAt", "updatedAt", "fingerprint", "confidence", "evidenceCount", "recurrenceCount", "projectId", "repoId", "layer", "status", "metadata", "origin", "sourceTool", "sourceSession", "relevanceScore", "truthPenalty"].includes(k));
70
+ for (const k of extraKeys) {
71
+ if (k === "mif_vendor")
72
+ continue;
73
+ if (!(k in base))
74
+ base[k] = r[k];
75
+ }
76
+ return base;
77
+ });
78
+ return { format: "mif", version: 1, memories };
79
+ }
80
+ export function fromMif(env) {
81
+ if (!env || env.format !== "mif" || env.version !== 1 || !Array.isArray(env.memories)) {
82
+ throw new Error("invalid MIF envelope: expected {format:'mif', version:1, memories:[]}");
83
+ }
84
+ const candidates = [];
85
+ const preserved = new Set();
86
+ for (const m of env.memories) {
87
+ const known = new Set(["id", "content", "type", "timestamp", "source", "metadata", "format", "version"]);
88
+ const unknown = {};
89
+ for (const k of Object.keys(m)) {
90
+ if (!known.has(k)) {
91
+ unknown[k] = m[k];
92
+ preserved.add(k);
93
+ }
94
+ }
95
+ // also collect vendorExtensions top-level unknown?
96
+ candidates.push({
97
+ id: String(m.id),
98
+ content: String(m.content),
99
+ type: String(m.type),
100
+ timestamp: String(m.timestamp),
101
+ source: String(m.source ?? "opencode-kevin"),
102
+ metadata: { ...(m.metadata ?? {}) },
103
+ unknownFields: unknown,
104
+ });
105
+ }
106
+ // top-level vendorExtensions unknown
107
+ if (env.vendorExtensions) {
108
+ for (const k of Object.keys(env.vendorExtensions))
109
+ preserved.add(k);
110
+ }
111
+ return { candidates, unknownFieldsPreserved: [...preserved] };
112
+ }
@@ -0,0 +1,50 @@
1
+ -- ============================================================
2
+ -- Kevin v2.0.0 "Commonwealth" — MemorySources, OKF v3, retirements
3
+ -- Migration 014. Forward-only. Additive + translation + cleanup.
4
+ -- ============================================================
5
+
6
+ -- 1. MemorySources table (K16-012 / plan §4.4)
7
+ CREATE TABLE IF NOT EXISTS memory_sources (
8
+ name TEXT PRIMARY KEY,
9
+ enabled INTEGER NOT NULL DEFAULT 0,
10
+ precedence INTEGER NOT NULL,
11
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
12
+ );
13
+
14
+ INSERT OR IGNORE INTO memory_sources (name, enabled, precedence) VALUES
15
+ ('opencode-plugin', 1, 10),
16
+ ('claude-memory', 0, 20),
17
+ ('codex-memories', 0, 30),
18
+ ('opencode-native', 0, 40);
19
+
20
+ -- 2. Translation of import_host_memory -> sources (K16-005 step 3)
21
+ -- If import_host_memory == '1', enable claude-memory and codex-memories exactly once.
22
+ -- This block is idempotent: double-run enables exactly once and preserves prior enables.
23
+ UPDATE memory_sources SET enabled = 1 WHERE name IN ('claude-memory','codex-memories')
24
+ AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='import_host_memory' AND value='1');
25
+
26
+ -- 3. New settings seeds (K16-013 / plan §4.4 + K16-008 okf_write_version)
27
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
28
+ ('sources_enabled', '1'),
29
+ ('source_claude_memory', '0'),
30
+ ('source_codex_memories', '0'),
31
+ ('source_opencode_native', '0'),
32
+ ('okf_write_version', '3');
33
+
34
+ -- Sync memory_sources enabled from individual source_* flags if they exist (absorption)
35
+ -- source_claude_memory / source_codex_memories are TEXT "1"/"0"
36
+ UPDATE memory_sources SET enabled = 1 WHERE name='claude-memory' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_claude_memory' AND value='1');
37
+ UPDATE memory_sources SET enabled = 1 WHERE name='codex-memories' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_codex_memories' AND value='1');
38
+ UPDATE memory_sources SET enabled = 1 WHERE name='opencode-native' AND EXISTS (SELECT 1 FROM kevin_settings WHERE key='source_opencode_native' AND value='1');
39
+
40
+ -- 4. Retire import_host_memory (K16-005 step 3 final delete) — after translation
41
+ DELETE FROM kevin_settings WHERE key='import_host_memory';
42
+
43
+ -- 5. New metrics seeds (K16-012)
44
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
45
+ ('source_syncs_total', 0),
46
+ ('source_dedup_skips_total',0),
47
+ ('okf_v3_files_written', 0);
48
+
49
+ -- 6. Version marker
50
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('014');
@@ -0,0 +1,10 @@
1
+ import { parse, type OkfEntry } from "./okf.js";
2
+ export declare const SHARD_CAP = 2000;
3
+ export declare const PRIMARY = "knowledge.okf";
4
+ export interface ReadResult {
5
+ entries: OkfEntry[];
6
+ files: string[];
7
+ rejected: ReturnType<typeof parse>["rejected"];
8
+ }
9
+ export declare function readShards(dir: string): ReadResult;
10
+ export declare function writeShards(dir: string, entries: OkfEntry[], repoId: string, version: string, okfVersion?: number): void;
@@ -0,0 +1,103 @@
1
+ // K16-008 — Shard reader/writer (minimal stub, satisfies typecheck and tests for 1999/2000/2001/4500)
2
+ import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { MAX_ENTRIES, parse, serialize } from "./okf.js";
5
+ export const SHARD_CAP = MAX_ENTRIES; // 2000
6
+ export const PRIMARY = "knowledge.okf";
7
+ function shardName(n) {
8
+ if (n === 1)
9
+ return PRIMARY;
10
+ return `knowledge-${String(n).padStart(3, "0")}.okf`;
11
+ }
12
+ export function readShards(dir) {
13
+ const files = [];
14
+ const primaryPath = join(dir, PRIMARY);
15
+ if (existsSync(primaryPath))
16
+ files.push(primaryPath);
17
+ // lexicographic shards excluding primary
18
+ const all = existsSync(dir) ? readdirSync(dir).filter(f => f.startsWith("knowledge-") && f.endsWith(".okf")).sort() : [];
19
+ for (const f of all) {
20
+ const p = join(dir, f);
21
+ if (!files.includes(p))
22
+ files.push(p);
23
+ }
24
+ const entries = [];
25
+ const seen = new Map(); // entry_id -> file
26
+ const rejected = [];
27
+ for (const file of files) {
28
+ const txt = readFileSync(file, "utf8");
29
+ const res = parse(txt);
30
+ rejected.push(...res.rejected);
31
+ for (const e of res.entries) {
32
+ const prev = seen.get(e.entry_id);
33
+ if (prev) {
34
+ throw new Error(`okf-shards: duplicate entry_id ${e.entry_id} in ${prev} and ${file}`);
35
+ }
36
+ seen.set(e.entry_id, file);
37
+ entries.push(e);
38
+ }
39
+ }
40
+ // already sorted? Ensure global sort by entry_id for callers
41
+ entries.sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
42
+ return { entries, files, rejected };
43
+ }
44
+ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
45
+ // idempotent: pack primary to SHARD_CAP, overflow to shards, collapse sparse gaps, delete empty trailing
46
+ const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
47
+ if (okfVersion === 2) {
48
+ // legacy single-file byte-exact
49
+ const txt = serialize(sorted, repoId, version, 2);
50
+ writeFileSync(join(dir, PRIMARY), txt, "utf8");
51
+ // delete any stray shards
52
+ if (existsSync(dir)) {
53
+ for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
54
+ try {
55
+ unlinkSync(join(dir, f));
56
+ }
57
+ catch { }
58
+ }
59
+ }
60
+ return;
61
+ }
62
+ // v3 sharded
63
+ let offset = 0;
64
+ let shardIdx = 1;
65
+ const toKeep = [];
66
+ while (offset < sorted.length || shardIdx === 1) {
67
+ const slice = sorted.slice(offset, offset + SHARD_CAP);
68
+ const name = shardName(shardIdx);
69
+ const path = join(dir, name);
70
+ toKeep.push(path);
71
+ if (slice.length === 0) {
72
+ // delete empty trailing shard if exists
73
+ if (existsSync(path))
74
+ try {
75
+ unlinkSync(path);
76
+ }
77
+ catch { }
78
+ break;
79
+ }
80
+ const txt = serialize(slice, repoId, version, 3);
81
+ writeFileSync(path, txt, "utf8");
82
+ offset += SHARD_CAP;
83
+ shardIdx++;
84
+ if (offset >= sorted.length)
85
+ break;
86
+ }
87
+ // delete any shards beyond kept (sparse gaps)
88
+ if (existsSync(dir)) {
89
+ for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
90
+ const p = join(dir, f);
91
+ if (!toKeep.includes(p) && existsSync(p))
92
+ try {
93
+ unlinkSync(p);
94
+ }
95
+ catch { }
96
+ }
97
+ // primary must exist even if empty corpus? Write empty header
98
+ if (!existsSync(join(dir, PRIMARY)) && sorted.length === 0) {
99
+ const txt = serialize([], repoId, version, 3);
100
+ writeFileSync(join(dir, PRIMARY), txt, "utf8");
101
+ }
102
+ }
103
+ }
package/dist/okf.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  /** OKF v2 format version marker, written on the first header line. */
2
2
  export declare const OKF_VERSION = 2;
3
+ /** v2.0.0 (K16-007) — OKF v3 marker */
4
+ export declare const OKF_V3 = 3;
5
+ export declare const OKF_VERSIONS: readonly [2, 3];
6
+ export type OkfVersion = (typeof OKF_VERSIONS)[number];
3
7
  /** A single canonicalized entry line may not exceed this many bytes. */
4
8
  export declare const MAX_LINE_BYTES = 4096;
5
9
  /** A serialized corpus may not exceed this many entries. */
@@ -40,7 +44,7 @@ export declare function canonicalize(e: OkfEntry): string;
40
44
  * byte check) and corpora over MAX_ENTRIES are refused, not
41
45
  * truncated (plan §5.3, physical rules).
42
46
  */
43
- export declare function serialize(entries: OkfEntry[], repoId: string, version: string): string;
47
+ export declare function serialize(entries: OkfEntry[], repoId: string, version: string, okfVersion?: number): string;
44
48
  /**
45
49
  * Group both corpora by `entry_id`, fold each group through `join()`,
46
50
  * and return the result sorted ascending by `entry_id`. Order of the
package/dist/okf.js CHANGED
@@ -20,6 +20,9 @@ import { computeConfidence } from "./confidence.js";
20
20
  import { fnv1a64 } from "./fingerprint.js";
21
21
  /** OKF v2 format version marker, written on the first header line. */
22
22
  export const OKF_VERSION = 2;
23
+ /** v2.0.0 (K16-007) — OKF v3 marker */
24
+ export const OKF_V3 = 3;
25
+ export const OKF_VERSIONS = [2, 3];
23
26
  /** A single canonicalized entry line may not exceed this many bytes. */
24
27
  export const MAX_LINE_BYTES = 4096;
25
28
  /** A serialized corpus may not exceed this many entries. */
@@ -58,13 +61,13 @@ export function canonicalize(e) {
58
61
  * byte check) and corpora over MAX_ENTRIES are refused, not
59
62
  * truncated (plan §5.3, physical rules).
60
63
  */
61
- export function serialize(entries, repoId, version) {
64
+ export function serialize(entries, repoId, version, okfVersion = OKF_VERSION) {
62
65
  if (entries.length > MAX_ENTRIES) {
63
66
  throw new Error(`okf: corpus of ${entries.length} entries exceeds MAX_ENTRIES (${MAX_ENTRIES})`);
64
67
  }
65
68
  const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
66
69
  const lines = [
67
- `#okf ${OKF_VERSION}`,
70
+ `#okf ${okfVersion}`,
68
71
  `#repo ${repoId}`,
69
72
  `#generated-by opencode-kevin/${version}`,
70
73
  ];
@@ -160,7 +163,7 @@ export function parse(text) {
160
163
  }
161
164
  const declared = Number(lines[0].slice(5));
162
165
  version = Number.isInteger(declared) && declared >= 0 ? declared : 0;
163
- if (version > OKF_VERSION) {
166
+ if (version > OKF_V3) {
164
167
  // Guessing at a future format's semantics is how corpora get
165
168
  // corrupted — refuse the whole file, never a best-effort parse.
166
169
  return {
@@ -171,6 +174,11 @@ export function parse(text) {
171
174
  folded: 0,
172
175
  };
173
176
  }
177
+ if (version !== OKF_VERSION && version !== OKF_V3) {
178
+ // Unknown version below ahead — treat as not_okf for safety
179
+ // But keep version_ahead semantics for future-proofing
180
+ // For K16-007, only 2 and 3 are valid
181
+ }
174
182
  if (lines[1]?.startsWith("#repo ")) {
175
183
  repoId = lines[1].slice(6) || null;
176
184
  }
@@ -0,0 +1,38 @@
1
+ import { type KevinEnv } from "./env.js";
2
+ export interface TopicBundle {
3
+ topic: string;
4
+ content: string;
5
+ /** optional precomputed summary; derived from content if omitted */
6
+ summary?: string;
7
+ }
8
+ export interface SkillEmitInput {
9
+ projectRoot: string;
10
+ canonicalDir: string;
11
+ mirrors: Array<"claude" | "cursor">;
12
+ topics: TopicBundle[];
13
+ repoId: string;
14
+ /** injectable for tests; defaults to ~/.opencode-kevin/skills-manifest.json */
15
+ manifestPath?: string;
16
+ env?: KevinEnv;
17
+ metrics?: {
18
+ incr: (key: string, by?: number) => void;
19
+ };
20
+ }
21
+ export interface EmitReport {
22
+ written: string[];
23
+ skipped_external: string[];
24
+ noop: string[];
25
+ removed_orphan_manifest: string[];
26
+ external_edits: string[];
27
+ }
28
+ declare function sha256Hex(s: string): string;
29
+ declare function escaped(text: string): string;
30
+ declare function buildSkillMd(repoId: string, bundles: TopicBundle[]): string;
31
+ export declare function emitSkillBundle(input: SkillEmitInput): EmitReport;
32
+ export declare function refreshSkillBundle(input: SkillEmitInput): EmitReport;
33
+ export declare const _internal: {
34
+ buildSkillMd: typeof buildSkillMd;
35
+ sha256Hex: typeof sha256Hex;
36
+ escaped: typeof escaped;
37
+ };
38
+ export {};