@jmtrin/opencode-kevin 0.5.0 → 0.6.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 +580 -436
- package/dist/migrations/007_v06_pull.sql +145 -0
- package/dist/plugin/Archiver.js +4 -3
- package/dist/plugin/Archiver.js.map +1 -1
- package/dist/plugin/ArtifactWriter.d.ts +50 -0
- package/dist/plugin/ArtifactWriter.js +240 -0
- package/dist/plugin/ArtifactWriter.js.map +1 -0
- package/dist/plugin/ContextInjector.d.ts +15 -0
- package/dist/plugin/ContextInjector.js +46 -11
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/Curator.d.ts +96 -0
- package/dist/plugin/Curator.js +252 -0
- package/dist/plugin/Curator.js.map +1 -0
- package/dist/plugin/Feedback.js +4 -3
- package/dist/plugin/Feedback.js.map +1 -1
- package/dist/plugin/Materializer.d.ts +59 -0
- package/dist/plugin/Materializer.js +237 -0
- package/dist/plugin/Materializer.js.map +1 -0
- package/dist/plugin/MemoryService.d.ts +19 -0
- package/dist/plugin/MemoryService.js +150 -19
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +20 -0
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/QualityGate.d.ts +15 -1
- package/dist/plugin/QualityGate.js +12 -0
- package/dist/plugin/QualityGate.js.map +1 -1
- package/dist/plugin/Retrospective.d.ts +1 -0
- package/dist/plugin/Retrospective.js +12 -1
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/capabilities.d.ts +15 -0
- package/dist/plugin/capabilities.js +42 -0
- package/dist/plugin/capabilities.js.map +1 -0
- package/dist/plugin/diff.d.ts +8 -0
- package/dist/plugin/diff.js +183 -0
- package/dist/plugin/diff.js.map +1 -0
- package/dist/plugin/index.d.ts +3 -1
- package/dist/plugin/index.js +221 -2
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/inferability.d.ts +32 -0
- package/dist/plugin/inferability.js +89 -0
- package/dist/plugin/inferability.js.map +1 -0
- package/dist/plugin/kevin_approve.d.ts +34 -0
- package/dist/plugin/kevin_approve.js +50 -0
- package/dist/plugin/kevin_approve.js.map +1 -0
- package/dist/plugin/kevin_audit.d.ts +47 -1
- package/dist/plugin/kevin_audit.js +93 -1
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/kevin_propose.d.ts +23 -0
- package/dist/plugin/kevin_propose.js +15 -0
- package/dist/plugin/kevin_propose.js.map +1 -0
- package/dist/plugin/kevin_publish.d.ts +38 -0
- package/dist/plugin/kevin_publish.js +19 -0
- package/dist/plugin/kevin_publish.js.map +1 -0
- package/dist/plugin/metrics.d.ts +12 -1
- package/dist/plugin/metrics.js +39 -0
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/replay-types.d.ts +6 -6
- package/dist/plugin/replay.js +10 -0
- package/dist/plugin/replay.js.map +1 -1
- package/migrations/007_v06_pull.sql +145 -0
- package/package.json +1 -1
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 007_v06_pull.sql — v0.6.0 "Pull"
|
|
3
|
+
--
|
|
4
|
+
-- Distribution: curated artifacts instead of a per-prompt token tax.
|
|
5
|
+
--
|
|
6
|
+
-- Section 1: curation_proposals — the persisted human decision record.
|
|
7
|
+
-- Section 2: artifact_writes — the disk audit trail, including refusals.
|
|
8
|
+
-- Section 3: memories curation + inferability columns.
|
|
9
|
+
-- Section 4: metric seeds.
|
|
10
|
+
-- Section 5: setting seeds.
|
|
11
|
+
-- Section 6: conditional push-budget demotion.
|
|
12
|
+
-- Section 7: schema_version.
|
|
13
|
+
--
|
|
14
|
+
-- Additive only. Every CHECK constraint introduced here is on a NEW table;
|
|
15
|
+
-- no existing constraint is widened, so unlike migration 006 there is no
|
|
16
|
+
-- table rebuild in this file.
|
|
17
|
+
-- ============================================================================
|
|
18
|
+
|
|
19
|
+
-- ---------------------------------------------------------------------------
|
|
20
|
+
-- 1. curation_proposals — one row per proposed artifact change.
|
|
21
|
+
--
|
|
22
|
+
-- This table is the "explicit human decision between generation and
|
|
23
|
+
-- application" of Principle 22. `kevin_propose` writes 'pending' rows and
|
|
24
|
+
-- nothing else; `kevin_approve` moves them to 'approved' → 'applied' or to
|
|
25
|
+
-- 'rejected'. Rows are never deleted: rejection history is the evidence
|
|
26
|
+
-- base for roadmap kill criterion K4 ("proposals rejected more often than
|
|
27
|
+
-- approved"), which is uncheckable if rejections are discarded.
|
|
28
|
+
--
|
|
29
|
+
-- memory_id has no REFERENCES clause. Store sets PRAGMA foreign_keys = ON,
|
|
30
|
+
-- and a hard FK would block deleting a memory that a historical proposal
|
|
31
|
+
-- once mentioned — the same reasoning as superseded_by in migration 006.
|
|
32
|
+
-- ---------------------------------------------------------------------------
|
|
33
|
+
CREATE TABLE IF NOT EXISTS curation_proposals (
|
|
34
|
+
id TEXT PRIMARY KEY,
|
|
35
|
+
project_id TEXT NOT NULL,
|
|
36
|
+
memory_id TEXT NOT NULL,
|
|
37
|
+
kind TEXT NOT NULL CHECK (kind IN ('agents_md','skill','reference')),
|
|
38
|
+
target_path TEXT NOT NULL,
|
|
39
|
+
proposed_text TEXT NOT NULL,
|
|
40
|
+
diff TEXT NOT NULL,
|
|
41
|
+
status TEXT NOT NULL DEFAULT 'pending'
|
|
42
|
+
CHECK (status IN ('pending','approved','rejected','applied','superseded')),
|
|
43
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
44
|
+
decided_at TEXT,
|
|
45
|
+
applied_at TEXT
|
|
46
|
+
);
|
|
47
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_status ON curation_proposals(status);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_memory ON curation_proposals(memory_id);
|
|
49
|
+
CREATE INDEX IF NOT EXISTS idx_proposals_project ON curation_proposals(project_id);
|
|
50
|
+
|
|
51
|
+
-- ---------------------------------------------------------------------------
|
|
52
|
+
-- 2. artifact_writes — the append-only audit trail for every disk operation.
|
|
53
|
+
--
|
|
54
|
+
-- A row is written for EVERY ArtifactWriter.apply() call, including
|
|
55
|
+
-- outcome='noop' and outcome='refused'. A refusal that leaves no trace is
|
|
56
|
+
-- indistinguishable from a write that never happened, and the whole point
|
|
57
|
+
-- of §5.1 rule 3 is that a refusal is a reportable event.
|
|
58
|
+
--
|
|
59
|
+
-- hash_before / hash_after are SHA-256 of the full file contents, not of
|
|
60
|
+
-- the marker block. That is what makes rule 4 (bytes outside the markers
|
|
61
|
+
-- are byte-identical) auditable after the fact rather than only at test
|
|
62
|
+
-- time.
|
|
63
|
+
-- ---------------------------------------------------------------------------
|
|
64
|
+
CREATE TABLE IF NOT EXISTS artifact_writes (
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
proposal_id TEXT,
|
|
67
|
+
project_id TEXT NOT NULL,
|
|
68
|
+
path TEXT NOT NULL,
|
|
69
|
+
bytes_before INTEGER,
|
|
70
|
+
bytes_after INTEGER,
|
|
71
|
+
hash_before TEXT,
|
|
72
|
+
hash_after TEXT,
|
|
73
|
+
outcome TEXT NOT NULL CHECK (outcome IN ('written','noop','refused')),
|
|
74
|
+
reason TEXT,
|
|
75
|
+
wrote_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
76
|
+
);
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_artifact_writes_path ON artifact_writes(path);
|
|
78
|
+
|
|
79
|
+
-- ---------------------------------------------------------------------------
|
|
80
|
+
-- 3. memories: curation state and inferability.
|
|
81
|
+
--
|
|
82
|
+
-- curated / curated_at record that a memory has already been published,
|
|
83
|
+
-- so the Curator does not re-propose it on every session idle.
|
|
84
|
+
--
|
|
85
|
+
-- inferable is deliberately NULLABLE with no default. Three states are
|
|
86
|
+
-- needed, not two: 1 = inferable (a self-describing diagnostic the model
|
|
87
|
+
-- resolves for free), 0 = non-inferable (project truth), NULL = unknown
|
|
88
|
+
-- (not yet classified, or classified as 'unknown'). The Curator predicate
|
|
89
|
+
-- is `inferable != 1`, so NULL rows stay eligible — an unclassified memory
|
|
90
|
+
-- must not be silently withheld from curation.
|
|
91
|
+
-- ---------------------------------------------------------------------------
|
|
92
|
+
ALTER TABLE memories ADD COLUMN curated INTEGER NOT NULL DEFAULT 0;
|
|
93
|
+
ALTER TABLE memories ADD COLUMN curated_at TEXT;
|
|
94
|
+
ALTER TABLE memories ADD COLUMN inferable INTEGER;
|
|
95
|
+
|
|
96
|
+
CREATE INDEX IF NOT EXISTS idx_memories_curated ON memories(curated);
|
|
97
|
+
CREATE INDEX IF NOT EXISTS idx_memories_inferable ON memories(inferable);
|
|
98
|
+
|
|
99
|
+
-- ---------------------------------------------------------------------------
|
|
100
|
+
-- 4. Metric seeds. Order matches the additions to METRIC_KEYS in metrics.ts.
|
|
101
|
+
-- injections_blocked_confidence is the sixth member of the v0.5 blocked
|
|
102
|
+
-- family and MUST be counted like the other five (Principle 16).
|
|
103
|
+
-- ---------------------------------------------------------------------------
|
|
104
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
|
|
105
|
+
('proposals_created', 0),
|
|
106
|
+
('proposals_approved', 0),
|
|
107
|
+
('proposals_rejected', 0),
|
|
108
|
+
('artifact_writes_total', 0),
|
|
109
|
+
('artifact_writes_noop', 0),
|
|
110
|
+
('injections_blocked_confidence', 0);
|
|
111
|
+
|
|
112
|
+
-- ---------------------------------------------------------------------------
|
|
113
|
+
-- 5. Setting seeds. Values are TEXT, always. Read them with an explicit
|
|
114
|
+
-- string comparison or an explicit Number() parse — never `=== 1`.
|
|
115
|
+
-- (That exact mistake kept cross_project_enabled unreachable for the
|
|
116
|
+
-- whole of v0.3.0.)
|
|
117
|
+
--
|
|
118
|
+
-- skill_emission_enabled and reference_emission_enabled default to '0':
|
|
119
|
+
-- the pull channels ship OFF and are opted into, because they depend on a
|
|
120
|
+
-- v2 domain Kevin does not pin.
|
|
121
|
+
-- ---------------------------------------------------------------------------
|
|
122
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
|
|
123
|
+
('curation_enabled', '1'),
|
|
124
|
+
('agents_md_path', 'AGENTS.md'),
|
|
125
|
+
('skill_emission_enabled', '0'),
|
|
126
|
+
('reference_emission_enabled', '0'),
|
|
127
|
+
('injection_confidence_floor', '0.6');
|
|
128
|
+
|
|
129
|
+
-- ---------------------------------------------------------------------------
|
|
130
|
+
-- 6. Push-budget demotion.
|
|
131
|
+
--
|
|
132
|
+
-- Lower the default push budget only where the user has not overridden it.
|
|
133
|
+
-- A user who deliberately set 1200 (or 1500, or 200) keeps their value;
|
|
134
|
+
-- only an installation still sitting on the v0.5 default of '900' is
|
|
135
|
+
-- moved to '400'. An unconditional UPDATE here would silently discard a
|
|
136
|
+
-- deliberate configuration choice, which is a worse defect than the token
|
|
137
|
+
-- cost it would save.
|
|
138
|
+
-- ---------------------------------------------------------------------------
|
|
139
|
+
UPDATE kevin_settings SET value = '400'
|
|
140
|
+
WHERE key = 'pre_prompt_budget_tokens' AND value = '900';
|
|
141
|
+
|
|
142
|
+
-- ---------------------------------------------------------------------------
|
|
143
|
+
-- 7. Version marker.
|
|
144
|
+
-- ---------------------------------------------------------------------------
|
|
145
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('007');
|
package/dist/plugin/Archiver.js
CHANGED
|
@@ -74,18 +74,19 @@ export class Archiver {
|
|
|
74
74
|
return hasArchivedColumnCached(this.store);
|
|
75
75
|
}
|
|
76
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.
|
|
77
79
|
const archivedColumnCache = new WeakMap();
|
|
78
80
|
function hasArchivedColumnCached(store) {
|
|
79
81
|
const cached = archivedColumnCache.get(store);
|
|
80
|
-
if (cached
|
|
81
|
-
return
|
|
82
|
+
if (cached === true)
|
|
83
|
+
return true;
|
|
82
84
|
try {
|
|
83
85
|
store.prepare("SELECT archived_at FROM memories LIMIT 1").get();
|
|
84
86
|
archivedColumnCache.set(store, true);
|
|
85
87
|
return true;
|
|
86
88
|
}
|
|
87
89
|
catch {
|
|
88
|
-
archivedColumnCache.set(store, false);
|
|
89
90
|
return false;
|
|
90
91
|
}
|
|
91
92
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Archiver.js","sourceRoot":"","sources":["../../plugin/Archiver.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,QAAQ;IAKF;IACA;IALD,OAAO,CAAiB;IACxB,GAAG,CAAa;IAEjC,YACkB,KAAY,EACZ,aAA4B,EAC7C,OAAwB,EACxB,MAAkB,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;QAHjB,UAAK,GAAL,KAAK,CAAO;QACZ,kBAAa,GAAb,aAAa,CAAe;QAI7C,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC;QAC/B,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACH,GAAG;QACF,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE;YAAE,OAAO,CAAC,CAAC;QACxC,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACrC,IAAI,IAAI,IAAI,CAAC;YAAE,OAAO,CAAC,CAAC;QAExB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,MAAM,MAAM,GAAG,eAAe,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,IAAI,GAAG,UAAU,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,KAAK;aACR,OAAO,CACP;;;;;2BAKuB,CACvB;aACA,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,eAAe,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1D,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,uBAAuB,CAAC,CAAC,GAAG,EAE9D,CAAC;QACF,MAAM,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;QACpB,IAAI,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,mBAAmB,EAAE,CAAC,CAAC,CAAC;QACtD,OAAO,CAAC,CAAC;IACV,CAAC;IAEO,gBAAgB;QACvB,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC;YACtE,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YACtB,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACpC,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,EAAE,CAAC;QACX,CAAC;IACF,CAAC;IAEO,iBAAiB;QACxB,OAAO,uBAAuB,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,CAAC;CACD;AAED,MAAM,mBAAmB,GAAG,IAAI,OAAO,EAAkB,CAAC;AAC1D,SAAS,uBAAuB,CAAC,KAAY;IAC5C,MAAM,MAAM,GAAG,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9C,IAAI,MAAM,KAAK,
|
|
1
|
+
{"version":3,"file":"Archiver.js","sourceRoot":"","sources":["../../plugin/Archiver.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Store } from "./Store.js";
|
|
2
|
+
import type { Metrics } from "./metrics.js";
|
|
3
|
+
export declare const MARKER_BEGIN = "<!-- kevin:begin \u2014 curated by opencode-kevin, safe to edit -->";
|
|
4
|
+
export declare const MARKER_END = "<!-- kevin:end -->";
|
|
5
|
+
export type WriteOutcome = "written" | "noop" | "refused";
|
|
6
|
+
export interface WritePlan {
|
|
7
|
+
readonly path: string;
|
|
8
|
+
readonly before: string;
|
|
9
|
+
readonly after: string;
|
|
10
|
+
readonly diff: string;
|
|
11
|
+
readonly outcome: WriteOutcome;
|
|
12
|
+
readonly reason?: string;
|
|
13
|
+
readonly hashBefore: string;
|
|
14
|
+
readonly hashAfter: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Rule 9 (K6-009 / plan §5.1, D6-02) — three layers applied to the body
|
|
18
|
+
* before splicing:
|
|
19
|
+
* (a) the escaping discipline of `plugin/memory-format.ts`;
|
|
20
|
+
* (b) strip any line containing `kevin:begin` or `kevin:end`, in any
|
|
21
|
+
* casing, anywhere in the line;
|
|
22
|
+
* (c) strip HTML comment terminators (`-->`).
|
|
23
|
+
* Without this, a memory containing a literal `<!-- kevin:end -->` line would
|
|
24
|
+
* close the marker comment early and let subsequent content escape the
|
|
25
|
+
* curated region on the next regeneration — a marker-injection variant of the
|
|
26
|
+
* v0.1.5 prompt-injection defect (plan §3.5).
|
|
27
|
+
*/
|
|
28
|
+
export declare function sanitizeArtifactBody(body: string): string;
|
|
29
|
+
/**
|
|
30
|
+
* v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
|
|
31
|
+
*
|
|
32
|
+
* `plan()` is pure: it reads the target file, locates the marker pair, splices
|
|
33
|
+
* the body between the markers and returns a {@link WritePlan}. It performs no
|
|
34
|
+
* writes — rule 1. `apply()` is implemented by K6-007; until then it is a stub
|
|
35
|
+
* that throws, so no caller can accidentally write before the audit trail
|
|
36
|
+
* exists.
|
|
37
|
+
*
|
|
38
|
+
* `projectId` is a constructor argument rather than a per-call argument so that
|
|
39
|
+
* every audit row is attributed without the call site having to remember.
|
|
40
|
+
*/
|
|
41
|
+
export declare class ArtifactWriter {
|
|
42
|
+
private readonly store;
|
|
43
|
+
private readonly projectId;
|
|
44
|
+
private readonly metrics;
|
|
45
|
+
constructor(store: Store, projectId: string, metrics?: Metrics | null);
|
|
46
|
+
plan(path: string, body: string): WritePlan;
|
|
47
|
+
apply(plan: WritePlan, proposalId?: string): WriteOutcome;
|
|
48
|
+
private renameTemp;
|
|
49
|
+
private audit;
|
|
50
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, fsyncSync, openSync, readFileSync, renameSync, unlinkSync, writeSync, } from "node:fs";
|
|
3
|
+
import { unifiedDiff } from "./diff.js";
|
|
4
|
+
import { uuidv7 } from "./uuid.js";
|
|
5
|
+
// v0.6.0 (K6-005 / plan §5.1, D6-02) — the frozen marker contract. The exact
|
|
6
|
+
// byte sequences are load-bearing: README, v1.0.0 plan C-01 and the round-trip
|
|
7
|
+
// test all depend on them.
|
|
8
|
+
export const MARKER_BEGIN = "<!-- kevin:begin — curated by opencode-kevin, safe to edit -->";
|
|
9
|
+
export const MARKER_END = "<!-- kevin:end -->";
|
|
10
|
+
function sha256(text) {
|
|
11
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Rule 5 (K6-008 / plan §5.1, D6-02) — the line-ending style comes from the
|
|
15
|
+
* FIRST line ending in the existing file: CRLF if it is CRLF, otherwise LF.
|
|
16
|
+
* A CRLF file whose last line lacks a terminator, and a mixed-ending file,
|
|
17
|
+
* are both resolved by the same first-ending rule, deterministically.
|
|
18
|
+
*/
|
|
19
|
+
function detectEol(text) {
|
|
20
|
+
const nl = text.indexOf("\n");
|
|
21
|
+
if (nl === -1)
|
|
22
|
+
return "\n";
|
|
23
|
+
return nl > 0 && text[nl - 1] === "\r" ? "\r\n" : "\n";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Rule 5 (K6-008) — normalize the generated body into the file's line-ending
|
|
27
|
+
* style so a CRLF file stays CRLF everywhere, including inside the block.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeEol(body, eol) {
|
|
30
|
+
return body.replace(/\r\n/g, "\n").replace(/\n/g, eol);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
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 (`&`, `<`, `>`, `&#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 `&lt;` on
|
|
38
|
+
* every regeneration (K6-009 / plan §5.1 rule 9).
|
|
39
|
+
*/
|
|
40
|
+
function escapeIdempotent(text) {
|
|
41
|
+
return text
|
|
42
|
+
.replace(/&(?!(amp|lt|gt|#\d+);)/g, "&")
|
|
43
|
+
.replace(/</g, "<")
|
|
44
|
+
.replace(/>/g, ">");
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
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).
|
|
57
|
+
*/
|
|
58
|
+
export function sanitizeArtifactBody(body) {
|
|
59
|
+
const escaped = escapeIdempotent(body);
|
|
60
|
+
const kept = escaped
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter((line) => !/kevin:begin|kevin:end/i.test(line))
|
|
63
|
+
.join("\n");
|
|
64
|
+
return kept.replace(/-->/g, "");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* v0.6.0 (K6-005 / plan §5.1) — the single write path to disk (D6-01).
|
|
68
|
+
*
|
|
69
|
+
* `plan()` is pure: it reads the target file, locates the marker pair, splices
|
|
70
|
+
* the body between the markers and returns a {@link WritePlan}. It performs no
|
|
71
|
+
* writes — rule 1. `apply()` is implemented by K6-007; until then it is a stub
|
|
72
|
+
* that throws, so no caller can accidentally write before the audit trail
|
|
73
|
+
* exists.
|
|
74
|
+
*
|
|
75
|
+
* `projectId` is a constructor argument rather than a per-call argument so that
|
|
76
|
+
* every audit row is attributed without the call site having to remember.
|
|
77
|
+
*/
|
|
78
|
+
export class ArtifactWriter {
|
|
79
|
+
store;
|
|
80
|
+
projectId;
|
|
81
|
+
metrics;
|
|
82
|
+
constructor(store, projectId, metrics) {
|
|
83
|
+
this.store = store;
|
|
84
|
+
this.projectId = projectId;
|
|
85
|
+
this.metrics = metrics ?? null;
|
|
86
|
+
}
|
|
87
|
+
plan(path, body) {
|
|
88
|
+
let before;
|
|
89
|
+
try {
|
|
90
|
+
// Read as Buffer, not as utf8 text: readFileSync's text decoding
|
|
91
|
+
// strips a leading BOM, which would silently drop it on the next
|
|
92
|
+
// write. Buffer.toString keeps \uFEFF as a character of `before`.
|
|
93
|
+
before = readFileSync(path).toString("utf8");
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
97
|
+
before = "";
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const eol = detectEol(before);
|
|
104
|
+
// Rule 9 — sanitation happens in plan(), before hashing, so the hashes
|
|
105
|
+
// describe what was actually written.
|
|
106
|
+
const bodyEol = normalizeEol(sanitizeArtifactBody(body), eol);
|
|
107
|
+
const firstBegin = before.indexOf(MARKER_BEGIN);
|
|
108
|
+
const firstEnd = before.indexOf(MARKER_END);
|
|
109
|
+
let after;
|
|
110
|
+
let outcome = "written";
|
|
111
|
+
let reason;
|
|
112
|
+
if (firstBegin === -1 && firstEnd === -1) {
|
|
113
|
+
// Rule 2 — create: the block is appended at the end of the content,
|
|
114
|
+
// preceded by a blank line. For an empty file (missing file treated
|
|
115
|
+
// as "") the result is exactly: blank line, MARKER_BEGIN, body,
|
|
116
|
+
// MARKER_END, trailing newline.
|
|
117
|
+
const separator = before === "" ? eol : before.endsWith(eol) ? eol : eol + eol;
|
|
118
|
+
after =
|
|
119
|
+
before +
|
|
120
|
+
separator +
|
|
121
|
+
MARKER_BEGIN +
|
|
122
|
+
eol +
|
|
123
|
+
bodyEol +
|
|
124
|
+
eol +
|
|
125
|
+
MARKER_END +
|
|
126
|
+
eol;
|
|
127
|
+
}
|
|
128
|
+
else if (firstBegin === -1 || firstEnd === -1) {
|
|
129
|
+
// Rule 3 — exactly one marker present.
|
|
130
|
+
after = before;
|
|
131
|
+
outcome = "refused";
|
|
132
|
+
reason =
|
|
133
|
+
firstBegin === -1
|
|
134
|
+
? "kevin:end marker present without kevin:begin"
|
|
135
|
+
: "kevin:begin marker present without kevin:end";
|
|
136
|
+
}
|
|
137
|
+
else if (firstEnd < firstBegin) {
|
|
138
|
+
// Rule 3 — MARKER_END precedes MARKER_BEGIN.
|
|
139
|
+
after = before;
|
|
140
|
+
outcome = "refused";
|
|
141
|
+
reason = "kevin:end appears before kevin:begin";
|
|
142
|
+
}
|
|
143
|
+
else if (before.indexOf(MARKER_BEGIN, firstBegin + MARKER_BEGIN.length) !== -1 ||
|
|
144
|
+
before.indexOf(MARKER_END, firstEnd + MARKER_END.length) !== -1) {
|
|
145
|
+
// Rule 3 — more than one pair (any additional marker occurrence).
|
|
146
|
+
after = before;
|
|
147
|
+
outcome = "refused";
|
|
148
|
+
reason = "more than one marker pair present";
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Rule 4 — bytes outside the marker pair are byte-identical. The
|
|
152
|
+
// block between the markers (begin marker, old block, end marker)
|
|
153
|
+
// is replaced by the regenerated block.
|
|
154
|
+
const blockEnd = firstEnd + MARKER_END.length;
|
|
155
|
+
after =
|
|
156
|
+
before.slice(0, firstBegin) +
|
|
157
|
+
MARKER_BEGIN +
|
|
158
|
+
eol +
|
|
159
|
+
bodyEol +
|
|
160
|
+
eol +
|
|
161
|
+
MARKER_END +
|
|
162
|
+
before.slice(blockEnd);
|
|
163
|
+
// Rule 6 — unchanged content is a noop, never a write.
|
|
164
|
+
outcome = after === before ? "noop" : "written";
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
path,
|
|
168
|
+
before,
|
|
169
|
+
after,
|
|
170
|
+
// v0.6.0 (K6-006 / plan §5.2, D6-05) — approval prompts show bytes,
|
|
171
|
+
// never prose. Identical inputs yield "".
|
|
172
|
+
diff: unifiedDiff(path, before, after),
|
|
173
|
+
outcome,
|
|
174
|
+
...(reason !== undefined ? { reason } : {}),
|
|
175
|
+
hashBefore: sha256(before),
|
|
176
|
+
hashAfter: sha256(after),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
|
|
180
|
+
apply(plan, proposalId) {
|
|
181
|
+
// Rule 8 — refusals and noops still leave an audit trail; a refusal
|
|
182
|
+
// that leaves no trace is indistinguishable from a write that never
|
|
183
|
+
// happened. Rule 6 — a noop creates no temp file and writes nothing.
|
|
184
|
+
if (plan.outcome === "noop" || plan.outcome === "refused") {
|
|
185
|
+
if (plan.outcome === "noop") {
|
|
186
|
+
this.metrics?.incr("artifact_writes_noop", 1);
|
|
187
|
+
}
|
|
188
|
+
this.audit(plan, proposalId);
|
|
189
|
+
return plan.outcome;
|
|
190
|
+
}
|
|
191
|
+
// Rule 7 — write to `<path>.kevin.tmp` in the same directory (same
|
|
192
|
+
// filesystem, so rename is atomic), fsync, close, then rename over the
|
|
193
|
+
// target. Never write the target path directly, never truncate-then-write.
|
|
194
|
+
const tmpPath = `${plan.path}.kevin.tmp`;
|
|
195
|
+
let fd;
|
|
196
|
+
try {
|
|
197
|
+
fd = openSync(tmpPath, "w");
|
|
198
|
+
writeSync(fd, plan.after, null, "utf8");
|
|
199
|
+
fsyncSync(fd);
|
|
200
|
+
closeSync(fd);
|
|
201
|
+
fd = undefined;
|
|
202
|
+
this.renameTemp(tmpPath, plan.path);
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
// Never leave .kevin.tmp litter next to the user's file.
|
|
206
|
+
if (fd !== undefined) {
|
|
207
|
+
try {
|
|
208
|
+
closeSync(fd);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// already closed or unusable; unlink below is the cleanup
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
unlinkSync(tmpPath);
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
// nothing to clean up
|
|
219
|
+
}
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
this.metrics?.incr("artifact_writes_total", 1);
|
|
223
|
+
this.audit(plan, proposalId);
|
|
224
|
+
return "written";
|
|
225
|
+
}
|
|
226
|
+
// Fault-injection seam for the atomicity test: the rename is the point
|
|
227
|
+
// where a failure must leave the target untouched and the temp file gone.
|
|
228
|
+
renameTemp(tmpPath, target) {
|
|
229
|
+
renameSync(tmpPath, target);
|
|
230
|
+
}
|
|
231
|
+
audit(plan, proposalId) {
|
|
232
|
+
this.store
|
|
233
|
+
.prepare(`INSERT INTO artifact_writes
|
|
234
|
+
(id, proposal_id, project_id, path, bytes_before, bytes_after,
|
|
235
|
+
hash_before, hash_after, outcome, reason)
|
|
236
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
237
|
+
.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);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=ArtifactWriter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ArtifactWriter.js","sourceRoot":"","sources":["../../plugin/ArtifactWriter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EACN,SAAS,EACT,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,UAAU,EACV,SAAS,GACT,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAEnC,6EAA6E;AAC7E,+EAA+E;AAC/E,2BAA2B;AAC3B,MAAM,CAAC,MAAM,YAAY,GACxB,gEAAgE,CAAC;AAClE,MAAM,CAAC,MAAM,UAAU,GAAG,oBAAoB,CAAC;AAe/C,SAAS,MAAM,CAAC,IAAY;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAChE,CAAC;AAED;;;;;GAKG;AACH,SAAS,SAAS,CAAC,IAAY;IAC9B,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,EAAE,KAAK,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3B,OAAO,EAAE,GAAG,CAAC,IAAI,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;AACxD,CAAC;AAED;;;GAGG;AACH,SAAS,YAAY,CAAC,IAAY,EAAE,GAAkB;IACrD,OAAO,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACrC,OAAO,IAAI;SACT,OAAO,CAAC,yBAAyB,EAAE,OAAO,CAAC;SAC3C,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACzB,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAChD,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,OAAO;SAClB,KAAK,CAAC,IAAI,CAAC;SACX,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;SACtD,IAAI,CAAC,IAAI,CAAC,CAAC;IACb,OAAO,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,cAAc;IACT,KAAK,CAAQ;IACb,SAAS,CAAS;IAClB,OAAO,CAAiB;IAEzC,YAAY,KAAY,EAAE,SAAiB,EAAE,OAAwB;QACpE,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,IAAI,IAAI,CAAC;IAChC,CAAC;IAED,IAAI,CAAC,IAAY,EAAE,IAAY;QAC9B,IAAI,MAAc,CAAC;QACnB,IAAI,CAAC;YACJ,iEAAiE;YACjE,iEAAiE;YACjE,kEAAkE;YAClE,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAC9C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,IAAI,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACpE,MAAM,GAAG,EAAE,CAAC;YACb,CAAC;iBAAM,CAAC;gBACP,MAAM,GAAG,CAAC;YACX,CAAC;QACF,CAAC;QAED,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAC9B,uEAAuE;QACvE,sCAAsC;QACtC,MAAM,OAAO,GAAG,YAAY,CAAC,oBAAoB,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;QAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;QAChD,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAC5C,IAAI,KAAa,CAAC;QAClB,IAAI,OAAO,GAAiB,SAAS,CAAC;QACtC,IAAI,MAA0B,CAAC;QAE/B,IAAI,UAAU,KAAK,CAAC,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;YAC1C,oEAAoE;YACpE,oEAAoE;YACpE,gEAAgE;YAChE,gCAAgC;YAChC,MAAM,SAAS,GACd,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC;YAC9D,KAAK;gBACJ,MAAM;oBACN,SAAS;oBACT,YAAY;oBACZ,GAAG;oBACH,OAAO;oBACP,GAAG;oBACH,UAAU;oBACV,GAAG,CAAC;QACN,CAAC;aAAM,IAAI,UAAU,KAAK,CAAC,CAAC,IAAI,QAAQ,KAAK,CAAC,CAAC,EAAE,CAAC;YACjD,uCAAuC;YACvC,KAAK,GAAG,MAAM,CAAC;YACf,OAAO,GAAG,SAAS,CAAC;YACpB,MAAM;gBACL,UAAU,KAAK,CAAC,CAAC;oBAChB,CAAC,CAAC,8CAA8C;oBAChD,CAAC,CAAC,8CAA8C,CAAC;QACpD,CAAC;aAAM,IAAI,QAAQ,GAAG,UAAU,EAAE,CAAC;YAClC,6CAA6C;YAC7C,KAAK,GAAG,MAAM,CAAC;YACf,OAAO,GAAG,SAAS,CAAC;YACpB,MAAM,GAAG,sCAAsC,CAAC;QACjD,CAAC;aAAM,IACN,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,UAAU,GAAG,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACrE,MAAM,CAAC,OAAO,CAAC,UAAU,EAAE,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAC9D,CAAC;YACF,kEAAkE;YAClE,KAAK,GAAG,MAAM,CAAC;YACf,OAAO,GAAG,SAAS,CAAC;YACpB,MAAM,GAAG,mCAAmC,CAAC;QAC9C,CAAC;aAAM,CAAC;YACP,iEAAiE;YACjE,kEAAkE;YAClE,wCAAwC;YACxC,MAAM,QAAQ,GAAG,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC;YAC9C,KAAK;gBACJ,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC;oBAC3B,YAAY;oBACZ,GAAG;oBACH,OAAO;oBACP,GAAG;oBACH,UAAU;oBACV,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;YACxB,uDAAuD;YACvD,OAAO,GAAG,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;QACjD,CAAC;QAED,OAAO;YACN,IAAI;YACJ,MAAM;YACN,KAAK;YACL,oEAAoE;YACpE,0CAA0C;YAC1C,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;YACtC,OAAO;YACP,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;YAC1B,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC;SACxB,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,KAAK,CAAC,IAAe,EAAE,UAAmB;QACzC,oEAAoE;QACpE,oEAAoE;QACpE,qEAAqE;QACrE,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;YAC3D,IAAI,IAAI,CAAC,OAAO,KAAK,MAAM,EAAE,CAAC;gBAC7B,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,CAAC,CAAC,CAAC;YAC/C,CAAC;YACD,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;YAC7B,OAAO,IAAI,CAAC,OAAO,CAAC;QACrB,CAAC;QAED,mEAAmE;QACnE,uEAAuE;QACvE,2EAA2E;QAC3E,MAAM,OAAO,GAAG,GAAG,IAAI,CAAC,IAAI,YAAY,CAAC;QACzC,IAAI,EAAsB,CAAC;QAC3B,IAAI,CAAC;YACJ,EAAE,GAAG,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAC5B,SAAS,CAAC,EAAE,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;YACxC,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,EAAE,GAAG,SAAS,CAAC;YACf,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QACrC,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACd,yDAAyD;YACzD,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;gBACtB,IAAI,CAAC;oBACJ,SAAS,CAAC,EAAE,CAAC,CAAC;gBACf,CAAC;gBAAC,MAAM,CAAC;oBACR,0DAA0D;gBAC3D,CAAC;YACF,CAAC;YACD,IAAI,CAAC;gBACJ,UAAU,CAAC,OAAO,CAAC,CAAC;YACrB,CAAC;YAAC,MAAM,CAAC;gBACR,sBAAsB;YACvB,CAAC;YACD,MAAM,GAAG,CAAC;QACX,CAAC;QACD,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAC;QAC/C,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC;QAC7B,OAAO,SAAS,CAAC;IAClB,CAAC;IAED,uEAAuE;IACvE,0EAA0E;IAClE,UAAU,CAAC,OAAe,EAAE,MAAc;QACjD,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC7B,CAAC;IAEO,KAAK,CAAC,IAAe,EAAE,UAA8B;QAC5D,IAAI,CAAC,KAAK;aACR,OAAO,CACP;;;2CAGuC,CACvC;aACA,GAAG,CACH,MAAM,EAAE,EACR,UAAU,IAAI,IAAI,EAClB,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,IAAI,EACT,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,EACrC,IAAI,CAAC,UAAU,EACf,IAAI,CAAC,SAAS,EACd,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,MAAM,IAAI,IAAI,CACnB,CAAC;IACJ,CAAC;CACD"}
|
|
@@ -4,6 +4,14 @@ import { type GateReason } from "./QualityGate.js";
|
|
|
4
4
|
import { type Metrics } from "./metrics.js";
|
|
5
5
|
export declare const QUALITY_GATE_SETTING = "quality_gate_enabled";
|
|
6
6
|
export declare const SNIPPET_INJECTION_SETTING = "lesson_snippet_injection";
|
|
7
|
+
/**
|
|
8
|
+
* v0.6.0 (K6-023 / plan §5.8) — the effective pre-prompt budget shared by
|
|
9
|
+
* `ContextInjector.prePromptCap()` and `kevin_audit`'s channels block
|
|
10
|
+
* (budget_tokens). Single source of truth for the K6-021 clamp:
|
|
11
|
+
* default 400 when the raw value is missing/non-numeric, clamped to
|
|
12
|
+
* [0, 4000]; 0 means "push off".
|
|
13
|
+
*/
|
|
14
|
+
export declare function effectivePrePromptCap(raw: string | undefined): number;
|
|
7
15
|
export interface ChatMessage {
|
|
8
16
|
role: string;
|
|
9
17
|
content: string;
|
|
@@ -142,6 +150,13 @@ export declare class ContextInjector {
|
|
|
142
150
|
* read at call time from `pre_prompt_budget_tokens` (seeded "900" by
|
|
143
151
|
* migration 006). Clamped to [100, 4000]; a non-numeric value falls
|
|
144
152
|
* back to 900. `kevin_trace` reports the value used via `plan().cap`.
|
|
153
|
+
*
|
|
154
|
+
* v0.6.0 (K6-021 / plan §5.8) — default becomes "400" and the lower
|
|
155
|
+
* clamp bound drops to **0**: the roadmap's kill criterion K1 prescribes
|
|
156
|
+
* cutting the push budget to zero when coverage is poor, and v0.5's
|
|
157
|
+
* [100, 4000] clamp made that response unimplementable. A non-numeric
|
|
158
|
+
* value falls back to 400. `onSystemTransform` treats 0 as off and
|
|
159
|
+
* returns before any retrieval (see K6-021 acceptance).
|
|
145
160
|
*/
|
|
146
161
|
private prePromptCap;
|
|
147
162
|
/**
|
|
@@ -4,13 +4,26 @@ import { estimateTokens } from "./metrics.js";
|
|
|
4
4
|
import { STOP_WORDS } from "./query-tokenizer.js";
|
|
5
5
|
export const QUALITY_GATE_SETTING = "quality_gate_enabled";
|
|
6
6
|
export const SNIPPET_INJECTION_SETTING = "lesson_snippet_injection";
|
|
7
|
+
/**
|
|
8
|
+
* v0.6.0 (K6-023 / plan §5.8) — the effective pre-prompt budget shared by
|
|
9
|
+
* `ContextInjector.prePromptCap()` and `kevin_audit`'s channels block
|
|
10
|
+
* (budget_tokens). Single source of truth for the K6-021 clamp:
|
|
11
|
+
* default 400 when the raw value is missing/non-numeric, clamped to
|
|
12
|
+
* [0, 4000]; 0 means "push off".
|
|
13
|
+
*/
|
|
14
|
+
export function effectivePrePromptCap(raw) {
|
|
15
|
+
const n = Number(raw);
|
|
16
|
+
if (!Number.isFinite(n))
|
|
17
|
+
return 400;
|
|
18
|
+
return Math.min(4000, Math.max(0, Math.round(n)));
|
|
19
|
+
}
|
|
7
20
|
// v0.4.0 default pre-prompt budget. v0.5.0 (K5-017 / plan §8.11, D5-11):
|
|
8
21
|
// kept as the compile-time fallback, but the EFFECTIVE cap is read at call
|
|
9
|
-
// time from the `pre_prompt_budget_tokens` setting (
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
22
|
+
// time from the `pre_prompt_budget_tokens` setting. v0.6.0 (K6-021 / plan
|
|
23
|
+
// §5.8): the setting's default becomes "400" and the lower clamp bound
|
|
24
|
+
// drops to 0 (off) — the confound fix in K5-005 showed a large share of
|
|
25
|
+
// injections are `inconclusive`, and the roadmap's kill criterion K1
|
|
26
|
+
// requires "off" to be a reachable, supported configuration.
|
|
14
27
|
const SYSTEM_TRANSFORM_TOKENS = 900;
|
|
15
28
|
const COMPACTING_TOKENS = 2000;
|
|
16
29
|
// v0.5.0 (K5-007 / plan §8.10, D5-04) — every rejection reason maps 1:1 to
|
|
@@ -19,6 +32,8 @@ const COMPACTING_TOKENS = 2000;
|
|
|
19
32
|
// `ok` reason admits and must never increment anything.
|
|
20
33
|
const BLOCKED_METRIC = {
|
|
21
34
|
ok: null,
|
|
35
|
+
// v0.6.0 (K6-022 / plan §5.8) — sixth reason, sixth counter.
|
|
36
|
+
low_confidence: "injections_blocked_confidence",
|
|
22
37
|
seen_this_session: "injections_blocked_seen",
|
|
23
38
|
ignored: "injections_blocked_ignored",
|
|
24
39
|
not_active: "injections_blocked_stale",
|
|
@@ -283,6 +298,13 @@ Consider adding this convention to AGENTS.md:
|
|
|
283
298
|
*/
|
|
284
299
|
evaluate(memories, sessionId) {
|
|
285
300
|
const qualityGateEnabled = this.memoryService.getSetting(QUALITY_GATE_SETTING, "1") === "1";
|
|
301
|
+
// v0.6.0 (K6-022 / plan §5.8) — the floor is read ONCE per
|
|
302
|
+
// plan/inject call (this method is the shared gate evaluation),
|
|
303
|
+
// never per memory. A non-numeric setting degrades to undefined,
|
|
304
|
+
// which disables the branch for every memory this call.
|
|
305
|
+
const rawFloor = this.memoryService.getSetting("injection_confidence_floor", "0.6");
|
|
306
|
+
const floor = Number(rawFloor);
|
|
307
|
+
const confidenceFloor = Number.isFinite(floor) ? floor : undefined;
|
|
286
308
|
const seen = new Set(this.seenBySession.get(sessionId) ?? []);
|
|
287
309
|
const recurrences = this.ledger?.postInjectionRecurrencesFor(sessionId) ??
|
|
288
310
|
new Map();
|
|
@@ -297,6 +319,9 @@ Consider adding this convention to AGENTS.md:
|
|
|
297
319
|
// v0.5.0 (K5-009) — the `ignored` flag is now a first-class
|
|
298
320
|
// Memory field via mapRow (D5-07).
|
|
299
321
|
ignored: m.ignored === true,
|
|
322
|
+
// v0.6.0 (K6-022) — the memory's computed confidence
|
|
323
|
+
// (may be null for rows without evidence).
|
|
324
|
+
confidence: m.confidence ?? undefined,
|
|
300
325
|
}, {
|
|
301
326
|
seenThisSession: seen,
|
|
302
327
|
// v0.4.0 (K4-025) — plan §5.1 rule 4: a causal pattern
|
|
@@ -310,6 +335,7 @@ Consider adding this convention to AGENTS.md:
|
|
|
310
335
|
: m.fingerprint
|
|
311
336
|
? (recurrences.get(m.fingerprint) ?? 0)
|
|
312
337
|
: 0,
|
|
338
|
+
confidenceFloor,
|
|
313
339
|
}, qualityGateEnabled);
|
|
314
340
|
verdicts.push({ ...verdict, memory: m });
|
|
315
341
|
}
|
|
@@ -320,13 +346,16 @@ Consider adding this convention to AGENTS.md:
|
|
|
320
346
|
* read at call time from `pre_prompt_budget_tokens` (seeded "900" by
|
|
321
347
|
* migration 006). Clamped to [100, 4000]; a non-numeric value falls
|
|
322
348
|
* back to 900. `kevin_trace` reports the value used via `plan().cap`.
|
|
349
|
+
*
|
|
350
|
+
* v0.6.0 (K6-021 / plan §5.8) — default becomes "400" and the lower
|
|
351
|
+
* clamp bound drops to **0**: the roadmap's kill criterion K1 prescribes
|
|
352
|
+
* cutting the push budget to zero when coverage is poor, and v0.5's
|
|
353
|
+
* [100, 4000] clamp made that response unimplementable. A non-numeric
|
|
354
|
+
* value falls back to 400. `onSystemTransform` treats 0 as off and
|
|
355
|
+
* returns before any retrieval (see K6-021 acceptance).
|
|
323
356
|
*/
|
|
324
357
|
prePromptCap() {
|
|
325
|
-
|
|
326
|
-
const n = Number(raw);
|
|
327
|
-
if (!Number.isFinite(n))
|
|
328
|
-
return 900;
|
|
329
|
-
return Math.min(4000, Math.max(100, Math.round(n)));
|
|
358
|
+
return effectivePrePromptCap(this.memoryService.getSetting("pre_prompt_budget_tokens", "400"));
|
|
330
359
|
}
|
|
331
360
|
/**
|
|
332
361
|
* v0.4.0 (K4-017) — QualityGate admission: filters the ranked slice to
|
|
@@ -469,10 +498,16 @@ Consider adding this convention to AGENTS.md:
|
|
|
469
498
|
return tokens.join(" ");
|
|
470
499
|
}
|
|
471
500
|
onSystemTransform(input, output) {
|
|
501
|
+
// v0.6.0 (K6-021 / plan §5.8) — a cap of 0 means off: return before
|
|
502
|
+
// any retrieval, gate evaluation or metric write. "Off" must not
|
|
503
|
+
// run a hidden query and throw away the result.
|
|
504
|
+
const cap = this.prePromptCap();
|
|
505
|
+
if (cap === 0)
|
|
506
|
+
return;
|
|
472
507
|
const query = this.deriveQuery(input.messages);
|
|
473
508
|
if (!query)
|
|
474
509
|
return;
|
|
475
|
-
const block = this.inject(query, "context",
|
|
510
|
+
const block = this.inject(query, "context", cap, "tokens_injected_pre_prompt", input.sessionID ?? "");
|
|
476
511
|
if (block)
|
|
477
512
|
output.system.push(block);
|
|
478
513
|
}
|