@jmtrin/opencode-kevin 0.6.0 → 0.8.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 +771 -580
- package/dist/migrations/001_initial.sql +91 -91
- package/dist/migrations/003_v02_signal.sql +57 -57
- package/dist/migrations/004_v03_knowledge.sql +138 -138
- package/dist/migrations/005_v04_signal.sql +57 -57
- package/dist/migrations/006_v05_glassbox.sql +118 -118
- package/dist/migrations/007_v06_pull.sql +144 -144
- package/dist/migrations/008_v07_truth.sql +124 -0
- package/dist/migrations/009_v08_team.sql +100 -0
- package/dist/plugin/ArtifactWriter.d.ts +28 -0
- package/dist/plugin/ArtifactWriter.js +35 -2
- package/dist/plugin/ArtifactWriter.js.map +1 -1
- package/dist/plugin/ConflictDetector.d.ts +35 -0
- package/dist/plugin/ConflictDetector.js +283 -0
- package/dist/plugin/ConflictDetector.js.map +1 -0
- package/dist/plugin/ContextInjector.d.ts +9 -0
- package/dist/plugin/ContextInjector.js +14 -3
- package/dist/plugin/ContextInjector.js.map +1 -1
- package/dist/plugin/ConventionMiner.d.ts +35 -0
- package/dist/plugin/ConventionMiner.js +243 -0
- package/dist/plugin/ConventionMiner.js.map +1 -0
- package/dist/plugin/Curator.d.ts +22 -2
- package/dist/plugin/Curator.js +101 -28
- package/dist/plugin/Curator.js.map +1 -1
- package/dist/plugin/InjectionLedger.d.ts +6 -0
- package/dist/plugin/InjectionLedger.js +6 -0
- package/dist/plugin/InjectionLedger.js.map +1 -1
- package/dist/plugin/Materializer.d.ts +4 -5
- package/dist/plugin/Materializer.js +9 -6
- package/dist/plugin/Materializer.js.map +1 -1
- package/dist/plugin/MemoryService.d.ts +49 -2
- package/dist/plugin/MemoryService.js +306 -8
- package/dist/plugin/MemoryService.js.map +1 -1
- package/dist/plugin/Migrate.js +61 -2
- package/dist/plugin/Migrate.js.map +1 -1
- package/dist/plugin/Reflector.js +13 -0
- package/dist/plugin/Reflector.js.map +1 -1
- package/dist/plugin/RepoIdentity.d.ts +113 -0
- package/dist/plugin/RepoIdentity.js +266 -0
- package/dist/plugin/RepoIdentity.js.map +1 -0
- package/dist/plugin/RepoTruth.d.ts +80 -0
- package/dist/plugin/RepoTruth.js +600 -0
- package/dist/plugin/RepoTruth.js.map +1 -0
- package/dist/plugin/Retrospective.js +17 -0
- package/dist/plugin/Retrospective.js.map +1 -1
- package/dist/plugin/SharedLayer.d.ts +159 -0
- package/dist/plugin/SharedLayer.js +463 -0
- package/dist/plugin/SharedLayer.js.map +1 -0
- package/dist/plugin/index.d.ts +36 -2
- package/dist/plugin/index.js +591 -10
- package/dist/plugin/index.js.map +1 -1
- package/dist/plugin/kevin_approve.js +7 -4
- package/dist/plugin/kevin_approve.js.map +1 -1
- package/dist/plugin/kevin_audit.d.ts +59 -1
- package/dist/plugin/kevin_audit.js +190 -3
- package/dist/plugin/kevin_audit.js.map +1 -1
- package/dist/plugin/kevin_conflicts.d.ts +9 -0
- package/dist/plugin/kevin_conflicts.js +51 -0
- package/dist/plugin/kevin_conflicts.js.map +1 -0
- package/dist/plugin/kevin_facts.d.ts +42 -0
- package/dist/plugin/kevin_facts.js +37 -0
- package/dist/plugin/kevin_facts.js.map +1 -0
- package/dist/plugin/metrics.d.ts +3 -3
- package/dist/plugin/metrics.js +21 -2
- package/dist/plugin/metrics.js.map +1 -1
- package/dist/plugin/okf-export.d.ts +2 -2
- package/dist/plugin/okf-export.js +15 -7
- package/dist/plugin/okf-export.js.map +1 -1
- package/dist/plugin/okf.d.ts +107 -0
- package/dist/plugin/okf.js +304 -0
- package/dist/plugin/okf.js.map +1 -0
- package/dist/plugin/replay-types.d.ts +29 -287
- package/dist/plugin/replay-types.js +145 -53
- package/dist/plugin/replay-types.js.map +1 -1
- package/migrations/001_initial.sql +91 -91
- package/migrations/003_v02_signal.sql +57 -57
- package/migrations/004_v03_knowledge.sql +138 -138
- package/migrations/005_v04_signal.sql +57 -57
- package/migrations/006_v05_glassbox.sql +118 -118
- package/migrations/007_v06_pull.sql +144 -144
- package/migrations/008_v07_truth.sql +124 -0
- package/migrations/009_v08_team.sql +100 -0
- package/package.json +4 -4
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin 0.7.0 — Migration 008: Project Truth (additive)
|
|
3
|
+
-- ============================================================
|
|
4
|
+
-- Backward-compatible, additive only. Two new tables, two new
|
|
5
|
+
-- nullable/defaulted columns on `memories`, one index, five
|
|
6
|
+
-- metric seeds, four setting seeds. NO table rebuild: nothing
|
|
7
|
+
-- here widens a CHECK constraint on an existing table.
|
|
8
|
+
--
|
|
9
|
+
-- Section 1: repo_facts — the ground-truth store.
|
|
10
|
+
-- Section 2: memory_conflicts — surfaced, never auto-resolved.
|
|
11
|
+
-- Section 3: memories truth columns.
|
|
12
|
+
-- Section 4: metric seeds.
|
|
13
|
+
-- Section 5: setting seeds.
|
|
14
|
+
-- Section 6: schema_version.
|
|
15
|
+
-- ============================================================
|
|
16
|
+
|
|
17
|
+
-- ------------------------------------------------------------
|
|
18
|
+
-- 1. repo_facts — facts extracted from package.json and
|
|
19
|
+
-- tsconfig.json. Both are JSON, parsed with JSON.parse; this
|
|
20
|
+
-- release adds NO parser and NO runtime dependency.
|
|
21
|
+
--
|
|
22
|
+
-- The UNIQUE index INCLUDES project_id and that is load-
|
|
23
|
+
-- bearing (D7-02). Kevin's DB is global
|
|
24
|
+
-- (~/.opencode-kevin/kevin.db, projectId =
|
|
25
|
+
-- fingerprint(process.cwd())). Without project_id, project A's
|
|
26
|
+
-- packageManager=npm would overwrite project B's
|
|
27
|
+
-- packageManager=pnpm and contradictions() would then flag A's
|
|
28
|
+
-- memories against B's repository.
|
|
29
|
+
--
|
|
30
|
+
-- source_mtime lets scan() skip re-parsing an unchanged file.
|
|
31
|
+
-- ------------------------------------------------------------
|
|
32
|
+
CREATE TABLE IF NOT EXISTS repo_facts (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
project_id TEXT NOT NULL,
|
|
35
|
+
file TEXT NOT NULL,
|
|
36
|
+
key_path TEXT NOT NULL,
|
|
37
|
+
value TEXT NOT NULL,
|
|
38
|
+
fingerprint TEXT NOT NULL,
|
|
39
|
+
source_mtime TEXT,
|
|
40
|
+
scanned_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
41
|
+
);
|
|
42
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_repo_facts ON repo_facts(project_id, file, key_path);
|
|
43
|
+
CREATE INDEX IF NOT EXISTS idx_repo_facts_fp ON repo_facts(fingerprint);
|
|
44
|
+
|
|
45
|
+
-- ------------------------------------------------------------
|
|
46
|
+
-- 2. memory_conflicts — detection only. status moves
|
|
47
|
+
-- open → acknowledged → resolved, and ONLY the kevin_conflicts
|
|
48
|
+
-- tool may move it to 'resolved' (D7-06). No session.idle path
|
|
49
|
+
-- writes 'resolved'.
|
|
50
|
+
--
|
|
51
|
+
-- memory_a / memory_b / fact_id carry NO REFERENCES clause on
|
|
52
|
+
-- purpose: Store sets PRAGMA foreign_keys = ON, and a hard FK
|
|
53
|
+
-- would block deleting a memory that participates in a
|
|
54
|
+
-- conflict.
|
|
55
|
+
-- ------------------------------------------------------------
|
|
56
|
+
CREATE TABLE IF NOT EXISTS memory_conflicts (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
project_id TEXT NOT NULL,
|
|
59
|
+
memory_a TEXT NOT NULL,
|
|
60
|
+
memory_b TEXT,
|
|
61
|
+
fact_id TEXT,
|
|
62
|
+
kind TEXT NOT NULL CHECK (kind IN ('repo_truth','decision_pair','temporal')),
|
|
63
|
+
detail TEXT,
|
|
64
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open','acknowledged','resolved')),
|
|
65
|
+
detected_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
66
|
+
resolved_at TEXT
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_status ON memory_conflicts(status);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_conflicts_memory ON memory_conflicts(memory_a);
|
|
71
|
+
|
|
72
|
+
-- ------------------------------------------------------------
|
|
73
|
+
-- 3. memories: the de-ranking column and its timestamp.
|
|
74
|
+
--
|
|
75
|
+
-- truth_penalty is clamped by application code to [0, 0.5] and
|
|
76
|
+
-- multiplies rankScore as (1 - truth_penalty), AFTER the
|
|
77
|
+
-- existing BM25 × origin_boost × recency_decay chain. At 0.0
|
|
78
|
+
-- the v0.6.0 ranking is reproduced exactly (D7-04).
|
|
79
|
+
--
|
|
80
|
+
-- There is deliberately no status transition here. Contra-
|
|
81
|
+
-- diction de-ranks; it never deletes (Principle 24, D7-03).
|
|
82
|
+
-- ------------------------------------------------------------
|
|
83
|
+
ALTER TABLE memories ADD COLUMN truth_penalty REAL NOT NULL DEFAULT 0.0;
|
|
84
|
+
ALTER TABLE memories ADD COLUMN contradicted_at TEXT;
|
|
85
|
+
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_memories_truth_penalty ON memories(truth_penalty);
|
|
87
|
+
|
|
88
|
+
-- ------------------------------------------------------------
|
|
89
|
+
-- 4. Metric seeds. Order matches the additions to METRIC_KEYS
|
|
90
|
+
-- in metrics.ts (28 → 33).
|
|
91
|
+
-- ------------------------------------------------------------
|
|
92
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
|
|
93
|
+
('repo_facts_scanned', 0),
|
|
94
|
+
('memories_contradicted', 0),
|
|
95
|
+
('conventions_mined', 0),
|
|
96
|
+
('conflicts_detected', 0),
|
|
97
|
+
('error_lessons_suppressed', 0);
|
|
98
|
+
|
|
99
|
+
-- ------------------------------------------------------------
|
|
100
|
+
-- 5. Setting seeds. Values are TEXT, always. Read them with an
|
|
101
|
+
-- explicit string comparison or an explicit Number() parse —
|
|
102
|
+
-- never `=== 1`, and never for truthiness.
|
|
103
|
+
--
|
|
104
|
+
-- error_lesson_mode is a TEXT ENUM ('all' | 'triage_only').
|
|
105
|
+
-- `if (mode)` is true for BOTH values and would put every
|
|
106
|
+
-- installation into triage mode on upgrade. Compare with
|
|
107
|
+
-- === "triage_only" (D7-12). Defaults to 'all', preserving
|
|
108
|
+
-- v0.6.0 behaviour exactly.
|
|
109
|
+
--
|
|
110
|
+
-- The three feature flags default to '0' (off): a release that
|
|
111
|
+
-- changes nothing until asked must not silently start scanning
|
|
112
|
+
-- the repository or mining conventions on upgrade. They are
|
|
113
|
+
-- opted into explicitly. (Task K7-001 / plan §5 & §12.)
|
|
114
|
+
-- ------------------------------------------------------------
|
|
115
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
|
|
116
|
+
('repo_truth_enabled', '0'),
|
|
117
|
+
('convention_mining_enabled', '0'),
|
|
118
|
+
('conflict_detection_enabled', '0'),
|
|
119
|
+
('error_lesson_mode', 'all');
|
|
120
|
+
|
|
121
|
+
-- ------------------------------------------------------------
|
|
122
|
+
-- 6. Version marker.
|
|
123
|
+
-- ------------------------------------------------------------
|
|
124
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('008');
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
-- ============================================================
|
|
2
|
+
-- Kevin 0.8.0 - Migration 009: Team (additive)
|
|
3
|
+
-- ============================================================
|
|
4
|
+
-- Backward-compatible, additive only. All new columns are
|
|
5
|
+
-- nullable or carry a NOT NULL DEFAULT so legacy rows keep
|
|
6
|
+
-- working without a destructive rebuild.
|
|
7
|
+
--
|
|
8
|
+
-- Scope note: this migration introduces `repo_id`, a SECOND
|
|
9
|
+
-- scoping dimension. `project_id` is retained on every table,
|
|
10
|
+
-- unchanged, as local-path provenance (D8-02). Nothing that
|
|
11
|
+
-- reads `project_id` today stops working.
|
|
12
|
+
-- ============================================================
|
|
13
|
+
|
|
14
|
+
-- 1. shared_entries: the local projection of the committed OKF file.
|
|
15
|
+
-- One row per (repo_id, entry_id). Rewritten by SharedLayer.import(),
|
|
16
|
+
-- never edited by hand, never the source of truth - the file is.
|
|
17
|
+
-- No REFERENCES to memories: an entry may arrive from a teammate
|
|
18
|
+
-- before any local memory corresponds to it (D8-12).
|
|
19
|
+
CREATE TABLE IF NOT EXISTS shared_entries (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
repo_id TEXT NOT NULL,
|
|
22
|
+
entry_id TEXT NOT NULL,
|
|
23
|
+
type TEXT NOT NULL CHECK (type IN ('decision', 'rule', 'pattern', 'solution')),
|
|
24
|
+
statement TEXT NOT NULL,
|
|
25
|
+
scope TEXT,
|
|
26
|
+
confidence REAL NOT NULL DEFAULT 0.0,
|
|
27
|
+
evidence INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
origin TEXT NOT NULL DEFAULT 'shared',
|
|
29
|
+
author_hash TEXT,
|
|
30
|
+
op TEXT NOT NULL CHECK (op IN ('assert', 'tombstone')) DEFAULT 'assert',
|
|
31
|
+
supersedes TEXT,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
-- 1b. Identity is (repo_id, entry_id). The UNIQUE index is what makes
|
|
37
|
+
-- import() an idempotent upsert instead of an append.
|
|
38
|
+
CREATE UNIQUE INDEX IF NOT EXISTS uq_shared_entries
|
|
39
|
+
ON shared_entries(repo_id, entry_id);
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_shared_entries_op
|
|
41
|
+
ON shared_entries(op);
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_shared_entries_type
|
|
43
|
+
ON shared_entries(type);
|
|
44
|
+
|
|
45
|
+
-- 2. okf_imports: append-only audit of every read of the shared file,
|
|
46
|
+
-- including no-op reads and refusals. `file_hash` drives the skip
|
|
47
|
+
-- path in SharedLayer.import() (D8-14).
|
|
48
|
+
CREATE TABLE IF NOT EXISTS okf_imports (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
repo_id TEXT NOT NULL,
|
|
51
|
+
path TEXT NOT NULL,
|
|
52
|
+
file_hash TEXT,
|
|
53
|
+
entries_parsed INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
entries_folded INTEGER NOT NULL DEFAULT 0,
|
|
55
|
+
entries_rejected INTEGER NOT NULL DEFAULT 0,
|
|
56
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
57
|
+
imported_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_okf_imports_repo
|
|
61
|
+
ON okf_imports(repo_id, imported_at);
|
|
62
|
+
|
|
63
|
+
-- 3. memories: the second scoping dimension and the layer marker.
|
|
64
|
+
-- repo_id is NULLABLE and back-filled by the post-apply hook, not by
|
|
65
|
+
-- a DEFAULT: the value depends on the row's existing project_id and
|
|
66
|
+
-- SQLite cannot express that in a column default.
|
|
67
|
+
-- layer carries NO CHECK constraint - widening it later would force
|
|
68
|
+
-- the migration-004 rebuild path (D8-07). Enforced in TypeScript.
|
|
69
|
+
ALTER TABLE memories ADD COLUMN repo_id TEXT;
|
|
70
|
+
ALTER TABLE memories ADD COLUMN layer TEXT NOT NULL DEFAULT 'local';
|
|
71
|
+
ALTER TABLE memories ADD COLUMN shared_entry_id TEXT;
|
|
72
|
+
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_memories_repo_id
|
|
74
|
+
ON memories(repo_id, status);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_memories_layer
|
|
76
|
+
ON memories(layer);
|
|
77
|
+
|
|
78
|
+
-- 4. kevin_metrics: seed the six v0.8 counters (33 -> 39).
|
|
79
|
+
INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES
|
|
80
|
+
('shared_entries_total', 0),
|
|
81
|
+
('shared_entries_imported', 0),
|
|
82
|
+
('shared_entries_exported', 0),
|
|
83
|
+
('okf_merge_folds', 0),
|
|
84
|
+
('rekey_events', 0),
|
|
85
|
+
('injections_from_shared', 0);
|
|
86
|
+
|
|
87
|
+
-- 5. kevin_settings: seed the five v0.8 flags (18 -> 23).
|
|
88
|
+
-- shared_layer_enabled defaults OFF: this release must be opted into,
|
|
89
|
+
-- because its first side effect is a new file in the user's repository.
|
|
90
|
+
-- shared_confidence_floor (0.7) is deliberately STRICTER than
|
|
91
|
+
-- injection_confidence_floor (0.6) - see 5.7.
|
|
92
|
+
INSERT OR IGNORE INTO kevin_settings (key, value) VALUES
|
|
93
|
+
('shared_layer_enabled', '0'),
|
|
94
|
+
('okf_path', '.kevin/knowledge.okf'),
|
|
95
|
+
('share_requires_approval', '1'),
|
|
96
|
+
('author_identity_mode', 'hashed'),
|
|
97
|
+
('shared_confidence_floor', '0.7');
|
|
98
|
+
|
|
99
|
+
-- 6. Seed version 009.
|
|
100
|
+
INSERT OR IGNORE INTO schema_version (version) VALUES ('009');
|
|
@@ -3,6 +3,27 @@ import type { Metrics } from "./metrics.js";
|
|
|
3
3
|
export declare const MARKER_BEGIN = "<!-- kevin:begin \u2014 curated by opencode-kevin, safe to edit -->";
|
|
4
4
|
export declare const MARKER_END = "<!-- kevin:end -->";
|
|
5
5
|
export type WriteOutcome = "written" | "noop" | "refused";
|
|
6
|
+
/**
|
|
7
|
+
* v0.8.0 (K8-019 / D8-08) — the two write modes. `markers` is the
|
|
8
|
+
* v0.6.0 behaviour, byte for byte: a splice between the two marker
|
|
9
|
+
* comments, used for `AGENTS.md`, a file humans edit. `whole` replaces
|
|
10
|
+
* the entire file and is used only for Kevin-owned paths such as
|
|
11
|
+
* `.kevin/knowledge.okf` — a file humans do not hand-edit.
|
|
12
|
+
*/
|
|
13
|
+
export type WriteMode = "markers" | "whole";
|
|
14
|
+
export interface WriteRequest {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly mode: WriteMode;
|
|
17
|
+
/** marker block body ("markers"), or whole-file content ("whole"). */
|
|
18
|
+
readonly content: string;
|
|
19
|
+
/**
|
|
20
|
+
* Caller-side refusal reason (K8-020 / D6-03): when present, the plan
|
|
21
|
+
* is refused — nothing is written, and the refusal is audited with
|
|
22
|
+
* both hashes. The refusal conditions belong to the caller; the
|
|
23
|
+
* writer only records them.
|
|
24
|
+
*/
|
|
25
|
+
readonly refusal?: string;
|
|
26
|
+
}
|
|
6
27
|
export interface WritePlan {
|
|
7
28
|
readonly path: string;
|
|
8
29
|
readonly before: string;
|
|
@@ -44,6 +65,13 @@ export declare class ArtifactWriter {
|
|
|
44
65
|
private readonly metrics;
|
|
45
66
|
constructor(store: Store, projectId: string, metrics?: Metrics | null);
|
|
46
67
|
plan(path: string, body: string): WritePlan;
|
|
68
|
+
plan(request: WriteRequest): WritePlan;
|
|
69
|
+
/**
|
|
70
|
+
* K8-019 (D8-08) — the single write funnel: every file Kevin writes
|
|
71
|
+
* goes through this method, which is the ONLY call site of `apply()`
|
|
72
|
+
* in the plugin (asserted by tests/unit/single_write_path.test.ts).
|
|
73
|
+
*/
|
|
74
|
+
write(request: WriteRequest, proposalId?: string): WriteOutcome;
|
|
47
75
|
apply(plan: WritePlan, proposalId?: string): WriteOutcome;
|
|
48
76
|
private renameTemp;
|
|
49
77
|
private audit;
|
|
@@ -84,7 +84,11 @@ export class ArtifactWriter {
|
|
|
84
84
|
this.projectId = projectId;
|
|
85
85
|
this.metrics = metrics ?? null;
|
|
86
86
|
}
|
|
87
|
-
plan(
|
|
87
|
+
plan(pathOrRequest, body) {
|
|
88
|
+
const request = typeof pathOrRequest === "string"
|
|
89
|
+
? { path: pathOrRequest, mode: "markers", content: body ?? "" }
|
|
90
|
+
: pathOrRequest;
|
|
91
|
+
const { path, mode, content } = request;
|
|
88
92
|
let before;
|
|
89
93
|
try {
|
|
90
94
|
// Read as Buffer, not as utf8 text: readFileSync's text decoding
|
|
@@ -100,10 +104,31 @@ export class ArtifactWriter {
|
|
|
100
104
|
throw err;
|
|
101
105
|
}
|
|
102
106
|
}
|
|
107
|
+
if (mode === "whole") {
|
|
108
|
+
// K8-019 (D8-08) — the whole-file path. The file is Kevin-owned,
|
|
109
|
+
// so there are no markers, no sanitization and no EOL
|
|
110
|
+
// normalization: the rendered bytes are written as-is, which is
|
|
111
|
+
// what makes a re-render of the same content a `noop`. A
|
|
112
|
+
// caller-side refusal leaves the file untouched and is audited
|
|
113
|
+
// like any other refusal: after = before, both hashes recorded.
|
|
114
|
+
const refusal = request.refusal;
|
|
115
|
+
const refused = refusal !== undefined;
|
|
116
|
+
const after = refused ? before : content;
|
|
117
|
+
return {
|
|
118
|
+
path,
|
|
119
|
+
before,
|
|
120
|
+
after,
|
|
121
|
+
diff: unifiedDiff(path, before, after),
|
|
122
|
+
outcome: refused ? "refused" : after === before ? "noop" : "written",
|
|
123
|
+
...(refusal !== undefined ? { reason: refusal } : {}),
|
|
124
|
+
hashBefore: sha256(before),
|
|
125
|
+
hashAfter: sha256(after),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
103
128
|
const eol = detectEol(before);
|
|
104
129
|
// Rule 9 — sanitation happens in plan(), before hashing, so the hashes
|
|
105
130
|
// describe what was actually written.
|
|
106
|
-
const bodyEol = normalizeEol(sanitizeArtifactBody(
|
|
131
|
+
const bodyEol = normalizeEol(sanitizeArtifactBody(content), eol);
|
|
107
132
|
const firstBegin = before.indexOf(MARKER_BEGIN);
|
|
108
133
|
const firstEnd = before.indexOf(MARKER_END);
|
|
109
134
|
let after;
|
|
@@ -176,6 +201,14 @@ export class ArtifactWriter {
|
|
|
176
201
|
hashAfter: sha256(after),
|
|
177
202
|
};
|
|
178
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* K8-019 (D8-08) — the single write funnel: every file Kevin writes
|
|
206
|
+
* goes through this method, which is the ONLY call site of `apply()`
|
|
207
|
+
* in the plugin (asserted by tests/unit/single_write_path.test.ts).
|
|
208
|
+
*/
|
|
209
|
+
write(request, proposalId) {
|
|
210
|
+
return this.apply(this.plan(request), proposalId);
|
|
211
|
+
}
|
|
179
212
|
// v0.6.0 (K6-007 / plan §5.1, rules 7–8) — atomic write + audit row.
|
|
180
213
|
apply(plan, proposalId) {
|
|
181
214
|
// Rule 8 — refusals and noops still leave an audit trail; a refusal
|
|
@@ -1 +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;
|
|
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;AAsC/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;IAQD,IAAI,CAAC,aAAoC,EAAE,IAAa;QACvD,MAAM,OAAO,GACZ,OAAO,aAAa,KAAK,QAAQ;YAChC,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,IAAI,EAAE,EAAE;YAC/D,CAAC,CAAC,aAAa,CAAC;QAClB,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QACxC,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,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACtB,iEAAiE;YACjE,sDAAsD;YACtD,gEAAgE;YAChE,yDAAyD;YACzD,+DAA+D;YAC/D,gEAAgE;YAChE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;YAChC,MAAM,OAAO,GAAG,OAAO,KAAK,SAAS,CAAC;YACtC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;YACzC,OAAO;gBACN,IAAI;gBACJ,MAAM;gBACN,KAAK;gBACL,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC;gBACtC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;gBACpE,GAAG,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrD,UAAU,EAAE,MAAM,CAAC,MAAM,CAAC;gBAC1B,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC;aACxB,CAAC;QACH,CAAC;QAED,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;QAC9B,uEAAuE;QACvE,sCAAsC;QACtC,MAAM,OAAO,GAAG,YAAY,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,GAAG,CAAC,CAAC;QACjE,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;;;;OAIG;IACH,KAAK,CAAC,OAAqB,EAAE,UAAmB;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,CAAC;IACnD,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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MemoryService } from "./MemoryService.js";
|
|
2
|
+
import type { RepoTruth } from "./RepoTruth.js";
|
|
3
|
+
import type { Store } from "./Store.js";
|
|
4
|
+
import type { Metrics } from "./metrics.js";
|
|
5
|
+
export type ConflictKind = "repo_truth" | "decision_pair" | "temporal";
|
|
6
|
+
export interface Conflict {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly kind: ConflictKind;
|
|
9
|
+
readonly memoryA: string;
|
|
10
|
+
readonly memoryB?: string;
|
|
11
|
+
readonly factId?: string;
|
|
12
|
+
readonly detail: string;
|
|
13
|
+
}
|
|
14
|
+
export interface RepoTruthConflictInput {
|
|
15
|
+
readonly memoryId: string;
|
|
16
|
+
readonly factId?: string | null;
|
|
17
|
+
readonly reasons: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
export declare class ConflictDetector {
|
|
20
|
+
private readonly store;
|
|
21
|
+
private readonly projectId;
|
|
22
|
+
private readonly repoTruth?;
|
|
23
|
+
private readonly memoryService?;
|
|
24
|
+
private readonly metrics;
|
|
25
|
+
constructor(store: Store, projectId: string, metrics?: Metrics | null, repoTruth?: RepoTruth | undefined, memoryService?: MemoryService | undefined);
|
|
26
|
+
detect(repoTruthInput?: readonly RepoTruthConflictInput[]): Conflict[];
|
|
27
|
+
acknowledge(id: string): void;
|
|
28
|
+
/** Human-only operation. It resolves the conflict row, never a memory. */
|
|
29
|
+
resolve(id: string, keepMemoryId: string): void;
|
|
30
|
+
private detectRepoTruth;
|
|
31
|
+
private repoTruthInputs;
|
|
32
|
+
private detectDecisionPairs;
|
|
33
|
+
private detectTemporal;
|
|
34
|
+
private insertIfNew;
|
|
35
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { normalize } from "./fingerprint.js";
|
|
2
|
+
import { uuidv7 } from "./uuid.js";
|
|
3
|
+
const POLARITY_RULES = [
|
|
4
|
+
{
|
|
5
|
+
positive: ["use"],
|
|
6
|
+
negative: [
|
|
7
|
+
["do", "not", "use"],
|
|
8
|
+
["don't", "use"],
|
|
9
|
+
["never", "use"],
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
{ positive: ["always"], negative: [["never"]] },
|
|
13
|
+
{ positive: ["required"], negative: [["forbidden"], ["not", "required"]] },
|
|
14
|
+
{ positive: ["enable"], negative: [["disable"]] },
|
|
15
|
+
{ positive: ["prefer"], negative: [["avoid"]] },
|
|
16
|
+
];
|
|
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
|
+
const POLARITY_WORDS = new Set([
|
|
45
|
+
"use",
|
|
46
|
+
"do",
|
|
47
|
+
"not",
|
|
48
|
+
// "don't" tokenizes to ["don", "t"] (see tokens()); the lexicon phrase
|
|
49
|
+
// ["don't","use"] is matched by tokenizing the phrase the same way, so
|
|
50
|
+
// both fragments must be excluded from subjects() too (K7-014).
|
|
51
|
+
"don",
|
|
52
|
+
"t",
|
|
53
|
+
"never",
|
|
54
|
+
"always",
|
|
55
|
+
"required",
|
|
56
|
+
"forbidden",
|
|
57
|
+
"enable",
|
|
58
|
+
"disable",
|
|
59
|
+
"prefer",
|
|
60
|
+
"avoid",
|
|
61
|
+
]);
|
|
62
|
+
export class ConflictDetector {
|
|
63
|
+
store;
|
|
64
|
+
projectId;
|
|
65
|
+
repoTruth;
|
|
66
|
+
memoryService;
|
|
67
|
+
metrics;
|
|
68
|
+
constructor(store, projectId, metrics, repoTruth, memoryService) {
|
|
69
|
+
this.store = store;
|
|
70
|
+
this.projectId = projectId;
|
|
71
|
+
this.repoTruth = repoTruth;
|
|
72
|
+
this.memoryService = memoryService;
|
|
73
|
+
this.metrics = metrics ?? null;
|
|
74
|
+
}
|
|
75
|
+
detect(repoTruthInput) {
|
|
76
|
+
const detected = [];
|
|
77
|
+
detected.push(...this.detectRepoTruth(repoTruthInput));
|
|
78
|
+
detected.push(...this.detectDecisionPairs());
|
|
79
|
+
detected.push(...this.detectTemporal());
|
|
80
|
+
return detected;
|
|
81
|
+
}
|
|
82
|
+
acknowledge(id) {
|
|
83
|
+
this.store
|
|
84
|
+
.prepare("UPDATE memory_conflicts SET status = 'acknowledged' WHERE id = ? AND project_id = ? AND status = 'open'")
|
|
85
|
+
.run(id, this.projectId);
|
|
86
|
+
}
|
|
87
|
+
/** Human-only operation. It resolves the conflict row, never a memory. */
|
|
88
|
+
resolve(id, keepMemoryId) {
|
|
89
|
+
const row = this.store
|
|
90
|
+
.prepare(`SELECT memory_a, memory_b FROM memory_conflicts
|
|
91
|
+
WHERE id = ? AND project_id = ? AND status <> 'resolved'`)
|
|
92
|
+
.get(id, this.projectId);
|
|
93
|
+
if (!row)
|
|
94
|
+
throw new Error(`conflict not found: ${id}`);
|
|
95
|
+
if (keepMemoryId !== row.memory_a && keepMemoryId !== row.memory_b) {
|
|
96
|
+
throw new Error("keep must identify one of the conflict memories");
|
|
97
|
+
}
|
|
98
|
+
this.store
|
|
99
|
+
.prepare("UPDATE memory_conflicts SET status = 'resolved', resolved_at = datetime('now') WHERE id = ? AND project_id = ?")
|
|
100
|
+
.run(id, this.projectId);
|
|
101
|
+
}
|
|
102
|
+
detectRepoTruth(input) {
|
|
103
|
+
const items = input ?? this.repoTruthInputs();
|
|
104
|
+
const out = [];
|
|
105
|
+
for (const item of items) {
|
|
106
|
+
for (const reason of item.reasons) {
|
|
107
|
+
this.memoryService?.applyTruthPenalty(item.memoryId, 0.5, reason);
|
|
108
|
+
const id = this.insertIfNew({
|
|
109
|
+
kind: "repo_truth",
|
|
110
|
+
memoryA: item.memoryId,
|
|
111
|
+
memoryB: null,
|
|
112
|
+
factId: item.factId ?? null,
|
|
113
|
+
detail: reason,
|
|
114
|
+
});
|
|
115
|
+
if (id)
|
|
116
|
+
out.push({
|
|
117
|
+
id,
|
|
118
|
+
kind: "repo_truth",
|
|
119
|
+
memoryA: item.memoryId,
|
|
120
|
+
factId: item.factId ?? undefined,
|
|
121
|
+
detail: reason,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// v0.7.0 (D7-03) — "recoverable by re-scanning": a memory whose
|
|
126
|
+
// contradiction has lapsed (the repo no longer contradicts it) is
|
|
127
|
+
// de-ranked no longer. The conflict row stays open — resolution is
|
|
128
|
+
// human-initiated only (D7-06) — but the score penalty lifts, so
|
|
129
|
+
// fixing the repo genuinely recovers the memory. Runs only on the
|
|
130
|
+
// production path (no explicit input): explicit-input callers control
|
|
131
|
+
// every write themselves.
|
|
132
|
+
if (!input && this.repoTruth && this.memoryService) {
|
|
133
|
+
const penalized = this.store
|
|
134
|
+
.prepare("SELECT id FROM memories WHERE project_id = ? AND truth_penalty > 0 AND status = 'active'")
|
|
135
|
+
.all(this.projectId);
|
|
136
|
+
for (const row of penalized) {
|
|
137
|
+
const memory = this.memoryService.getById(row.id);
|
|
138
|
+
if (!memory)
|
|
139
|
+
continue;
|
|
140
|
+
if ((this.repoTruth.contradictions(memory) ?? []).length === 0) {
|
|
141
|
+
this.memoryService.applyTruthPenalty(row.id, 0, "");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
repoTruthInputs() {
|
|
148
|
+
if (!this.repoTruth)
|
|
149
|
+
return [];
|
|
150
|
+
const rows = this.store
|
|
151
|
+
.prepare(`SELECT id, type, content, scope, relevance_score, source_tool,
|
|
152
|
+
source_session, metadata, created_at, updated_at, expires_at,
|
|
153
|
+
project_id, fingerprint, origin, evidence_count, last_verified_at,
|
|
154
|
+
status, fix_args, ignored, superseded_by, feedback_positive,
|
|
155
|
+
feedback_negative, curated, curated_at, inferable,
|
|
156
|
+
truth_penalty, contradicted_at
|
|
157
|
+
FROM memories WHERE project_id = ? AND status = 'active'`)
|
|
158
|
+
.all(this.projectId);
|
|
159
|
+
return rows.flatMap((row) => {
|
|
160
|
+
const memory = row;
|
|
161
|
+
const reasons = this.repoTruth?.contradictions(memory) ?? [];
|
|
162
|
+
return reasons.length > 0 ? [{ memoryId: row.id, reasons }] : [];
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
detectDecisionPairs() {
|
|
166
|
+
const rows = this.store
|
|
167
|
+
.prepare(`SELECT id, type, content, fingerprint FROM memories
|
|
168
|
+
WHERE project_id = ? AND status = 'active'
|
|
169
|
+
AND type IN ('decision', 'rule') ORDER BY id`)
|
|
170
|
+
.all(this.projectId);
|
|
171
|
+
const out = [];
|
|
172
|
+
for (let i = 0; i < rows.length; i++) {
|
|
173
|
+
for (let j = i + 1; j < rows.length; j++) {
|
|
174
|
+
const a = rows[i];
|
|
175
|
+
const b = rows[j];
|
|
176
|
+
if (!a || !b || a.fingerprint === b.fingerprint)
|
|
177
|
+
continue;
|
|
178
|
+
const pair = oppositePair(a.content, b.content);
|
|
179
|
+
if (!pair)
|
|
180
|
+
continue;
|
|
181
|
+
const id = this.insertIfNew({
|
|
182
|
+
kind: "decision_pair",
|
|
183
|
+
memoryA: a.id,
|
|
184
|
+
memoryB: b.id,
|
|
185
|
+
factId: null,
|
|
186
|
+
detail: pair,
|
|
187
|
+
});
|
|
188
|
+
if (id)
|
|
189
|
+
out.push({
|
|
190
|
+
id,
|
|
191
|
+
kind: "decision_pair",
|
|
192
|
+
memoryA: a.id,
|
|
193
|
+
memoryB: b.id,
|
|
194
|
+
detail: pair,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
detectTemporal() {
|
|
201
|
+
const rows = this.store
|
|
202
|
+
.prepare(`SELECT m.id,
|
|
203
|
+
MAX(CASE WHEN i.outcome = 'effective' THEN i.injected_at END) AS effective_at,
|
|
204
|
+
MAX(CASE WHEN i.outcome = 'ineffective' THEN i.injected_at END) AS ineffective_at
|
|
205
|
+
FROM memories m JOIN kevin_injections i ON i.memory_id = m.id
|
|
206
|
+
WHERE m.project_id = ? AND m.status = 'active'
|
|
207
|
+
GROUP BY m.id
|
|
208
|
+
HAVING effective_at IS NOT NULL AND ineffective_at IS NOT NULL
|
|
209
|
+
AND ineffective_at > effective_at`)
|
|
210
|
+
.all(this.projectId);
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const row of rows) {
|
|
213
|
+
const detail = `memory was effective at ${row.effective_at} and became ineffective at ${row.ineffective_at}`;
|
|
214
|
+
const id = this.insertIfNew({
|
|
215
|
+
kind: "temporal",
|
|
216
|
+
memoryA: row.id,
|
|
217
|
+
memoryB: null,
|
|
218
|
+
factId: null,
|
|
219
|
+
detail,
|
|
220
|
+
});
|
|
221
|
+
if (id)
|
|
222
|
+
out.push({ id, kind: "temporal", memoryA: row.id, detail });
|
|
223
|
+
}
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
insertIfNew(input) {
|
|
227
|
+
const existing = this.store
|
|
228
|
+
.prepare(`SELECT id FROM memory_conflicts
|
|
229
|
+
WHERE project_id = ? AND kind = ? AND memory_a = ?
|
|
230
|
+
AND (memory_b IS ? OR (memory_b IS NULL AND ? IS NULL))
|
|
231
|
+
AND (fact_id IS ? OR (fact_id IS NULL AND ? IS NULL))
|
|
232
|
+
AND status = 'open' LIMIT 1`)
|
|
233
|
+
.get(this.projectId, input.kind, input.memoryA, input.memoryB, input.memoryB, input.factId, input.factId);
|
|
234
|
+
if (existing)
|
|
235
|
+
return null;
|
|
236
|
+
const id = uuidv7();
|
|
237
|
+
this.store
|
|
238
|
+
.prepare(`INSERT INTO memory_conflicts
|
|
239
|
+
(id, project_id, memory_a, memory_b, fact_id, kind, detail)
|
|
240
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`)
|
|
241
|
+
.run(id, this.projectId, input.memoryA, input.memoryB, input.factId, input.kind, input.detail);
|
|
242
|
+
this.metrics?.incr("conflicts_detected", 1);
|
|
243
|
+
return id;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function tokens(text) {
|
|
247
|
+
return normalize(text).match(/[a-z0-9@._/-]+/g) ?? [];
|
|
248
|
+
}
|
|
249
|
+
function hasPhrase(words, phrase) {
|
|
250
|
+
if (phrase.length === 1)
|
|
251
|
+
return words.includes(phrase[0] ?? "");
|
|
252
|
+
for (let i = 0; i <= words.length - phrase.length; i++) {
|
|
253
|
+
if (phrase.every((word, offset) => words[i + offset] === word))
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
function polarity(text, rule) {
|
|
259
|
+
const words = tokens(text);
|
|
260
|
+
// Lexicon phrases are run through the same tokenizer as the text: "don't
|
|
261
|
+
// use" → ["don","t","use"], which is exactly how the text's own apostrophe
|
|
262
|
+
// is split, so a phrase never becomes unreachable (K7-014).
|
|
263
|
+
const match = (phrase) => hasPhrase(words, tokens(phrase.join(" ")));
|
|
264
|
+
const negative = rule.negative.some(match);
|
|
265
|
+
return { positive: !negative && match(rule.positive), negative };
|
|
266
|
+
}
|
|
267
|
+
function subjects(text) {
|
|
268
|
+
return new Set(tokens(text).filter((token) => !STOP_WORDS.has(token) && !POLARITY_WORDS.has(token)));
|
|
269
|
+
}
|
|
270
|
+
function oppositePair(a, b) {
|
|
271
|
+
const shared = [...subjects(a)].some((token) => subjects(b).has(token));
|
|
272
|
+
if (!shared)
|
|
273
|
+
return null;
|
|
274
|
+
for (const rule of POLARITY_RULES) {
|
|
275
|
+
const pa = polarity(a, rule);
|
|
276
|
+
const pb = polarity(b, rule);
|
|
277
|
+
if ((pa.positive && pb.negative) || (pa.negative && pb.positive)) {
|
|
278
|
+
return `statements carry opposite ${rule.positive.join(" ")} polarity for shared subject ${shared}`;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
//# sourceMappingURL=ConflictDetector.js.map
|