@jmtrin/kevin-core 2.0.0 → 2.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.
package/dist/RepoTruth.js CHANGED
@@ -393,7 +393,12 @@ export class RepoTruth {
393
393
  for (const file of ["package.json", "tsconfig.json"]) {
394
394
  try {
395
395
  const st = statSync(join(this.projectRoot, file));
396
- out[file] = String(st.mtimeMs);
396
+ // Include size alongside mtime so a same-tick rewrite with
397
+ // different bytes is still detected on Windows where mtime
398
+ // granularity is ~15ms. The stored string is compared as an
399
+ // opaque token, so legacy rows (bare mtime) will mismatch once
400
+ // and trigger a re-parse.
401
+ out[file] = `${String(st.mtimeMs)}:${String(st.size)}`;
397
402
  }
398
403
  catch {
399
404
  out[file] = null;
@@ -1,6 +1,6 @@
1
- import { type KevinEnv } from "./env.js";
2
1
  import type { MemoryService } from "./MemoryService.js";
3
2
  import type { Store } from "./Store.js";
3
+ import { type KevinEnv } from "./env.js";
4
4
  import type { Metrics } from "./metrics.js";
5
5
  export interface RetrospectiveOptions {
6
6
  dir?: string;
@@ -95,6 +95,8 @@ export const METRIC_KEY_LABELS = {
95
95
  source_syncs_total: "Sincronizaciones de fuentes (total)",
96
96
  source_dedup_skips_total: "Skips por dedup de fuentes (total)",
97
97
  okf_v3_files_written: "Archivos OKF v3 escritos",
98
+ // v2.1.0 (K21-005) — Relay deletion metric
99
+ source_deletions_total: "Borrados de fuente sincronizados (total)",
98
100
  };
99
101
  function originLabel(origin) {
100
102
  if (origin === "reflector")
package/dist/contract.js CHANGED
@@ -3,8 +3,8 @@ import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
4
  import { dirname, join } from "node:path";
5
5
  import { MARKER_BEGIN, MARKER_END } from "./ArtifactWriter.js";
6
- import { resolveEnv } from "./env.js";
7
6
  import { METRIC_KEY_LABELS } from "./Retrospective.js";
7
+ import { resolveEnv } from "./env.js";
8
8
  import { fnv1a64 } from "./fingerprint.js";
9
9
  import { KEVIN_CONFIG_KEYS } from "./index.js";
10
10
  import { MAX_ENTRIES, MAX_LINE_BYTES } from "./okf.js";
@@ -122,7 +122,12 @@ export const CONTRACT_SKILLS_LAYOUT = {
122
122
  canonical_dir: ".agents/skills",
123
123
  skill_dir: "kevin-knowledge",
124
124
  files: ["SKILL.md", "references/*.md"],
125
- frontmatter_fields: ["name", "description", "metadata.generator", "metadata.repo_id"],
125
+ frontmatter_fields: [
126
+ "name",
127
+ "description",
128
+ "metadata.generator",
129
+ "metadata.repo_id",
130
+ ],
126
131
  mirror_policy: "copy (not symlink) to .claude/skills and .cursor/skills when enabled",
127
132
  };
128
133
  // v2.0.0 (K16-001) — C-13 MIF profile, since 1.5.0
@@ -220,6 +225,7 @@ export const CONTRACT_METRIC_ADDITIONS = [
220
225
  { name: "source_syncs_total", since: "2.0.0" },
221
226
  { name: "source_dedup_skips_total", since: "2.0.0" },
222
227
  { name: "okf_v3_files_written", since: "2.0.0" },
228
+ { name: "source_deletions_total", since: "2.1.0" },
223
229
  ];
224
230
  /**
225
231
  * v1.4.0 (K14-006 / plan §4.3) — config keys added after the freeze,
@@ -240,6 +246,7 @@ export const CONTRACT_CONFIG_ADDITIONS = [
240
246
  { name: "source_codex_memories", since: "2.0.0" },
241
247
  { name: "source_opencode_native", since: "2.0.0" },
242
248
  { name: "sources_enabled", since: "2.0.0" },
249
+ { name: "source_deletion_sync", since: "2.1.0" },
243
250
  ];
244
251
  /**
245
252
  * v2.0.0 (K16-004 / plan §5.1) — removed settings (retirements)
@@ -378,7 +385,7 @@ export function describeContract(_input) {
378
385
  stability: "forward-only",
379
386
  since: "0.1.0",
380
387
  value: {
381
- schema_version: "014",
388
+ schema_version: "015",
382
389
  migrations_forward_only: true,
383
390
  },
384
391
  },
@@ -417,7 +424,9 @@ export function describeContract(_input) {
417
424
  title: "Core public exports",
418
425
  stability: "frozen",
419
426
  since: "1.3.0",
420
- value: { exports: [...CONTRACT_CORE_EXPORTS].sort((a, b) => a.name.localeCompare(b.name)) },
427
+ value: {
428
+ exports: [...CONTRACT_CORE_EXPORTS].sort((a, b) => a.name.localeCompare(b.name)),
429
+ },
421
430
  },
422
431
  {
423
432
  id: "C-11",
@@ -1,6 +1,6 @@
1
- import { type KevinEnv } from "./env.js";
2
1
  import type { MemoryService } from "./MemoryService.js";
3
2
  import type { Store } from "./Store.js";
3
+ import { type KevinEnv } from "./env.js";
4
4
  import type { Metrics } from "./metrics.js";
5
5
  export interface HostImportReport {
6
6
  files_scanned: number;
@@ -1,10 +1,10 @@
1
1
  // K15-011/012/013 — Host importers (plan §4.5)
2
2
  // Defensive markdown parsers, no YAML lib, same naive parser as validator.
3
- import { existsSync, lstatSync, readdirSync, readFileSync } from "node:fs";
3
+ import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
4
4
  import { join } from "node:path";
5
+ import { QualityGate } from "./QualityGate.js";
5
6
  import { resolveEnv } from "./env.js";
6
7
  import { fingerprint as computeFingerprint } from "./fingerprint.js";
7
- import { QualityGate } from "./QualityGate.js";
8
8
  const MAX_FILE_BYTES = 1 * 1024 * 1024;
9
9
  const MAX_CANDIDATES = 5000;
10
10
  const CLAUDE_TYPE_MAP = {
@@ -134,7 +134,9 @@ export function parseClaudeMemory(dataRoot, env) {
134
134
  }
135
135
  // if frontmatter exists but type missing, map to context
136
136
  }
137
- const mapped = rawType ? (CLAUDE_TYPE_MAP[rawType.toLowerCase()] ?? "context") : "context";
137
+ const mapped = rawType
138
+ ? (CLAUDE_TYPE_MAP[rawType.toLowerCase()] ?? "context")
139
+ : "context";
138
140
  // bullets
139
141
  const bullets = extractBullets(content);
140
142
  if (bullets.length === 0) {
@@ -201,18 +203,31 @@ export function parseCodexMemories(dataRoot, env) {
201
203
  }
202
204
  export function importHostMemories(opts) {
203
205
  const dataRoot = opts.dataRoot ?? resolveEnv(opts.env).dataRoot;
204
- // gate
205
- const gate = opts.memoryService.getSetting("import_host_memory", "0");
206
- if (gate !== "1") {
207
- return {
208
- files_scanned: 0,
209
- candidates: 0,
210
- saved: 0,
211
- duplicates: 0,
212
- skipped_weak: 0,
213
- error: "disabled",
214
- hint: "Enable with kevin_config set import_host_memory 1",
215
- };
206
+ // gate — v2.0.0 retired import_host_memory (K16-005); check sources framework.
207
+ // Legacy fallback kept for pre-014 DBs that still carry the key before migration 014 deletes it.
208
+ const legacy = opts.memoryService.getSetting("import_host_memory", "0");
209
+ if (legacy === "1") {
210
+ // translated by 014; allow but hint new path
211
+ }
212
+ else {
213
+ const master = opts.memoryService.getSetting("sources_enabled", "0") === "1";
214
+ const perKey = opts.source === "claude-memory"
215
+ ? "source_claude_memory"
216
+ : "source_codex_memories";
217
+ const per = opts.memoryService.getSetting(perKey, "0") === "1";
218
+ if (!master || !per) {
219
+ return {
220
+ files_scanned: 0,
221
+ candidates: 0,
222
+ saved: 0,
223
+ duplicates: 0,
224
+ skipped_weak: 0,
225
+ error: "disabled",
226
+ hint: opts.source === "claude-memory"
227
+ ? "Enable with kevin_config set sources_enabled 1 and kevin_config set source_claude_memory 1"
228
+ : "Enable with kevin_config set sources_enabled 1 and kevin_config set source_codex_memories 1",
229
+ };
230
+ }
216
231
  }
217
232
  let parsed;
218
233
  if (opts.source === "claude-memory") {
@@ -233,7 +248,9 @@ export function importHostMemories(opts) {
233
248
  // dedup intra-run via fingerprint set
234
249
  const seenFingerprints = new Set();
235
250
  // existing fingerprints from DB
236
- const existingRows = opts.store.prepare("SELECT fingerprint FROM memories WHERE fingerprint IS NOT NULL").all();
251
+ const existingRows = opts.store
252
+ .prepare("SELECT fingerprint FROM memories WHERE fingerprint IS NOT NULL")
253
+ .all();
237
254
  for (const r of existingRows)
238
255
  seenFingerprints.add(r.fingerprint);
239
256
  for (const c of candidates) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export * from "./Store.js";
2
2
  export * from "./Migrate.js";
3
- export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn } from "./MemoryService.js";
4
- export type { Memory, SlimMemory, SlimMemoryWithEvidence, SaveInput, QueryInput, GetRelevantInput, MemoryType, MemoryScope, MemoryOrigin, MemoryUpdateResult } from "./MemoryService.js";
3
+ export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn, } from "./MemoryService.js";
4
+ export type { Memory, SlimMemory, SlimMemoryWithEvidence, SaveInput, QueryInput, GetRelevantInput, MemoryType, MemoryScope, MemoryOrigin, MemoryUpdateResult, } from "./MemoryService.js";
5
5
  export * from "./ToolCallObserver.js";
6
6
  export * from "./Reflector.js";
7
7
  export * from "./ContextInjector.js";
@@ -38,23 +38,23 @@ export * from "./memory-format.js";
38
38
  export * from "./redact.js";
39
39
  export * from "./uuid.js";
40
40
  export * from "./sqlite-adapter.js";
41
- export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable } from "./columns.js";
41
+ export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable, } from "./columns.js";
42
42
  export * from "./time-ms.js";
43
43
  export * from "./idle-pipeline.js";
44
44
  export * from "./replay.js";
45
45
  export * from "./replay-types.js";
46
46
  export * from "./ChatBridge.js";
47
47
  export * from "./DashboardHtml.js";
48
- export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox } from "./TuiActions.js";
49
- export type { MailboxReadResult, PendingProposal, ProcessDeps, ActionStatus } from "./TuiActions.js";
48
+ export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox, } from "./TuiActions.js";
49
+ export type { MailboxReadResult, PendingProposal, ProcessDeps, ActionStatus, } from "./TuiActions.js";
50
50
  export * from "./TuiSnapshots.js";
51
- export type { ProposalView, ConflictView, HealthView, TuiSnapshotSet, TuiAction, ActionResult } from "./tui-types.js";
51
+ export type { ProposalView, ConflictView, HealthView, TuiSnapshotSet, TuiAction, ActionResult, } from "./tui-types.js";
52
52
  export * from "./capabilities.js";
53
53
  export * from "./host.js";
54
54
  export { V2_SPECIFIER } from "./native.js";
55
- export type { NativeDeps, NativeRegistration, SettingsReader } from "./native.js";
55
+ export type { NativeDeps, NativeRegistration, SettingsReader, } from "./native.js";
56
56
  export * from "./kevin_approve.js";
57
- export { buildAudit, type AuditReport, type ChannelReport, type CurationReport, type TruthReport, type EmissionState } from "./kevin_audit.js";
57
+ export { buildAudit, type AuditReport, type ChannelReport, type CurationReport, type TruthReport, type EmissionState, } from "./kevin_audit.js";
58
58
  export * from "./kevin_bench.js";
59
59
  export * from "./kevin_conflicts.js";
60
60
  export * from "./kevin_contract.js";
@@ -73,7 +73,8 @@ export * from "./import-host.js";
73
73
  export * from "./okf-shards.js";
74
74
  export * from "./sources/MemorySource.js";
75
75
  export * from "./sources/IdleSync.js";
76
+ export * from "./sources/deletion.js";
76
77
  export * from "./env.js";
77
- export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version"];
78
+ export declare const KEVIN_CONFIG_KEYS: readonly ["quality_gate_enabled", "lesson_snippet_injection", "patternminer_enabled", "cross_project_enabled", "llm_reflection_enabled", "tool_calls_dedup_enabled", "deterministic_retrieval", "pre_prompt_budget_tokens", "archive_after_days", "curation_enabled", "agents_md_path", "skill_emission_enabled", "reference_emission_enabled", "injection_confidence_floor", "repo_truth_enabled", "convention_mining_enabled", "conflict_detection_enabled", "error_lesson_mode", "shared_layer_enabled", "okf_path", "share_requires_approval", "author_identity_mode", "shared_confidence_floor", "hook_liveness_enabled", "native_registration_enabled", "host_probe_history_enabled", "dead_hook_report_threshold", "perf_enabled", "perf_ring_capacity", "perf_flush_on_idle", "contract_report_enabled", "tui_snapshots_enabled", "mcp_write_enabled", "mcp_approve_enabled", "mcp_repo_override", "skills_canonical_dir", "skills_mirror_claude", "skills_mirror_cursor", "sources_enabled", "source_claude_memory", "source_codex_memories", "source_opencode_native", "okf_write_version", "source_deletion_sync"];
78
79
  export declare const ERROR_LESSON_MODE_VALUES: readonly ["all", "triage_only"];
79
- export declare const KEVIN_VERSION = "2.0.0";
80
+ export declare const KEVIN_VERSION = "2.1.0";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  export * from "./Store.js";
4
4
  export * from "./Migrate.js";
5
5
  // MemoryService has duplicate hasRepoIdColumn with columns.ts — export selectively (keep MemoryService's hasRepoIdColumn, columns provides other helpers)
6
- export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn } from "./MemoryService.js";
6
+ export { MemoryService, DATE_NOW, mapRow, readOriginCallId, countSupersedeCandidates, hasRepoIdColumn, } from "./MemoryService.js";
7
7
  export * from "./ToolCallObserver.js";
8
8
  export * from "./Reflector.js";
9
9
  export * from "./ContextInjector.js";
@@ -41,7 +41,7 @@ export * from "./redact.js";
41
41
  export * from "./uuid.js";
42
42
  export * from "./sqlite-adapter.js";
43
43
  // columns has duplicate hasRepoIdColumn with MemoryService — export without that function (MemoryService's version is canonical via index)
44
- export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable } from "./columns.js";
44
+ export { hasColumn, hasIgnoredColumn, hasCuratedColumn, hasTruthColumns, hasLayerColumn, hasRecurrenceColumn, hasArchivedColumn, hasFeedbackColumns, hasFeedbackTable, } from "./columns.js";
45
45
  export * from "./time-ms.js";
46
46
  export * from "./idle-pipeline.js";
47
47
  export * from "./replay.js";
@@ -49,7 +49,7 @@ export * from "./replay-types.js";
49
49
  export * from "./ChatBridge.js";
50
50
  export * from "./DashboardHtml.js";
51
51
  // TuiActions duplicates proposalToken with DashboardHtml — export selectively (exclude proposalToken)
52
- export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox } from "./TuiActions.js";
52
+ export { deleteMailbox, processActions, readMailbox, writeResults, verifyFresh, consumeMailbox, } from "./TuiActions.js";
53
53
  export * from "./TuiSnapshots.js";
54
54
  // shims for isolation
55
55
  export * from "./capabilities.js";
@@ -57,7 +57,7 @@ export * from "./host.js";
57
57
  export { V2_SPECIFIER } from "./native.js";
58
58
  // kevin_* handlers — avoid duplicate EmissionState / NativeDeps
59
59
  export * from "./kevin_approve.js";
60
- export { buildAudit } from "./kevin_audit.js";
60
+ export { buildAudit, } from "./kevin_audit.js";
61
61
  export * from "./kevin_bench.js";
62
62
  export * from "./kevin_conflicts.js";
63
63
  export * from "./kevin_contract.js";
@@ -75,6 +75,7 @@ export * from "./import-host.js";
75
75
  export * from "./okf-shards.js";
76
76
  export * from "./sources/MemorySource.js";
77
77
  export * from "./sources/IdleSync.js";
78
+ export * from "./sources/deletion.js";
78
79
  // KEVIN_CONFIG_KEYS and related constants — moved from adapter index so core owns the source of truth (C-04).
79
80
  // Duplicated here to allow adapter to import from core; adapter will re-export them.
80
81
  export * from "./env.js";
@@ -122,6 +123,7 @@ export const KEVIN_CONFIG_KEYS = [
122
123
  "source_codex_memories",
123
124
  "source_opencode_native",
124
125
  "okf_write_version",
126
+ "source_deletion_sync",
125
127
  ];
126
128
  export const ERROR_LESSON_MODE_VALUES = ["all", "triage_only"];
127
- export const KEVIN_VERSION = "2.0.0";
129
+ export const KEVIN_VERSION = "2.1.0";
package/dist/metrics.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { Store } from "./Store.js";
6
6
  * underlying table is empty (e.g., before 003 is applied, on a fresh
7
7
  * :memory: test DB, or after a manual wipe).
8
8
  */
9
- export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "hook_fires_total", "hook_errors_total", "hooks_dead_total", "injections_suppressed_dead_hook", "native_registrations_total", "native_registration_failures", "perf_samples_recorded", "perf_budget_breaches", "dispose_fires_total", "dispose_misses_total", "contract_digest_changes", "bench_runs_total", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total", "skills_emitted_total", "mif_exports_total", "mif_imports_total", "source_syncs_total", "source_dedup_skips_total", "okf_v3_files_written"];
9
+ export declare const METRIC_KEYS: readonly ["tokens_injected_pre_prompt", "tokens_injected_compacting", "reflections_throttled", "duplicate_suppressions", "tool_calls_deduped", "patterns_mined", "patterns_causal", "causal_links", "memories_superseded", "injections_total", "injections_effective", "injections_ineffective", "patterns_promoted_new", "injections_inconclusive", "injections_blocked_seen", "injections_blocked_weak", "injections_blocked_recurrence", "injections_blocked_stale", "injections_blocked_ignored", "feedback_positive_total", "feedback_negative_total", "memories_archived", "proposals_created", "proposals_approved", "proposals_rejected", "artifact_writes_total", "artifact_writes_noop", "injections_blocked_confidence", "repo_facts_scanned", "memories_contradicted", "conventions_mined", "conflicts_detected", "error_lessons_suppressed", "shared_entries_total", "shared_entries_imported", "shared_entries_exported", "okf_merge_folds", "rekey_events", "injections_from_shared", "hook_fires_total", "hook_errors_total", "hooks_dead_total", "injections_suppressed_dead_hook", "native_registrations_total", "native_registration_failures", "perf_samples_recorded", "perf_budget_breaches", "dispose_fires_total", "dispose_misses_total", "contract_digest_changes", "bench_runs_total", "bench_regression_failures", "forget_requests_total", "forget_tombstones_published", "tui_snapshots_flushed", "tui_actions_invoked", "mcp_requests_total", "mcp_reads_served", "mcp_writes_accepted", "mcp_writes_refused", "mcp_errors_total", "skills_emitted_total", "mif_exports_total", "mif_imports_total", "source_syncs_total", "source_dedup_skips_total", "okf_v3_files_written", "source_deletions_total"];
10
10
  export type MetricKey = (typeof METRIC_KEYS)[number];
11
11
  /**
12
12
  * Cheap token estimate used when bumping the `tokens_injected_*` counters.
package/dist/metrics.js CHANGED
@@ -93,6 +93,8 @@ export const METRIC_KEYS = [
93
93
  "source_syncs_total",
94
94
  "source_dedup_skips_total",
95
95
  "okf_v3_files_written",
96
+ // v2.1.0 (K21-005 / plan §4.3) — Relay deletion metric (opt-in, default 0)
97
+ "source_deletions_total",
96
98
  ];
97
99
  const DEFAULT_FLUSH_MS = 1000;
98
100
  function zeroCache() {
@@ -0,0 +1,20 @@
1
+ -- ============================================================
2
+ -- Kevin v2.1.0 "Relay" — Source deletion + Relay metrics
3
+ -- Migration 015. Forward-only. Additive only.
4
+ -- ============================================================
5
+
6
+ -- 1. Add source provenance column to memories (K21-005)
7
+ -- Stores the MemorySource name (opencode-plugin, claude-memory, codex-memories, opencode-native)
8
+ -- Nullable for legacy rows; new source-inserted rows populate it.
9
+ ALTER TABLE memories ADD COLUMN source TEXT;
10
+
11
+ CREATE INDEX IF NOT EXISTS idx_memories_source ON memories(source) WHERE source IS NOT NULL;
12
+
13
+ -- 2. New metric: source_deletions_total (K21-005)
14
+ INSERT OR IGNORE INTO kevin_metrics (key, value) VALUES ('source_deletions_total', 0);
15
+
16
+ -- 3. New settings seed (K21-005, D21-03: opt-in default 0)
17
+ INSERT OR IGNORE INTO kevin_settings (key, value) VALUES ('source_deletion_sync', '0');
18
+
19
+ -- 4. Version marker
20
+ INSERT OR IGNORE INTO schema_version (version) VALUES ('015');
@@ -1,4 +1,4 @@
1
- import { parse, type OkfEntry } from "./okf.js";
1
+ import { type OkfEntry, parse } from "./okf.js";
2
2
  export declare const SHARD_CAP = 2000;
3
3
  export declare const PRIMARY = "knowledge.okf";
4
4
  export interface ReadResult {
@@ -1,5 +1,5 @@
1
1
  // K16-008 — Shard reader/writer (minimal stub, satisfies typecheck and tests for 1999/2000/2001/4500)
2
- import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync, writeFileSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { MAX_ENTRIES, parse, serialize } from "./okf.js";
5
5
  export const SHARD_CAP = MAX_ENTRIES; // 2000
@@ -15,7 +15,11 @@ export function readShards(dir) {
15
15
  if (existsSync(primaryPath))
16
16
  files.push(primaryPath);
17
17
  // lexicographic shards excluding primary
18
- const all = existsSync(dir) ? readdirSync(dir).filter(f => f.startsWith("knowledge-") && f.endsWith(".okf")).sort() : [];
18
+ const all = existsSync(dir)
19
+ ? readdirSync(dir)
20
+ .filter((f) => f.startsWith("knowledge-") && f.endsWith(".okf"))
21
+ .sort()
22
+ : [];
19
23
  for (const f of all) {
20
24
  const p = join(dir, f);
21
25
  if (!files.includes(p))
@@ -42,6 +46,7 @@ export function readShards(dir) {
42
46
  return { entries, files, rejected };
43
47
  }
44
48
  export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
49
+ mkdirSync(dir, { recursive: true });
45
50
  // idempotent: pack primary to SHARD_CAP, overflow to shards, collapse sparse gaps, delete empty trailing
46
51
  const sorted = [...entries].sort((a, b) => a.entry_id < b.entry_id ? -1 : a.entry_id > b.entry_id ? 1 : 0);
47
52
  if (okfVersion === 2) {
@@ -50,7 +55,7 @@ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
50
55
  writeFileSync(join(dir, PRIMARY), txt, "utf8");
51
56
  // delete any stray shards
52
57
  if (existsSync(dir)) {
53
- for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
58
+ for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
54
59
  try {
55
60
  unlinkSync(join(dir, f));
56
61
  }
@@ -86,7 +91,7 @@ export function writeShards(dir, entries, repoId, version, okfVersion = 2) {
86
91
  }
87
92
  // delete any shards beyond kept (sparse gaps)
88
93
  if (existsSync(dir)) {
89
- for (const f of readdirSync(dir).filter(x => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
94
+ for (const f of readdirSync(dir).filter((x) => x.startsWith("knowledge-") && x.endsWith(".okf"))) {
90
95
  const p = join(dir, f);
91
96
  if (!toKeep.includes(p) && existsSync(p))
92
97
  try {
package/dist/okf.js CHANGED
@@ -175,9 +175,8 @@ export function parse(text) {
175
175
  };
176
176
  }
177
177
  if (version !== OKF_VERSION && version !== OKF_V3) {
178
- // Unknown version below ahead — treat as not_okf for safety
179
- // But keep version_ahead semantics for future-proofing
180
- // For K16-007, only 2 and 3 are valid
178
+ reject(1, "not_okf");
179
+ return { version, repoId: null, entries: [], rejected, folded: 0 };
181
180
  }
182
181
  if (lines[1]?.startsWith("#repo ")) {
183
182
  repoId = lines[1].slice(6) || null;
@@ -1,7 +1,7 @@
1
1
  // K16-014 — ClaudeMemorySource (read-only mirror of ~/.claude/memory)
2
- import { existsSync, readFileSync } from "node:fs";
3
- import { join } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
4
3
  import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
5
  export class ClaudeMemorySource {
6
6
  enabledFlag;
7
7
  root;
@@ -11,26 +11,52 @@ export class ClaudeMemorySource {
11
11
  this.enabledFlag = enabledFlag;
12
12
  this.root = root;
13
13
  }
14
- enabled() { return this.enabledFlag(); }
14
+ enabled() {
15
+ return this.enabledFlag();
16
+ }
15
17
  async fetch() {
16
18
  if (!this.enabled())
17
19
  return [];
18
20
  const memPath = join(this.root, "memory");
19
- // Could be directory of markdown files or single file — best-effort
20
21
  if (!existsSync(memPath))
21
22
  return [];
23
+ const out = [];
22
24
  try {
23
- const txt = readFileSync(memPath, "utf8");
24
- // naive: split lines non-empty as statements
25
- return txt.split("\n").map(s => s.trim()).filter(Boolean).slice(0, 100).map(statement => ({
26
- statement,
27
- type: "rule",
28
- scope: null,
29
- source: this.name,
30
- }));
25
+ const st = statSync(memPath);
26
+ if (st.isDirectory()) {
27
+ for (const f of readdirSync(memPath)) {
28
+ if (!f.endsWith(".md") && !f.endsWith(".txt"))
29
+ continue;
30
+ try {
31
+ const txt = readFileSync(join(memPath, f), "utf8");
32
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
33
+ if (out.length >= 100)
34
+ break;
35
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
36
+ }
37
+ }
38
+ catch { }
39
+ }
40
+ return out;
41
+ }
42
+ if (st.isFile()) {
43
+ const txt = readFileSync(memPath, "utf8");
44
+ return txt
45
+ .split("\n")
46
+ .map((s) => s.trim())
47
+ .filter(Boolean)
48
+ .slice(0, 100)
49
+ .map((statement) => ({
50
+ statement,
51
+ type: "rule",
52
+ scope: null,
53
+ source: this.name,
54
+ }));
55
+ }
31
56
  }
32
57
  catch {
33
- return [];
58
+ return out;
34
59
  }
60
+ return out;
35
61
  }
36
62
  }
@@ -1,7 +1,7 @@
1
1
  // K16-015 — CodexMemoriesSource (mirror of ~/.codex/memories)
2
2
  import { existsSync, readFileSync, readdirSync } from "node:fs";
3
- import { join } from "node:path";
4
3
  import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
5
  export class CodexMemoriesSource {
6
6
  enabledFlag;
7
7
  root;
@@ -11,7 +11,9 @@ export class CodexMemoriesSource {
11
11
  this.enabledFlag = enabledFlag;
12
12
  this.root = root;
13
13
  }
14
- enabled() { return this.enabledFlag(); }
14
+ enabled() {
15
+ return this.enabledFlag();
16
+ }
15
17
  async fetch() {
16
18
  if (!this.enabled())
17
19
  return [];
@@ -24,8 +26,17 @@ export class CodexMemoriesSource {
24
26
  continue;
25
27
  try {
26
28
  const txt = readFileSync(join(this.root, f), "utf8");
27
- for (const line of txt.split("\n").map(s => s.trim()).filter(Boolean).slice(0, 50)) {
28
- out.push({ statement: line, type: "rule", scope: null, source: this.name });
29
+ for (const line of txt
30
+ .split("\n")
31
+ .map((s) => s.trim())
32
+ .filter(Boolean)
33
+ .slice(0, 50)) {
34
+ out.push({
35
+ statement: line,
36
+ type: "rule",
37
+ scope: null,
38
+ source: this.name,
39
+ });
29
40
  }
30
41
  }
31
42
  catch { }
@@ -1,3 +1,4 @@
1
+ import type { SharedLayer } from "../SharedLayer.js";
1
2
  import type { Store } from "../Store.js";
2
3
  import type { MemorySource, SourceSyncResult } from "./MemorySource.js";
3
4
  export interface SyncDeps {
@@ -6,5 +7,7 @@ export interface SyncDeps {
6
7
  metrics?: {
7
8
  incr(key: string, by?: number): void;
8
9
  };
10
+ sharedLayer?: SharedLayer;
11
+ okfPath?: string;
9
12
  }
10
13
  export declare function idleSync(deps: SyncDeps): Promise<SourceSyncResult[]>;
@@ -1,14 +1,28 @@
1
1
  import { fingerprint } from "../fingerprint.js";
2
+ import { collectDeletions } from "./deletion.js";
2
3
  function dedupKey(e) {
3
4
  // lower precedence wins attribution: fingerprint over normalized statement + scope
4
5
  return fingerprint(`${e.type}\0${e.statement}\0${e.scope ?? ""}`);
5
6
  }
7
+ function isDeletionSyncEnabled(store) {
8
+ try {
9
+ const row = store
10
+ .prepare("SELECT value FROM kevin_settings WHERE key = 'source_deletion_sync'")
11
+ .get();
12
+ return row?.value === "1";
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
6
18
  export async function idleSync(deps) {
7
19
  const results = [];
8
20
  const seen = new Set();
9
21
  // Pre-populate dedup with existing memories fingerprints (lowest precedence already wins)
10
22
  try {
11
- const rows = deps.store.prepare("SELECT fingerprint FROM memories").all();
23
+ const rows = deps.store
24
+ .prepare("SELECT fingerprint FROM memories")
25
+ .all();
12
26
  for (const r of rows)
13
27
  if (r.fingerprint)
14
28
  seen.add(r.fingerprint);
@@ -16,10 +30,27 @@ export async function idleSync(deps) {
16
30
  catch { }
17
31
  for (const src of deps.sources.sort((a, b) => a.precedence - b.precedence)) {
18
32
  if (!src.enabled()) {
19
- results.push({ source: src.name, fetched: 0, dedupSkipped: 0, inserted: 0 });
33
+ results.push({
34
+ source: src.name,
35
+ fetched: 0,
36
+ dedupSkipped: 0,
37
+ inserted: 0,
38
+ });
20
39
  continue;
21
40
  }
22
- const entries = await src.fetch().catch(() => []);
41
+ let entries = [];
42
+ let fetchOk = true;
43
+ try {
44
+ entries = await src.fetch();
45
+ }
46
+ catch {
47
+ fetchOk = false;
48
+ entries = [];
49
+ }
50
+ // current fingerprints for deletion diff (raw, before dedup)
51
+ const currentFps = new Set();
52
+ for (const e of entries)
53
+ currentFps.add(dedupKey(e));
23
54
  let dedupSkipped = 0;
24
55
  let inserted = 0;
25
56
  for (const e of entries) {
@@ -29,10 +60,24 @@ export async function idleSync(deps) {
29
60
  continue;
30
61
  }
31
62
  seen.add(fp);
32
- // Insert as memory with source provenance (simplified)
63
+ // Insert as memory with source provenance (K21-005: source column)
64
+ // Use relevance_score (not confidence) per schema; source col added in 015.
65
+ // Normalize scope: null → 'project' (schema CHECK)
66
+ const normScope = e.scope ?? "project";
33
67
  try {
34
- deps.store.prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, confidence, origin, source, created_at)
35
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`).run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, e.scope, fp, 0.5, src.name, src.name);
68
+ try {
69
+ deps.store
70
+ .prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source, created_at)
71
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
72
+ .run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
73
+ }
74
+ catch {
75
+ // pre-015 DB without source column: store source in source_tool as fallback
76
+ deps.store
77
+ .prepare(`INSERT OR IGNORE INTO memories (id, project_id, type, content, scope, fingerprint, relevance_score, origin, source_tool, created_at)
78
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`)
79
+ .run(`src-${fp.slice(0, 12)}-${src.name}`, "default", e.type, e.statement, normScope, fp, 0.5, "agent", src.name);
80
+ }
36
81
  inserted++;
37
82
  }
38
83
  catch {
@@ -43,7 +88,110 @@ export async function idleSync(deps) {
43
88
  deps.metrics?.incr("source_dedup_skips_total", dedupSkipped);
44
89
  if (inserted > 0)
45
90
  deps.metrics?.incr("source_syncs_total", 1);
46
- results.push({ source: src.name, fetched: entries.length, dedupSkipped, inserted });
91
+ // K21-005 deletion sync: if a previously-saved memory from this source
92
+ // is no longer in the current fetch, archive it (+ tombstone if exported).
93
+ // Gated by source_deletion_sync='1' (opt-in in 2.1.0, D21-03).
94
+ // Only run when fetch succeeded; transient errors must not mass-archive.
95
+ if (fetchOk && isDeletionSyncEnabled(deps.store)) {
96
+ try {
97
+ let prevRows = [];
98
+ try {
99
+ prevRows = deps.store
100
+ .prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source = ? AND status != 'archived' AND fingerprint IS NOT NULL")
101
+ .all(src.name);
102
+ }
103
+ catch {
104
+ // pre-015 DB: source column missing → use source_tool as provenance
105
+ try {
106
+ prevRows = deps.store
107
+ .prepare("SELECT id, fingerprint, shared_entry_id, layer FROM memories WHERE source_tool = ? AND status != 'archived' AND fingerprint IS NOT NULL")
108
+ .all(src.name);
109
+ }
110
+ catch {
111
+ prevRows = [];
112
+ }
113
+ }
114
+ const prevFps = new Set();
115
+ const rowByFp = new Map();
116
+ for (const r of prevRows) {
117
+ if (!r.fingerprint)
118
+ continue;
119
+ prevFps.add(r.fingerprint);
120
+ // keep first id per fingerprint
121
+ if (!rowByFp.has(r.fingerprint))
122
+ rowByFp.set(r.fingerprint, { id: r.id, shared_entry_id: r.shared_entry_id, layer: r.layer });
123
+ }
124
+ const deletions = collectDeletions(prevFps, currentFps, src.name);
125
+ for (const d of deletions) {
126
+ const info = rowByFp.get(d.fingerprint);
127
+ if (!info)
128
+ continue;
129
+ // archive locally
130
+ try {
131
+ deps.store
132
+ .prepare("UPDATE memories SET status='archived', archived_at=datetime('now'), updated_at=datetime('now') WHERE id=? AND status!='archived'")
133
+ .run(info.id);
134
+ const ch = deps.store.prepare("SELECT changes() AS c").get();
135
+ if (ch.c === 0)
136
+ continue;
137
+ }
138
+ catch {
139
+ continue;
140
+ }
141
+ // tombstone if ever exported (shared layer)
142
+ const isShared = info.layer === "shared" || info.shared_entry_id !== null;
143
+ if (isShared && deps.sharedLayer && deps.okfPath) {
144
+ try {
145
+ const entryId = info.shared_entry_id;
146
+ if (entryId) {
147
+ const plan = deps.sharedLayer.planTombstone([entryId], deps.okfPath);
148
+ if (plan.write.outcome !== "refused") {
149
+ deps.sharedLayer.applyExport(plan);
150
+ }
151
+ }
152
+ }
153
+ catch {
154
+ // best-effort, never throw
155
+ }
156
+ }
157
+ else if (!isShared && deps.sharedLayer && deps.okfPath) {
158
+ // For non-shared memories that were previously exported via shared_entries check
159
+ // we attempt tombstone via content-derived entry_id only if a shared entry exists
160
+ try {
161
+ const row = deps.store
162
+ .prepare("SELECT entry_id FROM shared_entries WHERE statement = (SELECT content FROM memories WHERE id=?) LIMIT 1")
163
+ .get(info.id);
164
+ if (row?.entry_id) {
165
+ const plan = deps.sharedLayer.planTombstone([row.entry_id], deps.okfPath);
166
+ if (plan.write.outcome !== "refused")
167
+ deps.sharedLayer.applyExport(plan);
168
+ }
169
+ }
170
+ catch { }
171
+ }
172
+ try {
173
+ if (deps.metrics) {
174
+ deps.metrics.incr("source_deletions_total", 1);
175
+ }
176
+ else {
177
+ deps.store
178
+ .prepare(`INSERT INTO kevin_metrics (key, value, updated_at) VALUES ('source_deletions_total', 1, datetime('now')) ON CONFLICT(key) DO UPDATE SET value = value + 1, updated_at = datetime('now')`)
179
+ .run();
180
+ }
181
+ }
182
+ catch { }
183
+ }
184
+ }
185
+ catch {
186
+ // best-effort, never break sync
187
+ }
188
+ }
189
+ results.push({
190
+ source: src.name,
191
+ fetched: entries.length,
192
+ dedupSkipped,
193
+ inserted,
194
+ });
47
195
  }
48
196
  return results;
49
197
  }
@@ -1,4 +1,5 @@
1
1
  import type { MemorySource, SourceEntry } from "./MemorySource.js";
2
+ export declare const NATIVE_CANDIDATE_PATHS: readonly [".opencode/memory/*.md", ".opencode/MEMORY.md"];
2
3
  export declare class OpencodeNativeSource implements MemorySource {
3
4
  private enabledFlag;
4
5
  private projectRoot;
@@ -7,4 +8,8 @@ export declare class OpencodeNativeSource implements MemorySource {
7
8
  constructor(enabledFlag: () => boolean, projectRoot?: string);
8
9
  enabled(): boolean;
9
10
  fetch(): Promise<SourceEntry[]>;
11
+ health(): {
12
+ status: "ok" | "absent";
13
+ detail: string;
14
+ };
10
15
  }
@@ -1,6 +1,20 @@
1
1
  // K16-016 — OpencodeNativeSource (native opencode memories, e.g. .opencode/memory)
2
- import { existsSync, readFileSync } from "node:fs";
2
+ // K21-006 Relay probe activation: single-source location list, absent-safe
3
+ import { readdirSync, readFileSync, statSync } from "node:fs";
3
4
  import { join } from "node:path";
5
+ export const NATIVE_CANDIDATE_PATHS = [
6
+ ".opencode/memory/*.md",
7
+ ".opencode/MEMORY.md",
8
+ ];
9
+ function globMdFiles(dir) {
10
+ try {
11
+ const entries = readdirSync(dir);
12
+ return entries.filter((f) => f.endsWith(".md")).map((f) => join(dir, f));
13
+ }
14
+ catch {
15
+ return [];
16
+ }
17
+ }
4
18
  export class OpencodeNativeSource {
5
19
  enabledFlag;
6
20
  projectRoot;
@@ -10,24 +24,112 @@ export class OpencodeNativeSource {
10
24
  this.enabledFlag = enabledFlag;
11
25
  this.projectRoot = projectRoot;
12
26
  }
13
- enabled() { return this.enabledFlag(); }
27
+ enabled() {
28
+ return this.enabledFlag();
29
+ }
14
30
  async fetch() {
15
31
  if (!this.enabled())
16
32
  return [];
17
- const p = join(this.projectRoot, ".opencode", "memory");
18
- if (!existsSync(p))
19
- return [];
20
- try {
21
- const txt = readFileSync(p, "utf8");
22
- return txt.split("\n").map(s => s.trim()).filter(Boolean).slice(0, 100).map(statement => ({
23
- statement,
24
- type: "rule",
25
- scope: null,
26
- source: this.name,
27
- }));
33
+ const out = [];
34
+ const seen = new Set();
35
+ for (const pattern of NATIVE_CANDIDATE_PATHS) {
36
+ try {
37
+ if (pattern.includes("*")) {
38
+ // e.g. .opencode/memory/*.md
39
+ const base = pattern.split("*")[0].replace(/\/$/, "");
40
+ const dir = join(this.projectRoot, base);
41
+ let st = null;
42
+ try {
43
+ st = statSync(dir);
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ if (!st.isDirectory())
49
+ continue;
50
+ for (const file of globMdFiles(dir)) {
51
+ if (seen.has(file))
52
+ continue;
53
+ seen.add(file);
54
+ try {
55
+ const txt = readFileSync(file, "utf8");
56
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 50)) {
57
+ if (out.length >= 100)
58
+ break;
59
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
60
+ }
61
+ }
62
+ catch {
63
+ // absent-safe: skip unreadable file
64
+ }
65
+ }
66
+ }
67
+ else {
68
+ const abs = join(this.projectRoot, pattern);
69
+ let st = null;
70
+ try {
71
+ st = statSync(abs);
72
+ }
73
+ catch {
74
+ continue;
75
+ }
76
+ if (!st.isFile())
77
+ continue;
78
+ if (seen.has(abs))
79
+ continue;
80
+ seen.add(abs);
81
+ try {
82
+ const txt = readFileSync(abs, "utf8");
83
+ for (const line of txt.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 100)) {
84
+ if (out.length >= 100)
85
+ break;
86
+ out.push({ statement: line, type: "rule", scope: null, source: this.name });
87
+ }
88
+ }
89
+ catch {
90
+ // skip
91
+ }
92
+ }
93
+ }
94
+ catch {
95
+ // never throw
96
+ }
28
97
  }
29
- catch {
30
- return [];
98
+ return out;
99
+ }
100
+ health() {
101
+ let found = 0;
102
+ for (const pattern of NATIVE_CANDIDATE_PATHS) {
103
+ try {
104
+ if (pattern.includes("*")) {
105
+ const base = pattern.split("*")[0].replace(/\/$/, "");
106
+ const dir = join(this.projectRoot, base);
107
+ try {
108
+ const st = statSync(dir);
109
+ if (!st.isDirectory())
110
+ continue;
111
+ found += globMdFiles(dir).length;
112
+ }
113
+ catch {
114
+ continue;
115
+ }
116
+ }
117
+ else {
118
+ const abs = join(this.projectRoot, pattern);
119
+ try {
120
+ const st = statSync(abs);
121
+ if (st.isFile())
122
+ found++;
123
+ }
124
+ catch {
125
+ continue;
126
+ }
127
+ }
128
+ }
129
+ catch { }
31
130
  }
131
+ if (found > 0)
132
+ return { status: "ok", detail: `found ${found} files` };
133
+ return { status: "absent", detail: "no native memory found at probe paths" };
32
134
  }
33
135
  }
@@ -5,7 +5,9 @@ export class OpencodePluginSource {
5
5
  constructor(enabledFlag) {
6
6
  this.enabledFlag = enabledFlag;
7
7
  }
8
- enabled() { return this.enabledFlag(); }
8
+ enabled() {
9
+ return this.enabledFlag();
10
+ }
9
11
  async fetch() {
10
12
  // Plugin source is the local DB itself — no fetch, handled elsewhere
11
13
  return [];
@@ -0,0 +1,18 @@
1
+ export interface DeletedInfo {
2
+ source: string;
3
+ fingerprint: string;
4
+ file?: string;
5
+ }
6
+ /**
7
+ * Pure diff: fingerprints present in prev but absent from current.
8
+ * Both sets are expected to be non-null; empty sets return [].
9
+ * Malformed inputs (null, non-Set) return [] never throw.
10
+ */
11
+ export declare function collectDeletions(prevFingerprints: Set<string> | string[] | null | undefined, currentFingerprints: Set<string> | string[] | null | undefined, source: string): DeletedInfo[];
12
+ /**
13
+ * Variant that parses memory_sources.meta_json shape if present.
14
+ * Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
15
+ * For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
16
+ * Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
17
+ */
18
+ export declare function collectDeletionsFromMeta(prevMetaJson: string | null, currentFiles: Set<string>, source: string, fingerprintByFile?: Map<string, string>): DeletedInfo[];
@@ -0,0 +1,54 @@
1
+ // K21-005 — Source deletion sync helper
2
+ // Detects memories from a source that have disappeared from the source's current fetch.
3
+ // The source of truth is fingerprints: a memory whose fingerprint is no longer in the
4
+ // fetched set is a candidate for archival + tombstone.
5
+ // File-level meta_json diff (plan §4.3) is not present in v2.0's IdleSync — this layer
6
+ // diffs at fingerprint granularity, which is source-agnostic and cross-file safe.
7
+ // Cross-source protection is enforced by the caller: only memories with matching source
8
+ // are considered deletions.
9
+ /**
10
+ * Pure diff: fingerprints present in prev but absent from current.
11
+ * Both sets are expected to be non-null; empty sets return [].
12
+ * Malformed inputs (null, non-Set) return [] never throw.
13
+ */
14
+ export function collectDeletions(prevFingerprints, currentFingerprints, source) {
15
+ if (!prevFingerprints || !currentFingerprints)
16
+ return [];
17
+ const prev = prevFingerprints instanceof Set ? prevFingerprints : new Set(prevFingerprints);
18
+ const curr = currentFingerprints instanceof Set ? currentFingerprints : new Set(currentFingerprints);
19
+ const out = [];
20
+ for (const fp of prev) {
21
+ if (typeof fp !== "string" || fp.length === 0)
22
+ continue;
23
+ if (!curr.has(fp)) {
24
+ out.push({ source, fingerprint: fp });
25
+ }
26
+ }
27
+ return out;
28
+ }
29
+ /**
30
+ * Variant that parses memory_sources.meta_json shape if present.
31
+ * Expected shape: {"files":{"path":{"mtime":123,"size":456}}} or legacy null/string.
32
+ * For v2.1, meta_json may be absent (no per-file tracking yet) — returns [] gracefully.
33
+ * Kept for spec compatibility (plan §4.3 collectDeletions(prevMetaJson, currentFiles)).
34
+ */
35
+ export function collectDeletionsFromMeta(prevMetaJson, currentFiles, source, fingerprintByFile) {
36
+ if (!prevMetaJson)
37
+ return [];
38
+ try {
39
+ const parsed = JSON.parse(prevMetaJson);
40
+ if (!parsed || typeof parsed.files !== "object" || parsed.files === null)
41
+ return [];
42
+ const out = [];
43
+ for (const file of Object.keys(parsed.files)) {
44
+ if (!currentFiles.has(file)) {
45
+ const fp = fingerprintByFile?.get(file) ?? file;
46
+ out.push({ source, file, fingerprint: fp });
47
+ }
48
+ }
49
+ return out;
50
+ }
51
+ catch {
52
+ return [];
53
+ }
54
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmtrin/kevin-core",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Kevin core — host-agnostic deterministic brain (Bedrock)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",