@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.
- package/README.md +559 -771
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/plugin/Archiver.js +0 -1
- package/dist/plugin/ArtifactWriter.d.ts +25 -10
- package/dist/plugin/ArtifactWriter.js +43 -25
- package/dist/plugin/CausalChain.js +0 -1
- package/dist/plugin/ConflictDetector.js +0 -1
- package/dist/plugin/ContextInjector.js +0 -1
- package/dist/plugin/ConventionMiner.js +0 -1
- package/dist/plugin/Curator.js +0 -1
- package/dist/plugin/Feedback.js +0 -1
- package/dist/plugin/HookLiveness.d.ts +87 -0
- package/dist/plugin/HookLiveness.js +338 -0
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.d.ts +25 -0
- package/dist/plugin/Materializer.js +39 -11
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -0
- package/dist/plugin/Migrate.js +69 -6
- package/dist/plugin/PatternMiner.js +0 -1
- package/dist/plugin/QualityGate.js +0 -1
- package/dist/plugin/Reflector.js +0 -1
- package/dist/plugin/RepoIdentity.d.ts +20 -9
- package/dist/plugin/RepoIdentity.js +44 -10
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +16 -1
- package/dist/plugin/SharedLayer.js +0 -1
- package/dist/plugin/Store.js +0 -1
- package/dist/plugin/ToolCallObserver.js +0 -1
- package/dist/plugin/capabilities.js +0 -1
- package/dist/plugin/confidence.js +0 -1
- package/dist/plugin/contract.d.ts +46 -0
- package/dist/plugin/contract.js +368 -0
- package/dist/plugin/diff.js +0 -1
- package/dist/plugin/escape.d.ts +39 -0
- package/dist/plugin/escape.js +60 -0
- package/dist/plugin/fingerprint.js +0 -1
- package/dist/plugin/host.d.ts +70 -0
- package/dist/plugin/host.js +250 -0
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +542 -222
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +60 -0
- package/dist/plugin/kevin_audit.js +144 -1
- package/dist/plugin/kevin_bench.d.ts +10 -0
- package/dist/plugin/kevin_bench.js +82 -0
- package/dist/plugin/kevin_conflicts.js +0 -1
- package/dist/plugin/kevin_contract.d.ts +8 -0
- package/dist/plugin/kevin_contract.js +59 -0
- package/dist/plugin/kevin_doctor.d.ts +62 -0
- package/dist/plugin/kevin_doctor.js +196 -0
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_native.d.ts +29 -0
- package/dist/plugin/kevin_native.js +79 -0
- package/dist/plugin/kevin_propose.js +0 -1
- package/dist/plugin/kevin_publish.js +0 -1
- package/dist/plugin/kevin_why.js +0 -1
- package/dist/plugin/memory-format.js +0 -1
- package/dist/plugin/metrics.js +0 -1
- package/dist/plugin/native.d.ts +92 -0
- package/dist/plugin/native.js +190 -0
- package/dist/plugin/okf-export.js +0 -1
- package/dist/plugin/okf-import.js +0 -1
- package/dist/plugin/okf.js +0 -1
- package/dist/plugin/perf.d.ts +35 -0
- package/dist/plugin/perf.js +174 -0
- package/dist/plugin/query-tokenizer.js +0 -1
- package/dist/plugin/redact.js +0 -1
- package/dist/plugin/replay-types.js +0 -1
- package/dist/plugin/replay.js +0 -1
- package/dist/plugin/sqlite-adapter.js +0 -1
- package/dist/plugin/uuid.js +0 -1
- package/package.json +22 -7
- package/dist/plugin/Archiver.js.map +0 -1
- package/dist/plugin/ArtifactWriter.js.map +0 -1
- package/dist/plugin/CausalChain.js.map +0 -1
- package/dist/plugin/ConflictDetector.js.map +0 -1
- package/dist/plugin/ContextInjector.js.map +0 -1
- package/dist/plugin/ConventionMiner.js.map +0 -1
- package/dist/plugin/Curator.js.map +0 -1
- package/dist/plugin/Feedback.js.map +0 -1
- package/dist/plugin/InjectionLedger.js.map +0 -1
- package/dist/plugin/LessonFixer.js.map +0 -1
- package/dist/plugin/Materializer.js.map +0 -1
- package/dist/plugin/MemoryService.js.map +0 -1
- package/dist/plugin/Migrate.js.map +0 -1
- package/dist/plugin/PatternMiner.js.map +0 -1
- package/dist/plugin/QualityGate.js.map +0 -1
- package/dist/plugin/Reflector.js.map +0 -1
- package/dist/plugin/RepoIdentity.js.map +0 -1
- package/dist/plugin/RepoTruth.js.map +0 -1
- package/dist/plugin/Retrospective.js.map +0 -1
- package/dist/plugin/SharedLayer.js.map +0 -1
- package/dist/plugin/Store.js.map +0 -1
- package/dist/plugin/ToolCallObserver.js.map +0 -1
- package/dist/plugin/capabilities.js.map +0 -1
- package/dist/plugin/confidence.js.map +0 -1
- package/dist/plugin/diff.js.map +0 -1
- package/dist/plugin/fingerprint.js.map +0 -1
- package/dist/plugin/index.js.map +0 -1
- package/dist/plugin/inferability.js.map +0 -1
- package/dist/plugin/kevin_approve.js.map +0 -1
- package/dist/plugin/kevin_audit.js.map +0 -1
- package/dist/plugin/kevin_conflicts.js.map +0 -1
- package/dist/plugin/kevin_facts.js.map +0 -1
- package/dist/plugin/kevin_propose.js.map +0 -1
- package/dist/plugin/kevin_publish.js.map +0 -1
- package/dist/plugin/kevin_why.js.map +0 -1
- package/dist/plugin/memory-format.js.map +0 -1
- package/dist/plugin/metrics.js.map +0 -1
- package/dist/plugin/okf-export.js.map +0 -1
- package/dist/plugin/okf-import.js.map +0 -1
- package/dist/plugin/okf.js.map +0 -1
- package/dist/plugin/query-tokenizer.js.map +0 -1
- package/dist/plugin/redact.js.map +0 -1
- package/dist/plugin/replay-types.js.map +0 -1
- package/dist/plugin/replay.js.map +0 -1
- package/dist/plugin/sqlite-adapter.js.map +0 -1
- package/dist/plugin/uuid.js.map +0 -1
- package/migrations/001_initial.sql +0 -92
- package/migrations/002_indexes.sql +0 -14
- package/migrations/003_v02_signal.sql +0 -58
- package/migrations/004_v03_knowledge.sql +0 -138
- package/migrations/005_v04_signal.sql +0 -57
- package/migrations/006_v05_glassbox.sql +0 -118
- package/migrations/007_v06_pull.sql +0 -145
- package/migrations/008_v07_truth.sql +0 -124
- 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
|
|
153
|
-
* sources in order (plan §5.
|
|
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. `
|
|
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`,
|
|
166
|
-
*
|
|
167
|
-
* `repoId`, regardless of
|
|
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.
|
|
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.
|
|
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
|
package/dist/plugin/RepoTruth.js
CHANGED
|
@@ -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
|
package/dist/plugin/Store.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/plugin/diff.js
CHANGED
|
@@ -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, "&")
|
|
25
|
+
.replace(/</g, "<")
|
|
26
|
+
.replace(/>/g, ">");
|
|
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) => "`".repeat(run.length))
|
|
38
|
+
.replace(/~{3,}/g, (run) => "~".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
|
+
}
|