@jmtrin/opencode-kevin 0.9.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 +558 -823
- 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 +6 -0
- package/dist/plugin/HookLiveness.js +15 -1
- package/dist/plugin/InjectionLedger.js +0 -1
- package/dist/plugin/LessonFixer.js +0 -1
- package/dist/plugin/Materializer.js +0 -1
- package/dist/plugin/MemoryService.js +0 -1
- package/dist/plugin/Migrate.d.ts +1 -1
- package/dist/plugin/Migrate.js +25 -3
- 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.js +0 -1
- package/dist/plugin/RepoTruth.js +0 -1
- package/dist/plugin/Retrospective.js +7 -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.js +0 -1
- package/dist/plugin/index.d.ts +2 -2
- package/dist/plugin/index.js +425 -227
- package/dist/plugin/inferability.js +0 -1
- package/dist/plugin/kevin_approve.js +0 -1
- package/dist/plugin/kevin_audit.d.ts +31 -0
- package/dist/plugin/kevin_audit.js +60 -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 +5 -0
- package/dist/plugin/kevin_doctor.js +30 -2
- package/dist/plugin/kevin_facts.js +0 -1
- package/dist/plugin/kevin_native.js +0 -1
- 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.js +0 -1
- 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 +21 -6
- 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/HookLiveness.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/host.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_doctor.js.map +0 -1
- package/dist/plugin/kevin_facts.js.map +0 -1
- package/dist/plugin/kevin_native.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/native.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
- package/migrations/010_v09_native.sql +0 -78
|
@@ -122,6 +122,37 @@ export interface AuditReport {
|
|
|
122
122
|
};
|
|
123
123
|
verdict: "healthy" | "degraded" | "unknown";
|
|
124
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* v1.0.0 (K10-020 / plan §5.6) — the perf block: per scope, the most
|
|
127
|
+
* recent `perf_samples` aggregate row (count/p50/p95/max) plus the
|
|
128
|
+
* stored budget and verdict. The stored aggregates are used DIRECTLY —
|
|
129
|
+
* re-aggregating aggregates would produce a number with no meaning
|
|
130
|
+
* (plan anti-pattern 42), and using the most recent row per scope is
|
|
131
|
+
* exactly what `bench:check` does, so `within_budget` agrees with it
|
|
132
|
+
* on the same data. Omitted (not zero-valued) on a pre-011 database,
|
|
133
|
+
* following the host precedent.
|
|
134
|
+
*/
|
|
135
|
+
perf?: {
|
|
136
|
+
scopes: Record<string, {
|
|
137
|
+
count: number;
|
|
138
|
+
p50: number;
|
|
139
|
+
p95: number;
|
|
140
|
+
max: number;
|
|
141
|
+
budget_p95: number;
|
|
142
|
+
within_budget: boolean;
|
|
143
|
+
}>;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* v1.0.0 (K10-020 / plan §5.6) — the contract block: the live contract's
|
|
147
|
+
* identity, computed from source rather than the database, so it is
|
|
148
|
+
* present even on a pre-011 database.
|
|
149
|
+
*/
|
|
150
|
+
contract?: {
|
|
151
|
+
contract_version: number;
|
|
152
|
+
digest: string;
|
|
153
|
+
clause_count: number;
|
|
154
|
+
deprecated_count: number;
|
|
155
|
+
};
|
|
125
156
|
partial: boolean;
|
|
126
157
|
}
|
|
127
158
|
/**
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { effectivePrePromptCap } from "./ContextInjector.js";
|
|
2
2
|
import { hasRepoIdColumn } from "./MemoryService.js";
|
|
3
|
+
import { contractDigest, describeContract } from "./contract.js";
|
|
4
|
+
import { BUDGETS } from "./perf.js";
|
|
3
5
|
const ALL_FALSE_CAPABILITIES = {
|
|
4
6
|
skills: false,
|
|
5
7
|
references: false,
|
|
@@ -484,6 +486,62 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
|
|
|
484
486
|
// marking the report partial — the host block is additive, unlike
|
|
485
487
|
// channels/curation which are the v0.6.0 scoreboard. Existing
|
|
486
488
|
// v0.8.0-era tests run on 007/008 stores and expect partial false.
|
|
489
|
+
// v1.0.0 (K10-020 / plan §5.6) — perf block: the most recent aggregate
|
|
490
|
+
// row per scope, read directly from perf_samples. A scope with no rows
|
|
491
|
+
// reports count 0, zero timings and within_budget true — never NULL,
|
|
492
|
+
// never NaN. Omitted on a pre-011 database (host precedent).
|
|
493
|
+
let perf;
|
|
494
|
+
let hasSchema011 = false;
|
|
495
|
+
try {
|
|
496
|
+
const row = store
|
|
497
|
+
.prepare("SELECT 1 AS n FROM sqlite_master WHERE type='table' AND name='perf_samples'")
|
|
498
|
+
.get();
|
|
499
|
+
hasSchema011 = !!row;
|
|
500
|
+
}
|
|
501
|
+
catch {
|
|
502
|
+
hasSchema011 = false;
|
|
503
|
+
}
|
|
504
|
+
if (hasSchema011) {
|
|
505
|
+
try {
|
|
506
|
+
const scopes = {};
|
|
507
|
+
const latest = store.prepare(`SELECT sample_count AS count, p50_ms AS p50, p95_ms AS p95,
|
|
508
|
+
max_ms AS max, budget_p95_ms AS budget_p95, within_budget
|
|
509
|
+
FROM perf_samples WHERE scope = ? ORDER BY id DESC LIMIT 1`);
|
|
510
|
+
for (const budget of BUDGETS) {
|
|
511
|
+
const row = latest.get(budget.scope);
|
|
512
|
+
scopes[budget.scope] = row
|
|
513
|
+
? {
|
|
514
|
+
count: row.count,
|
|
515
|
+
p50: row.p50,
|
|
516
|
+
p95: row.p95,
|
|
517
|
+
max: row.max,
|
|
518
|
+
budget_p95: row.budget_p95,
|
|
519
|
+
within_budget: row.within_budget === 1,
|
|
520
|
+
}
|
|
521
|
+
: {
|
|
522
|
+
count: 0,
|
|
523
|
+
p50: 0,
|
|
524
|
+
p95: 0,
|
|
525
|
+
max: 0,
|
|
526
|
+
budget_p95: budget.p95Ms,
|
|
527
|
+
within_budget: true,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
perf = { scopes };
|
|
531
|
+
}
|
|
532
|
+
catch {
|
|
533
|
+
perf = undefined;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
// v1.0.0 (K10-020 / plan §5.6) — contract block: pure function of the
|
|
537
|
+
// source, no database involvement.
|
|
538
|
+
const liveContract = describeContract();
|
|
539
|
+
const contract = {
|
|
540
|
+
contract_version: liveContract.contractVersion,
|
|
541
|
+
digest: contractDigest(liveContract),
|
|
542
|
+
clause_count: liveContract.clauses.length,
|
|
543
|
+
deprecated_count: liveContract.clauses.filter((c) => c.deprecated).length,
|
|
544
|
+
};
|
|
487
545
|
return {
|
|
488
546
|
memories,
|
|
489
547
|
injections,
|
|
@@ -498,7 +556,8 @@ export function buildAudit(store, metrics, capabilities = ALL_FALSE_CAPABILITIES
|
|
|
498
556
|
curation,
|
|
499
557
|
team,
|
|
500
558
|
host,
|
|
559
|
+
perf,
|
|
560
|
+
contract,
|
|
501
561
|
partial,
|
|
502
562
|
};
|
|
503
563
|
}
|
|
504
|
-
//# sourceMappingURL=kevin_audit.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Store } from "./Store.js";
|
|
2
|
+
export interface KevinBenchDeps {
|
|
3
|
+
store: Store;
|
|
4
|
+
/** Working directory used to locate bench/corpus (defaults process.cwd()). */
|
|
5
|
+
cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface KevinBenchArgs {
|
|
8
|
+
action: "status" | "last";
|
|
9
|
+
}
|
|
10
|
+
export declare function buildKevinBench(deps: KevinBenchDeps, args: KevinBenchArgs): Record<string, unknown>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K10-019 — kevin_bench: reports what `npm run bench` recorded.
|
|
3
|
+
*
|
|
4
|
+
* It NEVER runs the benchmark — running it from inside a live session
|
|
5
|
+
* would measure the session and pollute the user's database. The module
|
|
6
|
+
* imports nothing from scripts/bench.ts; the corpus digest of the on-disk
|
|
7
|
+
* corpus is recomputed here with the same fnv1a64 discipline so a result
|
|
8
|
+
* computed from a different corpus is visibly a different measurement.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, statSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fnv1a64 } from "./fingerprint.js";
|
|
13
|
+
function onDiskCorpusDigest(cwd) {
|
|
14
|
+
try {
|
|
15
|
+
const dir = join(cwd, "bench", "corpus");
|
|
16
|
+
statSync(dir);
|
|
17
|
+
const mem = readFileSync(join(dir, "memories.jsonl"), "utf8");
|
|
18
|
+
const q = readFileSync(join(dir, "queries.jsonl"), "utf8");
|
|
19
|
+
return {
|
|
20
|
+
digest: fnv1a64(`${mem}\n${q}`),
|
|
21
|
+
queries: q.split("\n").filter((l) => l.trim().length > 0).length,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function noRuns() {
|
|
29
|
+
return {
|
|
30
|
+
has_runs: false,
|
|
31
|
+
hint: "npm run bench",
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function buildKevinBench(deps, args) {
|
|
35
|
+
if (args.action === "status") {
|
|
36
|
+
const count = deps.store
|
|
37
|
+
.prepare("SELECT COUNT(*) AS c FROM bench_runs")
|
|
38
|
+
.get();
|
|
39
|
+
if (!count || count.c === 0)
|
|
40
|
+
return noRuns();
|
|
41
|
+
const latest = deps.store
|
|
42
|
+
.prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
|
|
43
|
+
.get();
|
|
44
|
+
if (!latest)
|
|
45
|
+
return noRuns();
|
|
46
|
+
const disk = onDiskCorpusDigest(deps.cwd ?? process.cwd());
|
|
47
|
+
return {
|
|
48
|
+
has_runs: true,
|
|
49
|
+
total_runs: count.c,
|
|
50
|
+
corpus_digest: latest.corpus_digest,
|
|
51
|
+
contract_digest: latest.contract_digest,
|
|
52
|
+
package_version: latest.package_version,
|
|
53
|
+
runtime: latest.runtime,
|
|
54
|
+
last_ran_at: latest.ran_at,
|
|
55
|
+
corpus_on_disk: disk === null ? null : disk.digest,
|
|
56
|
+
matches_disk_corpus: disk !== null && disk.digest === latest.corpus_digest,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const latest = deps.store
|
|
60
|
+
.prepare("SELECT corpus_digest, contract_digest, package_version, runtime, ran_at FROM bench_runs ORDER BY id DESC LIMIT 1")
|
|
61
|
+
.get();
|
|
62
|
+
if (!latest)
|
|
63
|
+
return noRuns();
|
|
64
|
+
const rows = deps.store
|
|
65
|
+
.prepare("SELECT arm, k, precision_at_k, recall_at_k, mrr FROM bench_runs WHERE corpus_digest = ? AND ran_at = ?")
|
|
66
|
+
.all(latest.corpus_digest, latest.ran_at);
|
|
67
|
+
return {
|
|
68
|
+
has_runs: true,
|
|
69
|
+
corpus_digest: latest.corpus_digest,
|
|
70
|
+
contract_digest: latest.contract_digest,
|
|
71
|
+
package_version: latest.package_version,
|
|
72
|
+
runtime: latest.runtime,
|
|
73
|
+
ran_at: latest.ran_at,
|
|
74
|
+
arms: rows.map((r) => ({
|
|
75
|
+
arm: r.arm,
|
|
76
|
+
k: r.k,
|
|
77
|
+
precision_at_k: r.precision_at_k,
|
|
78
|
+
recall_at_k: r.recall_at_k,
|
|
79
|
+
mrr: r.mrr,
|
|
80
|
+
})),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface KevinContractDeps {
|
|
2
|
+
packageVersion: string;
|
|
3
|
+
}
|
|
4
|
+
export interface KevinContractArgs {
|
|
5
|
+
clause?: string;
|
|
6
|
+
format?: "summary" | "full";
|
|
7
|
+
}
|
|
8
|
+
export declare function buildKevinContract(deps: KevinContractDeps, args: KevinContractArgs): Record<string, unknown>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* K10-018 — kevin_contract: the frozen surface, inspectable at runtime.
|
|
3
|
+
*
|
|
4
|
+
* Read-only. Returns the live contract with its digest and per-clause
|
|
5
|
+
* stability/since/deprecation; with `clause`, one clause's full value.
|
|
6
|
+
* No filesystem paths, no project ids, no LLM, no network.
|
|
7
|
+
*/
|
|
8
|
+
import { contractDigest, describeContract, } from "./contract.js";
|
|
9
|
+
export function buildKevinContract(deps, args) {
|
|
10
|
+
const contract = describeContract();
|
|
11
|
+
const digest = contractDigest(contract);
|
|
12
|
+
const format = args.format ?? "summary";
|
|
13
|
+
if (args.clause !== undefined) {
|
|
14
|
+
const clause = contract.clauses.find((c) => c.id === args.clause);
|
|
15
|
+
if (!clause) {
|
|
16
|
+
return {
|
|
17
|
+
error: "unknown_clause",
|
|
18
|
+
clause: args.clause,
|
|
19
|
+
known_clauses: contract.clauses.map((c) => c.id),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
contract_version: contract.contractVersion,
|
|
24
|
+
digest,
|
|
25
|
+
package_version: deps.packageVersion,
|
|
26
|
+
clause: {
|
|
27
|
+
id: clause.id,
|
|
28
|
+
title: clause.title,
|
|
29
|
+
stability: clause.stability,
|
|
30
|
+
since: clause.since,
|
|
31
|
+
...(clause.deprecated ? { deprecated: clause.deprecated } : {}),
|
|
32
|
+
...(clause.replacement ? { replacement: clause.replacement } : {}),
|
|
33
|
+
value: clause.value,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (format === "full") {
|
|
38
|
+
return {
|
|
39
|
+
contract_version: contract.contractVersion,
|
|
40
|
+
digest,
|
|
41
|
+
package_version: deps.packageVersion,
|
|
42
|
+
clauses: contract.clauses,
|
|
43
|
+
deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
contract_version: contract.contractVersion,
|
|
48
|
+
digest,
|
|
49
|
+
package_version: deps.packageVersion,
|
|
50
|
+
clauses: contract.clauses.map((c) => ({
|
|
51
|
+
id: c.id,
|
|
52
|
+
title: c.title,
|
|
53
|
+
stability: c.stability,
|
|
54
|
+
since: c.since,
|
|
55
|
+
...(c.deprecated ? { deprecated: c.deprecated } : {}),
|
|
56
|
+
})),
|
|
57
|
+
deprecated: contract.clauses.filter((c) => c.deprecated).map((c) => c.id),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -36,6 +36,11 @@ export interface DoctorReport {
|
|
|
36
36
|
readonly reference: boolean;
|
|
37
37
|
};
|
|
38
38
|
};
|
|
39
|
+
/** v1.0.0 (K10-021) — scopes whose most recent sample exceeded their
|
|
40
|
+
* p95 budget. Omitted entirely on pre-011 databases (no table). */
|
|
41
|
+
readonly perf?: {
|
|
42
|
+
readonly scopes_over_budget: readonly string[];
|
|
43
|
+
};
|
|
39
44
|
readonly verdict: "healthy" | "degraded" | "unknown";
|
|
40
45
|
readonly reason: string;
|
|
41
46
|
readonly partial: boolean;
|
|
@@ -106,6 +106,22 @@ export function countZodCopies(cwd) {
|
|
|
106
106
|
return { copies: null, note: "zod resolution walk failed" };
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
+
/** v1.0.0 (K10-021) — scopes whose most recent perf sample breached the
|
|
110
|
+
* p95 budget. The within_budget flag is materialized by Perf.flush()
|
|
111
|
+
* against the frozen BUDGETS; this block reads that persisted verdict
|
|
112
|
+
* and never re-aggregates. A missing table (pre-011 DB) yields no
|
|
113
|
+
* block rather than an empty one, matching the hooks precedent. */
|
|
114
|
+
function perfOverBudget(store) {
|
|
115
|
+
try {
|
|
116
|
+
const rows = store
|
|
117
|
+
.prepare("SELECT DISTINCT scope FROM perf_samples WHERE within_budget = 0 ORDER BY scope")
|
|
118
|
+
.all();
|
|
119
|
+
return rows.map((r) => r.scope);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
109
125
|
function lastRegistration(store, surface) {
|
|
110
126
|
try {
|
|
111
127
|
const row = store
|
|
@@ -124,7 +140,7 @@ export function buildDoctor(store, host, settings, options = {}) {
|
|
|
124
140
|
// construction — kevin_doctor never re-probes (K9-004: probe once,
|
|
125
141
|
// freeze, reuse).
|
|
126
142
|
const hooks = hooksBlock(store);
|
|
127
|
-
|
|
143
|
+
let verdict = reduceVerdict(hooks.hooks.map((h) => ({
|
|
128
144
|
hook: h.hook,
|
|
129
145
|
experimental: h.experimental,
|
|
130
146
|
state: h.state,
|
|
@@ -134,6 +150,18 @@ export function buildDoctor(store, host, settings, options = {}) {
|
|
|
134
150
|
expectedCount: h.expected_count,
|
|
135
151
|
deadSince: h.since ?? null,
|
|
136
152
|
})));
|
|
153
|
+
// v1.0.0 (K10-021) — a slow plugin is not healthy even when every
|
|
154
|
+
// hook is live. A budget breach degrades the verdict and names the
|
|
155
|
+
// scope; a dead hook keeps its reason; `unknown` is never rounded up.
|
|
156
|
+
const breaches = perfOverBudget(store);
|
|
157
|
+
if (breaches !== null &&
|
|
158
|
+
breaches.length > 0 &&
|
|
159
|
+
verdict.verdict !== "degraded") {
|
|
160
|
+
verdict = {
|
|
161
|
+
verdict: "degraded",
|
|
162
|
+
reason: `perf budget exceeded: ${breaches.join(", ")}`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
137
165
|
const zod = countZodCopies(options.zodRoot ?? process.cwd());
|
|
138
166
|
const enabled = settings.getSetting("native_registration_enabled", "0") === "1";
|
|
139
167
|
return {
|
|
@@ -162,7 +190,7 @@ export function buildDoctor(store, host, settings, options = {}) {
|
|
|
162
190
|
},
|
|
163
191
|
verdict: verdict.verdict,
|
|
164
192
|
reason: verdict.reason,
|
|
193
|
+
...(breaches !== null ? { perf: { scopes_over_budget: breaches } } : {}),
|
|
165
194
|
partial: hooks.partial,
|
|
166
195
|
};
|
|
167
196
|
}
|
|
168
|
-
//# sourceMappingURL=kevin_doctor.js.map
|
package/dist/plugin/kevin_why.js
CHANGED
package/dist/plugin/metrics.js
CHANGED
package/dist/plugin/native.js
CHANGED
package/dist/plugin/okf.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/plugin/redact.js
CHANGED
package/dist/plugin/replay.js
CHANGED