@jmtrin/opencode-kevin 1.0.0 → 1.2.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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -8
  3. package/dist/migrations/001_initial.sql +91 -91
  4. package/dist/migrations/002_indexes.sql +13 -13
  5. package/dist/migrations/003_v02_signal.sql +57 -57
  6. package/dist/migrations/004_v03_knowledge.sql +138 -138
  7. package/dist/migrations/005_v04_signal.sql +57 -57
  8. package/dist/migrations/006_v05_glassbox.sql +118 -118
  9. package/dist/migrations/007_v06_pull.sql +144 -144
  10. package/dist/migrations/012_v11_drift.sql +24 -0
  11. package/dist/plugin/Archiver.js +2 -17
  12. package/dist/plugin/CausalChain.js +32 -13
  13. package/dist/plugin/ChatBridge.d.ts +41 -0
  14. package/dist/plugin/ChatBridge.js +103 -0
  15. package/dist/plugin/ConflictDetector.js +7 -29
  16. package/dist/plugin/DashboardHtml.d.ts +5 -0
  17. package/dist/plugin/DashboardHtml.js +180 -0
  18. package/dist/plugin/Feedback.js +2 -19
  19. package/dist/plugin/HookLiveness.d.ts +1 -0
  20. package/dist/plugin/HookLiveness.js +11 -27
  21. package/dist/plugin/InjectionLedger.js +104 -57
  22. package/dist/plugin/Materializer.js +1 -88
  23. package/dist/plugin/MemoryService.d.ts +59 -1
  24. package/dist/plugin/MemoryService.js +13 -106
  25. package/dist/plugin/Migrate.js +5 -0
  26. package/dist/plugin/Retrospective.js +7 -0
  27. package/dist/plugin/ToolCallObserver.js +18 -5
  28. package/dist/plugin/TuiActions.d.ts +43 -0
  29. package/dist/plugin/TuiActions.js +181 -0
  30. package/dist/plugin/TuiSnapshots.d.ts +24 -0
  31. package/dist/plugin/TuiSnapshots.js +158 -0
  32. package/dist/plugin/capabilities.d.ts +2 -0
  33. package/dist/plugin/capabilities.js +3 -0
  34. package/dist/plugin/columns.d.ts +11 -0
  35. package/dist/plugin/columns.js +54 -0
  36. package/dist/plugin/contract.d.ts +8 -0
  37. package/dist/plugin/contract.js +23 -5
  38. package/dist/plugin/index.d.ts +2 -2
  39. package/dist/plugin/index.js +315 -10
  40. package/dist/plugin/kevin_audit.d.ts +17 -1
  41. package/dist/plugin/kevin_audit.js +69 -1
  42. package/dist/plugin/kevin_forget.d.ts +33 -0
  43. package/dist/plugin/kevin_forget.js +260 -0
  44. package/dist/plugin/kevin_why.js +1 -18
  45. package/dist/plugin/metrics.d.ts +1 -1
  46. package/dist/plugin/metrics.js +8 -0
  47. package/dist/plugin/query-tokenizer.js +56 -8
  48. package/dist/plugin/time-ms.d.ts +1 -0
  49. package/dist/plugin/time-ms.js +16 -0
  50. package/dist/plugin/tui-types.d.ts +59 -0
  51. package/dist/plugin/tui-types.js +4 -0
  52. package/dist/plugin/tui.d.ts +18 -0
  53. package/dist/plugin/tui.js +198 -0
  54. package/package.json +8 -2
@@ -1,3 +1,6 @@
1
+ import { existsSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
1
4
  import { effectivePrePromptCap } from "./ContextInjector.js";
2
5
  import { hasRepoIdColumn } from "./MemoryService.js";
3
6
  import { contractDigest, describeContract } from "./contract.js";
@@ -21,7 +24,7 @@ function scalar(store, sql, ...params) {
21
24
  const row = store.prepare(sql).get(...params);
22
25
  return row.n ?? 0;
23
26
  }
24
- export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES, projectId, repoId) {
27
+ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES, projectId, repoId, tuiRoot) {
25
28
  let partial = false;
26
29
  // Memories block: `total` + the three GROUP BYs are pre-006 safe; the
27
30
  // lifecycle counters need the 006 columns and fall back separately.
@@ -542,6 +545,70 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
542
545
  clause_count: liveContract.clauses.length,
543
546
  deprecated_count: liveContract.clauses.filter((c) => c.deprecated).length,
544
547
  };
548
+ // v1.2.0 (K12-014/K12-019) — TUI block: best-effort fs introspection (absent → nulls)
549
+ let tui;
550
+ try {
551
+ const enabled_setting = settings.tui_snapshots_enabled ?? "0";
552
+ const root = tuiRoot ?? join(homedir(), ".opencode-kevin", "tui");
553
+ let last_flush_age_s = null;
554
+ try {
555
+ const metaRaw = readFileSync(join(root, "meta.json"), "utf8");
556
+ const meta = JSON.parse(metaRaw);
557
+ if (meta.generatedAt) {
558
+ const age = (Date.now() - Date.parse(meta.generatedAt)) / 1000;
559
+ if (Number.isFinite(age) && age >= 0)
560
+ last_flush_age_s = Math.floor(age);
561
+ }
562
+ }
563
+ catch { }
564
+ let mailbox_depth = null;
565
+ try {
566
+ if (existsSync(join(root, "actions.json"))) {
567
+ const raw = readFileSync(join(root, "actions.json"), "utf8");
568
+ const parsed = JSON.parse(raw);
569
+ if (Array.isArray(parsed.actions))
570
+ mailbox_depth = parsed.actions.length;
571
+ else
572
+ mailbox_depth = null;
573
+ }
574
+ else {
575
+ mailbox_depth = null;
576
+ }
577
+ }
578
+ catch {
579
+ mailbox_depth = null;
580
+ }
581
+ let last_results = null;
582
+ try {
583
+ const raw = readFileSync(join(root, "results.json"), "utf8");
584
+ last_results = JSON.parse(raw);
585
+ }
586
+ catch {
587
+ last_results = null;
588
+ }
589
+ let dashboard_last_write_age_s = null;
590
+ try {
591
+ const st = statSync(join(root, "dashboard.html"));
592
+ const age = (Date.now() - st.mtimeMs) / 1000;
593
+ if (Number.isFinite(age) && age >= 0)
594
+ dashboard_last_write_age_s = Math.floor(age);
595
+ }
596
+ catch {
597
+ dashboard_last_write_age_s = null;
598
+ }
599
+ const bridge_interceptions = metrics.get("tui_actions_invoked") ?? 0;
600
+ tui = {
601
+ enabled_setting,
602
+ last_flush_age_s,
603
+ mailbox_depth,
604
+ last_results,
605
+ dashboard_last_write_age_s,
606
+ bridge_interceptions,
607
+ };
608
+ }
609
+ catch {
610
+ tui = undefined;
611
+ }
545
612
  return {
546
613
  memories,
547
614
  injections,
@@ -558,6 +625,7 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
558
625
  host,
559
626
  perf,
560
627
  contract,
628
+ tui,
561
629
  partial,
562
630
  };
563
631
  }
@@ -0,0 +1,33 @@
1
+ import type { MemoryService } from "./MemoryService.js";
2
+ import type { SharedLayer } from "./SharedLayer.js";
3
+ import type { Store } from "./Store.js";
4
+ import type { Metrics } from "./metrics.js";
5
+ export interface ForgetInput {
6
+ ids: string[];
7
+ confirm?: boolean;
8
+ }
9
+ export interface Deps {
10
+ store: Store;
11
+ memoryService: MemoryService;
12
+ sharedLayer: SharedLayer;
13
+ okfPath: string;
14
+ metrics: Metrics;
15
+ }
16
+ export interface ForgetResult {
17
+ action: "forget";
18
+ ok: boolean;
19
+ dry_run: boolean;
20
+ per_id: Array<{
21
+ id: string;
22
+ archived: boolean;
23
+ reason?: string;
24
+ tombstone?: {
25
+ entry_id: string;
26
+ planned: boolean;
27
+ applied: boolean;
28
+ };
29
+ }>;
30
+ noop?: boolean;
31
+ reason?: string;
32
+ }
33
+ export declare function handleForget(input: ForgetInput, deps: Deps): ForgetResult;
@@ -0,0 +1,260 @@
1
+ import { computeEntryId } from "./okf.js";
2
+ export function handleForget(input, deps) {
3
+ // v1.1.0 — every invocation counts, including dry runs and refusals (K11-005)
4
+ try {
5
+ deps.metrics.incr("forget_requests_total", 1);
6
+ }
7
+ catch {
8
+ // best-effort
9
+ }
10
+ if (!input.ids || input.ids.length === 0) {
11
+ return {
12
+ action: "forget",
13
+ ok: false,
14
+ dry_run: input.confirm !== true,
15
+ per_id: [],
16
+ reason: "no_ids",
17
+ };
18
+ }
19
+ const isDry = input.confirm !== true;
20
+ const per_id = [];
21
+ let anyChange = false;
22
+ let anyTombstonePlanned = false;
23
+ // Helper to compute entry_id for a memory row
24
+ const getEntryId = (row) => {
25
+ if (row.shared_entry_id)
26
+ return row.shared_entry_id;
27
+ return computeEntryId(row.type, row.content, row.scope);
28
+ };
29
+ // For dry_run: just plan, mutate nothing
30
+ if (isDry) {
31
+ for (const id of input.ids) {
32
+ const row = deps.store
33
+ .prepare("SELECT id, type, content, scope, status, layer, shared_entry_id FROM memories WHERE id = ?")
34
+ .get(id);
35
+ if (!row) {
36
+ per_id.push({ id, archived: false, reason: "not_found" });
37
+ continue;
38
+ }
39
+ if (row.status === "archived") {
40
+ // Idempotence: already archived
41
+ const isShared = row.layer === "shared" || row.shared_entry_id !== null;
42
+ if (isShared) {
43
+ const entryId = getEntryId(row);
44
+ try {
45
+ const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
46
+ const planned = plan.write.outcome !== "refused" && plan.entriesAdded > 0;
47
+ // Already archived implies tombstone already applied, so planned should be false (noop)
48
+ per_id.push({
49
+ id,
50
+ archived: false,
51
+ reason: "already_archived",
52
+ tombstone: { entry_id: entryId, planned: false, applied: false },
53
+ });
54
+ void plan;
55
+ void planned;
56
+ }
57
+ catch {
58
+ per_id.push({
59
+ id,
60
+ archived: false,
61
+ reason: "already_archived",
62
+ tombstone: { entry_id: entryId, planned: false, applied: false },
63
+ });
64
+ }
65
+ }
66
+ else {
67
+ per_id.push({ id, archived: false, reason: "already_archived" });
68
+ }
69
+ continue;
70
+ }
71
+ // Would archive
72
+ anyChange = true;
73
+ const isShared = row.layer === "shared" || row.shared_entry_id !== null;
74
+ if (isShared) {
75
+ const entryId = getEntryId(row);
76
+ let planned = false;
77
+ try {
78
+ const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
79
+ // planTombstone returns ExportPlan with write outcome; if refused, planned false
80
+ planned = plan.write.outcome !== "refused";
81
+ anyTombstonePlanned = anyTombstonePlanned || planned;
82
+ per_id.push({
83
+ id,
84
+ archived: true,
85
+ tombstone: { entry_id: entryId, planned, applied: false },
86
+ });
87
+ }
88
+ catch {
89
+ per_id.push({
90
+ id,
91
+ archived: true,
92
+ tombstone: { entry_id: entryId, planned: false, applied: false },
93
+ });
94
+ }
95
+ }
96
+ else {
97
+ per_id.push({ id, archived: true });
98
+ }
99
+ }
100
+ const noop = !anyChange;
101
+ return {
102
+ action: "forget",
103
+ ok: true,
104
+ dry_run: true,
105
+ per_id,
106
+ ...(noop ? { noop: true } : {}),
107
+ };
108
+ }
109
+ // Apply mode: confirm === true
110
+ // We need to archive locally and publish tombstones through applyExport
111
+ let partial = false;
112
+ let appliedTombstones = 0;
113
+ for (const id of input.ids) {
114
+ const row = deps.store
115
+ .prepare("SELECT id, type, content, scope, status, layer, shared_entry_id FROM memories WHERE id = ?")
116
+ .get(id);
117
+ if (!row) {
118
+ per_id.push({ id, archived: false, reason: "not_found" });
119
+ continue;
120
+ }
121
+ if (row.status === "archived") {
122
+ const isShared = row.layer === "shared" || row.shared_entry_id !== null;
123
+ if (isShared) {
124
+ const entryId = getEntryId(row);
125
+ per_id.push({
126
+ id,
127
+ archived: false,
128
+ reason: "already_archived",
129
+ tombstone: { entry_id: entryId, planned: false, applied: false },
130
+ });
131
+ }
132
+ else {
133
+ per_id.push({ id, archived: false, reason: "already_archived" });
134
+ }
135
+ continue;
136
+ }
137
+ // Archive locally in a transaction
138
+ let archived = false;
139
+ try {
140
+ deps.store.transaction(() => {
141
+ deps.store
142
+ .prepare(`UPDATE memories SET status = 'archived', archived_at = datetime('now'), updated_at = datetime('now') WHERE id = ? AND status != 'archived'`)
143
+ .run(id);
144
+ const changes = deps.store.prepare("SELECT changes() AS c").get();
145
+ if (changes.c > 0)
146
+ archived = true;
147
+ });
148
+ }
149
+ catch (e) {
150
+ per_id.push({ id, archived: false, reason: "db_error" });
151
+ partial = true;
152
+ continue;
153
+ }
154
+ anyChange = anyChange || archived;
155
+ const isShared = row.layer === "shared" || row.shared_entry_id !== null;
156
+ if (!isShared) {
157
+ per_id.push({ id, archived });
158
+ continue;
159
+ }
160
+ const entryId = getEntryId(row);
161
+ // Plan tombstone
162
+ let plan;
163
+ try {
164
+ plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
165
+ }
166
+ catch (e) {
167
+ // plan failure: rollback DB archive for this id (best-effort)
168
+ try {
169
+ deps.store
170
+ .prepare(`UPDATE memories SET status = 'active', archived_at = NULL WHERE id = ?`)
171
+ .run(id);
172
+ }
173
+ catch { }
174
+ per_id.push({
175
+ id,
176
+ archived: false,
177
+ reason: "plan_failed",
178
+ tombstone: { entry_id: entryId, planned: false, applied: false },
179
+ });
180
+ partial = true;
181
+ continue;
182
+ }
183
+ if (plan.write.outcome === "refused") {
184
+ // Refusal reasons are reused verbatim (repo_mismatch, unknown_entry)
185
+ const reason = plan.write.reason ?? "refused";
186
+ per_id.push({
187
+ id,
188
+ archived,
189
+ reason,
190
+ tombstone: { entry_id: entryId, planned: false, applied: false },
191
+ });
192
+ // DB archive already done; keep it (local archive is independent of shared refusal)
193
+ continue;
194
+ }
195
+ // Apply through single write funnel
196
+ try {
197
+ const applied = deps.sharedLayer.applyExport(plan);
198
+ const wasWritten = applied.applied === "written";
199
+ const wasNoop = applied.applied === "noop";
200
+ if (wasWritten)
201
+ appliedTombstones++;
202
+ // per_id tombstone: planned true if not refused, applied true only if written
203
+ per_id.push({
204
+ id,
205
+ archived,
206
+ tombstone: {
207
+ entry_id: entryId,
208
+ planned: true,
209
+ applied: wasWritten,
210
+ },
211
+ });
212
+ void wasNoop;
213
+ }
214
+ catch (e) {
215
+ // v1.1.0 — failure mid-way: transaction rollback restores DB; already-applied OKF write is reported honestly
216
+ // For this id, DB was already archived, but file write failed. We attempt to rollback DB for this id.
217
+ try {
218
+ deps.store
219
+ .prepare(`UPDATE memories SET status = 'active', archived_at = NULL WHERE id = ?`)
220
+ .run(id);
221
+ archived = false;
222
+ }
223
+ catch { }
224
+ per_id.push({
225
+ id,
226
+ archived: false,
227
+ reason: "partial",
228
+ tombstone: { entry_id: entryId, planned: true, applied: false },
229
+ });
230
+ partial = true;
231
+ // Continue to next id? According spec, failure mid-way reports ok:false reason partial
232
+ // We keep processing remaining ids? For now we continue but mark partial.
233
+ }
234
+ }
235
+ // Metrics: increment forget_tombstones_published by applied count (only when written, not noop)
236
+ if (appliedTombstones > 0) {
237
+ try {
238
+ deps.metrics.incr("forget_tombstones_published", appliedTombstones);
239
+ }
240
+ catch { }
241
+ }
242
+ const noop = !anyChange && per_id.every((p) => p.archived === false);
243
+ if (partial) {
244
+ return {
245
+ action: "forget",
246
+ ok: false,
247
+ dry_run: false,
248
+ per_id,
249
+ reason: "partial",
250
+ ...(noop ? { noop: true } : {}),
251
+ };
252
+ }
253
+ return {
254
+ action: "forget",
255
+ ok: true,
256
+ dry_run: false,
257
+ per_id,
258
+ ...(noop ? { noop: true } : {}),
259
+ };
260
+ }
@@ -1,24 +1,7 @@
1
1
  import { TS_CODE_RULES } from "./Reflector.js";
2
+ import { hasFeedbackColumns } from "./columns.js";
2
3
  import { computeConfidence } from "./confidence.js";
3
4
  import { toMatchClause, tokenizeQuery } from "./query-tokenizer.js";
4
- // v0.5.0 (K5-010 / plan §5.3) — pre-006 DBs lack the feedback columns;
5
- // kevin_why must not reference them (it degrades via the try/catch below,
6
- // which is for missing ROWS, not missing COLUMNS). Probe once per store.
7
- const feedbackColumnCache = new WeakMap();
8
- function hasFeedbackColumns(store) {
9
- const cached = feedbackColumnCache.get(store);
10
- if (cached !== undefined)
11
- return cached;
12
- try {
13
- store.prepare("SELECT feedback_positive FROM memories LIMIT 1").get();
14
- feedbackColumnCache.set(store, true);
15
- return true;
16
- }
17
- catch {
18
- feedbackColumnCache.set(store, false);
19
- return false;
20
- }
21
- }
22
5
  export function kevinWhy(store, query) {
23
6
  // v0.3.0 fix (bug #7) — the old code wrapped the WHOLE query in a
24
7
  // single quoted phrase, so it only matched the exact full-string
@@ -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"];
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"];
10
10
  export type MetricKey = (typeof METRIC_KEYS)[number];
11
11
  /**
12
12
  * Cheap token estimate used when bumping the `tokens_injected_*` counters.
@@ -57,6 +57,14 @@ export const METRIC_KEYS = [
57
57
  "okf_merge_folds",
58
58
  "rekey_events",
59
59
  "injections_from_shared",
60
+ // v1.1.0 (K11-001 / plan §4, D11-01) — drift metrics; order matches 012 seed.
61
+ "bench_regression_failures",
62
+ "forget_requests_total",
63
+ "forget_tombstones_published",
64
+ // v1.2.0 (K12-001 / plan §4) — surface metrics; no migration this
65
+ // release — rows are created on first incr via upsert (K12-001).
66
+ "tui_snapshots_flushed",
67
+ "tui_actions_invoked",
60
68
  ];
61
69
  const DEFAULT_FLUSH_MS = 1000;
62
70
  function zeroCache() {
@@ -4,68 +4,116 @@
4
4
  * Injection recall ORs the quoted tokens (`"t1" OR "t2"`) while
5
5
  * `kevin_why` ANDs them (`"t1" AND "t2"`).
6
6
  */
7
+ // v1.1.0 (K11-012 / plan §5.5, D11-05) — single source for STOP_WORDS (union of three lists)
7
8
  export const STOP_WORDS = new Set([
8
9
  "a",
10
+ "about",
11
+ "after",
12
+ "again",
13
+ "all",
14
+ "also",
9
15
  "an",
10
16
  "and",
17
+ "any",
11
18
  "are",
19
+ "as",
12
20
  "at",
13
21
  "be",
14
22
  "been",
23
+ "before",
24
+ "being",
15
25
  "but",
16
26
  "by",
27
+ "can",
28
+ "como",
29
+ "con",
30
+ "could",
31
+ "de",
17
32
  "did",
18
33
  "do",
19
34
  "does",
20
35
  "el",
36
+ "en",
21
37
  "eso",
22
38
  "for",
39
+ "from",
40
+ "had",
41
+ "has",
42
+ "have",
43
+ "he",
44
+ "her",
45
+ "his",
23
46
  "how",
24
47
  "i",
25
48
  "if",
26
49
  "in",
50
+ "into",
27
51
  "is",
28
52
  "it",
53
+ "its",
29
54
  "la",
30
55
  "las",
31
56
  "los",
57
+ "may",
32
58
  "mi",
59
+ "might",
60
+ "more",
61
+ "most",
62
+ "must",
33
63
  "my",
64
+ "not",
34
65
  "o",
35
66
  "of",
36
67
  "on",
68
+ "one",
37
69
  "or",
70
+ "our",
38
71
  "para",
72
+ "per",
39
73
  "por",
40
74
  "que",
75
+ "shall",
41
76
  "she",
77
+ "sin",
78
+ "so",
42
79
  "su",
80
+ "than",
43
81
  "that",
44
82
  "the",
83
+ "their",
84
+ "them",
85
+ "then",
86
+ "there",
87
+ "these",
88
+ "they",
45
89
  "this",
90
+ "those",
91
+ "through",
46
92
  "to",
93
+ "too",
47
94
  "tu",
48
95
  "un",
49
96
  "una",
97
+ "under",
98
+ "up",
99
+ "us",
100
+ "via",
101
+ "was",
50
102
  "we",
51
103
  "were",
52
104
  "what",
53
105
  "when",
54
106
  "where",
55
107
  "which",
108
+ "while",
56
109
  "who",
57
110
  "why",
111
+ "will",
58
112
  "with",
113
+ "would",
59
114
  "y",
60
115
  "you",
61
- "como",
62
- "con",
63
- "de",
64
- "en",
65
- "he",
66
- "they",
67
- "was",
68
- "sin",
116
+ "your",
69
117
  ]);
70
118
  /** Lowercase, split on whitespace, drop stopwords. Returns raw tokens. */
71
119
  export function tokenizeQuery(query) {
@@ -0,0 +1 @@
1
+ export declare function toMs(legacyValue: string | null | undefined, msValue: number | null | undefined): number | null;
@@ -0,0 +1,16 @@
1
+ // v1.1.0 (K11-003/K11-004 / plan §5.2, D11-01) — millisecond helper.
2
+ // Readers prefer _ms and fall back to legacy second-granularity column.
3
+ // The helper is the single implementation imported by InjectionLedger and
4
+ // CausalChain (moved here in K11-004 to avoid duplication).
5
+ export function toMs(legacyValue, msValue) {
6
+ if (typeof msValue === "number" && !Number.isNaN(msValue))
7
+ return msValue;
8
+ if (!legacyValue)
9
+ return null;
10
+ // SQLite datetime('now') is 'YYYY-MM-DD HH:MM:SS' UTC
11
+ const iso = legacyValue.includes("T")
12
+ ? legacyValue
13
+ : `${legacyValue.replace(" ", "T")}Z`;
14
+ const n = Date.parse(iso);
15
+ return Number.isNaN(n) ? null : n;
16
+ }
@@ -0,0 +1,59 @@
1
+ export interface ProposalView {
2
+ readonly id: string;
3
+ readonly kind: string;
4
+ readonly target_path: string;
5
+ readonly diff: string;
6
+ readonly memory_ids: readonly string[];
7
+ readonly created_at: string;
8
+ readonly truncated?: boolean;
9
+ readonly token?: string;
10
+ }
11
+ export interface ConflictView {
12
+ readonly id: string;
13
+ readonly kind: string;
14
+ readonly a_summary: string;
15
+ readonly b_summary: string;
16
+ readonly opened_at: string;
17
+ }
18
+ export interface HealthView {
19
+ readonly verdict: string;
20
+ readonly reason: string;
21
+ readonly hooks: readonly {
22
+ readonly hook: string;
23
+ readonly state: string;
24
+ readonly fire_count: number;
25
+ readonly expected_count: number;
26
+ }[];
27
+ readonly perf: readonly {
28
+ readonly scope: string;
29
+ readonly p95: number;
30
+ readonly budget_p95: number;
31
+ readonly within_budget: boolean;
32
+ }[];
33
+ readonly contract_digest: string;
34
+ readonly counters: Record<string, number>;
35
+ }
36
+ export interface TuiSnapshotSet {
37
+ readonly generatedAt: string;
38
+ readonly proposals: readonly ProposalView[];
39
+ readonly conflicts: readonly ConflictView[];
40
+ readonly health: HealthView;
41
+ }
42
+ export type TuiAction = {
43
+ readonly type: "approve";
44
+ readonly proposalId: string;
45
+ readonly token: string;
46
+ } | {
47
+ readonly type: "reject";
48
+ readonly proposalId: string;
49
+ readonly token: string;
50
+ readonly note?: string;
51
+ } | {
52
+ readonly type: "acknowledge";
53
+ readonly conflictId: string;
54
+ };
55
+ export interface ActionResult {
56
+ readonly action: TuiAction;
57
+ readonly status: "applied" | "rejected" | "stale_skipped" | "error";
58
+ readonly detail?: string;
59
+ }
@@ -0,0 +1,4 @@
1
+ // v1.2.0 (K12-002 / plan §4.2-§4.3) — shared view types (type-only module).
2
+ // This file MUST contain only type/interface exports — zero runtime values.
3
+ // The TUI module may import it ONLY as `import type`.
4
+ export {};
@@ -0,0 +1,18 @@
1
+ import type { TuiPlugin } from "@opencode-ai/plugin/tui";
2
+ import type { ConflictView, HealthView, ProposalView } from "./tui-types.js";
3
+ export declare function tuiRoot(): string;
4
+ export declare function readJsonSafe(name: string): {
5
+ data: unknown;
6
+ } | {
7
+ error: "missing" | "corrupt";
8
+ };
9
+ export declare function truncateSummary(text: string, max?: number): string;
10
+ export declare function formatProposalRow(p: ProposalView): string;
11
+ export declare function formatConflictRow(c: ConflictView): string;
12
+ export declare function formatHealthVerdict(h: HealthView): string;
13
+ export declare const tui: TuiPlugin;
14
+ declare const _default: {
15
+ id: string;
16
+ tui: TuiPlugin;
17
+ };
18
+ export default _default;