@jmtrin/opencode-kevin 1.2.0 → 1.4.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 (140) hide show
  1. package/README.md +84 -22
  2. package/dist/plugin/index.d.ts +5 -4
  3. package/dist/plugin/index.js +127 -113
  4. package/dist/plugin/native.d.ts +2 -2
  5. package/dist/plugin/native.js +2 -2
  6. package/package.json +56 -68
  7. package/dist/migrations/001_initial.sql +0 -92
  8. package/dist/migrations/002_indexes.sql +0 -14
  9. package/dist/migrations/003_v02_signal.sql +0 -58
  10. package/dist/migrations/004_v03_knowledge.sql +0 -138
  11. package/dist/migrations/005_v04_signal.sql +0 -57
  12. package/dist/migrations/006_v05_glassbox.sql +0 -118
  13. package/dist/migrations/007_v06_pull.sql +0 -145
  14. package/dist/migrations/008_v07_truth.sql +0 -124
  15. package/dist/migrations/009_v08_team.sql +0 -100
  16. package/dist/migrations/010_v09_native.sql +0 -78
  17. package/dist/migrations/011_v10_proven.sql +0 -69
  18. package/dist/migrations/012_v11_drift.sql +0 -24
  19. package/dist/plugin/Archiver.d.ts +0 -42
  20. package/dist/plugin/Archiver.js +0 -83
  21. package/dist/plugin/ArtifactWriter.d.ts +0 -93
  22. package/dist/plugin/ArtifactWriter.js +0 -291
  23. package/dist/plugin/CausalChain.d.ts +0 -22
  24. package/dist/plugin/CausalChain.js +0 -197
  25. package/dist/plugin/ChatBridge.d.ts +0 -41
  26. package/dist/plugin/ChatBridge.js +0 -103
  27. package/dist/plugin/ConflictDetector.d.ts +0 -35
  28. package/dist/plugin/ConflictDetector.js +0 -260
  29. package/dist/plugin/ContextInjector.d.ts +0 -222
  30. package/dist/plugin/ContextInjector.js +0 -533
  31. package/dist/plugin/ConventionMiner.d.ts +0 -35
  32. package/dist/plugin/ConventionMiner.js +0 -242
  33. package/dist/plugin/Curator.d.ts +0 -116
  34. package/dist/plugin/Curator.js +0 -324
  35. package/dist/plugin/DashboardHtml.d.ts +0 -5
  36. package/dist/plugin/DashboardHtml.js +0 -180
  37. package/dist/plugin/Feedback.d.ts +0 -67
  38. package/dist/plugin/Feedback.js +0 -120
  39. package/dist/plugin/HookLiveness.d.ts +0 -88
  40. package/dist/plugin/HookLiveness.js +0 -322
  41. package/dist/plugin/InjectionLedger.d.ts +0 -98
  42. package/dist/plugin/InjectionLedger.js +0 -295
  43. package/dist/plugin/LessonFixer.d.ts +0 -44
  44. package/dist/plugin/LessonFixer.js +0 -45
  45. package/dist/plugin/Materializer.d.ts +0 -83
  46. package/dist/plugin/Materializer.js +0 -181
  47. package/dist/plugin/MemoryService.d.ts +0 -360
  48. package/dist/plugin/MemoryService.js +0 -1242
  49. package/dist/plugin/Migrate.d.ts +0 -17
  50. package/dist/plugin/Migrate.js +0 -271
  51. package/dist/plugin/PatternMiner.d.ts +0 -49
  52. package/dist/plugin/PatternMiner.js +0 -132
  53. package/dist/plugin/QualityGate.d.ts +0 -124
  54. package/dist/plugin/QualityGate.js +0 -119
  55. package/dist/plugin/Reflector.d.ts +0 -91
  56. package/dist/plugin/Reflector.js +0 -307
  57. package/dist/plugin/RepoIdentity.d.ts +0 -124
  58. package/dist/plugin/RepoIdentity.js +0 -300
  59. package/dist/plugin/RepoTruth.d.ts +0 -80
  60. package/dist/plugin/RepoTruth.js +0 -599
  61. package/dist/plugin/Retrospective.d.ts +0 -17
  62. package/dist/plugin/Retrospective.js +0 -216
  63. package/dist/plugin/SharedLayer.d.ts +0 -159
  64. package/dist/plugin/SharedLayer.js +0 -462
  65. package/dist/plugin/Store.d.ts +0 -29
  66. package/dist/plugin/Store.js +0 -50
  67. package/dist/plugin/ToolCallObserver.d.ts +0 -32
  68. package/dist/plugin/ToolCallObserver.js +0 -201
  69. package/dist/plugin/TuiActions.d.ts +0 -43
  70. package/dist/plugin/TuiActions.js +0 -181
  71. package/dist/plugin/TuiSnapshots.d.ts +0 -24
  72. package/dist/plugin/TuiSnapshots.js +0 -158
  73. package/dist/plugin/columns.d.ts +0 -11
  74. package/dist/plugin/columns.js +0 -54
  75. package/dist/plugin/confidence.d.ts +0 -8
  76. package/dist/plugin/confidence.js +0 -34
  77. package/dist/plugin/contract.d.ts +0 -54
  78. package/dist/plugin/contract.js +0 -386
  79. package/dist/plugin/diff.d.ts +0 -8
  80. package/dist/plugin/diff.js +0 -182
  81. package/dist/plugin/escape.d.ts +0 -39
  82. package/dist/plugin/escape.js +0 -60
  83. package/dist/plugin/fingerprint.d.ts +0 -27
  84. package/dist/plugin/fingerprint.js +0 -73
  85. package/dist/plugin/inferability.d.ts +0 -32
  86. package/dist/plugin/inferability.js +0 -88
  87. package/dist/plugin/kevin_approve.d.ts +0 -34
  88. package/dist/plugin/kevin_approve.js +0 -52
  89. package/dist/plugin/kevin_audit.d.ts +0 -229
  90. package/dist/plugin/kevin_audit.js +0 -631
  91. package/dist/plugin/kevin_bench.d.ts +0 -10
  92. package/dist/plugin/kevin_bench.js +0 -82
  93. package/dist/plugin/kevin_conflicts.d.ts +0 -9
  94. package/dist/plugin/kevin_conflicts.js +0 -50
  95. package/dist/plugin/kevin_contract.d.ts +0 -8
  96. package/dist/plugin/kevin_contract.js +0 -59
  97. package/dist/plugin/kevin_doctor.d.ts +0 -62
  98. package/dist/plugin/kevin_doctor.js +0 -196
  99. package/dist/plugin/kevin_facts.d.ts +0 -42
  100. package/dist/plugin/kevin_facts.js +0 -36
  101. package/dist/plugin/kevin_forget.d.ts +0 -33
  102. package/dist/plugin/kevin_forget.js +0 -260
  103. package/dist/plugin/kevin_native.d.ts +0 -29
  104. package/dist/plugin/kevin_native.js +0 -79
  105. package/dist/plugin/kevin_propose.d.ts +0 -23
  106. package/dist/plugin/kevin_propose.js +0 -14
  107. package/dist/plugin/kevin_publish.d.ts +0 -38
  108. package/dist/plugin/kevin_publish.js +0 -18
  109. package/dist/plugin/kevin_why.d.ts +0 -23
  110. package/dist/plugin/kevin_why.js +0 -111
  111. package/dist/plugin/memory-format.d.ts +0 -20
  112. package/dist/plugin/memory-format.js +0 -59
  113. package/dist/plugin/metrics.d.ts +0 -97
  114. package/dist/plugin/metrics.js +0 -288
  115. package/dist/plugin/okf-export.d.ts +0 -3
  116. package/dist/plugin/okf-export.js +0 -134
  117. package/dist/plugin/okf-import.d.ts +0 -76
  118. package/dist/plugin/okf-import.js +0 -271
  119. package/dist/plugin/okf.d.ts +0 -107
  120. package/dist/plugin/okf.js +0 -303
  121. package/dist/plugin/perf.d.ts +0 -35
  122. package/dist/plugin/perf.js +0 -174
  123. package/dist/plugin/query-tokenizer.d.ts +0 -13
  124. package/dist/plugin/query-tokenizer.js +0 -133
  125. package/dist/plugin/redact.d.ts +0 -2
  126. package/dist/plugin/redact.js +0 -18
  127. package/dist/plugin/replay-types.d.ts +0 -69
  128. package/dist/plugin/replay-types.js +0 -156
  129. package/dist/plugin/replay.d.ts +0 -36
  130. package/dist/plugin/replay.js +0 -202
  131. package/dist/plugin/sqlite-adapter.d.ts +0 -12
  132. package/dist/plugin/sqlite-adapter.js +0 -81
  133. package/dist/plugin/time-ms.d.ts +0 -1
  134. package/dist/plugin/time-ms.js +0 -16
  135. package/dist/plugin/tui-types.d.ts +0 -59
  136. package/dist/plugin/tui-types.js +0 -4
  137. package/dist/plugin/tui.d.ts +0 -18
  138. package/dist/plugin/tui.js +0 -198
  139. package/dist/plugin/uuid.d.ts +0 -1
  140. package/dist/plugin/uuid.js +0 -50
@@ -1,271 +0,0 @@
1
- import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
- import { uuidv7 } from "./uuid.js";
3
- const PAIR_RE = /^([a-z_]+):\s*(.*)$/i;
4
- const ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
- /**
6
- * v0.3.0 fix — Clean state-machine parser for the frontmatter bundle
7
- * format produced by `okf-export.ts::exportOkf`. Each entry is:
8
- *
9
- * ---
10
- * id: <uuid>
11
- * type: <decision|rule|pattern>
12
- * confidence: 0.70
13
- * evidence_count: 2
14
- * last_verified_at: 2026-07-25 12:34:56
15
- * fingerprint: <hex>
16
- * created: 2026-07-25 12:00:00
17
- * scope: project
18
- * ---
19
- *
20
- * <content body, may span multiple lines, may include `---` lines
21
- * within — the body terminator is the NEXT top-level `---` followed
22
- * by an `id:` line, or EOF>
23
- *
24
- * The previous implementation only ever flagged `inFm = true` once and
25
- * never reset `contentStarted` between entries, so 2..N entries were
26
- * silently dropped (bug #1). This rewrite handles arbitrary numbers of
27
- * consecutive frontmatter sections.
28
- */
29
- export function parseMarkdownBundle(text) {
30
- const lines = text.replace(/\r\n/g, "\n").split("\n");
31
- const entries = [];
32
- let i = 0;
33
- let fm = null;
34
- let body = [];
35
- // True when we have just seen a `---` opener and are collecting
36
- // `key: value` pairs until the matching `---` closer.
37
- const isFmKey = (s) => PAIR_RE.test(s) || s.trim() === "";
38
- while (i < lines.length) {
39
- const line = lines[i];
40
- // Seek an opening `---`.
41
- if (line.trim() !== "---") {
42
- i++;
43
- continue;
44
- }
45
- // Trivially empty bundle (just opener/closer with no keys) — skip.
46
- if (lines[i + 1]?.trim() === "---") {
47
- i += 2;
48
- continue;
49
- }
50
- // Begin frontmatter collection.
51
- fm = {};
52
- body = [];
53
- i++;
54
- let closed = false;
55
- while (i < lines.length) {
56
- const cur = lines[i];
57
- if (cur.trim() === "---") {
58
- closed = true;
59
- i++;
60
- break;
61
- }
62
- const m = cur.match(PAIR_RE);
63
- if (m) {
64
- fm[m[1].toLowerCase()] = m[2].trim();
65
- }
66
- i++;
67
- }
68
- if (!closed)
69
- break;
70
- if (!fm.id)
71
- continue;
72
- // Collect body until the next top-level `---` opener. The opener
73
- // is recognizable as `---` followed by an `id:` pair in the next
74
- // frontmatter block; we look ahead conservatively: a `---` line
75
- // immediately followed by a `key:` line is an opener, anything
76
- // else is body content.
77
- while (i < lines.length) {
78
- const cur = lines[i];
79
- if (cur.trim() === "---") {
80
- // peek the NEXT non-empty line — if it looks like a PAIR,
81
- // this `---` is an opener for the next entry, so stop body
82
- // here without consuming.
83
- let j = i + 1;
84
- while (j < lines.length && lines[j].trim() === "")
85
- j++;
86
- if (j < lines.length && isFmKey(lines[j]) && PAIR_RE.test(lines[j])) {
87
- break;
88
- }
89
- // Otherwise treat as body content (e.g. thematic break).
90
- body.push(cur);
91
- i++;
92
- continue;
93
- }
94
- body.push(cur);
95
- i++;
96
- }
97
- const content = body.join("\n").trim();
98
- if (!content)
99
- continue;
100
- const type = (fm.type ?? "context").toLowerCase();
101
- const id = ID_RE.test(fm.id) ? fm.id : uuidv7();
102
- const fp = fm.fingerprint ?? null;
103
- const evidenceCount = Number.parseInt(fm.evidence_count ?? "0", 10) || 0;
104
- const recurrenceCount = Number.parseInt(fm.recurrence_count ?? "0", 10) || 0;
105
- const lastVerified = fm.last_verified_at ?? null;
106
- entries.push({
107
- id,
108
- type,
109
- content,
110
- fingerprint: fp,
111
- evidence_count: evidenceCount,
112
- recurrence_count: recurrenceCount,
113
- last_verified_at: lastVerified,
114
- });
115
- }
116
- return entries;
117
- }
118
- /**
119
- * v0.3.0 fix — Fallback parser for the markdown-style `##` heading
120
- * format produced by `okf-export.ts::exportMarkdown`. The previous
121
- * version extracted `fingerprint: null` for every entry (regex was
122
- * pinned to 16 hex chars while actual fingerprints are variable
123
- * length) and contaminated `content` with the heading line and
124
- * metadata bullets. This version cleanly separates metadata bullets
125
- * (looking for `**ID:**`, `**Fingerprint:**`, `**Evidence count:**`,
126
- * `**Last verified:**`, `**Scope:**` prefixes) from the content
127
- * body, which starts after the first blank line following the bullet
128
- * block and runs until the trailing `---` separator (or EOF).
129
- */
130
- export function parseMarkdownHeadings(text) {
131
- const normalized = text.replace(/\r\n/g, "\n");
132
- // Split on `## ` headings; first chunk is the document preamble.
133
- const chunks = normalized.split(/^##\s+/m).slice(1);
134
- const entries = [];
135
- for (const chunk of chunks) {
136
- const lines = chunk.split("\n");
137
- const typeMatch = lines[0]?.match(/^([A-Za-z]+):\s*/);
138
- const type = typeMatch ? typeMatch[1].toLowerCase() : "context";
139
- const fm = {};
140
- let bodyStart = -1;
141
- // True once at least one `- **Key:**` bullet was consumed; the
142
- // body starts at the FIRST blank line AFTER the bullet block.
143
- // Blank lines before any bullet (e.g. right after the `##` heading)
144
- // must be skipped, not treated as body delimiters.
145
- let sawBullet = false;
146
- for (let k = 1; k < lines.length; k++) {
147
- const ln = lines[k];
148
- const bulletId = ln.match(/^- \*\*ID:\*\*\s*`([^`]+)`/);
149
- const bulletFp = ln.match(/^- \*\*Fingerprint:\*\*\s*`([a-f0-9]+)`/i);
150
- const bulletEv = ln.match(/^- \*\*Evidence count:\*\*\s*(\d+)/);
151
- const bulletRec = ln.match(/^- \*\*Recurrence count:\*\*\s*(\d+)/);
152
- const bulletLv = ln.match(/^- \*\*Last verified:\*\*\s*(.+)$/);
153
- if (bulletId) {
154
- fm.id = bulletId[1];
155
- sawBullet = true;
156
- }
157
- else if (bulletFp) {
158
- fm.fingerprint = bulletFp[1];
159
- sawBullet = true;
160
- }
161
- else if (bulletEv) {
162
- fm.evidence_count = bulletEv[1];
163
- sawBullet = true;
164
- }
165
- else if (bulletRec) {
166
- fm.recurrence_count = bulletRec[1];
167
- sawBullet = true;
168
- }
169
- else if (bulletLv) {
170
- fm.last_verified_at = bulletLv[1].trim();
171
- sawBullet = true;
172
- }
173
- else if (ln.trim() === "") {
174
- // First blank line after bullet block — body starts here.
175
- if (sawBullet) {
176
- bodyStart = k + 1;
177
- break;
178
- }
179
- // Leading blank line (right after the heading) — skip.
180
- }
181
- else {
182
- // Other bullets (e.g. `- **Confidence:**`, `- **Scope:**`)
183
- // are not captured — skip them; the bullet block ends at
184
- // the next blank line.
185
- }
186
- }
187
- if (bodyStart < 0)
188
- bodyStart = 1;
189
- const bodyLines = [];
190
- for (let k = bodyStart; k < lines.length; k++) {
191
- const ln = lines[k];
192
- if (ln.trim() === "---")
193
- break;
194
- bodyLines.push(ln);
195
- }
196
- const content = bodyLines.join("\n").trim();
197
- if (!content)
198
- continue;
199
- const id = fm.id && ID_RE.test(fm.id) ? fm.id : uuidv7();
200
- entries.push({
201
- id,
202
- type,
203
- content,
204
- fingerprint: fm.fingerprint ?? null,
205
- evidence_count: Number.parseInt(fm.evidence_count ?? "0", 10) || 0,
206
- recurrence_count: Number.parseInt(fm.recurrence_count ?? "0", 10) || 0,
207
- last_verified_at: fm.last_verified_at ?? null,
208
- });
209
- }
210
- return entries;
211
- }
212
- const IMPORT_ALLOWED_TYPES = new Set([
213
- "decision",
214
- "rule",
215
- "pattern",
216
- "context",
217
- ]);
218
- /**
219
- * v0.3.0 fix — Ingest a bundle (frontmatter OR markdown) into the
220
- * local SQLite store as `context` memories with `origin='imported'`.
221
- *
222
- * Fixes over the v0.3.0 baseline:
223
- * * Multi-entry bundles now produce N imports instead of N=1 — the
224
- * parser bug is closed.
225
- * * `ParsedEntry.evidence_count` and `last_verified_at` are
226
- * threaded through so causal confidence survives a round-trip.
227
- * * `ImportResult.superseded` is populated using
228
- * `countSupersedeCandidates`, which mirrors the supersede logic
229
- * in `MemoryService.save()`. Previously it was hard-coded to 0.
230
- * * Generated ids use `uuidv7()` (the project's id generator)
231
- * instead of `crypto.randomUUID()` for consistency.
232
- */
233
- export function importOkf(bundle, memoryService) {
234
- let entries = parseMarkdownBundle(bundle);
235
- if (entries.length === 0) {
236
- entries = parseMarkdownHeadings(bundle);
237
- }
238
- let imported = 0;
239
- let superseded = 0;
240
- for (const entry of entries) {
241
- if (!IMPORT_ALLOWED_TYPES.has(entry.type))
242
- continue;
243
- const fp = entry.fingerprint ?? computeFingerprint(entry.content, undefined);
244
- // Count rows that save() will mark as superseded (decision/rule
245
- // with the same fingerprint). The supersede update itself runs
246
- // inside MemoryService.save() in a single transaction; we count
247
- // here to surface the value to the caller.
248
- superseded += memoryService.countSupersedeCandidates(entry.type, fp, null);
249
- memoryService.save({
250
- type: entry.type,
251
- // BUG-008 — preserve the bundle id so a round-trip (export →
252
- // import) keeps memory identity and `getById` stays stable.
253
- id: entry.id,
254
- // BUG-009 — the content is the bundle body verbatim. The old
255
- // code appended `[imported evidence_count=N, ...]`, which the
256
- // ContextInjector later injected verbatim into model prompts;
257
- // the values travel via the typed fields below instead.
258
- content: entry.content,
259
- scope: "project",
260
- origin: "imported",
261
- fingerprint: fp,
262
- evidenceCount: entry.evidence_count > 0 ? entry.evidence_count : undefined,
263
- // BUG-008 — restore the recurrence demotion so the re-imported
264
- // copy keeps the v0.4.0 two-sided confidence of the source.
265
- recurrenceCount: entry.recurrence_count > 0 ? entry.recurrence_count : undefined,
266
- lastVerifiedAt: entry.last_verified_at ?? undefined,
267
- });
268
- imported++;
269
- }
270
- return { imported, superseded };
271
- }
@@ -1,107 +0,0 @@
1
- /** OKF v2 format version marker, written on the first header line. */
2
- export declare const OKF_VERSION = 2;
3
- /** A single canonicalized entry line may not exceed this many bytes. */
4
- export declare const MAX_LINE_BYTES = 4096;
5
- /** A serialized corpus may not exceed this many entries. */
6
- export declare const MAX_ENTRIES = 2000;
7
- export type OkfOp = "assert" | "tombstone";
8
- /**
9
- * A v2 entry. The field set is plan §5.3/§5.4: the two integer
10
- * counters (`evidence`, `recurrence`) are transported and
11
- * `confidence` is DERIVED at read time — the file contains no
12
- * floats at all, so two machines computing a confidence slightly
13
- * differently still emit byte-identical lines.
14
- */
15
- export interface OkfEntry {
16
- entry_id: string;
17
- type: "decision" | "rule" | "pattern" | "solution";
18
- statement: string;
19
- scope: string | null;
20
- evidence: number;
21
- recurrence: number;
22
- origin: string;
23
- author_hash: string | null;
24
- op: OkfOp;
25
- created_at: string;
26
- supersedes: string | null;
27
- }
28
- /**
29
- * Canonical JSON of an entry: keys in alphabetical order, no space
30
- * argument, integers only (no float is ever written — plan §5.3).
31
- * The object is constructed explicitly so the source object's
32
- * insertion order can never leak into the bytes.
33
- */
34
- export declare function canonicalize(e: OkfEntry): string;
35
- /**
36
- * Emit the v2 file: three `#` header lines, then every entry sorted
37
- * ascending by `entry_id`, one per line, LF endings, exactly one
38
- * terminating newline. Entries over MAX_LINE_BYTES (measured in
39
- * BYTES — a Japanese statement passes a `.length` check and fails a
40
- * byte check) and corpora over MAX_ENTRIES are refused, not
41
- * truncated (plan §5.3, physical rules).
42
- */
43
- export declare function serialize(entries: OkfEntry[], repoId: string, version: string): string;
44
- /**
45
- * Group both corpora by `entry_id`, fold each group through `join()`,
46
- * and return the result sorted ascending by `entry_id`. Order of the
47
- * inputs does not matter: join() is commutative, and the fold order is
48
- * the only place input order could leak in — it cannot, by algebra.
49
- */
50
- export declare function merge(a: OkfEntry[], b: OkfEntry[]): OkfEntry[];
51
- /**
52
- * Derived, never serialized — reuses the v0.4.0 two-sided confidence
53
- * formula verbatim (plan §5.4). A lesson that keeps recurring is
54
- * DEMOTED rather than merely un-promoted; shared and local memories
55
- * are repriced identically by the same formula.
56
- */
57
- export declare function deriveConfidence(e: OkfEntry): number;
58
- export interface RejectedLine {
59
- line: number;
60
- reason: string;
61
- }
62
- export interface ParseResult {
63
- version: number;
64
- repoId: string | null;
65
- /** Folded via join() and sorted ascending by entry_id. */
66
- entries: OkfEntry[];
67
- /** parse never throws; bad lines are reported. */
68
- rejected: RejectedLine[];
69
- /** Duplicate entry_ids collapsed by join(). */
70
- folded: number;
71
- }
72
- /**
73
- * v0.8.0 (K8-013 / plan §5.4, D8-13) — the field lattice. Every field
74
- * resolves through a max, a min, or an absorbing boolean OR over a
75
- * totally ordered set; there is no "prefer the newer" anywhere.
76
- * Precondition: `a.entry_id === b.entry_id`.
77
- */
78
- export declare function join(a: OkfEntry, b: OkfEntry): OkfEntry;
79
- /**
80
- * parse() is a TOTAL function — it never throws, on any input,
81
- * including binary (D8-14). The file is expected to arrive damaged:
82
- * a conflict resolution leaves `<<<<<<< HEAD` markers, an editor
83
- * truncates the last line, a merge tool mangles an encoding. Every
84
- * unusable line becomes a `RejectedLine`; the good entries survive.
85
- *
86
- * Reason taxonomy (closed): not_okf, version_ahead, bad_json,
87
- * missing_field, wrong_type, id_mismatch, unknown_op, line_too_long,
88
- * corpus_too_large.
89
- */
90
- export declare function parse(text: string): ParseResult;
91
- /**
92
- * Compute the identity of an OKF entry: `fnv1a64("okf:v2\0" + type +
93
- * "\0" + statement + "\0" + (scope ?? ""))`.
94
- *
95
- * D8-05 — deliberately NOT `fingerprint()`:
96
- * - it salts with `project_id`, so every clone would produce a
97
- * different id for the same rule, the shared file would accumulate
98
- * one entry per developer per rule, and the merge fold would never
99
- * once fire;
100
- * - it runs `normalize()`, which lowercases and rewrites
101
- * `path.ext:line:col` — both destructive for a curated statement,
102
- * where casing and file paths carry meaning.
103
- *
104
- * NUL separators keep (`rule`, "ab", "c") distinct from
105
- * ("rule", "a", "bc") — plain concatenation could never.
106
- */
107
- export declare function computeEntryId(type: string, statement: string, scope?: string | null): string;
@@ -1,303 +0,0 @@
1
- // ============================================================
2
- // Kevin 0.8.0 — OKF v2 codec (F2, K8-010 … K8-015)
3
- // ============================================================
4
- // The Open Kevin Format: the committed, shareable artifact of the
5
- // shared layer (plan §3.3). Everything in this module is a pure
6
- // function — no database, no filesystem, no clock. The format is
7
- // the release: if `join()` is not a semilattice then silent git
8
- // merges, order-independent imports and correct both-sides
9
- // conflict resolution all evaporate.
10
- //
11
- // Entry ids are deliberately the THIRD fingerprint identity
12
- // dimension in the codebase (v0.7.0's Principle 26; plan §3.3's
13
- // table). The first is `fingerprint()` (error text, salted with
14
- // `project_id`, normalized) and the second is the convention /
15
- // decision fingerprints of v0.7.0 (derived from the normalized
16
- // statement). This one is unsalted and un-normalized, and it is
17
- // the ONLY cross-machine-stable dimension in the schema.
18
- // ============================================================
19
- import { computeConfidence } from "./confidence.js";
20
- import { fnv1a64 } from "./fingerprint.js";
21
- /** OKF v2 format version marker, written on the first header line. */
22
- export const OKF_VERSION = 2;
23
- /** A single canonicalized entry line may not exceed this many bytes. */
24
- export const MAX_LINE_BYTES = 4096;
25
- /** A serialized corpus may not exceed this many entries. */
26
- export const MAX_ENTRIES = 2000;
27
- const OKF_KEY_ORDER = [
28
- "author_hash",
29
- "created_at",
30
- "entry_id",
31
- "evidence",
32
- "op",
33
- "origin",
34
- "recurrence",
35
- "scope",
36
- "statement",
37
- "supersedes",
38
- "type",
39
- ];
40
- /**
41
- * Canonical JSON of an entry: keys in alphabetical order, no space
42
- * argument, integers only (no float is ever written — plan §5.3).
43
- * The object is constructed explicitly so the source object's
44
- * insertion order can never leak into the bytes.
45
- */
46
- export function canonicalize(e) {
47
- const ordered = {};
48
- for (const key of OKF_KEY_ORDER) {
49
- ordered[key] = e[key];
50
- }
51
- return JSON.stringify(ordered);
52
- }
53
- /**
54
- * Emit the v2 file: three `#` header lines, then every entry sorted
55
- * ascending by `entry_id`, one per line, LF endings, exactly one
56
- * terminating newline. Entries over MAX_LINE_BYTES (measured in
57
- * BYTES — a Japanese statement passes a `.length` check and fails a
58
- * byte check) and corpora over MAX_ENTRIES are refused, not
59
- * truncated (plan §5.3, physical rules).
60
- */
61
- export function serialize(entries, repoId, version) {
62
- if (entries.length > MAX_ENTRIES) {
63
- throw new Error(`okf: corpus of ${entries.length} entries exceeds MAX_ENTRIES (${MAX_ENTRIES})`);
64
- }
65
- const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
66
- const lines = [
67
- `#okf ${OKF_VERSION}`,
68
- `#repo ${repoId}`,
69
- `#generated-by opencode-kevin/${version}`,
70
- ];
71
- for (const e of sorted) {
72
- const line = canonicalize(e);
73
- if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
74
- throw new Error(`okf: entry ${e.entry_id} canonicalizes to ${Buffer.byteLength(line, "utf8")} bytes, over MAX_LINE_BYTES (${MAX_LINE_BYTES})`);
75
- }
76
- lines.push(line);
77
- }
78
- return `${lines.join("\n")}\n`;
79
- }
80
- /**
81
- * Group both corpora by `entry_id`, fold each group through `join()`,
82
- * and return the result sorted ascending by `entry_id`. Order of the
83
- * inputs does not matter: join() is commutative, and the fold order is
84
- * the only place input order could leak in — it cannot, by algebra.
85
- */
86
- export function merge(a, b) {
87
- const byId = new Map();
88
- for (const e of [...a, ...b]) {
89
- const existing = byId.get(e.entry_id);
90
- byId.set(e.entry_id, existing ? join(existing, e) : e);
91
- }
92
- return [...byId.values()].sort((x, y) => x.entry_id < y.entry_id ? -1 : x.entry_id > y.entry_id ? 1 : 0);
93
- }
94
- /**
95
- * Derived, never serialized — reuses the v0.4.0 two-sided confidence
96
- * formula verbatim (plan §5.4). A lesson that keeps recurring is
97
- * DEMOTED rather than merely un-promoted; shared and local memories
98
- * are repriced identically by the same formula.
99
- */
100
- export function deriveConfidence(e) {
101
- return computeConfidence(e.evidence, e.recurrence);
102
- }
103
- const OKF_TYPES = new Set(["decision", "rule", "pattern", "solution"]);
104
- function pickMin(a, b) {
105
- if (a === null)
106
- return b;
107
- if (b === null)
108
- return a;
109
- return a <= b ? a : b;
110
- }
111
- /**
112
- * v0.8.0 (K8-013 / plan §5.4, D8-13) — the field lattice. Every field
113
- * resolves through a max, a min, or an absorbing boolean OR over a
114
- * totally ordered set; there is no "prefer the newer" anywhere.
115
- * Precondition: `a.entry_id === b.entry_id`.
116
- */
117
- export function join(a, b) {
118
- return {
119
- entry_id: a.entry_id,
120
- // Equal by construction unless a hash collision — resolve by
121
- // lexicographic min so the function stays total and deterministic.
122
- type: a.type <= b.type ? a.type : b.type,
123
- statement: a.statement <= b.statement ? a.statement : b.statement,
124
- scope: pickMin(a.scope, b.scope),
125
- evidence: Math.max(a.evidence, b.evidence),
126
- recurrence: Math.max(a.recurrence, b.recurrence),
127
- origin: a.origin <= b.origin ? a.origin : b.origin,
128
- author_hash: pickMin(a.author_hash, b.author_hash),
129
- // Tombstone absorbs: no undelete, no timestamp tiebreak (D8-09).
130
- op: a.op === "tombstone" || b.op === "tombstone" ? "tombstone" : "assert",
131
- // Birthday semantics: min is the only choice stable under replay.
132
- created_at: a.created_at <= b.created_at ? a.created_at : b.created_at,
133
- supersedes: pickMin(a.supersedes, b.supersedes),
134
- };
135
- }
136
- /**
137
- * parse() is a TOTAL function — it never throws, on any input,
138
- * including binary (D8-14). The file is expected to arrive damaged:
139
- * a conflict resolution leaves `<<<<<<< HEAD` markers, an editor
140
- * truncates the last line, a merge tool mangles an encoding. Every
141
- * unusable line becomes a `RejectedLine`; the good entries survive.
142
- *
143
- * Reason taxonomy (closed): not_okf, version_ahead, bad_json,
144
- * missing_field, wrong_type, id_mismatch, unknown_op, line_too_long,
145
- * corpus_too_large.
146
- */
147
- export function parse(text) {
148
- const rejected = [];
149
- const reject = (line, reason) => {
150
- rejected.push({ line, reason });
151
- };
152
- // A UTF-8 BOM breaks the `#okf ` prefix check; strip it first.
153
- const body = text.replace(/^\uFEFF/, "");
154
- const lines = body.split(/\r\n|\r|\n/);
155
- let version = 0;
156
- let repoId = null;
157
- if (!(lines[0]?.startsWith("#okf ") ?? false)) {
158
- reject(1, "not_okf");
159
- return { version, repoId, entries: [], rejected, folded: 0 };
160
- }
161
- const declared = Number(lines[0].slice(5));
162
- version = Number.isInteger(declared) && declared >= 0 ? declared : 0;
163
- if (version > OKF_VERSION) {
164
- // Guessing at a future format's semantics is how corpora get
165
- // corrupted — refuse the whole file, never a best-effort parse.
166
- return {
167
- version,
168
- repoId: null,
169
- entries: [],
170
- rejected: [{ line: 1, reason: "version_ahead" }],
171
- folded: 0,
172
- };
173
- }
174
- if (lines[1]?.startsWith("#repo ")) {
175
- repoId = lines[1].slice(6) || null;
176
- }
177
- const byId = new Map();
178
- let folded = 0;
179
- let accepted = 0;
180
- for (let i = 2; i < lines.length; i++) {
181
- const line = lines[i];
182
- if (line === "")
183
- continue; // trailing newline / blank lines
184
- if (line.startsWith("#"))
185
- continue; // header/comment lines
186
- const lineNo = i + 1;
187
- if (Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) {
188
- reject(lineNo, "line_too_long");
189
- continue;
190
- }
191
- let raw;
192
- try {
193
- raw = JSON.parse(line);
194
- }
195
- catch {
196
- reject(lineNo, "bad_json");
197
- continue;
198
- }
199
- if (typeof raw !== "object" || raw === null) {
200
- reject(lineNo, "bad_json");
201
- continue;
202
- }
203
- const o = raw;
204
- const missing = (k) => o[k] === undefined;
205
- const wrong = (k, ok) => !missing(k) && !ok(o[k]);
206
- const stringField = (k) => !wrong(k, (v) => typeof v === "string");
207
- const nullableString = (k) => !wrong(k, (v) => v === null || typeof v === "string");
208
- const intField = (k) => !wrong(k, (v) => typeof v === "number" && Number.isInteger(v) && v >= 0);
209
- if (missing("entry_id"))
210
- reject(lineNo, "missing_field");
211
- else if (!stringField("entry_id"))
212
- reject(lineNo, "wrong_type");
213
- else if (missing("type"))
214
- reject(lineNo, "missing_field");
215
- else if (typeof o.type !== "string" || !OKF_TYPES.has(o.type))
216
- reject(lineNo, "wrong_type");
217
- else if (missing("statement"))
218
- reject(lineNo, "missing_field");
219
- else if (!stringField("statement"))
220
- reject(lineNo, "wrong_type");
221
- else if (!nullableString("scope"))
222
- reject(lineNo, "wrong_type");
223
- else if (missing("evidence"))
224
- reject(lineNo, "missing_field");
225
- else if (!intField("evidence"))
226
- reject(lineNo, "wrong_type");
227
- else if (missing("recurrence"))
228
- reject(lineNo, "missing_field");
229
- else if (!intField("recurrence"))
230
- reject(lineNo, "wrong_type");
231
- else if (missing("origin"))
232
- reject(lineNo, "missing_field");
233
- else if (!stringField("origin"))
234
- reject(lineNo, "wrong_type");
235
- else if (!nullableString("author_hash"))
236
- reject(lineNo, "wrong_type");
237
- else if (missing("op"))
238
- reject(lineNo, "missing_field");
239
- else if (o.op !== "assert" && o.op !== "tombstone")
240
- reject(lineNo, "unknown_op");
241
- else if (missing("created_at"))
242
- reject(lineNo, "missing_field");
243
- else if (!stringField("created_at"))
244
- reject(lineNo, "wrong_type");
245
- else if (!nullableString("supersedes"))
246
- reject(lineNo, "wrong_type");
247
- else {
248
- // The tamper-evident check: a hand-edited statement without
249
- // an updated entry_id is caught here, not silently ranked.
250
- const recomputed = computeEntryId(o.type, o.statement, (o.scope ?? null));
251
- if (recomputed !== o.entry_id) {
252
- reject(lineNo, "id_mismatch");
253
- continue;
254
- }
255
- const e = {
256
- entry_id: o.entry_id,
257
- type: o.type,
258
- statement: o.statement,
259
- scope: (o.scope ?? null),
260
- evidence: o.evidence,
261
- recurrence: o.recurrence,
262
- origin: o.origin,
263
- author_hash: (o.author_hash ?? null),
264
- op: o.op,
265
- created_at: o.created_at,
266
- supersedes: (o.supersedes ?? null),
267
- };
268
- const existing = byId.get(e.entry_id);
269
- if (existing) {
270
- byId.set(e.entry_id, join(existing, e));
271
- folded++;
272
- continue;
273
- }
274
- if (accepted >= MAX_ENTRIES) {
275
- reject(lineNo, "corpus_too_large");
276
- continue;
277
- }
278
- byId.set(e.entry_id, e);
279
- accepted++;
280
- }
281
- }
282
- const entries = [...byId.values()].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
283
- return { version, repoId, entries, rejected, folded };
284
- }
285
- /**
286
- * Compute the identity of an OKF entry: `fnv1a64("okf:v2\0" + type +
287
- * "\0" + statement + "\0" + (scope ?? ""))`.
288
- *
289
- * D8-05 — deliberately NOT `fingerprint()`:
290
- * - it salts with `project_id`, so every clone would produce a
291
- * different id for the same rule, the shared file would accumulate
292
- * one entry per developer per rule, and the merge fold would never
293
- * once fire;
294
- * - it runs `normalize()`, which lowercases and rewrites
295
- * `path.ext:line:col` — both destructive for a curated statement,
296
- * where casing and file paths carry meaning.
297
- *
298
- * NUL separators keep (`rule`, "ab", "c") distinct from
299
- * ("rule", "a", "bc") — plain concatenation could never.
300
- */
301
- export function computeEntryId(type, statement, scope) {
302
- return fnv1a64(`okf:v2\u0000${type}\u0000${statement}\u0000${scope ?? ""}`);
303
- }