@jmtrin/kevin-core 1.3.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/dist/Archiver.d.ts +42 -0
- package/dist/Archiver.js +83 -0
- package/dist/ArtifactWriter.d.ts +93 -0
- package/dist/ArtifactWriter.js +291 -0
- package/dist/CausalChain.d.ts +22 -0
- package/dist/CausalChain.js +197 -0
- package/dist/ChatBridge.d.ts +41 -0
- package/dist/ChatBridge.js +103 -0
- package/dist/ConflictDetector.d.ts +35 -0
- package/dist/ConflictDetector.js +260 -0
- package/dist/ContextInjector.d.ts +222 -0
- package/dist/ContextInjector.js +533 -0
- package/dist/ConventionMiner.d.ts +35 -0
- package/dist/ConventionMiner.js +242 -0
- package/dist/Curator.d.ts +118 -0
- package/dist/Curator.js +326 -0
- package/dist/DashboardHtml.d.ts +5 -0
- package/dist/DashboardHtml.js +180 -0
- package/dist/Feedback.d.ts +67 -0
- package/dist/Feedback.js +120 -0
- package/dist/HookLiveness.d.ts +88 -0
- package/dist/HookLiveness.js +322 -0
- package/dist/InjectionLedger.d.ts +98 -0
- package/dist/InjectionLedger.js +295 -0
- package/dist/LessonFixer.d.ts +44 -0
- package/dist/LessonFixer.js +45 -0
- package/dist/Materializer.d.ts +84 -0
- package/dist/Materializer.js +181 -0
- package/dist/MemoryService.d.ts +360 -0
- package/dist/MemoryService.js +1242 -0
- package/dist/Migrate.d.ts +24 -0
- package/dist/Migrate.js +285 -0
- package/dist/PatternMiner.d.ts +49 -0
- package/dist/PatternMiner.js +132 -0
- package/dist/QualityGate.d.ts +124 -0
- package/dist/QualityGate.js +119 -0
- package/dist/Reflector.d.ts +91 -0
- package/dist/Reflector.js +307 -0
- package/dist/RepoIdentity.d.ts +124 -0
- package/dist/RepoIdentity.js +300 -0
- package/dist/RepoTruth.d.ts +80 -0
- package/dist/RepoTruth.js +599 -0
- package/dist/Retrospective.d.ts +18 -0
- package/dist/Retrospective.js +216 -0
- package/dist/SharedLayer.d.ts +159 -0
- package/dist/SharedLayer.js +462 -0
- package/dist/Store.d.ts +29 -0
- package/dist/Store.js +50 -0
- package/dist/ToolCallObserver.d.ts +32 -0
- package/dist/ToolCallObserver.js +201 -0
- package/dist/TuiActions.d.ts +43 -0
- package/dist/TuiActions.js +181 -0
- package/dist/TuiSnapshots.d.ts +24 -0
- package/dist/TuiSnapshots.js +159 -0
- package/dist/capabilities.d.ts +6 -0
- package/dist/capabilities.js +1 -0
- package/dist/columns.d.ts +11 -0
- package/dist/columns.js +54 -0
- package/dist/confidence.d.ts +8 -0
- package/dist/confidence.js +34 -0
- package/dist/contract.d.ts +68 -0
- package/dist/contract.js +430 -0
- package/dist/diff.d.ts +8 -0
- package/dist/diff.js +182 -0
- package/dist/env.d.ts +5 -0
- package/dist/env.js +12 -0
- package/dist/escape.d.ts +39 -0
- package/dist/escape.js +60 -0
- package/dist/fingerprint.d.ts +27 -0
- package/dist/fingerprint.js +73 -0
- package/dist/host.d.ts +18 -0
- package/dist/host.js +7 -0
- package/dist/idle-pipeline.d.ts +9 -0
- package/dist/idle-pipeline.js +44 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +109 -0
- package/dist/inferability.d.ts +32 -0
- package/dist/inferability.js +88 -0
- package/dist/kevin_approve.d.ts +34 -0
- package/dist/kevin_approve.js +52 -0
- package/dist/kevin_audit.d.ts +230 -0
- package/dist/kevin_audit.js +631 -0
- package/dist/kevin_bench.d.ts +11 -0
- package/dist/kevin_bench.js +83 -0
- package/dist/kevin_conflicts.d.ts +9 -0
- package/dist/kevin_conflicts.js +50 -0
- package/dist/kevin_contract.d.ts +8 -0
- package/dist/kevin_contract.js +59 -0
- package/dist/kevin_doctor.d.ts +63 -0
- package/dist/kevin_doctor.js +197 -0
- package/dist/kevin_facts.d.ts +42 -0
- package/dist/kevin_facts.js +36 -0
- package/dist/kevin_forget.d.ts +33 -0
- package/dist/kevin_forget.js +260 -0
- package/dist/kevin_native.d.ts +29 -0
- package/dist/kevin_native.js +79 -0
- package/dist/kevin_propose.d.ts +23 -0
- package/dist/kevin_propose.js +14 -0
- package/dist/kevin_publish.d.ts +38 -0
- package/dist/kevin_publish.js +18 -0
- package/dist/kevin_why.d.ts +23 -0
- package/dist/kevin_why.js +111 -0
- package/dist/memory-format.d.ts +20 -0
- package/dist/memory-format.js +59 -0
- package/dist/metrics.d.ts +97 -0
- package/dist/metrics.js +288 -0
- package/dist/migrations/001_initial.sql +92 -0
- package/dist/migrations/002_indexes.sql +14 -0
- package/dist/migrations/003_v02_signal.sql +58 -0
- package/dist/migrations/004_v03_knowledge.sql +138 -0
- package/dist/migrations/005_v04_signal.sql +57 -0
- package/dist/migrations/006_v05_glassbox.sql +118 -0
- package/dist/migrations/007_v06_pull.sql +145 -0
- package/dist/migrations/008_v07_truth.sql +124 -0
- package/dist/migrations/009_v08_team.sql +100 -0
- package/dist/migrations/010_v09_native.sql +78 -0
- package/dist/migrations/011_v10_proven.sql +69 -0
- package/dist/migrations/012_v11_drift.sql +24 -0
- package/dist/native.d.ts +23 -0
- package/dist/native.js +1 -0
- package/dist/okf-export.d.ts +3 -0
- package/dist/okf-export.js +134 -0
- package/dist/okf-import.d.ts +76 -0
- package/dist/okf-import.js +271 -0
- package/dist/okf.d.ts +107 -0
- package/dist/okf.js +303 -0
- package/dist/perf.d.ts +35 -0
- package/dist/perf.js +174 -0
- package/dist/query-tokenizer.d.ts +13 -0
- package/dist/query-tokenizer.js +133 -0
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +18 -0
- package/dist/replay-types.d.ts +69 -0
- package/dist/replay-types.js +156 -0
- package/dist/replay.d.ts +44 -0
- package/dist/replay.js +215 -0
- package/dist/sqlite-adapter.d.ts +12 -0
- package/dist/sqlite-adapter.js +78 -0
- package/dist/time-ms.d.ts +1 -0
- package/dist/time-ms.js +16 -0
- package/dist/tui-types.d.ts +59 -0
- package/dist/tui-types.js +4 -0
- package/dist/uuid.d.ts +1 -0
- package/dist/uuid.js +50 -0
- package/package.json +28 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const CONFIDENCE_MIN = 0.05;
|
|
2
|
+
export declare const CONFIDENCE_MAX = 0.95;
|
|
3
|
+
export declare const CONFIDENCE_BASE = 0.5;
|
|
4
|
+
export declare const EVIDENCE_STEP = 0.1;
|
|
5
|
+
export declare const RECURRENCE_PENALTY = 0.15;
|
|
6
|
+
export declare const FEEDBACK_POSITIVE_STEP = 0.05;
|
|
7
|
+
export declare const FEEDBACK_NEGATIVE_STEP = 0.1;
|
|
8
|
+
export declare function computeConfidence(evidenceCount: number, recurrenceCount: number, positiveFeedback?: number, negativeFeedback?: number): number;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// v0.4.0 (K4-010) — shared two-sided confidence formula.
|
|
2
|
+
//
|
|
3
|
+
// Confidence is derived from both positive evidence (confirmed fixes) and
|
|
4
|
+
// negative evidence (recurrences): recurrence of the same fingerprint
|
|
5
|
+
// DEMOTES a pattern's confidence, so that a "causal pattern" with a
|
|
6
|
+
// history of recurring failures stops being presented as reliable.
|
|
7
|
+
//
|
|
8
|
+
// confidence = clamp(0.5 + 0.1 * evidenceCount - 0.15 * recurrenceCount, 0.05, 0.95)
|
|
9
|
+
//
|
|
10
|
+
// Used by `MemoryService.promoteToPattern`, `MemoryService.mapRow` and
|
|
11
|
+
// `kevin_why` — keep the formula here, never inline it again.
|
|
12
|
+
//
|
|
13
|
+
// v0.5.0 (K5-010 / plan §5.3, D5-02) — human feedback joins the formula as
|
|
14
|
+
// a second, independent evidence axis. Steps are deliberately SMALLER than
|
|
15
|
+
// the causal steps: feedback is sparse, one-sided, and judgemental, while
|
|
16
|
+
// evidence_count counts confirmed, reproducible fixes. A single negative
|
|
17
|
+
// verdict (0.1) can offset one confirmed fix (0.1), but cannot alone sink
|
|
18
|
+
// a well-evidenced pattern — the causal evidence must agree (plan §5.3,
|
|
19
|
+
// "feedback cannot override causal evidence, only nudge it").
|
|
20
|
+
export const CONFIDENCE_MIN = 0.05;
|
|
21
|
+
export const CONFIDENCE_MAX = 0.95;
|
|
22
|
+
export const CONFIDENCE_BASE = 0.5;
|
|
23
|
+
export const EVIDENCE_STEP = 0.1;
|
|
24
|
+
export const RECURRENCE_PENALTY = 0.15;
|
|
25
|
+
export const FEEDBACK_POSITIVE_STEP = 0.05;
|
|
26
|
+
export const FEEDBACK_NEGATIVE_STEP = 0.1;
|
|
27
|
+
export function computeConfidence(evidenceCount, recurrenceCount, positiveFeedback = 0, negativeFeedback = 0) {
|
|
28
|
+
const raw = CONFIDENCE_BASE +
|
|
29
|
+
EVIDENCE_STEP * evidenceCount -
|
|
30
|
+
RECURRENCE_PENALTY * recurrenceCount +
|
|
31
|
+
FEEDBACK_POSITIVE_STEP * positiveFeedback -
|
|
32
|
+
FEEDBACK_NEGATIVE_STEP * negativeFeedback;
|
|
33
|
+
return Math.min(CONFIDENCE_MAX, Math.max(CONFIDENCE_MIN, raw));
|
|
34
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type KevinEnv } from "./env.js";
|
|
2
|
+
/**
|
|
3
|
+
* K13-012 (D13-05) — scanRoots plumbing. Resolves monorepo vs packed locations.
|
|
4
|
+
* Monorepo: [packages/plugin/src, packages/core/src] when both dirs exist on disk.
|
|
5
|
+
* Packed: walk-up via require.resolve for @jmtrin/kevin-core and @jmtrin/opencode-kevin.
|
|
6
|
+
* Clause VALUES are unchanged; this only provides the roots future parsers would scan.
|
|
7
|
+
* Uses KevinEnv for cwd (K13-006) — defaults via resolveEnv — env.ts is the only
|
|
8
|
+
* core file allowed to call process cwd.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveScanRoots(env?: KevinEnv): string[];
|
|
11
|
+
export declare const CONTRACT_VERSION = 1;
|
|
12
|
+
export type Stability = "frozen" | "forward-only";
|
|
13
|
+
export interface ContractClause {
|
|
14
|
+
readonly id: string;
|
|
15
|
+
readonly title: string;
|
|
16
|
+
readonly stability: Stability;
|
|
17
|
+
readonly since: string;
|
|
18
|
+
readonly deprecated?: string;
|
|
19
|
+
readonly replacement?: string;
|
|
20
|
+
readonly value: unknown;
|
|
21
|
+
}
|
|
22
|
+
export interface PublicContract {
|
|
23
|
+
readonly contractVersion: number;
|
|
24
|
+
readonly clauses: readonly ContractClause[];
|
|
25
|
+
}
|
|
26
|
+
export interface ContractInput {
|
|
27
|
+
readonly packageName?: string;
|
|
28
|
+
readonly packageVersion?: string;
|
|
29
|
+
/** K13-012 (D13-05) — scan roots for source parsers. Monorepo mode:
|
|
30
|
+
* [packages/plugin/src, packages/core/src]; packed mode resolves
|
|
31
|
+
* installed locations via walk-up. Clause VALUES untouched. */
|
|
32
|
+
readonly scanRoots?: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
export declare const CONTRACT_TOOL_NAMES: readonly string[];
|
|
35
|
+
/**
|
|
36
|
+
* v1.0.0 (K10-018/K10-019 / plan §5.6) — tools added AFTER the initial
|
|
37
|
+
* freeze, each carrying the `since` the deprecation policy requires. A
|
|
38
|
+
* 1.x addition without an entry here fails the contract test as
|
|
39
|
+
* added_bare.
|
|
40
|
+
*/
|
|
41
|
+
export declare const CONTRACT_TOOL_ADDITIONS: readonly {
|
|
42
|
+
name: string;
|
|
43
|
+
since: string;
|
|
44
|
+
}[];
|
|
45
|
+
/**
|
|
46
|
+
* v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
|
|
47
|
+
* each carrying the `since` the deprecation policy requires (C-05).
|
|
48
|
+
*/
|
|
49
|
+
export declare const CONTRACT_METRIC_ADDITIONS: readonly {
|
|
50
|
+
name: string;
|
|
51
|
+
since: string;
|
|
52
|
+
}[];
|
|
53
|
+
/**
|
|
54
|
+
* v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
|
|
55
|
+
* not trusted: anything reaching an artifact or a prompt is escaped at
|
|
56
|
+
* the single write path, according to its container.
|
|
57
|
+
*/
|
|
58
|
+
export declare const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
|
|
59
|
+
export declare function describeContract(_input?: ContractInput): PublicContract;
|
|
60
|
+
export declare function contractDigest(c: PublicContract): string;
|
|
61
|
+
export type ContractDiffKind = "removed" | "changed" | "added_ok" | "added_bare";
|
|
62
|
+
export interface ContractDiff {
|
|
63
|
+
readonly clauseId: string;
|
|
64
|
+
readonly path: string;
|
|
65
|
+
readonly kind: ContractDiffKind;
|
|
66
|
+
readonly remedy: string;
|
|
67
|
+
}
|
|
68
|
+
export declare function diffContract(golden: PublicContract, live: PublicContract): readonly ContractDiff[];
|
package/dist/contract.js
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
// v1.0.0 (K10-006 / plan §5.1) — the surface, as data.
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { MARKER_BEGIN, MARKER_END } from "./ArtifactWriter.js";
|
|
6
|
+
import { resolveEnv } from "./env.js";
|
|
7
|
+
import { METRIC_KEY_LABELS } from "./Retrospective.js";
|
|
8
|
+
import { fnv1a64 } from "./fingerprint.js";
|
|
9
|
+
import { KEVIN_CONFIG_KEYS } from "./index.js";
|
|
10
|
+
import { MAX_ENTRIES, MAX_LINE_BYTES } from "./okf.js";
|
|
11
|
+
/**
|
|
12
|
+
* K13-012 (D13-05) — scanRoots plumbing. Resolves monorepo vs packed locations.
|
|
13
|
+
* Monorepo: [packages/plugin/src, packages/core/src] when both dirs exist on disk.
|
|
14
|
+
* Packed: walk-up via require.resolve for @jmtrin/kevin-core and @jmtrin/opencode-kevin.
|
|
15
|
+
* Clause VALUES are unchanged; this only provides the roots future parsers would scan.
|
|
16
|
+
* Uses KevinEnv for cwd (K13-006) — defaults via resolveEnv — env.ts is the only
|
|
17
|
+
* core file allowed to call process cwd.
|
|
18
|
+
*/
|
|
19
|
+
export function resolveScanRoots(env) {
|
|
20
|
+
const cwd = resolveEnv(env).projectRoot;
|
|
21
|
+
const monorepoCore = join(cwd, "packages", "core", "src");
|
|
22
|
+
const monorepoPlugin = join(cwd, "packages", "plugin", "src");
|
|
23
|
+
if (existsSync(monorepoCore) && existsSync(monorepoPlugin)) {
|
|
24
|
+
return [monorepoPlugin, monorepoCore];
|
|
25
|
+
}
|
|
26
|
+
// Packed mode — locate installed packages
|
|
27
|
+
const req = createRequire(import.meta.url);
|
|
28
|
+
const roots = [];
|
|
29
|
+
try {
|
|
30
|
+
const corePkg = req.resolve("@jmtrin/kevin-core/package.json");
|
|
31
|
+
const cand = join(dirname(corePkg), "dist");
|
|
32
|
+
if (existsSync(cand))
|
|
33
|
+
roots.push(cand);
|
|
34
|
+
}
|
|
35
|
+
catch { }
|
|
36
|
+
try {
|
|
37
|
+
const pluginPkg = req.resolve("@jmtrin/opencode-kevin/package.json");
|
|
38
|
+
const cand = join(dirname(pluginPkg), "dist", "plugin");
|
|
39
|
+
if (existsSync(cand))
|
|
40
|
+
roots.push(cand);
|
|
41
|
+
}
|
|
42
|
+
catch { }
|
|
43
|
+
if (roots.length > 0)
|
|
44
|
+
return roots;
|
|
45
|
+
// Fallback to monorepo relative (even if not yet built)
|
|
46
|
+
return [monorepoPlugin, monorepoCore];
|
|
47
|
+
}
|
|
48
|
+
export const CONTRACT_VERSION = 1;
|
|
49
|
+
// Tool names — the live source of truth for C-03. Any rename here must
|
|
50
|
+
// flow to the contract (K10-006 AC: renaming a tool changes C-03).
|
|
51
|
+
// Keep this array adjacent to the actual tool registrations in index.ts
|
|
52
|
+
// (checked by a test that parses both files).
|
|
53
|
+
export const CONTRACT_TOOL_NAMES = [
|
|
54
|
+
"kevin_save",
|
|
55
|
+
"kevin_query",
|
|
56
|
+
"kevin_get",
|
|
57
|
+
"kevin_recall",
|
|
58
|
+
"kevin_status",
|
|
59
|
+
"kevin_project",
|
|
60
|
+
"kevin_audit",
|
|
61
|
+
"kevin_doctor",
|
|
62
|
+
"kevin_native",
|
|
63
|
+
"kevin_retrospective",
|
|
64
|
+
"kevin_why",
|
|
65
|
+
"kevin_feedback",
|
|
66
|
+
"kevin_trace",
|
|
67
|
+
"kevin_export",
|
|
68
|
+
"kevin_import",
|
|
69
|
+
"kevin_config",
|
|
70
|
+
"kevin_facts",
|
|
71
|
+
"kevin_conflicts",
|
|
72
|
+
"kevin_propose",
|
|
73
|
+
"kevin_publish",
|
|
74
|
+
"kevin_approve",
|
|
75
|
+
"kevin_share",
|
|
76
|
+
"kevin_sync",
|
|
77
|
+
];
|
|
78
|
+
/**
|
|
79
|
+
* v1.0.0 (K10-018/K10-019 / plan §5.6) — tools added AFTER the initial
|
|
80
|
+
* freeze, each carrying the `since` the deprecation policy requires. A
|
|
81
|
+
* 1.x addition without an entry here fails the contract test as
|
|
82
|
+
* added_bare.
|
|
83
|
+
*/
|
|
84
|
+
export const CONTRACT_TOOL_ADDITIONS = [
|
|
85
|
+
{ name: "kevin_bench", since: "1.0.0" },
|
|
86
|
+
{ name: "kevin_contract", since: "1.0.0" },
|
|
87
|
+
{ name: "kevin_forget", since: "1.1.0" },
|
|
88
|
+
];
|
|
89
|
+
/**
|
|
90
|
+
* v1.1.0 (K11-007 / plan §5.1, D11-01) — metric keys added after the freeze,
|
|
91
|
+
* each carrying the `since` the deprecation policy requires (C-05).
|
|
92
|
+
*/
|
|
93
|
+
export const CONTRACT_METRIC_ADDITIONS = [
|
|
94
|
+
{ name: "bench_regression_failures", since: "1.1.0" },
|
|
95
|
+
{ name: "forget_requests_total", since: "1.1.0" },
|
|
96
|
+
{ name: "forget_tombstones_published", since: "1.1.0" },
|
|
97
|
+
// v1.2.0 (K12-001 / plan §4, D12-??) — surface metrics (no migration this release)
|
|
98
|
+
{ name: "tui_snapshots_flushed", since: "1.2.0" },
|
|
99
|
+
{ name: "tui_actions_invoked", since: "1.2.0" },
|
|
100
|
+
];
|
|
101
|
+
/**
|
|
102
|
+
* v1.0.0 (K10-027 / plan §5.7) — the C-09 boundary addition. Stored is
|
|
103
|
+
* not trusted: anything reaching an artifact or a prompt is escaped at
|
|
104
|
+
* the single write path, according to its container.
|
|
105
|
+
*/
|
|
106
|
+
export const BOUNDARY_INVARIANT = "untrusted-input escaping at the single write path";
|
|
107
|
+
// For canonical JSON: sort keys recursively, no floats.
|
|
108
|
+
function canonicalJson(value) {
|
|
109
|
+
if (value === null)
|
|
110
|
+
return "null";
|
|
111
|
+
if (typeof value === "boolean")
|
|
112
|
+
return value ? "true" : "false";
|
|
113
|
+
if (typeof value === "number") {
|
|
114
|
+
if (!Number.isFinite(value) || !Number.isInteger(value))
|
|
115
|
+
throw new Error("contract: non-integer number in canonical JSON");
|
|
116
|
+
return String(value);
|
|
117
|
+
}
|
|
118
|
+
if (typeof value === "string")
|
|
119
|
+
return JSON.stringify(value);
|
|
120
|
+
if (Array.isArray(value))
|
|
121
|
+
return `[${value.map(canonicalJson).join(",")}]`;
|
|
122
|
+
if (typeof value === "object") {
|
|
123
|
+
const obj = value;
|
|
124
|
+
const keys = Object.keys(obj).sort();
|
|
125
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
|
|
126
|
+
}
|
|
127
|
+
return JSON.stringify(value);
|
|
128
|
+
}
|
|
129
|
+
export function describeContract(_input) {
|
|
130
|
+
// K13-012 plumbing — resolve scan roots for future parsers, but VALUES stay untouched (D13-05).
|
|
131
|
+
const _scanRoots = _input?.scanRoots ?? resolveScanRoots();
|
|
132
|
+
void _scanRoots;
|
|
133
|
+
// Derive clause values from live source wherever possible (plan §5.1).
|
|
134
|
+
// C-03 members added after the freeze carry their `since` as objects;
|
|
135
|
+
// the original frozen set stays plain strings.
|
|
136
|
+
const toolAdditions = [...CONTRACT_TOOL_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
|
|
137
|
+
const toolValue = {
|
|
138
|
+
tools: [[...CONTRACT_TOOL_NAMES].sort(), toolAdditions].flat(),
|
|
139
|
+
};
|
|
140
|
+
const settingValue = { keys: [...KEVIN_CONFIG_KEYS].sort() };
|
|
141
|
+
// v1.1.0 — metric keys added after freeze carry `since` (C-05)
|
|
142
|
+
const metricAdditions = [...CONTRACT_METRIC_ADDITIONS].sort((a, b) => a.name.localeCompare(b.name));
|
|
143
|
+
const baseMetricKeys = Object.keys(METRIC_KEY_LABELS)
|
|
144
|
+
.filter((k) => !metricAdditions.some((a) => a.name === k))
|
|
145
|
+
.sort();
|
|
146
|
+
const metricValue = { keys: [...baseMetricKeys, ...metricAdditions].flat() };
|
|
147
|
+
const clauses = [
|
|
148
|
+
{
|
|
149
|
+
id: "C-01",
|
|
150
|
+
title: "AGENTS.md marker pair",
|
|
151
|
+
stability: "frozen",
|
|
152
|
+
since: "0.6.0",
|
|
153
|
+
value: {
|
|
154
|
+
markers: [MARKER_BEGIN, MARKER_END],
|
|
155
|
+
splice_rule: "bytes outside markers preserved verbatim",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "C-02",
|
|
160
|
+
title: "OKF v2 wire format",
|
|
161
|
+
stability: "frozen",
|
|
162
|
+
since: "0.8.0",
|
|
163
|
+
value: {
|
|
164
|
+
header_lines: 3,
|
|
165
|
+
field_order: ["entry_id", "type", "content", "scope", "created_at"],
|
|
166
|
+
entry_id_derivation: "fnv1a64 over canonical field ordering",
|
|
167
|
+
eol: "LF",
|
|
168
|
+
integers_only: true,
|
|
169
|
+
sort_order: "entry_id ascending",
|
|
170
|
+
max_line_bytes: MAX_LINE_BYTES,
|
|
171
|
+
max_entries: MAX_ENTRIES,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: "C-03",
|
|
176
|
+
title: "Tool names and argument shapes",
|
|
177
|
+
stability: "frozen",
|
|
178
|
+
since: "0.2.0",
|
|
179
|
+
value: toolValue,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "C-04",
|
|
183
|
+
title: "Setting keys, types and defaults",
|
|
184
|
+
stability: "frozen",
|
|
185
|
+
since: "0.2.0",
|
|
186
|
+
value: settingValue,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "C-05",
|
|
190
|
+
title: "Metric key names",
|
|
191
|
+
stability: "frozen",
|
|
192
|
+
since: "0.2.0",
|
|
193
|
+
value: metricValue,
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
id: "C-06",
|
|
197
|
+
title: "Package entry points",
|
|
198
|
+
stability: "frozen",
|
|
199
|
+
since: "0.1.0",
|
|
200
|
+
value: {
|
|
201
|
+
name: "@jmtrin/opencode-kevin",
|
|
202
|
+
main: "dist/plugin/index.js",
|
|
203
|
+
types: "dist/plugin/index.d.ts",
|
|
204
|
+
exports_order: ["types", "import"],
|
|
205
|
+
engines: ">=22.5.0",
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "C-07",
|
|
210
|
+
title: "Database schema",
|
|
211
|
+
stability: "forward-only",
|
|
212
|
+
since: "0.1.0",
|
|
213
|
+
value: {
|
|
214
|
+
schema_version: "012",
|
|
215
|
+
migrations_forward_only: true,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "C-08",
|
|
220
|
+
title: "Filesystem locations",
|
|
221
|
+
stability: "frozen",
|
|
222
|
+
since: "0.2.0",
|
|
223
|
+
value: {
|
|
224
|
+
db: "~/.opencode-kevin/kevin.db",
|
|
225
|
+
refs: "refs/",
|
|
226
|
+
skills: "skills/",
|
|
227
|
+
okf: ".kevin/knowledge.okf",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "C-09",
|
|
232
|
+
title: "Behavioural invariants",
|
|
233
|
+
stability: "frozen",
|
|
234
|
+
since: "0.8.0",
|
|
235
|
+
value: {
|
|
236
|
+
invariants: [
|
|
237
|
+
"zero process spawns",
|
|
238
|
+
"zero network calls",
|
|
239
|
+
"no raw author email written",
|
|
240
|
+
"single write path: ArtifactWriter is the only artifact writer",
|
|
241
|
+
],
|
|
242
|
+
// v1.0.0 (K10-027 / plan §5.7) — the untrusted-input boundary
|
|
243
|
+
// joins the clause as an addition carrying its `since`; the
|
|
244
|
+
// frozen invariant strings above are untouched.
|
|
245
|
+
boundary: [{ name: BOUNDARY_INVARIANT, since: "1.0.0" }],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
];
|
|
249
|
+
return { contractVersion: CONTRACT_VERSION, clauses };
|
|
250
|
+
}
|
|
251
|
+
export function contractDigest(c) {
|
|
252
|
+
const payload = canonicalJson(c.clauses);
|
|
253
|
+
return fnv1a64(payload);
|
|
254
|
+
}
|
|
255
|
+
function valueMembers(value) {
|
|
256
|
+
// Normalize clause value into a flat member map for diffing.
|
|
257
|
+
// If value is { tools: [...] } => members are tool names.
|
|
258
|
+
// If { keys: [...] } => member per key.
|
|
259
|
+
// Otherwise flatten object keys.
|
|
260
|
+
if (value !== null && typeof value === "object") {
|
|
261
|
+
const obj = value;
|
|
262
|
+
if (Array.isArray(obj.tools)) {
|
|
263
|
+
const m = new Map();
|
|
264
|
+
for (const t of obj.tools) {
|
|
265
|
+
if (typeof t === "string")
|
|
266
|
+
m.set(t, undefined);
|
|
267
|
+
else if (t !== null &&
|
|
268
|
+
typeof t === "object" &&
|
|
269
|
+
"name" in t)
|
|
270
|
+
m.set(t.name, t);
|
|
271
|
+
}
|
|
272
|
+
return m;
|
|
273
|
+
}
|
|
274
|
+
if (Array.isArray(obj.keys)) {
|
|
275
|
+
const m = new Map();
|
|
276
|
+
for (const k of obj.keys) {
|
|
277
|
+
if (typeof k === "string")
|
|
278
|
+
m.set(k, undefined);
|
|
279
|
+
else if (k !== null &&
|
|
280
|
+
typeof k === "object" &&
|
|
281
|
+
"name" in k)
|
|
282
|
+
m.set(k.name, k);
|
|
283
|
+
}
|
|
284
|
+
return m;
|
|
285
|
+
}
|
|
286
|
+
if (Array.isArray(obj.markers) || Array.isArray(obj.invariants)) {
|
|
287
|
+
const arr = (obj.markers ?? obj.invariants);
|
|
288
|
+
const m = new Map();
|
|
289
|
+
for (const v of arr)
|
|
290
|
+
m.set(v, v);
|
|
291
|
+
// v1.0.0 (K10-027) — boundary additions sit beside the frozen
|
|
292
|
+
// invariant strings and carry their `since` like tool additions.
|
|
293
|
+
if (Array.isArray(obj.boundary)) {
|
|
294
|
+
for (const b of obj.boundary) {
|
|
295
|
+
if (b !== null &&
|
|
296
|
+
typeof b === "object" &&
|
|
297
|
+
"name" in b)
|
|
298
|
+
m.set(b.name, b);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return m;
|
|
302
|
+
}
|
|
303
|
+
// Generic object: each key is a member
|
|
304
|
+
const m = new Map();
|
|
305
|
+
for (const [k, v] of Object.entries(obj))
|
|
306
|
+
m.set(k, v);
|
|
307
|
+
return m;
|
|
308
|
+
}
|
|
309
|
+
return new Map([["value", value]]);
|
|
310
|
+
}
|
|
311
|
+
export function diffContract(golden, live) {
|
|
312
|
+
const diffs = [];
|
|
313
|
+
const goldenById = new Map(golden.clauses.map((c) => [c.id, c]));
|
|
314
|
+
const liveById = new Map(live.clauses.map((c) => [c.id, c]));
|
|
315
|
+
// Removed clauses
|
|
316
|
+
for (const g of golden.clauses) {
|
|
317
|
+
if (!liveById.has(g.id)) {
|
|
318
|
+
diffs.push({
|
|
319
|
+
clauseId: g.id,
|
|
320
|
+
path: g.id,
|
|
321
|
+
kind: "removed",
|
|
322
|
+
remedy: `Clause ${g.id} was removed. Revert, or open a 2.0.0.`,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Added clauses — check since
|
|
327
|
+
for (const l of live.clauses) {
|
|
328
|
+
if (!goldenById.has(l.id)) {
|
|
329
|
+
const hasSince = typeof l.since === "string" &&
|
|
330
|
+
l.since.length > 0;
|
|
331
|
+
diffs.push({
|
|
332
|
+
clauseId: l.id,
|
|
333
|
+
path: l.id,
|
|
334
|
+
kind: hasSince ? "added_ok" : "added_bare",
|
|
335
|
+
remedy: hasSince
|
|
336
|
+
? `Clause ${l.id} was added with since. Allowed in 1.x.`
|
|
337
|
+
: `Clause ${l.id} was added without since. Add since or revert, or open a 2.0.0.`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// Within-clause diff
|
|
342
|
+
for (const g of golden.clauses) {
|
|
343
|
+
const l = liveById.get(g.id);
|
|
344
|
+
if (!l)
|
|
345
|
+
continue;
|
|
346
|
+
const gMembers = valueMembers(g.value);
|
|
347
|
+
const lMembers = valueMembers(l.value);
|
|
348
|
+
// Normalize members: for arrays we care about set equality
|
|
349
|
+
for (const [mem, _gv] of gMembers) {
|
|
350
|
+
if (!lMembers.has(mem)) {
|
|
351
|
+
diffs.push({
|
|
352
|
+
clauseId: g.id,
|
|
353
|
+
path: `${g.id}.${mem}`,
|
|
354
|
+
kind: "removed",
|
|
355
|
+
remedy: `Member ${mem} in ${g.id} was removed. Revert, or open a 2.0.0.`,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
const lv = lMembers.get(mem);
|
|
360
|
+
const gv = gMembers.get(mem);
|
|
361
|
+
if (JSON.stringify(lv) !== JSON.stringify(gv)) {
|
|
362
|
+
// Member present on both sides but its value moved (e.g. an
|
|
363
|
+
// addition object whose `since` was edited). Report it here,
|
|
364
|
+
// at member granularity, instead of letting it fall through
|
|
365
|
+
// to the coarser clause-level check below.
|
|
366
|
+
diffs.push({
|
|
367
|
+
clauseId: g.id,
|
|
368
|
+
path: `${g.id}.${mem}`,
|
|
369
|
+
kind: "changed",
|
|
370
|
+
remedy: `Member ${mem} in ${g.id} changed. Revert, or open a 2.0.0.`,
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
for (const [mem, _lv] of lMembers) {
|
|
376
|
+
if (!gMembers.has(mem)) {
|
|
377
|
+
const liveVal = l.value;
|
|
378
|
+
// Check if the live member carries `since` (additions may ride
|
|
379
|
+
// in the keys, tools or boundary arrays — K10-018/K10-027).
|
|
380
|
+
const lists = [liveVal.keys, liveVal.tools, liveVal.boundary];
|
|
381
|
+
let hasSince = false;
|
|
382
|
+
for (const list of lists) {
|
|
383
|
+
if (!Array.isArray(list))
|
|
384
|
+
continue;
|
|
385
|
+
const entry = list.find((e) => {
|
|
386
|
+
if (typeof e === "string")
|
|
387
|
+
return e === mem;
|
|
388
|
+
if (e !== null &&
|
|
389
|
+
typeof e === "object" &&
|
|
390
|
+
"name" in e)
|
|
391
|
+
return e.name === mem;
|
|
392
|
+
return false;
|
|
393
|
+
});
|
|
394
|
+
if (entry !== null &&
|
|
395
|
+
typeof entry === "object" &&
|
|
396
|
+
typeof entry.since === "string") {
|
|
397
|
+
hasSince = true;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
diffs.push({
|
|
402
|
+
clauseId: g.id,
|
|
403
|
+
path: `${g.id}.${mem}`,
|
|
404
|
+
kind: hasSince ? "added_ok" : "added_bare",
|
|
405
|
+
remedy: hasSince
|
|
406
|
+
? `Member ${mem} in ${g.id} was added with since. Allowed in 1.x.`
|
|
407
|
+
: `Member ${mem} in ${g.id} was added without since. Add since or revert, or open a 2.0.0.`,
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Also compare clause-level metadata that is not in valueMembers (e.g., package version in C-06)
|
|
412
|
+
// If the whole value JSON differs but members didn't capture it (e.g., name change), mark changed.
|
|
413
|
+
if (JSON.stringify(g.value) !== JSON.stringify(l.value)) {
|
|
414
|
+
// Only emit clause-level changed if no finer-grained diff already emitted for this clause
|
|
415
|
+
const hasFiner = diffs.some((d) => d.clauseId === g.id && d.path !== g.id);
|
|
416
|
+
if (!hasFiner) {
|
|
417
|
+
diffs.push({
|
|
418
|
+
clauseId: g.id,
|
|
419
|
+
path: g.id,
|
|
420
|
+
kind: "changed",
|
|
421
|
+
remedy: `Clause ${g.id} changed. Revert, or open a 2.0.0.`,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
diffs.sort((a, b) => a.clauseId === b.clauseId
|
|
427
|
+
? a.path.localeCompare(b.path)
|
|
428
|
+
: a.clauseId.localeCompare(b.clauseId));
|
|
429
|
+
return diffs;
|
|
430
|
+
}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified diff between `before` and `after` for `path`, in git's format:
|
|
3
|
+
* `--- a/<path>` / `+++ b/<path>` headers, `@@ -l,s +l,s @@` hunk headers
|
|
4
|
+
* (count omitted when 1, `0,0` for an empty side), three lines of context and
|
|
5
|
+
* adjacent hunks merged when their context windows overlap. Identical inputs
|
|
6
|
+
* yield the empty string.
|
|
7
|
+
*/
|
|
8
|
+
export declare function unifiedDiff(path: string, before: string, after: string): string;
|