@jmtrin/opencode-kevin 0.9.0 → 1.1.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +575 -823
  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/011_v10_proven.sql +69 -0
  11. package/dist/migrations/012_v11_drift.sql +24 -0
  12. package/dist/plugin/Archiver.js +2 -18
  13. package/dist/plugin/ArtifactWriter.d.ts +25 -10
  14. package/dist/plugin/ArtifactWriter.js +43 -25
  15. package/dist/plugin/CausalChain.js +32 -14
  16. package/dist/plugin/ConflictDetector.js +7 -30
  17. package/dist/plugin/ContextInjector.js +0 -1
  18. package/dist/plugin/ConventionMiner.js +0 -1
  19. package/dist/plugin/Curator.js +0 -1
  20. package/dist/plugin/Feedback.js +2 -20
  21. package/dist/plugin/HookLiveness.d.ts +7 -0
  22. package/dist/plugin/HookLiveness.js +26 -28
  23. package/dist/plugin/InjectionLedger.js +104 -58
  24. package/dist/plugin/LessonFixer.js +0 -1
  25. package/dist/plugin/Materializer.js +1 -89
  26. package/dist/plugin/MemoryService.d.ts +59 -1
  27. package/dist/plugin/MemoryService.js +13 -107
  28. package/dist/plugin/Migrate.d.ts +1 -1
  29. package/dist/plugin/Migrate.js +30 -3
  30. package/dist/plugin/PatternMiner.js +0 -1
  31. package/dist/plugin/QualityGate.js +0 -1
  32. package/dist/plugin/Reflector.js +0 -1
  33. package/dist/plugin/RepoIdentity.js +0 -1
  34. package/dist/plugin/RepoTruth.js +0 -1
  35. package/dist/plugin/Retrospective.js +11 -1
  36. package/dist/plugin/SharedLayer.js +0 -1
  37. package/dist/plugin/Store.js +0 -1
  38. package/dist/plugin/ToolCallObserver.js +18 -6
  39. package/dist/plugin/capabilities.js +0 -1
  40. package/dist/plugin/columns.d.ts +11 -0
  41. package/dist/plugin/columns.js +54 -0
  42. package/dist/plugin/confidence.js +0 -1
  43. package/dist/plugin/contract.d.ts +54 -0
  44. package/dist/plugin/contract.js +383 -0
  45. package/dist/plugin/diff.js +0 -1
  46. package/dist/plugin/escape.d.ts +39 -0
  47. package/dist/plugin/escape.js +60 -0
  48. package/dist/plugin/fingerprint.js +0 -1
  49. package/dist/plugin/host.js +0 -1
  50. package/dist/plugin/index.d.ts +2 -2
  51. package/dist/plugin/index.js +448 -229
  52. package/dist/plugin/inferability.js +0 -1
  53. package/dist/plugin/kevin_approve.js +0 -1
  54. package/dist/plugin/kevin_audit.d.ts +31 -0
  55. package/dist/plugin/kevin_audit.js +60 -1
  56. package/dist/plugin/kevin_bench.d.ts +10 -0
  57. package/dist/plugin/kevin_bench.js +82 -0
  58. package/dist/plugin/kevin_conflicts.js +0 -1
  59. package/dist/plugin/kevin_contract.d.ts +8 -0
  60. package/dist/plugin/kevin_contract.js +59 -0
  61. package/dist/plugin/kevin_doctor.d.ts +5 -0
  62. package/dist/plugin/kevin_doctor.js +30 -2
  63. package/dist/plugin/kevin_facts.js +0 -1
  64. package/dist/plugin/kevin_forget.d.ts +33 -0
  65. package/dist/plugin/kevin_forget.js +260 -0
  66. package/dist/plugin/kevin_native.js +0 -1
  67. package/dist/plugin/kevin_propose.js +0 -1
  68. package/dist/plugin/kevin_publish.js +0 -1
  69. package/dist/plugin/kevin_why.js +1 -19
  70. package/dist/plugin/memory-format.js +0 -1
  71. package/dist/plugin/metrics.d.ts +1 -1
  72. package/dist/plugin/metrics.js +4 -1
  73. package/dist/plugin/native.js +0 -1
  74. package/dist/plugin/okf-export.js +0 -1
  75. package/dist/plugin/okf-import.js +0 -1
  76. package/dist/plugin/okf.js +0 -1
  77. package/dist/plugin/perf.d.ts +35 -0
  78. package/dist/plugin/perf.js +174 -0
  79. package/dist/plugin/query-tokenizer.js +56 -9
  80. package/dist/plugin/redact.js +0 -1
  81. package/dist/plugin/replay-types.js +0 -1
  82. package/dist/plugin/replay.js +0 -1
  83. package/dist/plugin/sqlite-adapter.js +0 -1
  84. package/dist/plugin/time-ms.d.ts +1 -0
  85. package/dist/plugin/time-ms.js +16 -0
  86. package/dist/plugin/uuid.js +0 -1
  87. package/package.json +22 -6
  88. package/dist/plugin/Archiver.js.map +0 -1
  89. package/dist/plugin/ArtifactWriter.js.map +0 -1
  90. package/dist/plugin/CausalChain.js.map +0 -1
  91. package/dist/plugin/ConflictDetector.js.map +0 -1
  92. package/dist/plugin/ContextInjector.js.map +0 -1
  93. package/dist/plugin/ConventionMiner.js.map +0 -1
  94. package/dist/plugin/Curator.js.map +0 -1
  95. package/dist/plugin/Feedback.js.map +0 -1
  96. package/dist/plugin/HookLiveness.js.map +0 -1
  97. package/dist/plugin/InjectionLedger.js.map +0 -1
  98. package/dist/plugin/LessonFixer.js.map +0 -1
  99. package/dist/plugin/Materializer.js.map +0 -1
  100. package/dist/plugin/MemoryService.js.map +0 -1
  101. package/dist/plugin/Migrate.js.map +0 -1
  102. package/dist/plugin/PatternMiner.js.map +0 -1
  103. package/dist/plugin/QualityGate.js.map +0 -1
  104. package/dist/plugin/Reflector.js.map +0 -1
  105. package/dist/plugin/RepoIdentity.js.map +0 -1
  106. package/dist/plugin/RepoTruth.js.map +0 -1
  107. package/dist/plugin/Retrospective.js.map +0 -1
  108. package/dist/plugin/SharedLayer.js.map +0 -1
  109. package/dist/plugin/Store.js.map +0 -1
  110. package/dist/plugin/ToolCallObserver.js.map +0 -1
  111. package/dist/plugin/capabilities.js.map +0 -1
  112. package/dist/plugin/confidence.js.map +0 -1
  113. package/dist/plugin/diff.js.map +0 -1
  114. package/dist/plugin/fingerprint.js.map +0 -1
  115. package/dist/plugin/host.js.map +0 -1
  116. package/dist/plugin/index.js.map +0 -1
  117. package/dist/plugin/inferability.js.map +0 -1
  118. package/dist/plugin/kevin_approve.js.map +0 -1
  119. package/dist/plugin/kevin_audit.js.map +0 -1
  120. package/dist/plugin/kevin_conflicts.js.map +0 -1
  121. package/dist/plugin/kevin_doctor.js.map +0 -1
  122. package/dist/plugin/kevin_facts.js.map +0 -1
  123. package/dist/plugin/kevin_native.js.map +0 -1
  124. package/dist/plugin/kevin_propose.js.map +0 -1
  125. package/dist/plugin/kevin_publish.js.map +0 -1
  126. package/dist/plugin/kevin_why.js.map +0 -1
  127. package/dist/plugin/memory-format.js.map +0 -1
  128. package/dist/plugin/metrics.js.map +0 -1
  129. package/dist/plugin/native.js.map +0 -1
  130. package/dist/plugin/okf-export.js.map +0 -1
  131. package/dist/plugin/okf-import.js.map +0 -1
  132. package/dist/plugin/okf.js.map +0 -1
  133. package/dist/plugin/query-tokenizer.js.map +0 -1
  134. package/dist/plugin/redact.js.map +0 -1
  135. package/dist/plugin/replay-types.js.map +0 -1
  136. package/dist/plugin/replay.js.map +0 -1
  137. package/dist/plugin/sqlite-adapter.js.map +0 -1
  138. package/dist/plugin/uuid.js.map +0 -1
  139. package/migrations/001_initial.sql +0 -92
  140. package/migrations/002_indexes.sql +0 -14
  141. package/migrations/003_v02_signal.sql +0 -58
  142. package/migrations/004_v03_knowledge.sql +0 -138
  143. package/migrations/005_v04_signal.sql +0 -57
  144. package/migrations/006_v05_glassbox.sql +0 -118
  145. package/migrations/007_v06_pull.sql +0 -145
  146. package/migrations/008_v07_truth.sql +0 -124
  147. package/migrations/009_v08_team.sql +0 -100
  148. package/migrations/010_v09_native.sql +0 -78
@@ -86,4 +86,3 @@ export function classify(memory) {
86
86
  // Rule 5.
87
87
  return "unknown";
88
88
  }
89
- //# sourceMappingURL=inferability.js.map
@@ -50,4 +50,3 @@ export function kevinApprove(store, memoryService, curator, writer, metrics, arg
50
50
  function sqliteNow() {
51
51
  return new Date().toISOString().slice(0, 19).replace("T", " ");
52
52
  }
53
- //# sourceMappingURL=kevin_approve.js.map
@@ -122,6 +122,37 @@ export interface AuditReport {
122
122
  };
123
123
  verdict: "healthy" | "degraded" | "unknown";
124
124
  };
125
+ /**
126
+ * v1.0.0 (K10-020 / plan §5.6) — the perf block: per scope, the most
127
+ * recent `perf_samples` aggregate row (count/p50/p95/max) plus the
128
+ * stored budget and verdict. The stored aggregates are used DIRECTLY —
129
+ * re-aggregating aggregates would produce a number with no meaning
130
+ * (plan anti-pattern 42), and using the most recent row per scope is
131
+ * exactly what `bench:check` does, so `within_budget` agrees with it
132
+ * on the same data. Omitted (not zero-valued) on a pre-011 database,
133
+ * following the host precedent.
134
+ */
135
+ perf?: {
136
+ scopes: Record<string, {
137
+ count: number;
138
+ p50: number;
139
+ p95: number;
140
+ max: number;
141
+ budget_p95: number;
142
+ within_budget: boolean;
143
+ }>;
144
+ };
145
+ /**
146
+ * v1.0.0 (K10-020 / plan §5.6) — the contract block: the live contract's
147
+ * identity, computed from source rather than the database, so it is
148
+ * present even on a pre-011 database.
149
+ */
150
+ contract?: {
151
+ contract_version: number;
152
+ digest: string;
153
+ clause_count: number;
154
+ deprecated_count: number;
155
+ };
125
156
  partial: boolean;
126
157
  }
127
158
  /**
@@ -1,5 +1,7 @@
1
1
  import { effectivePrePromptCap } from "./ContextInjector.js";
2
2
  import { hasRepoIdColumn } from "./MemoryService.js";
3
+ import { contractDigest, describeContract } from "./contract.js";
4
+ import { BUDGETS } from "./perf.js";
3
5
  const ALL_FALSE_CAPABILITIES = {
4
6
  skills: false,
5
7
  references: false,
@@ -484,6 +486,62 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
484
486
  // marking the report partial — the host block is additive, unlike
485
487
  // channels/curation which are the v0.6.0 scoreboard. Existing
486
488
  // v0.8.0-era tests run on 007/008 stores and expect partial false.
489
+ // v1.0.0 (K10-020 / plan §5.6) — perf block: the most recent aggregate
490
+ // row per scope, read directly from perf_samples. A scope with no rows
491
+ // reports count 0, zero timings and within_budget true — never NULL,
492
+ // never NaN. Omitted on a pre-011 database (host precedent).
493
+ let perf;
494
+ let hasSchema011 = false;
495
+ try {
496
+ const row = store
497
+ .prepare("SELECT 1 AS n FROM sqlite_master WHERE type='table' AND name='perf_samples'")
498
+ .get();
499
+ hasSchema011 = !!row;
500
+ }
501
+ catch {
502
+ hasSchema011 = false;
503
+ }
504
+ if (hasSchema011) {
505
+ try {
506
+ const scopes = {};
507
+ const latest = store.prepare(`SELECT sample_count AS count, p50_ms AS p50, p95_ms AS p95,
508
+ max_ms AS max, budget_p95_ms AS budget_p95, within_budget
509
+ FROM perf_samples WHERE scope = ? ORDER BY id DESC LIMIT 1`);
510
+ for (const budget of BUDGETS) {
511
+ const row = latest.get(budget.scope);
512
+ scopes[budget.scope] = row
513
+ ? {
514
+ count: row.count,
515
+ p50: row.p50,
516
+ p95: row.p95,
517
+ max: row.max,
518
+ budget_p95: row.budget_p95,
519
+ within_budget: row.within_budget === 1,
520
+ }
521
+ : {
522
+ count: 0,
523
+ p50: 0,
524
+ p95: 0,
525
+ max: 0,
526
+ budget_p95: budget.p95Ms,
527
+ within_budget: true,
528
+ };
529
+ }
530
+ perf = { scopes };
531
+ }
532
+ catch {
533
+ perf = undefined;
534
+ }
535
+ }
536
+ // v1.0.0 (K10-020 / plan §5.6) — contract block: pure function of the
537
+ // source, no database involvement.
538
+ const liveContract = describeContract();
539
+ const contract = {
540
+ contract_version: liveContract.contractVersion,
541
+ digest: contractDigest(liveContract),
542
+ clause_count: liveContract.clauses.length,
543
+ deprecated_count: liveContract.clauses.filter((c) => c.deprecated).length,
544
+ };
487
545
  return {
488
546
  memories,
489
547
  injections,
@@ -498,7 +556,8 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
498
556
  curation,
499
557
  team,
500
558
  host,
559
+ perf,
560
+ contract,
501
561
  partial,
502
562
  };
503
563
  }
504
- //# sourceMappingURL=kevin_audit.js.map
@@ -0,0 +1,10 @@
1
+ import type { Store } from "./Store.js";
2
+ export interface KevinBenchDeps {
3
+ store: Store;
4
+ /** Working directory used to locate bench/corpus (defaults process.cwd()). */
5
+ cwd?: string;
6
+ }
7
+ export interface KevinBenchArgs {
8
+ action: "status" | "last";
9
+ }
10
+ export declare function buildKevinBench(deps: KevinBenchDeps, args: KevinBenchArgs): Record<string, unknown>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * K10-019 — kevin_bench: reports what `npm run bench` recorded.
3
+ *
4
+ * It NEVER runs the benchmark — running it from inside a live session
5
+ * would measure the session and pollute the user's database. The module
6
+ * imports nothing from scripts/bench.ts; the corpus digest of the on-disk
7
+ * corpus is recomputed here with the same fnv1a64 discipline so a result
8
+ * computed from a different corpus is visibly a different measurement.
9
+ */
10
+ import { readFileSync, statSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { fnv1a64 } from "./fingerprint.js";
13
+ function onDiskCorpusDigest(cwd) {
14
+ try {
15
+ const dir = join(cwd, "bench", "corpus");
16
+ statSync(dir);
17
+ const mem = readFileSync(join(dir, "memories.jsonl"), "utf8");
18
+ const q = readFileSync(join(dir, "queries.jsonl"), "utf8");
19
+ return {
20
+ digest: fnv1a64(`${mem}\n${q}`),
21
+ queries: q.split("\n").filter((l) => l.trim().length > 0).length,
22
+ };
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ function noRuns() {
29
+ return {
30
+ has_runs: false,
31
+ hint: "npm run bench",
32
+ };
33
+ }
34
+ export function buildKevinBench(deps, args) {
35
+ if (args.action === "status") {
36
+ const count = deps.store
37
+ .prepare("SELECT COUNT(*) AS c FROM bench_runs")
38
+ .get();
39
+ if (!count || count.c === 0)
40
+ return noRuns();
41
+ const latest = deps.store
42
+ .prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
43
+ .get();
44
+ if (!latest)
45
+ return noRuns();
46
+ const disk = onDiskCorpusDigest(deps.cwd ?? process.cwd());
47
+ return {
48
+ has_runs: true,
49
+ total_runs: count.c,
50
+ corpus_digest: latest.corpus_digest,
51
+ contract_digest: latest.contract_digest,
52
+ package_version: latest.package_version,
53
+ runtime: latest.runtime,
54
+ last_ran_at: latest.ran_at,
55
+ corpus_on_disk: disk === null ? null : disk.digest,
56
+ matches_disk_corpus: disk !== null && disk.digest === latest.corpus_digest,
57
+ };
58
+ }
59
+ const latest = deps.store
60
+ .prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
61
+ .get();
62
+ if (!latest)
63
+ return noRuns();
64
+ const rows = deps.store
65
+ .prepare("SELECT arm, k, precision_at_k, recall_at_k, mrr FROM bench_runs WHERE corpus_digest = ? AND ran_at = ?")
66
+ .all(latest.corpus_digest, latest.ran_at);
67
+ return {
68
+ has_runs: true,
69
+ corpus_digest: latest.corpus_digest,
70
+ contract_digest: latest.contract_digest,
71
+ package_version: latest.package_version,
72
+ runtime: latest.runtime,
73
+ ran_at: latest.ran_at,
74
+ arms: rows.map((r) => ({
75
+ arm: r.arm,
76
+ k: r.k,
77
+ precision_at_k: r.precision_at_k,
78
+ recall_at_k: r.recall_at_k,
79
+ mrr: r.mrr,
80
+ })),
81
+ };
82
+ }
@@ -48,4 +48,3 @@ export function executeKevinConflicts(deps, action, id, keep, requestedStatus) {
48
48
  conflicts: rows,
49
49
  };
50
50
  }
51
- //# sourceMappingURL=kevin_conflicts.js.map
@@ -0,0 +1,8 @@
1
+ export interface KevinContractDeps {
2
+ packageVersion: string;
3
+ }
4
+ export interface KevinContractArgs {
5
+ clause?: string;
6
+ format?: "summary" | "full";
7
+ }
8
+ export declare function buildKevinContract(deps: KevinContractDeps, args: KevinContractArgs): Record<string, unknown>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * K10-018 — kevin_contract: the frozen surface, inspectable at runtime.
3
+ *
4
+ * Read-only. Returns the live contract with its digest and per-clause
5
+ * stability/since/deprecation; with `clause`, one clause's full value.
6
+ * No filesystem paths, no project ids, no LLM, no network.
7
+ */
8
+ import { contractDigest, describeContract, } from "./contract.js";
9
+ export function buildKevinContract(deps, args) {
10
+ const contract = describeContract();
11
+ const digest = contractDigest(contract);
12
+ const format = args.format ?? "summary";
13
+ if (args.clause !== undefined) {
14
+ const clause = contract.clauses.find((c) => c.id === args.clause);
15
+ if (!clause) {
16
+ return {
17
+ error: "unknown_clause",
18
+ clause: args.clause,
19
+ known_clauses: contract.clauses.map((c) => c.id),
20
+ };
21
+ }
22
+ return {
23
+ contract_version: contract.contractVersion,
24
+ digest,
25
+ package_version: deps.packageVersion,
26
+ clause: {
27
+ id: clause.id,
28
+ title: clause.title,
29
+ stability: clause.stability,
30
+ since: clause.since,
31
+ ...(clause.deprecated ? { deprecated: clause.deprecated } : {}),
32
+ ...(clause.replacement ? { replacement: clause.replacement } : {}),
33
+ value: clause.value,
34
+ },
35
+ };
36
+ }
37
+ if (format === "full") {
38
+ return {
39
+ contract_version: contract.contractVersion,
40
+ digest,
41
+ package_version: deps.packageVersion,
42
+ clauses: contract.clauses,
43
+ deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
44
+ };
45
+ }
46
+ return {
47
+ contract_version: contract.contractVersion,
48
+ digest,
49
+ package_version: deps.packageVersion,
50
+ clauses: contract.clauses.map((c) => ({
51
+ id: c.id,
52
+ title: c.title,
53
+ stability: c.stability,
54
+ since: c.since,
55
+ ...(c.deprecated ? { deprecated: c.deprecated } : {}),
56
+ })),
57
+ deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
58
+ };
59
+ }
@@ -36,6 +36,11 @@ export interface DoctorReport {
36
36
  readonly reference: boolean;
37
37
  };
38
38
  };
39
+ /** v1.0.0 (K10-021) — scopes whose most recent sample exceeded their
40
+ * p95 budget. Omitted entirely on pre-011 databases (no table). */
41
+ readonly perf?: {
42
+ readonly scopes_over_budget: readonly string[];
43
+ };
39
44
  readonly verdict: "healthy" | "degraded" | "unknown";
40
45
  readonly reason: string;
41
46
  readonly partial: boolean;
@@ -106,6 +106,22 @@ export function countZodCopies(cwd) {
106
106
  return { copies: null, note: "zod resolution walk failed" };
107
107
  }
108
108
  }
109
+ /** v1.0.0 (K10-021) — scopes whose most recent perf sample breached the
110
+ * p95 budget. The within_budget flag is materialized by Perf.flush()
111
+ * against the frozen BUDGETS; this block reads that persisted verdict
112
+ * and never re-aggregates. A missing table (pre-011 DB) yields no
113
+ * block rather than an empty one, matching the hooks precedent. */
114
+ function perfOverBudget(store) {
115
+ try {
116
+ const rows = store
117
+ .prepare("SELECT DISTINCT scope FROM perf_samples WHERE within_budget = 0 ORDER BY scope")
118
+ .all();
119
+ return rows.map((r) => r.scope);
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ }
109
125
  function lastRegistration(store, surface) {
110
126
  try {
111
127
  const row = store
@@ -124,7 +140,7 @@ export function buildDoctor(store, host, settings, options = {}) {
124
140
  // construction — kevin_doctor never re-probes (K9-004: probe once,
125
141
  // freeze, reuse).
126
142
  const hooks = hooksBlock(store);
127
- const verdict = reduceVerdict(hooks.hooks.map((h) => ({
143
+ let verdict = reduceVerdict(hooks.hooks.map((h) => ({
128
144
  hook: h.hook,
129
145
  experimental: h.experimental,
130
146
  state: h.state,
@@ -134,6 +150,18 @@ export function buildDoctor(store, host, settings, options = {}) {
134
150
  expectedCount: h.expected_count,
135
151
  deadSince: h.since ?? null,
136
152
  })));
153
+ // v1.0.0 (K10-021) — a slow plugin is not healthy even when every
154
+ // hook is live. A budget breach degrades the verdict and names the
155
+ // scope; a dead hook keeps its reason; `unknown` is never rounded up.
156
+ const breaches = perfOverBudget(store);
157
+ if (breaches !== null &&
158
+ breaches.length > 0 &&
159
+ verdict.verdict !== "degraded") {
160
+ verdict = {
161
+ verdict: "degraded",
162
+ reason: `perf budget exceeded: ${breaches.join(", ")}`,
163
+ };
164
+ }
137
165
  const zod = countZodCopies(options.zodRoot ?? process.cwd());
138
166
  const enabled = settings.getSetting("native_registration_enabled", "0") === "1";
139
167
  return {
@@ -162,7 +190,7 @@ export function buildDoctor(store, host, settings, options = {}) {
162
190
  },
163
191
  verdict: verdict.verdict,
164
192
  reason: verdict.reason,
193
+ ...(breaches !== null ? { perf: { scopes_over_budget: breaches } } : {}),
165
194
  partial: hooks.partial,
166
195
  };
167
196
  }
168
- //# sourceMappingURL=kevin_doctor.js.map
@@ -34,4 +34,3 @@ export function buildKevinFacts(deps, refresh) {
34
34
  penalized,
35
35
  };
36
36
  }
37
- //# sourceMappingURL=kevin_facts.js.map
@@ -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
+ }
@@ -77,4 +77,3 @@ export function handleNative(action, deps) {
77
77
  };
78
78
  return report;
79
79
  }
80
- //# sourceMappingURL=kevin_native.js.map
@@ -12,4 +12,3 @@ export function kevinPropose(curator, writer, kind) {
12
12
  })),
13
13
  };
14
14
  }
15
- //# sourceMappingURL=kevin_propose.js.map