@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
@@ -298,4 +298,3 @@ export function initProjectFile(cwd, writer) {
298
298
  };
299
299
  }
300
300
  }
301
- //# sourceMappingURL=RepoIdentity.js.map
@@ -597,4 +597,3 @@ function normalizeBool(v) {
597
597
  return null;
598
598
  }
599
599
  export { MAX_FACTS_PER_PROJECT };
600
- //# sourceMappingURL=RepoTruth.js.map
@@ -67,6 +67,17 @@ export const METRIC_KEY_LABELS = {
67
67
  injections_suppressed_dead_hook: "Inyecciones suprimidas (hook muerto)",
68
68
  native_registrations_total: "Registros nativos (totales)",
69
69
  native_registration_failures: "Fallos de registro nativo",
70
+ // v1.0.0 (K10-005 / plan §6) — the six keys seeded by migration 011 section 3.
71
+ perf_samples_recorded: "Muestras de rendimiento registradas",
72
+ perf_budget_breaches: "Brechas de presupuesto de rendimiento",
73
+ dispose_fires_total: "Disparos de dispose (totales)",
74
+ dispose_misses_total: "Misses de dispose",
75
+ contract_digest_changes: "Cambios de digest de contrato",
76
+ bench_runs_total: "Ejecuciones de benchmark (totales)",
77
+ // v1.1.0 (K11-001 / plan §4) — drift metrics; labels required by BUG-014 regression.
78
+ bench_regression_failures: "Fallos de regresion de benchmark",
79
+ forget_requests_total: "Solicitudes de olvido totales",
80
+ forget_tombstones_published: "Tombstones publicados por olvido",
70
81
  };
71
82
  function originLabel(origin) {
72
83
  if (origin === "reflector")
@@ -200,4 +211,3 @@ export class Retrospective {
200
211
  return lines.join("\n");
201
212
  }
202
213
  }
203
- //# sourceMappingURL=Retrospective.js.map
@@ -460,4 +460,3 @@ export class SharedLayer {
460
460
  return n;
461
461
  }
462
462
  }
463
- //# sourceMappingURL=SharedLayer.js.map
@@ -48,4 +48,3 @@ export class Store {
48
48
  return this.db;
49
49
  }
50
50
  }
51
- //# sourceMappingURL=Store.js.map
@@ -1,3 +1,4 @@
1
+ import { hasColumn } from "./columns.js";
1
2
  import { fingerprint as computeFingerprint } from "./fingerprint.js";
2
3
  import { redactPaths, stripPrivate } from "./redact.js";
3
4
  import { uuidv7 } from "./uuid.js";
@@ -54,11 +55,23 @@ export class ToolCallObserver {
54
55
  return;
55
56
  }
56
57
  }
57
- this.store
58
- .prepare(`INSERT INTO tool_calls
59
- (id, session_id, ts, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
60
- VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
61
- .run(input.callID ?? uuidv7(), sessionId, input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
58
+ // v1.1.0 (K11-002 / plan §5.2, D11-01/D11-07) — dual-write: legacy `ts`
59
+ // stays (`datetime('now')`), new `ts_ms` holds Date.now() when the
60
+ // column exists. Probe is cached via columns registry (K11-011).
61
+ if (hasColumn(this.store, "tool_calls", "ts_ms")) {
62
+ this.store
63
+ .prepare(`INSERT INTO tool_calls
64
+ (id, session_id, ts, ts_ms, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
65
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
66
+ .run(input.callID ?? uuidv7(), sessionId, Date.now(), input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
67
+ }
68
+ else {
69
+ this.store
70
+ .prepare(`INSERT INTO tool_calls
71
+ (id, session_id, ts, tool, args_summary, success, duration_ms, agent, error_type, metadata, project_id, fingerprint)
72
+ VALUES (?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
73
+ .run(input.callID ?? uuidv7(), sessionId, input.tool, argsSummary, success, durationMs, agent, errorType, metadata, projectId, fp);
74
+ }
62
75
  }
63
76
  isDedupEnabled() {
64
77
  try {
@@ -186,4 +199,3 @@ export class ToolCallObserver {
186
199
  return v;
187
200
  }
188
201
  }
189
- //# sourceMappingURL=ToolCallObserver.js.map
@@ -39,4 +39,3 @@ export function probe(input) {
39
39
  return ALL_FALSE;
40
40
  }
41
41
  }
42
- //# sourceMappingURL=capabilities.js.map
@@ -0,0 +1,11 @@
1
+ import type { Store } from "./Store.js";
2
+ export declare function hasColumn(store: Store, table: string, column: string): boolean;
3
+ export declare function hasIgnoredColumn(store: Store): boolean;
4
+ export declare function hasCuratedColumn(store: Store): boolean;
5
+ export declare function hasTruthColumns(store: Store): boolean;
6
+ export declare function hasRepoIdColumn(store: Store): boolean;
7
+ export declare function hasLayerColumn(store: Store): boolean;
8
+ export declare function hasRecurrenceColumn(store: Store): boolean;
9
+ export declare function hasArchivedColumn(store: Store): boolean;
10
+ export declare function hasFeedbackColumns(store: Store): boolean;
11
+ export declare function hasFeedbackTable(store: Store): boolean;
@@ -0,0 +1,54 @@
1
+ const cache = new WeakMap();
2
+ export function hasColumn(store, table, column) {
3
+ const byStore = cache.get(store);
4
+ const key = `${table}.${column}`;
5
+ if (byStore?.get(key) === true)
6
+ return true;
7
+ try {
8
+ store.prepare(`SELECT ${column} FROM ${table} LIMIT 0`).get();
9
+ let m = byStore;
10
+ if (!m) {
11
+ m = new Map();
12
+ cache.set(store, m);
13
+ }
14
+ m.set(key, true);
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ // v1.1.0 (K11-011 / plan §5.5, D11-06) — named helpers delegating to the registry
22
+ export function hasIgnoredColumn(store) {
23
+ return hasColumn(store, "memories", "ignored");
24
+ }
25
+ export function hasCuratedColumn(store) {
26
+ return hasColumn(store, "memories", "curated");
27
+ }
28
+ export function hasTruthColumns(store) {
29
+ return hasColumn(store, "memories", "truth_penalty");
30
+ }
31
+ export function hasRepoIdColumn(store) {
32
+ return hasColumn(store, "memories", "repo_id");
33
+ }
34
+ export function hasLayerColumn(store) {
35
+ return hasColumn(store, "memories", "layer");
36
+ }
37
+ export function hasRecurrenceColumn(store) {
38
+ return hasColumn(store, "memories", "recurrence_count");
39
+ }
40
+ export function hasArchivedColumn(store) {
41
+ return hasColumn(store, "memories", "archived_at");
42
+ }
43
+ export function hasFeedbackColumns(store) {
44
+ return hasColumn(store, "memories", "feedback_positive");
45
+ }
46
+ export function hasFeedbackTable(store) {
47
+ try {
48
+ store.prepare("SELECT COUNT(*) FROM memory_feedback LIMIT 0").get();
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
@@ -32,4 +32,3 @@ export function computeConfidence(evidenceCount, recurrenceCount, positiveFeedba
32
32
  FEEDBACK_NEGATIVE_STEP * negativeFeedback;
33
33
  return Math.min(CONFIDENCE_MAX, Math.max(CONFIDENCE_MIN, raw));
34
34
  }
35
- //# sourceMappingURL=confidence.js.map
@@ -0,0 +1,54 @@
1
+ export declare const CONTRACT_VERSION = 1;
2
+ export type Stability = "frozen" | "forward-only";
3
+ export interface ContractClause {
4
+ readonly id: string;
5
+ readonly title: string;
6
+ readonly stability: Stability;
7
+ readonly since: string;
8
+ readonly deprecated?: string;
9
+ readonly replacement?: string;
10
+ readonly value: unknown;
11
+ }
12
+ export interface PublicContract {
13
+ readonly contractVersion: number;
14
+ readonly clauses: readonly ContractClause[];
15
+ }
16
+ export interface ContractInput {
17
+ readonly packageName?: string;
18
+ readonly packageVersion?: string;
19
+ }
20
+ export declare const CONTRACT_TOOL_NAMES: readonly string[];
21
+ /**
22
+ * v1.0.0 (K10-018/K10-019 / plan §5.6) — tools added AFTER the initial
23
+ * freeze, each carrying the `since` the deprecation policy requires. A
24
+ * 1.x addition without an entry here fails the contract test as
25
+ * added_bare.
26
+ */
27
+ export declare const CONTRACT_TOOL_ADDITIONS: readonly {
28
+ name: string;
29
+ since: string;
30
+ }[];
31
+ /**
32
+ * v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
33
+ * each carrying the `since` the deprecation policy requires (C-05).
34
+ */
35
+ export declare const CONTRACT_METRIC_ADDITIONS: readonly {
36
+ name: string;
37
+ since: string;
38
+ }[];
39
+ /**
40
+ * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
41
+ * not trusted: anything reaching an artifact or a prompt is escaped at
42
+ * the single write path, according to its container.
43
+ */
44
+ export declare const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
45
+ export declare function describeContract(_input?: ContractInput): PublicContract;
46
+ export declare function contractDigest(c: PublicContract): string;
47
+ export type ContractDiffKind = "removed" | "changed" | "added_ok" | "added_bare";
48
+ export interface ContractDiff {
49
+ readonly clauseId: string;
50
+ readonly path: string;
51
+ readonly kind: ContractDiffKind;
52
+ readonly remedy: string;
53
+ }
54
+ export declare function diffContract(golden: PublicContract, live: PublicContract): readonly ContractDiff[];
@@ -0,0 +1,383 @@
1
+ // v1.0.0 (K10-006 / plan §5.1) — the surface, as data.
2
+ import { MARKER_BEGIN, MARKER_END } from "./ArtifactWriter.js";
3
+ import { METRIC_KEY_LABELS } from "./Retrospective.js";
4
+ import { fnv1a64 } from "./fingerprint.js";
5
+ import { KEVIN_CONFIG_KEYS } from "./index.js";
6
+ import { MAX_ENTRIES, MAX_LINE_BYTES } from "./okf.js";
7
+ export const CONTRACT_VERSION = 1;
8
+ // Tool names — the live source of truth for C-03. Any rename here must
9
+ // flow to the contract (K10-006 AC: renaming a tool changes C-03).
10
+ // Keep this array adjacent to the actual tool registrations in index.ts
11
+ // (checked by a test that parses both files).
12
+ export const CONTRACT_TOOL_NAMES = [
13
+ "kevin_save",
14
+ "kevin_query",
15
+ "kevin_get",
16
+ "kevin_recall",
17
+ "kevin_status",
18
+ "kevin_project",
19
+ "kevin_audit",
20
+ "kevin_doctor",
21
+ "kevin_native",
22
+ "kevin_retrospective",
23
+ "kevin_why",
24
+ "kevin_feedback",
25
+ "kevin_trace",
26
+ "kevin_export",
27
+ "kevin_import",
28
+ "kevin_config",
29
+ "kevin_facts",
30
+ "kevin_conflicts",
31
+ "kevin_propose",
32
+ "kevin_publish",
33
+ "kevin_approve",
34
+ "kevin_share",
35
+ "kevin_sync",
36
+ ];
37
+ /**
38
+ * v1.0.0 (K10-018/K10-019 / plan §5.6) — tools added AFTER the initial
39
+ * freeze, each carrying the `since` the deprecation policy requires. A
40
+ * 1.x addition without an entry here fails the contract test as
41
+ * added_bare.
42
+ */
43
+ export const CONTRACT_TOOL_ADDITIONS = [
44
+ { name: "kevin_bench", since: "1.0.0" },
45
+ { name: "kevin_contract", since: "1.0.0" },
46
+ { name: "kevin_forget", since: "1.1.0" },
47
+ ];
48
+ /**
49
+ * v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
50
+ * each carrying the `since` the deprecation policy requires (C-05).
51
+ */
52
+ export const CONTRACT_METRIC_ADDITIONS = [
53
+ { name: "bench_regression_failures", since: "1.1.0" },
54
+ { name: "forget_requests_total", since: "1.1.0" },
55
+ { name: "forget_tombstones_published", since: "1.1.0" },
56
+ ];
57
+ /**
58
+ * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
59
+ * not trusted: anything reaching an artifact or a prompt is escaped at
60
+ * the single write path, according to its container.
61
+ */
62
+ export const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
63
+ // For canonical JSON: sort keys recursively, no floats.
64
+ function canonicalJson(value) {
65
+ if (value === null)
66
+ return "null";
67
+ if (typeof value === "boolean")
68
+ return value ? "true" : "false";
69
+ if (typeof value === "number") {
70
+ if (!Number.isFinite(value) || !Number.isInteger(value))
71
+ throw new Error("contract: non-integer number in canonical JSON");
72
+ return String(value);
73
+ }
74
+ if (typeof value === "string")
75
+ return JSON.stringify(value);
76
+ if (Array.isArray(value))
77
+ return `[${value.map(canonicalJson).join(",")}]`;
78
+ if (typeof value === "object") {
79
+ const obj = value;
80
+ const keys = Object.keys(obj).sort();
81
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
82
+ }
83
+ return JSON.stringify(value);
84
+ }
85
+ export function describeContract(_input) {
86
+ // Derive clause values from live source wherever possible (plan §5.1).
87
+ // C-03 members added after the freeze carry their `since` as objects;
88
+ // the original frozen set stays plain strings.
89
+ const toolAdditions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
90
+ const toolValue = {
91
+ tools: [[...CONTRACT_TOOL_NAMES].sort(), toolAdditions].flat(),
92
+ };
93
+ const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
94
+ // v1.1.0 — metric keys added after freeze carry `since` (C-05)
95
+ const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
96
+ const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
97
+ .filter((k) => !metricAdditions.some((a) => a.name === k))
98
+ .sort();
99
+ const metricValue = { keys: [...baseMetricKeys, ...metricAdditions].flat() };
100
+ const clauses = [
101
+ {
102
+ id: "C-01",
103
+ title: "AGENTS.md marker pair",
104
+ stability: "frozen",
105
+ since: "0.6.0",
106
+ value: {
107
+ markers: [MARKER_BEGIN, MARKER_END],
108
+ splice_rule: "bytes outside markers preserved verbatim",
109
+ },
110
+ },
111
+ {
112
+ id: "C-02",
113
+ title: "OKF v2 wire format",
114
+ stability: "frozen",
115
+ since: "0.8.0",
116
+ value: {
117
+ header_lines: 3,
118
+ field_order: ["entry_id", "type", "content", "scope", "created_at"],
119
+ entry_id_derivation: "fnv1a64 over canonical field ordering",
120
+ eol: "LF",
121
+ integers_only: true,
122
+ sort_order: "entry_id ascending",
123
+ max_line_bytes: MAX_LINE_BYTES,
124
+ max_entries: MAX_ENTRIES,
125
+ },
126
+ },
127
+ {
128
+ id: "C-03",
129
+ title: "Tool names and argument shapes",
130
+ stability: "frozen",
131
+ since: "0.2.0",
132
+ value: toolValue,
133
+ },
134
+ {
135
+ id: "C-04",
136
+ title: "Setting keys, types and defaults",
137
+ stability: "frozen",
138
+ since: "0.2.0",
139
+ value: settingValue,
140
+ },
141
+ {
142
+ id: "C-05",
143
+ title: "Metric key names",
144
+ stability: "frozen",
145
+ since: "0.2.0",
146
+ value: metricValue,
147
+ },
148
+ {
149
+ id: "C-06",
150
+ title: "Package entry points",
151
+ stability: "frozen",
152
+ since: "0.1.0",
153
+ value: {
154
+ name: "@jmtrin/opencode-kevin",
155
+ main: "dist/plugin/index.js",
156
+ types: "dist/plugin/index.d.ts",
157
+ exports_order: ["types", "import"],
158
+ engines: ">=22.5.0",
159
+ },
160
+ },
161
+ {
162
+ id: "C-07",
163
+ title: "Database schema",
164
+ stability: "forward-only",
165
+ since: "0.1.0",
166
+ value: {
167
+ schema_version: "012",
168
+ migrations_forward_only: true,
169
+ },
170
+ },
171
+ {
172
+ id: "C-08",
173
+ title: "Filesystem locations",
174
+ stability: "frozen",
175
+ since: "0.2.0",
176
+ value: {
177
+ db: "~/.opencode-kevin/kevin.db",
178
+ refs: "refs/",
179
+ skills: "skills/",
180
+ okf: ".kevin/knowledge.okf",
181
+ },
182
+ },
183
+ {
184
+ id: "C-09",
185
+ title: "Behavioural invariants",
186
+ stability: "frozen",
187
+ since: "0.8.0",
188
+ value: {
189
+ invariants: [
190
+ "zero process spawns",
191
+ "zero network calls",
192
+ "no raw author email written",
193
+ "single write path: ArtifactWriter is the only artifact writer",
194
+ ],
195
+ // v1.0.0 (K10-027 / plan §5.7) — the untrusted-input boundary
196
+ // joins the clause as an addition carrying its `since`; the
197
+ // frozen invariant strings above are untouched.
198
+ boundary: [{ name: BOUNDARY_INVARIANT, since: "1.0.0" }],
199
+ },
200
+ },
201
+ ];
202
+ return { contractVersion: CONTRACT_VERSION, clauses };
203
+ }
204
+ export function contractDigest(c) {
205
+ const payload = canonicalJson(c.clauses);
206
+ return fnv1a64(payload);
207
+ }
208
+ function valueMembers(value) {
209
+ // Normalize clause value into a flat member map for diffing.
210
+ // If value is { tools: [...] } => members are tool names.
211
+ // If { keys: [...] } => member per key.
212
+ // Otherwise flatten object keys.
213
+ if (value !== null && typeof value === "object") {
214
+ const obj = value;
215
+ if (Array.isArray(obj.tools)) {
216
+ const m = new Map();
217
+ for (const t of obj.tools) {
218
+ if (typeof t === "string")
219
+ m.set(t, undefined);
220
+ else if (t !== null &&
221
+ typeof t === "object" &&
222
+ "name" in t)
223
+ m.set(t.name, t);
224
+ }
225
+ return m;
226
+ }
227
+ if (Array.isArray(obj.keys)) {
228
+ const m = new Map();
229
+ for (const k of obj.keys) {
230
+ if (typeof k === "string")
231
+ m.set(k, undefined);
232
+ else if (k !== null &&
233
+ typeof k === "object" &&
234
+ "name" in k)
235
+ m.set(k.name, k);
236
+ }
237
+ return m;
238
+ }
239
+ if (Array.isArray(obj.markers) || Array.isArray(obj.invariants)) {
240
+ const arr = (obj.markers ?? obj.invariants);
241
+ const m = new Map();
242
+ for (const v of arr)
243
+ m.set(v, v);
244
+ // v1.0.0 (K10-027) — boundary additions sit beside the frozen
245
+ // invariant strings and carry their `since` like tool additions.
246
+ if (Array.isArray(obj.boundary)) {
247
+ for (const b of obj.boundary) {
248
+ if (b !== null &&
249
+ typeof b === "object" &&
250
+ "name" in b)
251
+ m.set(b.name, b);
252
+ }
253
+ }
254
+ return m;
255
+ }
256
+ // Generic object: each key is a member
257
+ const m = new Map();
258
+ for (const [k, v] of Object.entries(obj))
259
+ m.set(k, v);
260
+ return m;
261
+ }
262
+ return new Map([["value", value]]);
263
+ }
264
+ export function diffContract(golden, live) {
265
+ const diffs = [];
266
+ const goldenById = new Map(golden.clauses.map((c) => [c.id, c]));
267
+ const liveById = new Map(live.clauses.map((c) => [c.id, c]));
268
+ // Removed clauses
269
+ for (const g of golden.clauses) {
270
+ if (!liveById.has(g.id)) {
271
+ diffs.push({
272
+ clauseId: g.id,
273
+ path: g.id,
274
+ kind: "removed",
275
+ remedy: `Clause ${g.id} was removed. Revert, or open a 2.0.0.`,
276
+ });
277
+ }
278
+ }
279
+ // Added clauses — check since
280
+ for (const l of live.clauses) {
281
+ if (!goldenById.has(l.id)) {
282
+ const hasSince = typeof l.since === "string" &&
283
+ l.since.length > 0;
284
+ diffs.push({
285
+ clauseId: l.id,
286
+ path: l.id,
287
+ kind: hasSince ? "added_ok" : "added_bare",
288
+ remedy: hasSince
289
+ ? `Clause ${l.id} was added with since. Allowed in 1.x.`
290
+ : `Clause ${l.id} was added without since. Add since or revert, or open a 2.0.0.`,
291
+ });
292
+ }
293
+ }
294
+ // Within-clause diff
295
+ for (const g of golden.clauses) {
296
+ const l = liveById.get(g.id);
297
+ if (!l)
298
+ continue;
299
+ const gMembers = valueMembers(g.value);
300
+ const lMembers = valueMembers(l.value);
301
+ // Normalize members: for arrays we care about set equality
302
+ for (const [mem, _gv] of gMembers) {
303
+ if (!lMembers.has(mem)) {
304
+ diffs.push({
305
+ clauseId: g.id,
306
+ path: `${g.id}.${mem}`,
307
+ kind: "removed",
308
+ remedy: `Member ${mem} in ${g.id} was removed. Revert, or open a 2.0.0.`,
309
+ });
310
+ }
311
+ else {
312
+ const lv = lMembers.get(mem);
313
+ const gv = gMembers.get(mem);
314
+ if (JSON.stringify(lv) !== JSON.stringify(gv)) {
315
+ // Member present on both sides but its value moved (e.g. an
316
+ // addition object whose `since` was edited). Report it here,
317
+ // at member granularity, instead of letting it fall through
318
+ // to the coarser clause-level check below.
319
+ diffs.push({
320
+ clauseId: g.id,
321
+ path: `${g.id}.${mem}`,
322
+ kind: "changed",
323
+ remedy: `Member ${mem} in ${g.id} changed. Revert, or open a 2.0.0.`,
324
+ });
325
+ }
326
+ }
327
+ }
328
+ for (const [mem, _lv] of lMembers) {
329
+ if (!gMembers.has(mem)) {
330
+ const liveVal = l.value;
331
+ // Check if the live member carries `since` (additions may ride
332
+ // in the keys, tools or boundary arrays — K10-018/K10-027).
333
+ const lists = [liveVal.keys, liveVal.tools, liveVal.boundary];
334
+ let hasSince = false;
335
+ for (const list of lists) {
336
+ if (!Array.isArray(list))
337
+ continue;
338
+ const entry = list.find((e) => {
339
+ if (typeof e === "string")
340
+ return e === mem;
341
+ if (e !== null &&
342
+ typeof e === "object" &&
343
+ "name" in e)
344
+ return e.name === mem;
345
+ return false;
346
+ });
347
+ if (entry !== null &&
348
+ typeof entry === "object" &&
349
+ typeof entry.since === "string") {
350
+ hasSince = true;
351
+ break;
352
+ }
353
+ }
354
+ diffs.push({
355
+ clauseId: g.id,
356
+ path: `${g.id}.${mem}`,
357
+ kind: hasSince ? "added_ok" : "added_bare",
358
+ remedy: hasSince
359
+ ? `Member ${mem} in ${g.id} was added with since. Allowed in 1.x.`
360
+ : `Member ${mem} in ${g.id} was added without since. Add since or revert, or open a 2.0.0.`,
361
+ });
362
+ }
363
+ }
364
+ // Also compare clause-level metadata that is not in valueMembers (e.g., package version in C-06)
365
+ // If the whole value JSON differs but members didn't capture it (e.g., name change), mark changed.
366
+ if (JSON.stringify(g.value) !== JSON.stringify(l.value)) {
367
+ // Only emit clause-level changed if no finer-grained diff already emitted for this clause
368
+ const hasFiner = diffs.some((d) => d.clauseId === g.id && d.path !== g.id);
369
+ if (!hasFiner) {
370
+ diffs.push({
371
+ clauseId: g.id,
372
+ path: g.id,
373
+ kind: "changed",
374
+ remedy: `Clause ${g.id} changed. Revert, or open a 2.0.0.`,
375
+ });
376
+ }
377
+ }
378
+ }
379
+ diffs.sort((a, b) => a.clauseId === b.clauseId
380
+ ? a.path.localeCompare(b.path)
381
+ : a.clauseId.localeCompare(b.clauseId));
382
+ return diffs;
383
+ }
@@ -180,4 +180,3 @@ export function unifiedDiff(path, before, after) {
180
180
  const header = `--- a/${path}\n+++ b/${path}\n`;
181
181
  return `${header}${parts.join("\n")}\n`;
182
182
  }
183
- //# sourceMappingURL=diff.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * v1.0.0 (K10-027 / plan §5.7) — the untrusted-input boundary.
3
+ *
4
+ * One pure, total, idempotent escaping function per container Kevin
5
+ * writes stored text into. Every function here is a fixed point after
6
+ * one application — applying it twice equals applying it once — because
7
+ * a re-curated memory passes through the boundary on every regeneration
8
+ * and must not accumulate escaping.
9
+ *
10
+ * This module imports nothing and touches no filesystem. It is the
11
+ * boundary; the single enforcement point is ArtifactWriter (D6-01).
12
+ */
13
+ /**
14
+ * Neutralise text destined for the AGENTS.md marker block: HTML-escape
15
+ * (the memory-format discipline, made idempotent) so the literal
16
+ * `<!-- kevin:end -->` marker sequence and every comment terminator
17
+ * lose their meaning inside the block. The line-level strip of anything
18
+ * containing `kevin:begin`/`kevin:end` stays in
19
+ * ArtifactWriter.sanitizeArtifactBody — it is a filter, not an escape.
20
+ */
21
+ export declare function escapeForMarkerBlock(text: string): string;
22
+ /**
23
+ * Neutralise fenced-code delimiters: any run of three or more backticks
24
+ * or tildes would close (or open) a markdown fence around the rendered
25
+ * block and let stored text masquerade as un-fenced document content.
26
+ * Each byte of the run becomes its numeric HTML entity, which renders
27
+ * identically but can never form a delimiter again.
28
+ */
29
+ export declare function escapeForFence(text: string): string;
30
+ /**
31
+ * Neutralise bytes that would terminate an OKF v2 line: newline,
32
+ * carriage return and every other C0 control character become their
33
+ * JSON-style escapes, so a statement can never split into a second
34
+ * line even if a future writer forgets JSON.stringify. Quotation marks
35
+ * and backslashes are deliberately NOT touched here — the OKF line is
36
+ * canonical JSON and re-escaping them would corrupt parsing; JSON
37
+ * already guarantees they stay inside the string.
38
+ */
39
+ export declare function escapeForOkfLine(text: string): string;