@jmtrin/opencode-kevin 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +559 -771
  2. package/dist/migrations/010_v09_native.sql +78 -0
  3. package/dist/migrations/011_v10_proven.sql +69 -0
  4. package/dist/plugin/Archiver.js +0 -1
  5. package/dist/plugin/ArtifactWriter.d.ts +25 -10
  6. package/dist/plugin/ArtifactWriter.js +43 -25
  7. package/dist/plugin/CausalChain.js +0 -1
  8. package/dist/plugin/ConflictDetector.js +0 -1
  9. package/dist/plugin/ContextInjector.js +0 -1
  10. package/dist/plugin/ConventionMiner.js +0 -1
  11. package/dist/plugin/Curator.js +0 -1
  12. package/dist/plugin/Feedback.js +0 -1
  13. package/dist/plugin/HookLiveness.d.ts +87 -0
  14. package/dist/plugin/HookLiveness.js +338 -0
  15. package/dist/plugin/InjectionLedger.js +0 -1
  16. package/dist/plugin/LessonFixer.js +0 -1
  17. package/dist/plugin/Materializer.d.ts +25 -0
  18. package/dist/plugin/Materializer.js +39 -11
  19. package/dist/plugin/MemoryService.js +0 -1
  20. package/dist/plugin/Migrate.d.ts +1 -0
  21. package/dist/plugin/Migrate.js +69 -6
  22. package/dist/plugin/PatternMiner.js +0 -1
  23. package/dist/plugin/QualityGate.js +0 -1
  24. package/dist/plugin/Reflector.js +0 -1
  25. package/dist/plugin/RepoIdentity.d.ts +20 -9
  26. package/dist/plugin/RepoIdentity.js +44 -10
  27. package/dist/plugin/RepoTruth.js +0 -1
  28. package/dist/plugin/Retrospective.js +16 -1
  29. package/dist/plugin/SharedLayer.js +0 -1
  30. package/dist/plugin/Store.js +0 -1
  31. package/dist/plugin/ToolCallObserver.js +0 -1
  32. package/dist/plugin/capabilities.js +0 -1
  33. package/dist/plugin/confidence.js +0 -1
  34. package/dist/plugin/contract.d.ts +46 -0
  35. package/dist/plugin/contract.js +368 -0
  36. package/dist/plugin/diff.js +0 -1
  37. package/dist/plugin/escape.d.ts +39 -0
  38. package/dist/plugin/escape.js +60 -0
  39. package/dist/plugin/fingerprint.js +0 -1
  40. package/dist/plugin/host.d.ts +70 -0
  41. package/dist/plugin/host.js +250 -0
  42. package/dist/plugin/index.d.ts +2 -2
  43. package/dist/plugin/index.js +542 -222
  44. package/dist/plugin/inferability.js +0 -1
  45. package/dist/plugin/kevin_approve.js +0 -1
  46. package/dist/plugin/kevin_audit.d.ts +60 -0
  47. package/dist/plugin/kevin_audit.js +144 -1
  48. package/dist/plugin/kevin_bench.d.ts +10 -0
  49. package/dist/plugin/kevin_bench.js +82 -0
  50. package/dist/plugin/kevin_conflicts.js +0 -1
  51. package/dist/plugin/kevin_contract.d.ts +8 -0
  52. package/dist/plugin/kevin_contract.js +59 -0
  53. package/dist/plugin/kevin_doctor.d.ts +62 -0
  54. package/dist/plugin/kevin_doctor.js +196 -0
  55. package/dist/plugin/kevin_facts.js +0 -1
  56. package/dist/plugin/kevin_native.d.ts +29 -0
  57. package/dist/plugin/kevin_native.js +79 -0
  58. package/dist/plugin/kevin_propose.js +0 -1
  59. package/dist/plugin/kevin_publish.js +0 -1
  60. package/dist/plugin/kevin_why.js +0 -1
  61. package/dist/plugin/memory-format.js +0 -1
  62. package/dist/plugin/metrics.js +0 -1
  63. package/dist/plugin/native.d.ts +92 -0
  64. package/dist/plugin/native.js +190 -0
  65. package/dist/plugin/okf-export.js +0 -1
  66. package/dist/plugin/okf-import.js +0 -1
  67. package/dist/plugin/okf.js +0 -1
  68. package/dist/plugin/perf.d.ts +35 -0
  69. package/dist/plugin/perf.js +174 -0
  70. package/dist/plugin/query-tokenizer.js +0 -1
  71. package/dist/plugin/redact.js +0 -1
  72. package/dist/plugin/replay-types.js +0 -1
  73. package/dist/plugin/replay.js +0 -1
  74. package/dist/plugin/sqlite-adapter.js +0 -1
  75. package/dist/plugin/uuid.js +0 -1
  76. package/package.json +22 -7
  77. package/dist/plugin/Archiver.js.map +0 -1
  78. package/dist/plugin/ArtifactWriter.js.map +0 -1
  79. package/dist/plugin/CausalChain.js.map +0 -1
  80. package/dist/plugin/ConflictDetector.js.map +0 -1
  81. package/dist/plugin/ContextInjector.js.map +0 -1
  82. package/dist/plugin/ConventionMiner.js.map +0 -1
  83. package/dist/plugin/Curator.js.map +0 -1
  84. package/dist/plugin/Feedback.js.map +0 -1
  85. package/dist/plugin/InjectionLedger.js.map +0 -1
  86. package/dist/plugin/LessonFixer.js.map +0 -1
  87. package/dist/plugin/Materializer.js.map +0 -1
  88. package/dist/plugin/MemoryService.js.map +0 -1
  89. package/dist/plugin/Migrate.js.map +0 -1
  90. package/dist/plugin/PatternMiner.js.map +0 -1
  91. package/dist/plugin/QualityGate.js.map +0 -1
  92. package/dist/plugin/Reflector.js.map +0 -1
  93. package/dist/plugin/RepoIdentity.js.map +0 -1
  94. package/dist/plugin/RepoTruth.js.map +0 -1
  95. package/dist/plugin/Retrospective.js.map +0 -1
  96. package/dist/plugin/SharedLayer.js.map +0 -1
  97. package/dist/plugin/Store.js.map +0 -1
  98. package/dist/plugin/ToolCallObserver.js.map +0 -1
  99. package/dist/plugin/capabilities.js.map +0 -1
  100. package/dist/plugin/confidence.js.map +0 -1
  101. package/dist/plugin/diff.js.map +0 -1
  102. package/dist/plugin/fingerprint.js.map +0 -1
  103. package/dist/plugin/index.js.map +0 -1
  104. package/dist/plugin/inferability.js.map +0 -1
  105. package/dist/plugin/kevin_approve.js.map +0 -1
  106. package/dist/plugin/kevin_audit.js.map +0 -1
  107. package/dist/plugin/kevin_conflicts.js.map +0 -1
  108. package/dist/plugin/kevin_facts.js.map +0 -1
  109. package/dist/plugin/kevin_propose.js.map +0 -1
  110. package/dist/plugin/kevin_publish.js.map +0 -1
  111. package/dist/plugin/kevin_why.js.map +0 -1
  112. package/dist/plugin/memory-format.js.map +0 -1
  113. package/dist/plugin/metrics.js.map +0 -1
  114. package/dist/plugin/okf-export.js.map +0 -1
  115. package/dist/plugin/okf-import.js.map +0 -1
  116. package/dist/plugin/okf.js.map +0 -1
  117. package/dist/plugin/query-tokenizer.js.map +0 -1
  118. package/dist/plugin/redact.js.map +0 -1
  119. package/dist/plugin/replay-types.js.map +0 -1
  120. package/dist/plugin/replay.js.map +0 -1
  121. package/dist/plugin/sqlite-adapter.js.map +0 -1
  122. package/dist/plugin/uuid.js.map +0 -1
  123. package/migrations/001_initial.sql +0 -92
  124. package/migrations/002_indexes.sql +0 -14
  125. package/migrations/003_v02_signal.sql +0 -58
  126. package/migrations/004_v03_knowledge.sql +0 -138
  127. package/migrations/005_v04_signal.sql +0 -57
  128. package/migrations/006_v05_glassbox.sql +0 -118
  129. package/migrations/007_v06_pull.sql +0 -145
  130. package/migrations/008_v07_truth.sql +0 -124
  131. package/migrations/009_v08_team.sql +0 -100
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { fingerprint, fnv1a64 } from "./fingerprint.js";
4
4
  // ============================================================
5
5
  // Kevin 0.8.0 — RepoIdentity (K8-005/K8-006 / plan §5.1, D8-04)
6
+ // v0.9.0 — host identity source (K9-006 / plan §5.2, D9-13)
6
7
  // ============================================================
7
8
  // Repository-derived identity that survives a clone. Two clones
8
9
  // of the same remote must agree on one `repo_id` even though
@@ -149,8 +150,8 @@ export function computeRepoId(normalized) {
149
150
  return fnv1a64(`okf:repo:v1\u0000${normalized}`);
150
151
  }
151
152
  /**
152
- * Resolve the repository identity for `cwd`, trying the three
153
- * sources in order (plan §5.1):
153
+ * Resolve the repository identity for `cwd`, trying the four
154
+ * sources in order (plan §5.2, D9-13):
154
155
  *
155
156
  * 1. `declared` — `.kevin/project.json` → `id` (validated as
156
157
  * exactly 16 lowercase hex characters; anything else is ignored
@@ -159,14 +160,24 @@ export function computeRepoId(normalized) {
159
160
  * onto a typo);
160
161
  * 2. `remote` — `.git/config` → `[remote "origin"] url`,
161
162
  * normalized and hashed;
162
- * 3. `path` — `fingerprint(cwd)`, the v0.7.0 behaviour.
163
+ * 3. `host` — the value the host resolved for this directory,
164
+ * `host.project.worktree` first, `host.project.directory` as
165
+ * fallback; both empty or absent falls through to `path`.
166
+ * This is strictly better than `process.cwd()` — the host's own
167
+ * ToolContext documents "prefer this over `process.cwd()`" —
168
+ * but it sits below the explicit sources, because monorepos and
169
+ * D8-03's confirmed re-keying depend on `.kevin/project.json`
170
+ * winning;
171
+ * 4. `path` — `fingerprint(cwd)`, the v0.7.0 behaviour, preserved
172
+ * exactly when `host` is absent.
163
173
  *
164
174
  * Never throws: a directory that is not a git repository, an
165
- * unreadable `.git/config`, and a malformed `project.json` all fall
166
- * through to source 3. `projectId` is always returned alongside
167
- * `repoId`, regardless of which source won.
175
+ * unreadable `.git/config`, a malformed `project.json`, and a host
176
+ * with no usable project fields all fall through to source 4.
177
+ * `projectId` is always returned alongside `repoId`, regardless of
178
+ * which source won.
168
179
  */
169
- export function resolve(cwd) {
180
+ export function resolve(cwd, host) {
170
181
  const projectId = fingerprint(cwd);
171
182
  // 1. Declared.
172
183
  try {
@@ -201,7 +212,31 @@ export function resolve(cwd) {
201
212
  catch {
202
213
  // Unreadable or missing — fall through.
203
214
  }
204
- // 3. Path — the v0.7.0 behaviour, preserved exactly.
215
+ // 3. Host — the value the host already resolved for this
216
+ // directory (D9-13), inserted above `path` and below the two
217
+ // explicit sources. `worktree` first, `directory` as fallback;
218
+ // both empty or absent falls through. The chosen value feeds
219
+ // the unchanged `computeRepoId()` — this chain changes which
220
+ // string is hashed, never how.
221
+ const worktree = host?.project?.worktree;
222
+ if (typeof worktree === "string" && worktree.length > 0) {
223
+ return {
224
+ repoId: computeRepoId(worktree),
225
+ source: "host",
226
+ evidence: "host:worktree",
227
+ projectId,
228
+ };
229
+ }
230
+ const directory = host?.project?.directory;
231
+ if (typeof directory === "string" && directory.length > 0) {
232
+ return {
233
+ repoId: computeRepoId(directory),
234
+ source: "host",
235
+ evidence: "host:directory",
236
+ projectId,
237
+ };
238
+ }
239
+ // 4. Path — the v0.7.0 behaviour, preserved exactly.
205
240
  return { repoId: projectId, source: "path", evidence: "cwd", projectId };
206
241
  }
207
242
  /**
@@ -240,7 +275,7 @@ export function initProjectFile(cwd, writer) {
240
275
  const createdAt = new Date().toISOString();
241
276
  const payload = `${JSON.stringify({
242
277
  created_at: createdAt,
243
- generator: "opencode-kevin/0.8.0",
278
+ generator: "opencode-kevin/0.9.0",
244
279
  id,
245
280
  })}\n`;
246
281
  try {
@@ -263,4 +298,3 @@ export function initProjectFile(cwd, writer) {
263
298
  };
264
299
  }
265
300
  }
266
- //# 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
@@ -58,6 +58,22 @@ export const METRIC_KEY_LABELS = {
58
58
  okf_merge_folds: "Fusiones OKF (folds)",
59
59
  rekey_events: "Re-keys de repositorio",
60
60
  injections_from_shared: "Inyecciones desde la capa compartida",
61
+ // v0.9.0 (K9-003 / plan §8.16) — the K9 metric keys need their own
62
+ // Spanish labels; the audit regression forbids raw-key fallback for any
63
+ // key in METRIC_KEYS.
64
+ hook_fires_total: "Disparos de hooks (totales)",
65
+ hook_errors_total: "Errores de hooks",
66
+ hooks_dead_total: "Hooks muertos",
67
+ injections_suppressed_dead_hook: "Inyecciones suprimidas (hook muerto)",
68
+ native_registrations_total: "Registros nativos (totales)",
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)",
61
77
  };
62
78
  function originLabel(origin) {
63
79
  if (origin === "reflector")
@@ -191,4 +207,3 @@ export class Retrospective {
191
207
  return lines.join("\n");
192
208
  }
193
209
  }
194
- //# 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
@@ -186,4 +186,3 @@ export class ToolCallObserver {
186
186
  return v;
187
187
  }
188
188
  }
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
@@ -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,46 @@
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.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
33
+ * not trusted: anything reaching an artifact or a prompt is escaped at
34
+ * the single write path, according to its container.
35
+ */
36
+ export declare const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
37
+ export declare function describeContract(_input?: ContractInput): PublicContract;
38
+ export declare function contractDigest(c: PublicContract): string;
39
+ export type ContractDiffKind = "removed" | "changed" | "added_ok" | "added_bare";
40
+ export interface ContractDiff {
41
+ readonly clauseId: string;
42
+ readonly path: string;
43
+ readonly kind: ContractDiffKind;
44
+ readonly remedy: string;
45
+ }
46
+ export declare function diffContract(golden: PublicContract, live: PublicContract): readonly ContractDiff[];
@@ -0,0 +1,368 @@
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_contract", since: "1.0.0" },
45
+ { name: "kevin_bench", since: "1.0.0" },
46
+ ];
47
+ /**
48
+ * v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
49
+ * not trusted: anything reaching an artifact or a prompt is escaped at
50
+ * the single write path, according to its container.
51
+ */
52
+ export const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
53
+ // For canonical JSON: sort keys recursively, no floats.
54
+ function canonicalJson(value) {
55
+ if (value === null)
56
+ return "null";
57
+ if (typeof value === "boolean")
58
+ return value ? "true" : "false";
59
+ if (typeof value === "number") {
60
+ if (!Number.isFinite(value) || !Number.isInteger(value))
61
+ throw new Error("contract: non-integer number in canonical JSON");
62
+ return String(value);
63
+ }
64
+ if (typeof value === "string")
65
+ return JSON.stringify(value);
66
+ if (Array.isArray(value))
67
+ return `[${value.map(canonicalJson).join(",")}]`;
68
+ if (typeof value === "object") {
69
+ const obj = value;
70
+ const keys = Object.keys(obj).sort();
71
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
72
+ }
73
+ return JSON.stringify(value);
74
+ }
75
+ export function describeContract(_input) {
76
+ // Derive clause values from live source wherever possible (plan §5.1).
77
+ // C-03 members added after the freeze carry their `since` as objects;
78
+ // the original frozen set stays plain strings.
79
+ const additions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
80
+ const toolValue = {
81
+ tools: [[...CONTRACT_TOOL_NAMES].sort(), additions].flat(),
82
+ };
83
+ const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
84
+ const metricValue = { keys: Object.keys(METRIC_KEY_LABELS).sort() };
85
+ const clauses = [
86
+ {
87
+ id: "C-01",
88
+ title: "AGENTS.md marker pair",
89
+ stability: "frozen",
90
+ since: "0.6.0",
91
+ value: {
92
+ markers: [MARKER_BEGIN, MARKER_END],
93
+ splice_rule: "bytes outside markers preserved verbatim",
94
+ },
95
+ },
96
+ {
97
+ id: "C-02",
98
+ title: "OKF v2 wire format",
99
+ stability: "frozen",
100
+ since: "0.8.0",
101
+ value: {
102
+ header_lines: 3,
103
+ field_order: ["entry_id", "type", "content", "scope", "created_at"],
104
+ entry_id_derivation: "fnv1a64 over canonical field ordering",
105
+ eol: "LF",
106
+ integers_only: true,
107
+ sort_order: "entry_id ascending",
108
+ max_line_bytes: MAX_LINE_BYTES,
109
+ max_entries: MAX_ENTRIES,
110
+ },
111
+ },
112
+ {
113
+ id: "C-03",
114
+ title: "Tool names and argument shapes",
115
+ stability: "frozen",
116
+ since: "0.2.0",
117
+ value: toolValue,
118
+ },
119
+ {
120
+ id: "C-04",
121
+ title: "Setting keys, types and defaults",
122
+ stability: "frozen",
123
+ since: "0.2.0",
124
+ value: settingValue,
125
+ },
126
+ {
127
+ id: "C-05",
128
+ title: "Metric key names",
129
+ stability: "frozen",
130
+ since: "0.2.0",
131
+ value: metricValue,
132
+ },
133
+ {
134
+ id: "C-06",
135
+ title: "Package entry points",
136
+ stability: "frozen",
137
+ since: "0.1.0",
138
+ value: {
139
+ name: "@jmtrin/opencode-kevin",
140
+ main: "dist/plugin/index.js",
141
+ types: "dist/plugin/index.d.ts",
142
+ exports_order: ["types", "import"],
143
+ engines: ">=22.5.0",
144
+ },
145
+ },
146
+ {
147
+ id: "C-07",
148
+ title: "Database schema",
149
+ stability: "forward-only",
150
+ since: "0.1.0",
151
+ value: {
152
+ schema_version: "011",
153
+ migrations_forward_only: true,
154
+ },
155
+ },
156
+ {
157
+ id: "C-08",
158
+ title: "Filesystem locations",
159
+ stability: "frozen",
160
+ since: "0.2.0",
161
+ value: {
162
+ db: "~/.opencode-kevin/kevin.db",
163
+ refs: "refs/",
164
+ skills: "skills/",
165
+ okf: ".kevin/knowledge.okf",
166
+ },
167
+ },
168
+ {
169
+ id: "C-09",
170
+ title: "Behavioural invariants",
171
+ stability: "frozen",
172
+ since: "0.8.0",
173
+ value: {
174
+ invariants: [
175
+ "zero process spawns",
176
+ "zero network calls",
177
+ "no raw author email written",
178
+ "single write path: ArtifactWriter is the only artifact writer",
179
+ ],
180
+ // v1.0.0 (K10-027 / plan §5.7) — the untrusted-input boundary
181
+ // joins the clause as an addition carrying its `since`; the
182
+ // frozen invariant strings above are untouched.
183
+ boundary: [{ name: BOUNDARY_INVARIANT, since: "1.0.0" }],
184
+ },
185
+ },
186
+ ];
187
+ return { contractVersion: CONTRACT_VERSION, clauses };
188
+ }
189
+ export function contractDigest(c) {
190
+ const payload = canonicalJson(c.clauses);
191
+ return fnv1a64(payload);
192
+ }
193
+ function valueMembers(value) {
194
+ // Normalize clause value into a flat member map for diffing.
195
+ // If value is { tools: [...] } => members are tool names.
196
+ // If { keys: [...] } => member per key.
197
+ // Otherwise flatten object keys.
198
+ if (value !== null && typeof value === "object") {
199
+ const obj = value;
200
+ if (Array.isArray(obj.tools)) {
201
+ const m = new Map();
202
+ for (const t of obj.tools) {
203
+ if (typeof t === "string")
204
+ m.set(t, undefined);
205
+ else if (t !== null &&
206
+ typeof t === "object" &&
207
+ "name" in t)
208
+ m.set(t.name, t);
209
+ }
210
+ return m;
211
+ }
212
+ if (Array.isArray(obj.keys)) {
213
+ const m = new Map();
214
+ for (const k of obj.keys) {
215
+ if (typeof k === "string")
216
+ m.set(k, undefined);
217
+ else if (k !== null &&
218
+ typeof k === "object" &&
219
+ "name" in k)
220
+ m.set(k.name, k);
221
+ }
222
+ return m;
223
+ }
224
+ if (Array.isArray(obj.markers) || Array.isArray(obj.invariants)) {
225
+ const arr = (obj.markers ?? obj.invariants);
226
+ const m = new Map();
227
+ for (const v of arr)
228
+ m.set(v, v);
229
+ // v1.0.0 (K10-027) — boundary additions sit beside the frozen
230
+ // invariant strings and carry their `since` like tool additions.
231
+ if (Array.isArray(obj.boundary)) {
232
+ for (const b of obj.boundary) {
233
+ if (b !== null &&
234
+ typeof b === "object" &&
235
+ "name" in b)
236
+ m.set(b.name, b);
237
+ }
238
+ }
239
+ return m;
240
+ }
241
+ // Generic object: each key is a member
242
+ const m = new Map();
243
+ for (const [k, v] of Object.entries(obj))
244
+ m.set(k, v);
245
+ return m;
246
+ }
247
+ return new Map([["value", value]]);
248
+ }
249
+ export function diffContract(golden, live) {
250
+ const diffs = [];
251
+ const goldenById = new Map(golden.clauses.map((c) => [c.id, c]));
252
+ const liveById = new Map(live.clauses.map((c) => [c.id, c]));
253
+ // Removed clauses
254
+ for (const g of golden.clauses) {
255
+ if (!liveById.has(g.id)) {
256
+ diffs.push({
257
+ clauseId: g.id,
258
+ path: g.id,
259
+ kind: "removed",
260
+ remedy: `Clause ${g.id} was removed. Revert, or open a 2.0.0.`,
261
+ });
262
+ }
263
+ }
264
+ // Added clauses — check since
265
+ for (const l of live.clauses) {
266
+ if (!goldenById.has(l.id)) {
267
+ const hasSince = typeof l.since === "string" &&
268
+ l.since.length > 0;
269
+ diffs.push({
270
+ clauseId: l.id,
271
+ path: l.id,
272
+ kind: hasSince ? "added_ok" : "added_bare",
273
+ remedy: hasSince
274
+ ? `Clause ${l.id} was added with since. Allowed in 1.x.`
275
+ : `Clause ${l.id} was added without since. Add since or revert, or open a 2.0.0.`,
276
+ });
277
+ }
278
+ }
279
+ // Within-clause diff
280
+ for (const g of golden.clauses) {
281
+ const l = liveById.get(g.id);
282
+ if (!l)
283
+ continue;
284
+ const gMembers = valueMembers(g.value);
285
+ const lMembers = valueMembers(l.value);
286
+ // Normalize members: for arrays we care about set equality
287
+ for (const [mem, _gv] of gMembers) {
288
+ if (!lMembers.has(mem)) {
289
+ diffs.push({
290
+ clauseId: g.id,
291
+ path: `${g.id}.${mem}`,
292
+ kind: "removed",
293
+ remedy: `Member ${mem} in ${g.id} was removed. Revert, or open a 2.0.0.`,
294
+ });
295
+ }
296
+ else {
297
+ const lv = lMembers.get(mem);
298
+ const gv = gMembers.get(mem);
299
+ if (JSON.stringify(lv) !== JSON.stringify(gv)) {
300
+ // Member present on both sides but its value moved (e.g. an
301
+ // addition object whose `since` was edited). Report it here,
302
+ // at member granularity, instead of letting it fall through
303
+ // to the coarser clause-level check below.
304
+ diffs.push({
305
+ clauseId: g.id,
306
+ path: `${g.id}.${mem}`,
307
+ kind: "changed",
308
+ remedy: `Member ${mem} in ${g.id} changed. Revert, or open a 2.0.0.`,
309
+ });
310
+ }
311
+ }
312
+ }
313
+ for (const [mem, _lv] of lMembers) {
314
+ if (!gMembers.has(mem)) {
315
+ const liveVal = l.value;
316
+ // Check if the live member carries `since` (additions may ride
317
+ // in the keys, tools or boundary arrays — K10-018/K10-027).
318
+ const lists = [liveVal.keys, liveVal.tools, liveVal.boundary];
319
+ let hasSince = false;
320
+ for (const list of lists) {
321
+ if (!Array.isArray(list))
322
+ continue;
323
+ const entry = list.find((e) => {
324
+ if (typeof e === "string")
325
+ return e === mem;
326
+ if (e !== null &&
327
+ typeof e === "object" &&
328
+ "name" in e)
329
+ return e.name === mem;
330
+ return false;
331
+ });
332
+ if (entry !== null &&
333
+ typeof entry === "object" &&
334
+ typeof entry.since === "string") {
335
+ hasSince = true;
336
+ break;
337
+ }
338
+ }
339
+ diffs.push({
340
+ clauseId: g.id,
341
+ path: `${g.id}.${mem}`,
342
+ kind: hasSince ? "added_ok" : "added_bare",
343
+ remedy: hasSince
344
+ ? `Member ${mem} in ${g.id} was added with since. Allowed in 1.x.`
345
+ : `Member ${mem} in ${g.id} was added without since. Add since or revert, or open a 2.0.0.`,
346
+ });
347
+ }
348
+ }
349
+ // Also compare clause-level metadata that is not in valueMembers (e.g., package version in C-06)
350
+ // If the whole value JSON differs but members didn't capture it (e.g., name change), mark changed.
351
+ if (JSON.stringify(g.value) !== JSON.stringify(l.value)) {
352
+ // Only emit clause-level changed if no finer-grained diff already emitted for this clause
353
+ const hasFiner = diffs.some((d) => d.clauseId === g.id && d.path !== g.id);
354
+ if (!hasFiner) {
355
+ diffs.push({
356
+ clauseId: g.id,
357
+ path: g.id,
358
+ kind: "changed",
359
+ remedy: `Clause ${g.id} changed. Revert, or open a 2.0.0.`,
360
+ });
361
+ }
362
+ }
363
+ }
364
+ diffs.sort((a, b) => a.clauseId === b.clauseId
365
+ ? a.path.localeCompare(b.path)
366
+ : a.clauseId.localeCompare(b.clauseId));
367
+ return diffs;
368
+ }
@@ -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;
@@ -0,0 +1,60 @@
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
+ const ENTITY_HEADS = /&(?!(amp|lt|gt|#\d+);)/g;
14
+ /**
15
+ * Neutralise text destined for the AGENTS.md marker block: HTML-escape
16
+ * (the memory-format discipline, made idempotent) so the literal
17
+ * `<!-- kevin:end -->` marker sequence and every comment terminator
18
+ * lose their meaning inside the block. The line-level strip of anything
19
+ * containing `kevin:begin`/`kevin:end` stays in
20
+ * ArtifactWriter.sanitizeArtifactBody — it is a filter, not an escape.
21
+ */
22
+ export function escapeForMarkerBlock(text) {
23
+ return text
24
+ .replace(ENTITY_HEADS, "&amp;")
25
+ .replace(/</g, "&lt;")
26
+ .replace(/>/g, "&gt;");
27
+ }
28
+ /**
29
+ * Neutralise fenced-code delimiters: any run of three or more backticks
30
+ * or tildes would close (or open) a markdown fence around the rendered
31
+ * block and let stored text masquerade as un-fenced document content.
32
+ * Each byte of the run becomes its numeric HTML entity, which renders
33
+ * identically but can never form a delimiter again.
34
+ */
35
+ export function escapeForFence(text) {
36
+ return text
37
+ .replace(/`{3,}/g, (run) => "&#96;".repeat(run.length))
38
+ .replace(/~{3,}/g, (run) => "&#126;".repeat(run.length));
39
+ }
40
+ /**
41
+ * Neutralise bytes that would terminate an OKF v2 line: newline,
42
+ * carriage return and every other C0 control character become their
43
+ * JSON-style escapes, so a statement can never split into a second
44
+ * line even if a future writer forgets JSON.stringify. Quotation marks
45
+ * and backslashes are deliberately NOT touched here — the OKF line is
46
+ * canonical JSON and re-escaping them would corrupt parsing; JSON
47
+ * already guarantees they stay inside the string.
48
+ */
49
+ export function escapeForOkfLine(text) {
50
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: neutralising control bytes is this function's entire job
51
+ return text.replace(/[\u0000-\u001f]/g, (c) => {
52
+ if (c === "\n")
53
+ return "\\n";
54
+ if (c === "\r")
55
+ return "\\r";
56
+ if (c === "\t")
57
+ return "\\t";
58
+ return `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`;
59
+ });
60
+ }