@jmtrin/opencode-kevin 0.9.0 → 1.1.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +575 -823
  3. package/dist/migrations/001_initial.sql +91 -91
  4. package/dist/migrations/002_indexes.sql +13 -13
  5. package/dist/migrations/003_v02_signal.sql +57 -57
  6. package/dist/migrations/004_v03_knowledge.sql +138 -138
  7. package/dist/migrations/005_v04_signal.sql +57 -57
  8. package/dist/migrations/006_v05_glassbox.sql +118 -118
  9. package/dist/migrations/007_v06_pull.sql +144 -144
  10. package/dist/migrations/011_v10_proven.sql +69 -0
  11. package/dist/migrations/012_v11_drift.sql +24 -0
  12. package/dist/plugin/Archiver.js +2 -18
  13. package/dist/plugin/ArtifactWriter.d.ts +25 -10
  14. package/dist/plugin/ArtifactWriter.js +43 -25
  15. package/dist/plugin/CausalChain.js +32 -14
  16. package/dist/plugin/ConflictDetector.js +7 -30
  17. package/dist/plugin/ContextInjector.js +0 -1
  18. package/dist/plugin/ConventionMiner.js +0 -1
  19. package/dist/plugin/Curator.js +0 -1
  20. package/dist/plugin/Feedback.js +2 -20
  21. package/dist/plugin/HookLiveness.d.ts +7 -0
  22. package/dist/plugin/HookLiveness.js +26 -28
  23. package/dist/plugin/InjectionLedger.js +104 -58
  24. package/dist/plugin/LessonFixer.js +0 -1
  25. package/dist/plugin/Materializer.js +1 -89
  26. package/dist/plugin/MemoryService.d.ts +59 -1
  27. package/dist/plugin/MemoryService.js +13 -107
  28. package/dist/plugin/Migrate.d.ts +1 -1
  29. package/dist/plugin/Migrate.js +30 -3
  30. package/dist/plugin/PatternMiner.js +0 -1
  31. package/dist/plugin/QualityGate.js +0 -1
  32. package/dist/plugin/Reflector.js +0 -1
  33. package/dist/plugin/RepoIdentity.js +0 -1
  34. package/dist/plugin/RepoTruth.js +0 -1
  35. package/dist/plugin/Retrospective.js +11 -1
  36. package/dist/plugin/SharedLayer.js +0 -1
  37. package/dist/plugin/Store.js +0 -1
  38. package/dist/plugin/ToolCallObserver.js +18 -6
  39. package/dist/plugin/capabilities.js +0 -1
  40. package/dist/plugin/columns.d.ts +11 -0
  41. package/dist/plugin/columns.js +54 -0
  42. package/dist/plugin/confidence.js +0 -1
  43. package/dist/plugin/contract.d.ts +54 -0
  44. package/dist/plugin/contract.js +383 -0
  45. package/dist/plugin/diff.js +0 -1
  46. package/dist/plugin/escape.d.ts +39 -0
  47. package/dist/plugin/escape.js +60 -0
  48. package/dist/plugin/fingerprint.js +0 -1
  49. package/dist/plugin/host.js +0 -1
  50. package/dist/plugin/index.d.ts +2 -2
  51. package/dist/plugin/index.js +448 -229
  52. package/dist/plugin/inferability.js +0 -1
  53. package/dist/plugin/kevin_approve.js +0 -1
  54. package/dist/plugin/kevin_audit.d.ts +31 -0
  55. package/dist/plugin/kevin_audit.js +60 -1
  56. package/dist/plugin/kevin_bench.d.ts +10 -0
  57. package/dist/plugin/kevin_bench.js +82 -0
  58. package/dist/plugin/kevin_conflicts.js +0 -1
  59. package/dist/plugin/kevin_contract.d.ts +8 -0
  60. package/dist/plugin/kevin_contract.js +59 -0
  61. package/dist/plugin/kevin_doctor.d.ts +5 -0
  62. package/dist/plugin/kevin_doctor.js +30 -2
  63. package/dist/plugin/kevin_facts.js +0 -1
  64. package/dist/plugin/kevin_forget.d.ts +33 -0
  65. package/dist/plugin/kevin_forget.js +260 -0
  66. package/dist/plugin/kevin_native.js +0 -1
  67. package/dist/plugin/kevin_propose.js +0 -1
  68. package/dist/plugin/kevin_publish.js +0 -1
  69. package/dist/plugin/kevin_why.js +1 -19
  70. package/dist/plugin/memory-format.js +0 -1
  71. package/dist/plugin/metrics.d.ts +1 -1
  72. package/dist/plugin/metrics.js +4 -1
  73. package/dist/plugin/native.js +0 -1
  74. package/dist/plugin/okf-export.js +0 -1
  75. package/dist/plugin/okf-import.js +0 -1
  76. package/dist/plugin/okf.js +0 -1
  77. package/dist/plugin/perf.d.ts +35 -0
  78. package/dist/plugin/perf.js +174 -0
  79. package/dist/plugin/query-tokenizer.js +56 -9
  80. package/dist/plugin/redact.js +0 -1
  81. package/dist/plugin/replay-types.js +0 -1
  82. package/dist/plugin/replay.js +0 -1
  83. package/dist/plugin/sqlite-adapter.js +0 -1
  84. package/dist/plugin/time-ms.d.ts +1 -0
  85. package/dist/plugin/time-ms.js +16 -0
  86. package/dist/plugin/uuid.js +0 -1
  87. package/package.json +22 -6
  88. package/dist/plugin/Archiver.js.map +0 -1
  89. package/dist/plugin/ArtifactWriter.js.map +0 -1
  90. package/dist/plugin/CausalChain.js.map +0 -1
  91. package/dist/plugin/ConflictDetector.js.map +0 -1
  92. package/dist/plugin/ContextInjector.js.map +0 -1
  93. package/dist/plugin/ConventionMiner.js.map +0 -1
  94. package/dist/plugin/Curator.js.map +0 -1
  95. package/dist/plugin/Feedback.js.map +0 -1
  96. package/dist/plugin/HookLiveness.js.map +0 -1
  97. package/dist/plugin/InjectionLedger.js.map +0 -1
  98. package/dist/plugin/LessonFixer.js.map +0 -1
  99. package/dist/plugin/Materializer.js.map +0 -1
  100. package/dist/plugin/MemoryService.js.map +0 -1
  101. package/dist/plugin/Migrate.js.map +0 -1
  102. package/dist/plugin/PatternMiner.js.map +0 -1
  103. package/dist/plugin/QualityGate.js.map +0 -1
  104. package/dist/plugin/Reflector.js.map +0 -1
  105. package/dist/plugin/RepoIdentity.js.map +0 -1
  106. package/dist/plugin/RepoTruth.js.map +0 -1
  107. package/dist/plugin/Retrospective.js.map +0 -1
  108. package/dist/plugin/SharedLayer.js.map +0 -1
  109. package/dist/plugin/Store.js.map +0 -1
  110. package/dist/plugin/ToolCallObserver.js.map +0 -1
  111. package/dist/plugin/capabilities.js.map +0 -1
  112. package/dist/plugin/confidence.js.map +0 -1
  113. package/dist/plugin/diff.js.map +0 -1
  114. package/dist/plugin/fingerprint.js.map +0 -1
  115. package/dist/plugin/host.js.map +0 -1
  116. package/dist/plugin/index.js.map +0 -1
  117. package/dist/plugin/inferability.js.map +0 -1
  118. package/dist/plugin/kevin_approve.js.map +0 -1
  119. package/dist/plugin/kevin_audit.js.map +0 -1
  120. package/dist/plugin/kevin_conflicts.js.map +0 -1
  121. package/dist/plugin/kevin_doctor.js.map +0 -1
  122. package/dist/plugin/kevin_facts.js.map +0 -1
  123. package/dist/plugin/kevin_native.js.map +0 -1
  124. package/dist/plugin/kevin_propose.js.map +0 -1
  125. package/dist/plugin/kevin_publish.js.map +0 -1
  126. package/dist/plugin/kevin_why.js.map +0 -1
  127. package/dist/plugin/memory-format.js.map +0 -1
  128. package/dist/plugin/metrics.js.map +0 -1
  129. package/dist/plugin/native.js.map +0 -1
  130. package/dist/plugin/okf-export.js.map +0 -1
  131. package/dist/plugin/okf-import.js.map +0 -1
  132. package/dist/plugin/okf.js.map +0 -1
  133. package/dist/plugin/query-tokenizer.js.map +0 -1
  134. package/dist/plugin/redact.js.map +0 -1
  135. package/dist/plugin/replay-types.js.map +0 -1
  136. package/dist/plugin/replay.js.map +0 -1
  137. package/dist/plugin/sqlite-adapter.js.map +0 -1
  138. package/dist/plugin/uuid.js.map +0 -1
  139. package/migrations/001_initial.sql +0 -92
  140. package/migrations/002_indexes.sql +0 -14
  141. package/migrations/003_v02_signal.sql +0 -58
  142. package/migrations/004_v03_knowledge.sql +0 -138
  143. package/migrations/005_v04_signal.sql +0 -57
  144. package/migrations/006_v05_glassbox.sql +0 -118
  145. package/migrations/007_v06_pull.sql +0 -145
  146. package/migrations/008_v07_truth.sql +0 -124
  147. package/migrations/009_v08_team.sql +0 -100
  148. package/migrations/010_v09_native.sql +0 -78
@@ -0,0 +1,69 @@
1
+ -- =============================================================
2
+ -- Kevin v1.0.0 "Proven" — migration 011
3
+ -- Adds: perf sampling, benchmark results.
4
+ -- No ALTER TABLE (second release running); no column is retyped.
5
+ -- =============================================================
6
+
7
+ -- 1. Per-scope latency aggregates, flushed at session.idle.
8
+ -- Machine-scoped: no project_id, for the same reason hook_liveness
9
+ -- has none (v0.9.0 D9-08) — latency is a property of the install.
10
+ CREATE TABLE IF NOT EXISTS perf_samples (
11
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
12
+ scope TEXT NOT NULL,
13
+ sample_count INTEGER NOT NULL,
14
+ p50_ms REAL NOT NULL,
15
+ p95_ms REAL NOT NULL,
16
+ max_ms REAL NOT NULL,
17
+ budget_p95_ms REAL NOT NULL,
18
+ within_budget INTEGER NOT NULL DEFAULT 1,
19
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
20
+ );
21
+
22
+ CREATE INDEX IF NOT EXISTS idx_perf_samples_scope
23
+ ON perf_samples(scope, recorded_at DESC);
24
+ CREATE INDEX IF NOT EXISTS idx_perf_samples_breach
25
+ ON perf_samples(within_budget) WHERE within_budget = 0;
26
+
27
+ -- 2. Benchmark results. Append-only. One row per arm per run.
28
+ -- arm CHECK is closed by the plan; widening requires a table rebuild (intentional friction).
29
+ -- scope CHECK is intentionally absent: the scope union may gain members in a 1.x minor.
30
+ CREATE TABLE IF NOT EXISTS bench_runs (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ corpus_digest TEXT NOT NULL,
33
+ contract_digest TEXT NOT NULL,
34
+ package_version TEXT NOT NULL,
35
+ runtime TEXT NOT NULL,
36
+ arm TEXT NOT NULL,
37
+ k INTEGER NOT NULL,
38
+ precision_at_k REAL NOT NULL,
39
+ recall_at_k REAL NOT NULL,
40
+ mrr REAL NOT NULL,
41
+ ran_at TEXT NOT NULL DEFAULT (datetime('now')),
42
+ CHECK (arm IN ('none', 'recent-k', 'random-k', 'kevin'))
43
+ );
44
+
45
+ CREATE INDEX IF NOT EXISTS idx_bench_runs_corpus
46
+ ON bench_runs(corpus_digest, ran_at DESC);
47
+ CREATE INDEX IF NOT EXISTS idx_bench_runs_arm
48
+ ON bench_runs(arm, ran_at DESC);
49
+
50
+ -- 3. Metric seeds (45 -> 51).
51
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('perf_samples_recorded', 0);
52
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('perf_budget_breaches', 0);
53
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('dispose_fires_total', 0);
54
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('dispose_misses_total', 0);
55
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('contract_digest_changes', 0);
56
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('bench_runs_total', 0);
57
+
58
+ -- 4. Setting seeds (27 -> 31). All TEXT; '0'/'1' are strings.
59
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('perf_enabled', '1');
60
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('perf_ring_capacity', '512');
61
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('perf_flush_on_idle', '1');
62
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('contract_report_enabled', '1');
63
+
64
+ -- 5. `dispose` becomes the seventh instrumented hook (v0.9.0 migration 010
65
+ -- seeded six rows; this is a data seed, not a schema change).
66
+ INSERT OR IGNORE INTO hook_liveness (hook) VALUES ('dispose');
67
+
68
+ -- 6. Schema version.
69
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('011');
@@ -0,0 +1,24 @@
1
+ -- millisecond companions for the two tables whose ORDERING decides outcomes.
2
+ ALTER TABLE tool_calls ADD COLUMN ts_ms INTEGER;
3
+ ALTER TABLE kevin_injections ADD COLUMN injected_at_ms INTEGER;
4
+
5
+ -- conservative backfill: seconds -> ms (x1000). Rows keep their original
6
+ -- second-granularity value in the old column; nothing is rewritten there.
7
+ UPDATE tool_calls SET ts_ms =
8
+ CAST(strftime('%s', ts) AS INTEGER) * 1000
9
+ WHERE ts_ms IS NULL AND ts IS NOT NULL;
10
+
11
+ UPDATE kevin_injections SET injected_at_ms =
12
+ CAST(strftime('%s', injected_at) AS INTEGER) * 1000
13
+ WHERE injected_at_ms IS NULL AND injected_at IS NOT NULL;
14
+
15
+ CREATE INDEX idx_tool_calls_ts_ms ON tool_calls(ts_ms);
16
+ CREATE INDEX idx_injections_injected_ms ON kevin_injections(injected_at_ms);
17
+
18
+ -- metric seeds (C-05 additions carry since=1.1.0 in contract)
19
+ INSERT INTO kevin_metrics (key, value, updated_at) VALUES
20
+ ('bench_regression_failures', 0, datetime('now')),
21
+ ('forget_requests_total', 0, datetime('now')),
22
+ ('forget_tombstones_published', 0, datetime('now'));
23
+
24
+ INSERT INTO schema_version (version) VALUES ('012');
@@ -1,3 +1,4 @@
1
+ import { hasArchivedColumn } from "./columns.js";
1
2
  /**
2
3
  * v0.5.0 Archiver (K5-012 / plan §5.4, D5-05).
3
4
  *
@@ -71,23 +72,7 @@ export class Archiver {
71
72
  }
72
73
  }
73
74
  hasArchivedColumn() {
74
- return hasArchivedColumnCached(this.store);
75
- }
76
- }
77
- // v0.6.0 (K6-001a) — positive-only caching: a successful probe is cached,
78
- // a failed probe is NOT. A Store migrated in place heals on the next call.
79
- const archivedColumnCache = new WeakMap();
80
- function hasArchivedColumnCached(store) {
81
- const cached = archivedColumnCache.get(store);
82
- if (cached === true)
83
- return true;
84
- try {
85
- store.prepare("SELECT archived_at FROM memories LIMIT 1").get();
86
- archivedColumnCache.set(store, true);
87
- return true;
88
- }
89
- catch {
90
- return false;
75
+ return hasArchivedColumn(this.store);
91
76
  }
92
77
  }
93
78
  /** 'YYYY-MM-DD HH:MM:SS' UTC — the format SQLite `datetime('now')` uses,
@@ -96,4 +81,3 @@ function formatSqliteUtc(d) {
96
81
  const pad = (n) => String(n).padStart(2, "0");
97
82
  return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`;
98
83
  }
99
- //# sourceMappingURL=Archiver.js.map
@@ -35,18 +35,33 @@ export interface WritePlan {
35
35
  readonly hashAfter: string;
36
36
  }
37
37
  /**
38
- * Rule 9 (K6-009 / plan §5.1, D6-02) — three layers applied to the body
39
- * before splicing:
40
- * (a) the escaping discipline of `plugin/memory-format.ts`;
41
- * (b) strip any line containing `kevin:begin` or `kevin:end`, in any
42
- * casing, anywhere in the line;
43
- * (c) strip HTML comment terminators (`-->`).
44
- * Without this, a memory containing a literal `<!-- kevin:end -->` line would
45
- * close the marker comment early and let subsequent content escape the
46
- * curated region on the next regeneration a marker-injection variant of the
47
- * v0.1.5 prompt-injection defect (plan §3.5).
38
+ * Rule 9, layer (a) — superseded in v1.0.0 by `escapeForMarkerBlock`
39
+ * in `plugin/escape.ts` (K10-027), which carries the same idempotent
40
+ * entity discipline.
41
+ */
42
+ /**
43
+ * v1.0.0 (K10-027 / plan §5.7) the boundary functions now live in
44
+ * `plugin/escape.ts`; this composer applies them in fence-then-marker
45
+ * order. Layer (b) strip any line containing `kevin:begin` or
46
+ * `kevin:end`, in any casing is a filter rather than an escape and
47
+ * stays here. Without layer (b), a memory containing a literal
48
+ * `<!-- kevin:end -->` line would close the marker comment early and
49
+ * let subsequent content escape the curated region on the next
50
+ * regeneration — a marker-injection variant of the v0.1.5
51
+ * prompt-injection defect (plan §3.5). The trailing `-->` strip is kept
52
+ * as defence in depth although `escapeForMarkerBlock` already escapes
53
+ * every `>` that could form one.
48
54
  */
49
55
  export declare function sanitizeArtifactBody(body: string): string;
56
+ /**
57
+ * v1.0.0 (K10-027 / plan §5.7, rule 2) — whole-file writes escape by
58
+ * container. Only `.okf` files are line-oriented JSON: each line gets
59
+ * `escapeForOkfLine`, which is the identity on a well-formed OKF line
60
+ * (canonical JSON never contains raw control characters) and therefore
61
+ * preserves the re-render `noop`. Kevin-owned markdown paths keep their
62
+ * bytes — fences there are legitimate content.
63
+ */
64
+ export declare function escapeForContainer(path: string, content: string): string;
50
65
  /**
51
66
  * v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
52
67
  *
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { closeSync, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
3
3
  import { unifiedDiff } from "./diff.js";
4
+ import { escapeForFence, escapeForMarkerBlock, escapeForOkfLine, } from "./escape.js";
4
5
  import { uuidv7 } from "./uuid.js";
5
6
  // v0.6.0 (K6-005 / plan §5.1, D6-02) — the frozen marker contract. The exact
6
7
  // byte sequences are load-bearing: README, v1.0.0 plan C-01 and the round-trip
@@ -30,39 +31,55 @@ function normalizeEol(body, eol) {
30
31
  return body.replace(/\r\n/g, "\n").replace(/\n/g, eol);
31
32
  }
32
33
  /**
33
- * Rule 9, layer (a) — the escaping discipline of `plugin/memory-format.ts`
34
- * (`&`, `<`, `>`) made idempotent: an ampersand that already heads a known
35
- * entity (`&amp;`, `&lt;`, `&gt;`, `&#NNN;`) is left alone, so sanitizing
36
- * sanitized output is a fixed point. The round-trip property of the marker
37
- * block depends on this: a non-idempotent escape would grow `&amp;lt;` on
38
- * every regeneration (K6-009 / plan §5.1 rule 9).
34
+ * Rule 9, layer (a) — superseded in v1.0.0 by `escapeForMarkerBlock`
35
+ * in `plugin/escape.ts` (K10-027), which carries the same idempotent
36
+ * entity discipline.
39
37
  */
40
- function escapeIdempotent(text) {
41
- return text
42
- .replace(/&(?!(amp|lt|gt|#\d+);)/g, "&amp;")
43
- .replace(/</g, "&lt;")
44
- .replace(/>/g, "&gt;");
45
- }
46
38
  /**
47
- * Rule 9 (K6-009 / plan §5.1, D6-02) — three layers applied to the body
48
- * before splicing:
49
- * (a) the escaping discipline of `plugin/memory-format.ts`;
50
- * (b) strip any line containing `kevin:begin` or `kevin:end`, in any
51
- * casing, anywhere in the line;
52
- * (c) strip HTML comment terminators (`-->`).
53
- * Without this, a memory containing a literal `<!-- kevin:end -->` line would
54
- * close the marker comment early and let subsequent content escape the
55
- * curated region on the next regeneration a marker-injection variant of the
56
- * v0.1.5 prompt-injection defect (plan §3.5).
39
+ * v1.0.0 (K10-027 / plan §5.7) — the boundary functions now live in
40
+ * `plugin/escape.ts`; this composer applies them in fence-then-marker
41
+ * order. Layer (b) strip any line containing `kevin:begin` or
42
+ * `kevin:end`, in any casing is a filter rather than an escape and
43
+ * stays here. Without layer (b), a memory containing a literal
44
+ * `<!-- kevin:end -->` line would close the marker comment early and
45
+ * let subsequent content escape the curated region on the next
46
+ * regeneration a marker-injection variant of the v0.1.5
47
+ * prompt-injection defect (plan §3.5). The trailing `-->` strip is kept
48
+ * as defence in depth although `escapeForMarkerBlock` already escapes
49
+ * every `>` that could form one.
57
50
  */
58
51
  export function sanitizeArtifactBody(body) {
59
- const escaped = escapeIdempotent(body);
52
+ const escaped = escapeForMarkerBlock(escapeForFence(body));
60
53
  const kept = escaped
61
54
  .split("\n")
62
55
  .filter((line) => !/kevin:begin|kevin:end/i.test(line))
63
56
  .join("\n");
64
57
  return kept.replace(/-->/g, "");
65
58
  }
59
+ /**
60
+ * v1.0.0 (K10-027 / plan §5.7, rule 2) — whole-file writes escape by
61
+ * container. Only `.okf` files are line-oriented JSON: each line gets
62
+ * `escapeForOkfLine`, which is the identity on a well-formed OKF line
63
+ * (canonical JSON never contains raw control characters) and therefore
64
+ * preserves the re-render `noop`. Kevin-owned markdown paths keep their
65
+ * bytes — fences there are legitimate content.
66
+ */
67
+ export function escapeForContainer(path, content) {
68
+ if (!path.endsWith(".okf"))
69
+ return content;
70
+ return content
71
+ .split("\n")
72
+ .map((line) => {
73
+ // A CRLF file (healHeader preserves the original EOL) leaves a
74
+ // trailing \r on each line after the split — it is line-ending
75
+ // bytes, not statement content, so it must survive untouched.
76
+ if (line.endsWith("\r")) {
77
+ return `${escapeForOkfLine(line.slice(0, -1))}\r`;
78
+ }
79
+ return escapeForOkfLine(line);
80
+ })
81
+ .join("\n");
82
+ }
66
83
  /**
67
84
  * v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
68
85
  *
@@ -113,7 +130,9 @@ export class ArtifactWriter {
113
130
  // like any other refusal: after = before, both hashes recorded.
114
131
  const refusal = request.refusal;
115
132
  const refused = refusal !== undefined;
116
- const after = refused ? before : content;
133
+ // K10-027 the container boundary: OKF lines are escaped at the
134
+ // single write path, never by callers.
135
+ const after = refused ? before : escapeForContainer(path, content);
117
136
  return {
118
137
  path,
119
138
  before,
@@ -270,4 +289,3 @@ export class ArtifactWriter {
270
289
  .run(uuidv7(), proposalId ?? null, this.projectId, plan.path, Buffer.byteLength(plan.before, "utf8"), Buffer.byteLength(plan.after, "utf8"), plan.hashBefore, plan.hashAfter, plan.outcome, plan.reason ?? null);
271
290
  }
272
291
  }
273
- //# sourceMappingURL=ArtifactWriter.js.map
@@ -1,4 +1,6 @@
1
1
  import { extractFixArgs, } from "./LessonFixer.js";
2
+ import { hasColumn } from "./columns.js";
3
+ import { toMs } from "./time-ms.js";
2
4
  /** K3-007 — a success only links to a failure within this many calls. */
3
5
  const MAX_LINK_DISTANCE = 10;
4
6
  export class CausalChain {
@@ -24,11 +26,15 @@ export class CausalChain {
24
26
  // distance, so an unrelated success (e.g. an `ls` run after a
25
27
  // typecheck failure) was stamped as the fix for that error.
26
28
  onSuccess(_tool, _args, _projectId, sessionId) {
27
- const successRow = this.store
28
- .prepare(`SELECT rowid, tool, args_summary FROM tool_calls
29
- WHERE session_id = ? AND success = 1
30
- ORDER BY rowid DESC LIMIT 1`)
31
- .get(sessionId);
29
+ // v1.1.0 (K11-004 / plan §5.2, D11-01/D11-07) — ms-aware window: prefer _ms
30
+ const hasTsMs = hasColumn(this.store, "tool_calls", "ts_ms");
31
+ const successRow = (hasTsMs
32
+ ? this.store.prepare(`SELECT rowid, tool, args_summary, ts, ts_ms FROM tool_calls
33
+ WHERE session_id = ? AND success = 1
34
+ ORDER BY rowid DESC LIMIT 1`)
35
+ : this.store.prepare(`SELECT rowid, tool, args_summary, ts FROM tool_calls
36
+ WHERE session_id = ? AND success = 1
37
+ ORDER BY rowid DESC LIMIT 1`)).get(sessionId);
32
38
  if (!successRow)
33
39
  return;
34
40
  const linkedFps = new Set(this.store
@@ -40,20 +46,33 @@ export class CausalChain {
40
46
  // onLinkError) is the SAME identity dimension the error memory
41
47
  // uses; `fingerprint` is the legacy tool|args|success hash and
42
48
  // simply never matches a reflector error memory.
43
- const failRows = this.store
44
- .prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp
45
- FROM tool_calls
46
- WHERE session_id = ?
47
- AND success = 0
48
- AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
49
- ORDER BY rowid DESC LIMIT ?`)
50
- .all(sessionId, MAX_LINK_DISTANCE);
49
+ const failRows = (hasTsMs
50
+ ? this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts, ts_ms
51
+ FROM tool_calls
52
+ WHERE session_id = ?
53
+ AND success = 0
54
+ AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
55
+ ORDER BY rowid DESC LIMIT ?`)
56
+ : this.store.prepare(`SELECT rowid, COALESCE(error_fingerprint, fingerprint) AS fp, ts
57
+ FROM tool_calls
58
+ WHERE session_id = ?
59
+ AND success = 0
60
+ AND (error_fingerprint IS NOT NULL OR fingerprint IS NOT NULL)
61
+ ORDER BY rowid DESC LIMIT ?`)).all(sessionId, MAX_LINK_DISTANCE);
51
62
  for (const fail of failRows) {
52
63
  if (!fail.fp || linkedFps.has(fail.fp))
53
64
  continue;
54
65
  const dist = successRow.rowid - fail.rowid;
55
66
  if (dist <= 0 || dist > MAX_LINK_DISTANCE)
56
67
  continue;
68
+ // v1.1.0 — ≤24h window uses ms when available
69
+ const successMs = toMs(successRow.ts, successRow.ts_ms ?? null);
70
+ const failMs = toMs(fail.ts, fail.ts_ms ?? null);
71
+ if (successMs !== null && failMs !== null) {
72
+ const diff = successMs - failMs;
73
+ if (diff < 0 || diff > 86_400_000)
74
+ continue;
75
+ }
57
76
  const mem = this.store
58
77
  .prepare(`SELECT 1 FROM memories
59
78
  WHERE fingerprint = ? AND type = 'error'
@@ -176,4 +195,3 @@ export class CausalChain {
176
195
  return row?.value === "1";
177
196
  }
178
197
  }
179
- //# sourceMappingURL=CausalChain.js.map
@@ -1,4 +1,6 @@
1
+ import { mapRow } from "./MemoryService.js";
1
2
  import { normalize } from "./fingerprint.js";
3
+ import { STOP_WORDS } from "./query-tokenizer.js";
2
4
  import { uuidv7 } from "./uuid.js";
3
5
  const POLARITY_RULES = [
4
6
  {
@@ -14,33 +16,6 @@ const POLARITY_RULES = [
14
16
  { positive: ["enable"], negative: [["disable"]] },
15
17
  { positive: ["prefer"], negative: [["avoid"]] },
16
18
  ];
17
- const STOP_WORDS = new Set([
18
- "a",
19
- "an",
20
- "and",
21
- "are",
22
- "as",
23
- "at",
24
- "be",
25
- "by",
26
- "for",
27
- "from",
28
- "in",
29
- "is",
30
- "it",
31
- "of",
32
- "on",
33
- "or",
34
- "that",
35
- "the",
36
- "this",
37
- "to",
38
- "with",
39
- "we",
40
- "when",
41
- "where",
42
- "you",
43
- ]);
44
19
  const POLARITY_WORDS = new Set([
45
20
  "use",
46
21
  "do",
@@ -157,9 +132,12 @@ export class ConflictDetector {
157
132
  FROM memories WHERE project_id = ? AND status = 'active'`)
158
133
  .all(this.projectId);
159
134
  return rows.flatMap((row) => {
160
- const memory = row;
135
+ // v1.1.0 (K11-014) — route through mapRow so metadata is parsed and confidence derived
136
+ const memory = mapRow(row);
161
137
  const reasons = this.repoTruth?.contradictions(memory) ?? [];
162
- return reasons.length > 0 ? [{ memoryId: row.id, reasons }] : [];
138
+ return reasons.length > 0
139
+ ? [{ memoryId: row.id, reasons }]
140
+ : [];
163
141
  });
164
142
  }
165
143
  detectDecisionPairs() {
@@ -280,4 +258,3 @@ function oppositePair(a, b) {
280
258
  }
281
259
  return null;
282
260
  }
283
- //# sourceMappingURL=ConflictDetector.js.map
@@ -531,4 +531,3 @@ Consider adding this convention to AGENTS.md:
531
531
  output.context.push(block);
532
532
  }
533
533
  }
534
- //# sourceMappingURL=ContextInjector.js.map
@@ -240,4 +240,3 @@ function statementForSequence(a, b) {
240
240
  function statementForSequence3(a, b, c) {
241
241
  return `every ${a} is followed by ${b} and then ${c}`;
242
242
  }
243
- //# sourceMappingURL=ConventionMiner.js.map
@@ -322,4 +322,3 @@ function normalizeBullet(line) {
322
322
  .replace(/\s+\((?:verified|feedback)[^)]*\)\s*$/i, "");
323
323
  return normalize(statement);
324
324
  }
325
- //# sourceMappingURL=Curator.js.map
@@ -1,3 +1,4 @@
1
+ import { hasFeedbackTable } from "./columns.js";
1
2
  import { uuidv7 } from "./uuid.js";
2
3
  const POSITIVE_VERDICTS = ["useful"];
3
4
  const NEGATIVE_VERDICTS = [
@@ -5,25 +6,7 @@ const NEGATIVE_VERDICTS = [
5
6
  "outdated",
6
7
  "ignore",
7
8
  ];
8
- // v0.5.0 — pre-006 DBs lack `memory_feedback`; record() fails loudly with a
9
- // descriptive error instead of a bare "no such table" (the tool layer turns
10
- // it into a graceful message).
11
- // v0.6.0 (K6-001a) — positive-only caching: a successful probe is cached,
12
- // a failed probe is NOT. A Store migrated in place heals on the next call.
13
- const feedbackTableCache = new WeakMap();
14
- function hasFeedbackTable(store) {
15
- const cached = feedbackTableCache.get(store);
16
- if (cached === true)
17
- return true;
18
- try {
19
- store.prepare("SELECT COUNT(*) FROM memory_feedback").get();
20
- feedbackTableCache.set(store, true);
21
- return true;
22
- }
23
- catch {
24
- return false;
25
- }
26
- }
9
+ // v1.1.0 (K11-011) table probe delegates to columns registry
27
10
  export class Feedback {
28
11
  store;
29
12
  metrics;
@@ -135,4 +118,3 @@ export class Feedback {
135
118
  .run(memoryId, memoryId, memoryId);
136
119
  }
137
120
  }
138
- //# sourceMappingURL=Feedback.js.map
@@ -37,6 +37,7 @@ interface HookLivenessOptions {
37
37
  export declare class HookLiveness {
38
38
  private readonly store;
39
39
  private readonly options;
40
+ excessArityCount: number;
40
41
  private readonly counters;
41
42
  private readonly seenSessions;
42
43
  private readonly suppressedSessions;
@@ -68,6 +69,12 @@ export declare class HookLiveness {
68
69
  /** Persists every counter row. Called on the metrics.flush() cadence. */
69
70
  flush(): void;
70
71
  private makeWrapper;
72
+ /**
73
+ * v1.0.0 (K10-013 / plan §5.3) — records the `dispose` fire and
74
+ * flushes immediately, from inside the dispose delegate: this is the
75
+ * last write of the process, and nothing after it can persist.
76
+ */
77
+ recordDispose(): void;
71
78
  private recordSuccess;
72
79
  private recordError;
73
80
  private loadFromDb;
@@ -25,6 +25,8 @@ export function parseThreshold(text) {
25
25
  export class HookLiveness {
26
26
  store;
27
27
  options;
28
+ // v1.1.0 (K11-015) — debug counter for excess arity; never logged on hot path
29
+ excessArityCount = 0;
28
30
  counters;
29
31
  seenSessions;
30
32
  suppressedSessions = new Set();
@@ -212,6 +214,12 @@ export class HookLiveness {
212
214
  }
213
215
  makeWrapper(key, delegate) {
214
216
  const record = () => {
217
+ // v1.0.0 (K10-013) — dispose is recorded inside its own delegate
218
+ // (via recordDispose()) rather than here: the wrapper's post-return
219
+ // record would land after the store is closed and could never be
220
+ // persisted — the event being detected is the process ending.
221
+ if (key === "dispose")
222
+ return;
215
223
  if (HOOK_NAMES.includes(key)) {
216
224
  this.recordSuccess(key);
217
225
  }
@@ -222,34 +230,16 @@ export class HookLiveness {
222
230
  }
223
231
  throw e;
224
232
  };
225
- const n = delegate.length;
226
- if (n === 0) {
227
- return async () => {
228
- try {
229
- const result = await delegate();
230
- record();
231
- return result;
232
- }
233
- catch (e) {
234
- return recordError(e);
235
- }
236
- };
237
- }
238
- if (n === 1) {
239
- return async (a) => {
240
- try {
241
- const result = await delegate(a);
242
- record();
243
- return result;
244
- }
245
- catch (e) {
246
- return recordError(e);
247
- }
248
- };
249
- }
250
- return async (a, b) => {
233
+ // v1.1.0 (K11-015) — arity guard: maximum supported arity is 2 (plan §5.5).
234
+ // Excess args are sliced and counted via excessArityCount; never logged.
235
+ return async (...args) => {
236
+ let callArgs = args;
237
+ if (args.length > 2) {
238
+ this.excessArityCount++;
239
+ callArgs = args.slice(0, 2);
240
+ }
251
241
  try {
252
- const result = await delegate(a, b);
242
+ const result = await delegate(...callArgs);
253
243
  record();
254
244
  return result;
255
245
  }
@@ -258,6 +248,15 @@ export class HookLiveness {
258
248
  }
259
249
  };
260
250
  }
251
+ /**
252
+ * v1.0.0 (K10-013 / plan §5.3) — records the `dispose` fire and
253
+ * flushes immediately, from inside the dispose delegate: this is the
254
+ * last write of the process, and nothing after it can persist.
255
+ */
256
+ recordDispose() {
257
+ this.recordSuccess("dispose");
258
+ this.flush();
259
+ }
261
260
  recordSuccess(hook) {
262
261
  const c = this.counters.get(hook);
263
262
  if (!c)
@@ -321,4 +320,3 @@ export function reduceVerdict(reports) {
321
320
  reason: `${pending} hook(s) without checkpoint`,
322
321
  };
323
322
  }
324
- //# sourceMappingURL=HookLiveness.js.map