@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
@@ -0,0 +1,29 @@
1
+ import type { Store } from "./Store.js";
2
+ import type { HostSurface } from "./host.js";
3
+ import type { SettingsReader } from "./native.js";
4
+ export type NativeAction = "show" | "enable" | "disable";
5
+ export interface NativeRegistrationRow {
6
+ readonly surface: "skill" | "reference";
7
+ readonly registered: boolean;
8
+ readonly verified: boolean;
9
+ readonly attached_at: string | null;
10
+ }
11
+ export interface NativeReport {
12
+ readonly action: NativeAction;
13
+ /** The setting value: '1' or '0' as stored (TEXT). */
14
+ readonly value: "1" | "0";
15
+ /** Whether registration would be effective on THIS host (v2 subpath present). */
16
+ readonly effective: boolean;
17
+ /** Present when `effective` is false: why the intent is inert. */
18
+ readonly reason?: string;
19
+ /** The latest persisted outcome per surface (show only). */
20
+ readonly registrations?: NativeRegistrationRow[];
21
+ /** The restart note (enable/disable only). */
22
+ readonly note?: string;
23
+ }
24
+ export interface NativeDeps {
25
+ readonly host: HostSurface;
26
+ readonly store: Store;
27
+ readonly settings: SettingsReader;
28
+ }
29
+ export declare function handleNative(action: NativeAction, deps: NativeDeps): NativeReport;
@@ -0,0 +1,79 @@
1
+ // v0.9.0 (K9-019 / plan §5.5, D9-12) — `kevin_native`: inspect and toggle
2
+ // `native_registration_enabled`.
3
+ //
4
+ // Pure in the sense that matters: `show` reads only; `enable`/`disable`
5
+ // write `kevin_settings` and nothing else. Neither action re-attaches —
6
+ // the probe is frozen for the process lifetime (D9-12) — and the response
7
+ // says so explicitly, naming a restart as the requirement. `enable` on a
8
+ // host without the v2 subpath succeeds and reports the registration as
9
+ // inert: the setting is a statement of intent that becomes effective when
10
+ // the host catches up, and refusing would make it untestable on the
11
+ // majority of installations.
12
+ //
13
+ // The stored value is TEXT `'1'` or `'0'` — never a boolean, never
14
+ // `'true'` (kevin_settings.value is TEXT; compare with `=== "1"`).
15
+ function lastRows(store) {
16
+ try {
17
+ return store
18
+ .prepare(`SELECT surface, registered, verified, attached_at
19
+ FROM native_registrations
20
+ ORDER BY attached_at DESC, id DESC
21
+ LIMIT 20`)
22
+ .all()
23
+ .map((row) => {
24
+ const r = row;
25
+ return {
26
+ surface: r.surface,
27
+ registered: r.registered === 1,
28
+ verified: r.verified === 1,
29
+ attached_at: r.attached_at,
30
+ };
31
+ });
32
+ }
33
+ catch {
34
+ // pre-010 database: the table does not exist yet — no rows to
35
+ // report, and that is the truth.
36
+ return [];
37
+ }
38
+ }
39
+ /** v0.9.0 (K9-019 / plan §5.5, D9-12) — the pure decision: the probe is
40
+ * frozen for the process lifetime, so "effective" is derived from the
41
+ * resolved host surface, never from a fresh probe call. */
42
+ function effectiveOn(host) {
43
+ return host.v2.skill || host.v2.reference;
44
+ }
45
+ export function handleNative(action, deps) {
46
+ const current = deps.settings.getSetting("native_registration_enabled", "0");
47
+ const value = current === "1" ? "1" : "0";
48
+ if (action === "show") {
49
+ const report = {
50
+ action,
51
+ value,
52
+ effective: effectiveOn(deps.host),
53
+ registrations: lastRows(deps.store),
54
+ ...(effectiveOn(deps.host)
55
+ ? {}
56
+ : {
57
+ reason: "v2 subpath absent from the resolved host package — registration would be inert",
58
+ }),
59
+ };
60
+ return report;
61
+ }
62
+ const next = action === "enable" ? "1" : "0";
63
+ deps.store
64
+ .prepare(`INSERT INTO kevin_settings (key, value) VALUES (?, ?)
65
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`)
66
+ .run("native_registration_enabled", next);
67
+ const report = {
68
+ action,
69
+ value: next,
70
+ effective: effectiveOn(deps.host),
71
+ note: "the probe is frozen for the process lifetime — restart the host for the change to take effect",
72
+ ...(effectiveOn(deps.host)
73
+ ? {}
74
+ : {
75
+ reason: "v2 subpath absent from the resolved host package — registration would be inert",
76
+ }),
77
+ };
78
+ return report;
79
+ }
@@ -12,4 +12,3 @@ export function kevinPropose(curator, writer, kind) {
12
12
  })),
13
13
  };
14
14
  }
15
- //# sourceMappingURL=kevin_propose.js.map
@@ -16,4 +16,3 @@ export function kevinPublish(deps) {
16
16
  },
17
17
  };
18
18
  }
19
- //# sourceMappingURL=kevin_publish.js.map
@@ -126,4 +126,3 @@ export function kevinWhy(store, query) {
126
126
  related_rules: relatedRules,
127
127
  };
128
128
  }
129
- //# sourceMappingURL=kevin_why.js.map
@@ -57,4 +57,3 @@ export function formatMemorySnippets(memories, tag) {
57
57
  return "";
58
58
  return wrapBlock(memories.map(formatSnippet).join("\n"), tag);
59
59
  }
60
- //# sourceMappingURL=memory-format.js.map
@@ -278,4 +278,3 @@ export class Metrics {
278
278
  }
279
279
  }
280
280
  }
281
- //# sourceMappingURL=metrics.js.map
@@ -0,0 +1,92 @@
1
+ import { type Materializer } from "./Materializer.js";
2
+ import type { Store } from "./Store.js";
3
+ import type { HostSurface } from "./host.js";
4
+ /** The surface `attachNative()` returned. `verified` is filled by the
5
+ * read-back checks (K9-014/015) once the host ran `setup()`. */
6
+ export interface NativeRegistration {
7
+ registered: {
8
+ skill: boolean;
9
+ reference: boolean;
10
+ };
11
+ verified: {
12
+ skill: boolean;
13
+ reference: boolean;
14
+ };
15
+ notes: string[];
16
+ }
17
+ /** Minimal settings reader — matches MemoryService.getSetting(key, fallback). */
18
+ export interface SettingsReader {
19
+ getSetting(key: string, fallback?: string): string | null;
20
+ }
21
+ export interface NativeDeps {
22
+ materializer: Materializer;
23
+ settings: SettingsReader;
24
+ /**
25
+ * v0.9.0 (K9-014 / plan §5.4) — reports the outcome of each surface
26
+ * registration once the host ran `setup()`. `registered` reflects
27
+ * whether the transform completed; `verified` whether the read-back
28
+ * (`draft.list()`) contained the provided source. The host's own
29
+ * `setup()` runs asynchronously, so this callback may fire after
30
+ * `attachNative()` resolved.
31
+ */
32
+ onVerified?: (surface: "skill" | "reference", registered: boolean, verified: boolean) => void;
33
+ /**
34
+ * v0.9.0 (K9-017 / plan §6.2) — the persistence sink. When present,
35
+ * every surface outcome is appended to `native_registrations` and the
36
+ * two live counters are bumped. Attach time is construction, not a
37
+ * hot path, so a direct write is correct here and the
38
+ * `metrics.flush()` cadence does not apply.
39
+ */
40
+ store?: Store;
41
+ }
42
+ /** The only place in the repository allowed to name the v2 subpath. */
43
+ export declare const V2_SPECIFIER = "@opencode-ai/plugin/v2/promise";
44
+ /**
45
+ * Duck-typed mirror of the host's v2 PluginContext. The host's own
46
+ * compile-time types (dist/v2/promise/context.d.ts) are not importable
47
+ * here — a static type import would name the specifier outside a dynamic
48
+ * `import()` and break the containment scan.
49
+ */
50
+ export interface KevinNativeContext {
51
+ readonly skill: {
52
+ transform(hook: (draft: unknown) => Promise<void> | void): Promise<void> | void;
53
+ };
54
+ readonly reference: {
55
+ transform(hook: (draft: unknown) => Promise<void> | void): Promise<void> | void;
56
+ };
57
+ }
58
+ /**
59
+ * Duck-typed mirror of the v2 `Plugin` contract
60
+ * (dist/v2/promise/plugin.d.ts). `define()` is the identity function —
61
+ * 54 bytes, `return plugin` — so adopting it is neither a framework nor
62
+ * a commitment (D9-02); the object literal below already satisfies the
63
+ * contract.
64
+ */
65
+ export interface KevinNativePlugin {
66
+ readonly id: string;
67
+ readonly setup: (context: KevinNativeContext) => Promise<void> | void;
68
+ }
69
+ /** The skill body Kevin registers through `skill.transform`. */
70
+ export declare function kevinSkillSource(materializer: Materializer): string;
71
+ /**
72
+ * Build the v2 plugin. Registration replaces emission (D9-10): when the
73
+ * host runs `setup()`, the skill/reference surfaces are registered
74
+ * natively and the Materializer's `*_emission_enabled` file path is
75
+ * skipped for those surfaces (guard in K9-016).
76
+ */
77
+ export declare function buildNativePlugin(deps: NativeDeps): KevinNativePlugin;
78
+ export interface AttachOptions {
79
+ /** Injectable v2 loader for tests; defaults to the real dynamic import. */
80
+ importV2?: () => Promise<unknown>;
81
+ /** Collects notes for the caller when the result is null. */
82
+ notes?: string[];
83
+ }
84
+ /**
85
+ * Attach the native surface when the host exposes it AND the user asked
86
+ * for it. Returns `null` — cleanly, with a `note` — whenever the host
87
+ * lacks the subpath or `native_registration_enabled` is not `'1'`; that
88
+ * is the default on every existing installation, so the default
89
+ * behaviour of this release is byte-identical to v0.8.0. Never throws
90
+ * (D9-12): every failure path is a `null` plus a `note`.
91
+ */
92
+ export declare function attachNative(host: HostSurface, deps: NativeDeps, options?: AttachOptions): Promise<NativeRegistration | null>;
@@ -0,0 +1,190 @@
1
+ import { SKILL_TOPIC } from "./Materializer.js";
2
+ import { uuidv7 } from "./uuid.js";
3
+ /** The only place in the repository allowed to name the v2 subpath. */
4
+ export const V2_SPECIFIER = "@opencode-ai/plugin/v2/promise";
5
+ /** The skill body Kevin registers through `skill.transform`. */
6
+ export function kevinSkillSource(materializer) {
7
+ return materializer.skillBody();
8
+ }
9
+ /**
10
+ * Build the v2 plugin. Registration replaces emission (D9-10): when the
11
+ * host runs `setup()`, the skill/reference surfaces are registered
12
+ * natively and the Materializer's `*_emission_enabled` file path is
13
+ * skipped for those surfaces (guard in K9-016).
14
+ */
15
+ export function buildNativePlugin(deps) {
16
+ return {
17
+ id: "opencode-kevin",
18
+ setup: async (ctx) => {
19
+ try {
20
+ await ctx.skill.transform(async (draft) => {
21
+ const source = kevinSkillSource(deps.materializer);
22
+ // The draft is a mutable builder valid only for the
23
+ // duration of this callback; it never escapes (D9-10
24
+ // property 3). K9-014's source scan asserts that.
25
+ const draftApi = draft;
26
+ if (typeof draftApi.source !== "function") {
27
+ deps.onVerified?.("skill", false, false);
28
+ return;
29
+ }
30
+ draftApi.source(source);
31
+ // v0.9.0 (K9-014 / plan §5.4 property 2) — verification is a
32
+ // read-back: the registered source must show up in the
33
+ // draft's own list(). An unverified registration is a note
34
+ // (and a metric in K9-017), never a throw.
35
+ const verified = typeof draftApi.list === "function" &&
36
+ draftApi.list().some((entry) => entry === source);
37
+ deps.onVerified?.("skill", true, verified);
38
+ });
39
+ }
40
+ catch {
41
+ // A rejecting transform still reports, never throws (K9-014).
42
+ deps.onVerified?.("skill", false, false);
43
+ }
44
+ try {
45
+ await ctx.reference.transform(async (draft) => {
46
+ // v0.9.0 (K9-015 / plan §5.4, D6-14) — one add() per
47
+ // materialized ref target (topic !== SKILL_TOPIC), named
48
+ // `@kevin/<topic>`. Topics follow the v0.6.0 rule
49
+ // `<type>-<dominant token>`; dominantToken already
50
+ // excludes hex-like tokens, so a fingerprint prefix can
51
+ // never become a reference name. Sources use the v2
52
+ // `{ type: "local", path }` shape (read from the
53
+ // resolved SDK's ReferenceLocalSource, duck-typed).
54
+ const draftApi = draft;
55
+ if (typeof draftApi.add !== "function") {
56
+ deps.onVerified?.("reference", false, false);
57
+ return;
58
+ }
59
+ const added = [];
60
+ for (const target of deps.materializer.bundleTargets()) {
61
+ if (target.topic === SKILL_TOPIC)
62
+ continue;
63
+ const name = `@kevin/${target.topic}`;
64
+ draftApi.add(name, {
65
+ type: "local",
66
+ path: target.path,
67
+ });
68
+ added.push(name);
69
+ }
70
+ // Verification is the same read-back as K9-014: every
71
+ // name we added must show up in the draft's own list().
72
+ const listed = typeof draftApi.list === "function" ? draftApi.list() : [];
73
+ const listedNames = new Set(listed.map((entry) => {
74
+ const tuple = entry;
75
+ return tuple[0];
76
+ }));
77
+ const verified = added.every((name) => listedNames.has(name));
78
+ deps.onVerified?.("reference", true, verified);
79
+ });
80
+ }
81
+ catch {
82
+ deps.onVerified?.("reference", false, false);
83
+ }
84
+ },
85
+ };
86
+ }
87
+ /**
88
+ * Attach the native surface when the host exposes it AND the user asked
89
+ * for it. Returns `null` — cleanly, with a `note` — whenever the host
90
+ * lacks the subpath or `native_registration_enabled` is not `'1'`; that
91
+ * is the default on every existing installation, so the default
92
+ * behaviour of this release is byte-identical to v0.8.0. Never throws
93
+ * (D9-12): every failure path is a `null` plus a `note`.
94
+ */
95
+ export async function attachNative(host, deps, options = {}) {
96
+ const notes = options.notes ?? [];
97
+ const enabled = deps.settings.getSetting("native_registration_enabled", "0") === "1";
98
+ if (!enabled)
99
+ return null;
100
+ if (!host.v2.skill && !host.v2.reference) {
101
+ notes.push("v2 subpath absent from the resolved host package — native registration skipped");
102
+ return null;
103
+ }
104
+ let mod;
105
+ try {
106
+ mod = await (options.importV2 ?? (() => import(/* @vite-ignore */ V2_SPECIFIER)))();
107
+ }
108
+ catch {
109
+ notes.push("v2 subpath import rejected — native registration skipped");
110
+ return null;
111
+ }
112
+ const record = mod;
113
+ const define = typeof record.define === "function"
114
+ ? record.define
115
+ : null;
116
+ if (define === null) {
117
+ notes.push("v2 module exposes no define() — native registration skipped");
118
+ return null;
119
+ }
120
+ // `define()` is the identity function (D9-02): passing the built
121
+ // plugin through it validates the contract without adding a framework.
122
+ // v0.9.0 (K9-014 / plan §5.4 property 2) — the collector for the
123
+ // read-back results: `verified` is filled by the host running setup()
124
+ // (draft.list()), reported back through onVerified. The setup callback
125
+ // may fire after this function resolved; the caller keeps the returned
126
+ // registration as the final word.
127
+ const registered = { skill: host.v2.skill, reference: host.v2.reference };
128
+ const verified = { skill: false, reference: false };
129
+ const plugin = buildNativePlugin({
130
+ ...deps,
131
+ onVerified: (surface, didRegister, isVerified) => {
132
+ if (!didRegister)
133
+ registered[surface] = false;
134
+ verified[surface] = isVerified;
135
+ if (!isVerified) {
136
+ notes.push(`${surface} read-back does not contain the provided source — unverified registration`);
137
+ }
138
+ // v0.9.0 (K9-017 / plan §6.2) — append the outcome row at attach
139
+ // time (construction, not a hot path; a direct write is correct,
140
+ // the flush cadence does not apply). Only surfaces the host
141
+ // actually exposes are persisted: a surface the host cannot
142
+ // serve is not an outcome to record.
143
+ if (deps.store && host.v2[surface]) {
144
+ const note = !isVerified
145
+ ? `${surface} read-back does not contain the provided source — unverified registration`
146
+ : null;
147
+ persistRegistration(deps.store, surface, didRegister, isVerified, note);
148
+ }
149
+ },
150
+ });
151
+ define(plugin);
152
+ return {
153
+ registered,
154
+ verified,
155
+ notes,
156
+ };
157
+ }
158
+ /**
159
+ * v0.9.0 (K9-017 / plan §6.2) — one `native_registrations` row per
160
+ * surface per attach attempt, plus the two live counters. The counters
161
+ * live OUTSIDE the frozen `METRIC_KEYS` ladder (K7-004) but persist to
162
+ * the same `kevin_metrics` table, following the v0.6.0 `incrRegistered`
163
+ * precedent (K6-018/019): written immediately, never debounced. The
164
+ * `surface` CHECK lives in the schema (migration 010), so a bad surface
165
+ * fails loudly at the constraint rather than being coerced — the closed
166
+ * enumeration is deliberate (plan §6.2).
167
+ */
168
+ function persistRegistration(store, surface, registered, verified, note) {
169
+ store
170
+ .prepare(`INSERT INTO native_registrations (id, surface, registered, verified, note)
171
+ VALUES (?, ?, ?, ?, ?)`)
172
+ .run(uuidv7(), surface, registered ? 1 : 0, verified ? 1 : 0, note);
173
+ if (verified) {
174
+ bumpNativeCounter(store, "native_registrations_total");
175
+ }
176
+ else if (registered) {
177
+ // "Registered but unverified": the host accepted the call and did
178
+ // not honour it — the interesting state (K9-017).
179
+ bumpNativeCounter(store, "native_registration_failures");
180
+ }
181
+ }
182
+ function bumpNativeCounter(store, key) {
183
+ store
184
+ .prepare(`INSERT INTO kevin_metrics (key, value, updated_at)
185
+ VALUES (?, 1, datetime('now'))
186
+ ON CONFLICT(key) DO UPDATE SET
187
+ value = value + excluded.value,
188
+ updated_at = datetime('now')`)
189
+ .run(key);
190
+ }
@@ -132,4 +132,3 @@ export function exportMarkdown(store, projectId = null) {
132
132
  }
133
133
  return lines.join("\n");
134
134
  }
135
- //# sourceMappingURL=okf-export.js.map
@@ -269,4 +269,3 @@ export function importOkf(bundle, memoryService) {
269
269
  }
270
270
  return { imported, superseded };
271
271
  }
272
- //# sourceMappingURL=okf-import.js.map
@@ -301,4 +301,3 @@ export function parse(text) {
301
301
  export function computeEntryId(type, statement, scope) {
302
302
  return fnv1a64(`okf:v2\u0000${type}\u0000${statement}\u0000${scope ?? ""}`);
303
303
  }
304
- //# sourceMappingURL=okf.js.map
@@ -0,0 +1,35 @@
1
+ import type { Store } from "./Store.js";
2
+ export type PerfScope = "tool.execute.before" | "tool.execute.after" | "chat.message" | "chat.system.transform" | "session.compacting" | "event" | "session.idle" | "dispose";
3
+ export interface Budget {
4
+ readonly scope: PerfScope;
5
+ readonly p95Ms: number;
6
+ readonly maxMs: number;
7
+ }
8
+ export declare const BUDGETS: readonly Budget[];
9
+ export interface PerfStat {
10
+ readonly scope: PerfScope;
11
+ readonly count: number;
12
+ readonly p50: number;
13
+ readonly p95: number;
14
+ readonly max: number;
15
+ readonly budget: Budget;
16
+ readonly withinBudget: boolean;
17
+ }
18
+ export declare class Perf {
19
+ private readonly enabled;
20
+ private readonly capacity;
21
+ private readonly rings;
22
+ private readonly cursors;
23
+ private readonly counts;
24
+ constructor(opts: {
25
+ enabled: boolean;
26
+ capacity?: number | string;
27
+ });
28
+ static fromSettings(settings: Record<string, string | null | undefined>): Perf;
29
+ measure<T>(scope: PerfScope, fn: () => T): T;
30
+ measureAsync<T>(scope: PerfScope, fn: () => Promise<T>): Promise<T>;
31
+ private record;
32
+ stats(): readonly PerfStat[];
33
+ flush(store: Store): void;
34
+ reset(): void;
35
+ }
@@ -0,0 +1,174 @@
1
+ export const BUDGETS = [
2
+ { scope: "tool.execute.before", p95Ms: 2, maxMs: 10 },
3
+ { scope: "tool.execute.after", p95Ms: 5, maxMs: 25 },
4
+ { scope: "chat.message", p95Ms: 2, maxMs: 10 },
5
+ { scope: "chat.system.transform", p95Ms: 15, maxMs: 50 },
6
+ { scope: "session.compacting", p95Ms: 15, maxMs: 50 },
7
+ { scope: "event", p95Ms: 5, maxMs: 25 },
8
+ { scope: "session.idle", p95Ms: 150, maxMs: 600 },
9
+ { scope: "dispose", p95Ms: 50, maxMs: 250 },
10
+ ];
11
+ function clampCapacity(raw) {
12
+ if (raw === null || raw === undefined || raw === "")
13
+ return 512;
14
+ const n = Number.parseInt(raw, 10);
15
+ if (Number.isNaN(n))
16
+ return 512;
17
+ return Math.min(8192, Math.max(64, n));
18
+ }
19
+ export class Perf {
20
+ enabled;
21
+ capacity;
22
+ rings;
23
+ cursors;
24
+ counts;
25
+ constructor(opts) {
26
+ this.enabled = opts.enabled;
27
+ if (typeof opts.capacity === "string") {
28
+ this.capacity = clampCapacity(opts.capacity);
29
+ }
30
+ else if (typeof opts.capacity === "number") {
31
+ this.capacity = Math.min(8192, Math.max(64, Math.floor(opts.capacity)));
32
+ }
33
+ else {
34
+ this.capacity = 512;
35
+ }
36
+ this.rings = new Map();
37
+ this.cursors = new Map();
38
+ this.counts = new Map();
39
+ const scopes = BUDGETS.map((b) => b.scope);
40
+ for (const s of scopes) {
41
+ this.rings.set(s, new Float64Array(this.capacity));
42
+ this.cursors.set(s, 0);
43
+ this.counts.set(s, 0);
44
+ }
45
+ }
46
+ static fromSettings(settings) {
47
+ const enabled = settings.perf_enabled === "1";
48
+ const cap = clampCapacity(settings.perf_ring_capacity);
49
+ return new Perf({ enabled, capacity: cap });
50
+ }
51
+ measure(scope, fn) {
52
+ if (!this.enabled)
53
+ return fn();
54
+ const start = performance.now();
55
+ try {
56
+ return fn();
57
+ }
58
+ finally {
59
+ const dur = performance.now() - start;
60
+ this.record(scope, dur);
61
+ }
62
+ }
63
+ async measureAsync(scope, fn) {
64
+ if (!this.enabled)
65
+ return fn();
66
+ const start = performance.now();
67
+ try {
68
+ return await fn();
69
+ }
70
+ finally {
71
+ const dur = performance.now() - start;
72
+ this.record(scope, dur);
73
+ }
74
+ }
75
+ record(scope, ms) {
76
+ const ring = this.rings.get(scope);
77
+ if (!ring)
78
+ return;
79
+ const cur = this.cursors.get(scope) ?? 0;
80
+ ring[cur % this.capacity] = ms;
81
+ this.cursors.set(scope, (cur + 1) % this.capacity);
82
+ this.counts.set(scope, (this.counts.get(scope) ?? 0) + 1);
83
+ }
84
+ stats() {
85
+ const out = [];
86
+ for (const b of BUDGETS) {
87
+ const ring = this.rings.get(b.scope);
88
+ if (!ring)
89
+ continue;
90
+ const count = this.counts.get(b.scope) ?? 0;
91
+ if (count === 0) {
92
+ out.push({
93
+ scope: b.scope,
94
+ count: 0,
95
+ p50: 0,
96
+ p95: 0,
97
+ max: 0,
98
+ budget: b,
99
+ withinBudget: true,
100
+ });
101
+ continue;
102
+ }
103
+ const n = Math.min(count, this.capacity);
104
+ // Copy live samples: need to reconstruct in insertion order but for percentiles order doesn't matter.
105
+ // Take last n entries from ring (wrap-aware).
106
+ const samples = [];
107
+ const cur = this.cursors.get(b.scope) ?? 0;
108
+ // If count < capacity, samples are ring[0..count-1]; else ring[0..capacity-1] all valid but order is cur..cur+cap
109
+ if (count < this.capacity) {
110
+ for (let i = 0; i < count; i++)
111
+ samples.push(ring[i]);
112
+ }
113
+ else {
114
+ for (let i = 0; i < this.capacity; i++)
115
+ samples.push(ring[i]);
116
+ }
117
+ const sorted = [...samples].sort((a, b) => a - b);
118
+ // Percentile method: nearest-rank — p is the value at index ceil(p/100 * n) - 1
119
+ // of the ascending-sorted copy. Stated here so an implementer comparing against
120
+ // another percentile definition can see which one was chosen.
121
+ const p50 = sorted[Math.max(0, Math.ceil(sorted.length * 0.5) - 1)];
122
+ const p95 = sorted[Math.max(0, Math.ceil(sorted.length * 0.95) - 1)];
123
+ const max = sorted[sorted.length - 1];
124
+ out.push({
125
+ scope: b.scope,
126
+ count,
127
+ p50,
128
+ p95,
129
+ max,
130
+ budget: b,
131
+ withinBudget: p95 <= b.p95Ms,
132
+ });
133
+ }
134
+ return out;
135
+ }
136
+ flush(store) {
137
+ if (!this.enabled)
138
+ return;
139
+ const stats = this.stats();
140
+ let recorded = 0;
141
+ let breaches = 0;
142
+ for (const s of stats) {
143
+ if (s.count === 0)
144
+ continue;
145
+ store
146
+ .prepare("INSERT INTO perf_samples (scope, sample_count, p50_ms, p95_ms, max_ms, budget_p95_ms, within_budget) VALUES (?, ?, ?, ?, ?, ?, ?)")
147
+ .run(s.scope, s.count, s.p50, s.p95, s.max, s.budget.p95Ms, s.withinBudget ? 1 : 0);
148
+ recorded++;
149
+ if (!s.withinBudget)
150
+ breaches++;
151
+ }
152
+ if (recorded > 0) {
153
+ store
154
+ .prepare(`UPDATE kevin_metrics SET value = value + ? WHERE key = 'perf_samples_recorded'`)
155
+ .run(recorded);
156
+ }
157
+ if (breaches > 0) {
158
+ store
159
+ .prepare(`UPDATE kevin_metrics SET value = value + ? WHERE key = 'perf_budget_breaches'`)
160
+ .run(breaches);
161
+ }
162
+ this.reset();
163
+ }
164
+ reset() {
165
+ for (const b of BUDGETS) {
166
+ // Zero the ring too: cursors and counts alone would let the next
167
+ // period's stats() read stale pre-reset samples whenever
168
+ // count < capacity (review fix, v1.0.0).
169
+ this.rings.get(b.scope)?.fill(0);
170
+ this.cursors.set(b.scope, 0);
171
+ this.counts.set(b.scope, 0);
172
+ }
173
+ }
174
+ }
@@ -83,4 +83,3 @@ export function quoteToken(token) {
83
83
  export function toMatchClause(tokens, separator) {
84
84
  return tokens.map(quoteToken).join(separator);
85
85
  }
86
- //# sourceMappingURL=query-tokenizer.js.map
@@ -16,4 +16,3 @@ export function stripPrivate(text) {
16
16
  return `<private: redacted ${n} chars>`;
17
17
  });
18
18
  }
19
- //# sourceMappingURL=redact.js.map
@@ -154,4 +154,3 @@ function toSchema(parse) {
154
154
  }
155
155
  export const replayEventSchema = toSchema(parseEvent);
156
156
  export const replayTranscriptSchema = toSchema(parseTranscript);
157
- //# sourceMappingURL=replay-types.js.map
@@ -200,4 +200,3 @@ function totalMemories(store) {
200
200
  const row = store.prepare("SELECT COUNT(*) AS n FROM memories").get();
201
201
  return row.n ?? 0;
202
202
  }
203
- //# sourceMappingURL=replay.js.map