kodingo-cli 1.2.0 → 1.3.1

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.
@@ -104,6 +104,7 @@ function mapApiRecord(r) {
104
104
  },
105
105
  createdAt: new Date(r.createdAt),
106
106
  updatedAt: new Date(r.updatedAt),
107
+ evidenceKind: r.evidenceKind ?? "sourced",
107
108
  // Adapter-layer fields — not on core MemoryRecord but present in API responses
108
109
  externalId: r.externalId ?? null,
109
110
  correctsId: r.correctsId ?? null,
@@ -118,6 +119,8 @@ function mapApiRecord(r) {
118
119
  record.fingerprint = r.fingerprint;
119
120
  if (r.deniedAt)
120
121
  record.deniedAt = new Date(r.deniedAt);
122
+ if (r.contentAsOf)
123
+ record.contentAsOf = new Date(r.contentAsOf);
121
124
  return record;
122
125
  }
123
126
  // ── Public API (mirrors memory-persistence.ts) ────────────────────────────────
@@ -146,6 +149,7 @@ async function saveMemory(input) {
146
149
  status: input.status ?? "proposed",
147
150
  confidence: input.confidence ?? 0.3,
148
151
  tags: input.tags ?? [],
152
+ evidenceKind: input.evidenceKind,
149
153
  };
150
154
  if (input.title)
151
155
  body.title = input.title;
@@ -159,6 +163,8 @@ async function saveMemory(input) {
159
163
  body.correctsId = input.correctsId;
160
164
  if (input.correctedById)
161
165
  body.correctedById = input.correctedById;
166
+ if (input.contentAsOf)
167
+ body.contentAsOf = input.contentAsOf.toISOString();
162
168
  // Attribution
163
169
  try {
164
170
  const config = (0, persistence_config_1.readConfig)();
@@ -46,6 +46,26 @@ async function initDb() {
46
46
  ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS corrects_id uuid NULL;
47
47
  ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS corrected_by_id uuid NULL;
48
48
 
49
+ -- Real evidence provenance (mirrors kodingo-api's schema.ts) — local-mode
50
+ -- writes go straight here via psql, bypassing the API's enforcement
51
+ -- entirely, so the same real constraint needs to exist on this table too.
52
+ ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS evidence_kind text NOT NULL DEFAULT 'sourced';
53
+ DO $$
54
+ BEGIN
55
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'kodingo_memory_evidence_kind_check') THEN
56
+ ALTER TABLE kodingo_memory ADD CONSTRAINT kodingo_memory_evidence_kind_check CHECK (evidence_kind IN ('sourced', 'derived'));
57
+ END IF;
58
+ IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'kodingo_memory_sourced_requires_external_id') THEN
59
+ ALTER TABLE kodingo_memory ADD CONSTRAINT kodingo_memory_sourced_requires_external_id
60
+ CHECK (evidence_kind = 'derived' OR (external_id IS NOT NULL AND btrim(external_id) <> '')) NOT VALID;
61
+ END IF;
62
+ END $$;
63
+
64
+ -- Real, best-known age of the underlying decision/code, separate from
65
+ -- created_at (mirrors kodingo-api's schema.ts). NULL when genuinely
66
+ -- unknown — never defaulted to created_at.
67
+ ALTER TABLE kodingo_memory ADD COLUMN IF NOT EXISTS content_as_of timestamptz NULL;
68
+
49
69
  -- Indexes
50
70
  CREATE INDEX IF NOT EXISTS kodingo_memory_created_at_idx
51
71
  ON kodingo_memory (created_at);
@@ -97,11 +117,23 @@ async function saveMemory(input) {
97
117
  const id = (0, uuid_1.v4)();
98
118
  const status = input.status ?? "proposed";
99
119
  const confidence = clamp01(input.confidence ?? 0.3);
120
+ // Real mechanical enforcement (mirrors kodingo-core's
121
+ // validateMemoryProvenance) — local-mode writes go straight to Postgres
122
+ // via psql, bypassing kodingo-api's routes/memory.ts entirely, so this is
123
+ // the one place that enforcement can actually happen for this path. Same
124
+ // real, honest fallback as the API: a "sourced" capture with nothing
125
+ // client-supplied gets a real identifier naming the capture event itself,
126
+ // not a fabricated placeholder.
127
+ const externalId = input.externalId ??
128
+ (input.evidenceKind === "sourced" ? `manual:${input.repoPath ?? "unknown"}:${id}` : undefined);
129
+ if (input.evidenceKind === "sourced" && !externalId?.trim()) {
130
+ throw new Error("A 'sourced' memory must include a real externalId. If this genuinely has no single real source, use evidenceKind: 'derived' instead.");
131
+ }
100
132
  const sql = `
101
133
  WITH inserted AS (
102
134
  INSERT INTO kodingo_memory (
103
135
  id, type, title, content, tags, repo_path,
104
- status, confidence, symbol, external_id,
136
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
105
137
  corrects_id, corrected_by_id
106
138
  )
107
139
  VALUES (
@@ -114,13 +146,15 @@ async function saveMemory(input) {
114
146
  ${sqlLiteral(status)},
115
147
  ${sqlNumberLiteral(confidence)},
116
148
  ${sqlNullableLiteral(input.symbol)},
117
- ${sqlNullableLiteral(input.externalId)},
149
+ ${sqlNullableLiteral(externalId)},
150
+ ${sqlLiteral(input.evidenceKind)},
151
+ ${sqlNullableTimestampLiteral(input.contentAsOf)},
118
152
  ${sqlNullableUuidLiteral(input.correctsId)},
119
153
  ${sqlNullableUuidLiteral(input.correctedById)}
120
154
  )
121
155
  RETURNING
122
156
  id, type, title, content, tags, repo_path,
123
- status, confidence, symbol, external_id,
157
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
124
158
  corrects_id, corrected_by_id,
125
159
  created_at
126
160
  )
@@ -137,7 +171,7 @@ async function getMemoryById(id) {
137
171
  SELECT row_to_json(result) FROM (
138
172
  SELECT
139
173
  id, type, title, content, tags, repo_path,
140
- status, confidence, symbol, external_id,
174
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
141
175
  corrects_id, corrected_by_id,
142
176
  created_at
143
177
  FROM kodingo_memory
@@ -165,7 +199,7 @@ async function updateMemoryLifecycle(params) {
165
199
  WHERE id = ${sqlLiteral(params.id)}
166
200
  RETURNING
167
201
  id, type, title, content, tags, repo_path,
168
- status, confidence, symbol, external_id,
202
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
169
203
  corrects_id, corrected_by_id,
170
204
  created_at
171
205
  )
@@ -218,7 +252,7 @@ async function getMemoryByExternalId(externalId, repoPath) {
218
252
  SELECT row_to_json(result) FROM (
219
253
  SELECT
220
254
  id, type, title, content, tags, repo_path,
221
- status, confidence, symbol, external_id,
255
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
222
256
  corrects_id, corrected_by_id,
223
257
  created_at
224
258
  FROM kodingo_memory
@@ -243,7 +277,7 @@ async function getProposedOrIgnoredRecordBySymbol(params) {
243
277
  SELECT row_to_json(result) FROM (
244
278
  SELECT
245
279
  id, type, title, content, tags, repo_path,
246
- status, confidence, symbol, external_id,
280
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
247
281
  corrects_id, corrected_by_id,
248
282
  created_at
249
283
  FROM kodingo_memory
@@ -275,7 +309,7 @@ async function updateMemoryConfidence(params) {
275
309
  WHERE id = ${sqlLiteral(params.id)}
276
310
  RETURNING
277
311
  id, type, title, content, tags, repo_path,
278
- status, confidence, symbol, external_id,
312
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
279
313
  corrects_id, corrected_by_id,
280
314
  created_at
281
315
  )
@@ -318,7 +352,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
318
352
  SELECT row_to_json(result) FROM (
319
353
  SELECT DISTINCT ON (coalesce(symbol, id::text))
320
354
  id, type, title, content, tags, repo_path,
321
- status, confidence, symbol, external_id,
355
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
322
356
  corrects_id, corrected_by_id,
323
357
  created_at
324
358
  FROM kodingo_memory
@@ -349,7 +383,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
349
383
  SELECT row_to_json(result) FROM (
350
384
  SELECT
351
385
  id, type, title, content, tags, repo_path,
352
- status, confidence, symbol, external_id,
386
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
353
387
  corrects_id, corrected_by_id,
354
388
  created_at,
355
389
  CASE
@@ -413,7 +447,7 @@ async function queryMemoryBySymbol(params) {
413
447
  SELECT row_to_json(result) FROM (
414
448
  SELECT
415
449
  id, type, title, content, tags, repo_path,
416
- status, confidence, symbol, external_id,
450
+ status, confidence, symbol, external_id, evidence_kind, content_as_of,
417
451
  corrects_id, corrected_by_id,
418
452
  created_at
419
453
  FROM kodingo_memory
@@ -444,6 +478,8 @@ function mapRow(row) {
444
478
  confidence,
445
479
  symbol: row.symbol ?? null,
446
480
  externalId: row.external_id ?? null,
481
+ evidenceKind: row.evidence_kind ?? "sourced",
482
+ contentAsOf: row.content_as_of ? new Date(row.content_as_of) : null,
447
483
  correctsId: row.corrects_id ?? null,
448
484
  correctedById: row.corrected_by_id ?? null,
449
485
  createdAt: new Date(row.created_at),
@@ -509,6 +545,11 @@ function sqlNullableUuidLiteral(value) {
509
545
  return "NULL";
510
546
  return `${sqlLiteral(value)}::uuid`;
511
547
  }
548
+ function sqlNullableTimestampLiteral(value) {
549
+ if (!value)
550
+ return "NULL";
551
+ return `${sqlLiteral(value.toISOString())}::timestamptz`;
552
+ }
512
553
  function sqlArrayLiteral(values) {
513
554
  if (!values || values.length === 0)
514
555
  return "NULL";
@@ -17,10 +17,16 @@ async function captureCommand(options) {
17
17
  }
18
18
  const tags = parseTags(options.tags);
19
19
  const repoPath = findRepoRoot(process.cwd());
20
+ // A manual capture is genuinely sourced — to the real, specific event of
21
+ // this human's assertion, right now. Not tied to a pre-existing external
22
+ // artifact the way a commit or PR is, so no externalId is set here; both
23
+ // backends (local-mode saveMemory, kodingo-api's routes/memory.ts)
24
+ // construct a real one from the capture event's own identity when absent.
20
25
  const input = {
21
26
  type: options.type,
22
27
  content,
23
28
  repoPath,
29
+ evidenceKind: "sourced",
24
30
  };
25
31
  if (options.title)
26
32
  input.title = options.title;
@@ -37,12 +37,16 @@ async function denyCommand(options) {
37
37
  }
38
38
  const userTags = parseTags(options.tags);
39
39
  const tags = mergeTags(existing.tags, userTags);
40
+ // A denial's corrected replacement is a new, real human assertion — the
41
+ // correction event itself — same class as manual capture, not simply
42
+ // inherited from whatever the original record's provenance was.
40
43
  const input = {
41
44
  type: existing.type,
42
45
  content: correctionContent,
43
46
  status: "proposed",
44
47
  confidence: 0.7,
45
48
  correctsId: existing.id,
49
+ evidenceKind: "sourced",
46
50
  };
47
51
  if (existing.repo)
48
52
  input.repoPath = existing.repo;
@@ -58,6 +58,7 @@ async function runDoctor(results) {
58
58
  content: "doctor saved",
59
59
  tags: ["smoke", "doctor"],
60
60
  repoPath: process.cwd(),
61
+ evidenceKind: "derived", // a throwaway diagnostic row, not a real capture
61
62
  });
62
63
  tempId = inserted.id;
63
64
  }, results);
@@ -104,6 +104,7 @@ function extractMemoriesFromSession(messages, sessionId, repoPath) {
104
104
  repoPath,
105
105
  status: "proposed",
106
106
  confidence: Math.min(0.3 + score * 0.05, 0.6),
107
+ evidenceKind: "sourced",
107
108
  externalId: `claude-session:${sessionId}:${Buffer.from(para.slice(0, 40)).toString("base64").slice(0, 16)}`,
108
109
  });
109
110
  }
@@ -112,6 +112,8 @@ async function extractSymbols(filePath) {
112
112
  code: n.body.slice(0, 2000), // cap to avoid huge payloads
113
113
  file: filePath,
114
114
  fingerprint: n.fingerprint,
115
+ startLine: n.startLine,
116
+ endLine: n.endLine,
115
117
  }));
116
118
  }
117
119
  catch {
@@ -197,6 +199,18 @@ async function inferAndCapture(token, symbol, repo, graph, repoRoot) {
197
199
  machineId: (0, persistence_config_1.readConfig)().machineId ?? undefined,
198
200
  displayName: os.hostname(),
199
201
  fingerprint: symbol.fingerprint ?? undefined,
202
+ // An LLM's own inferred description of a function during a bulk scan
203
+ // — Kortex's own conclusion, not traceable to one real external
204
+ // event, same class as kodingo-api's githubScanner.ts.
205
+ evidenceKind: "derived",
206
+ // Real line range + repo-relative file path (Problem B) — lets
207
+ // kodingo-api resolve this symbol's real content age via git blame
208
+ // asynchronously after capture (it has the GitHub App token this
209
+ // CLI doesn't). Without these, content_as_of stays null — never
210
+ // guessed from scan timing.
211
+ startLine: symbol.startLine ?? undefined,
212
+ endLine: symbol.endLine ?? undefined,
213
+ filePath: path_1.default.relative(repoRoot, symbol.file).split(path_1.default.sep).join("/"),
200
214
  }),
201
215
  });
202
216
  return saveRes.ok;
@@ -58,6 +58,7 @@ class CloudEventPort {
58
58
  content: existing.content,
59
59
  symbol: existing.symbol ?? "",
60
60
  tags: existing.tags ?? [],
61
+ evidenceKind: existing.evidenceKind,
61
62
  status: existing.status,
62
63
  confidence: existing.confidence,
63
64
  evidence: {
@@ -100,8 +101,10 @@ class CloudEventPort {
100
101
  repoPath: this.repoPath,
101
102
  status: "proposed",
102
103
  confidence: decision.confidence ?? 0.3,
104
+ evidenceKind: "sourced",
103
105
  externalId,
104
106
  tags: ["signal:git", "kind:decision"],
107
+ contentAsOf: decision.proposedAt,
105
108
  };
106
109
  if (title)
107
110
  input.title = title;
@@ -52,6 +52,7 @@ class DbEventPort {
52
52
  content: existing.content,
53
53
  symbol: existing.symbol ?? "",
54
54
  tags: existing.tags ?? [],
55
+ evidenceKind: existing.evidenceKind,
55
56
  status: existing.status,
56
57
  confidence: existing.confidence,
57
58
  evidence: {
@@ -74,14 +75,21 @@ class DbEventPort {
74
75
  return;
75
76
  }
76
77
  }
77
- // No existing proposed/ignored record — insert new record as usual
78
+ // No existing proposed/ignored record — insert new record as usual.
79
+ // Git-signal-derived captures are genuinely sourced — externalId here is
80
+ // a real git:<commitHash> reference (see the caller). contentAsOf is the
81
+ // real commit's own author date (decision.proposedAt) — trivially in
82
+ // hand here, unlike bulk/autonomous scans which have no equivalent
83
+ // single real event to anchor to (see contentAge.ts, kodingo-api).
78
84
  const input = {
79
85
  type: "decision",
80
86
  content,
81
87
  repoPath: this.repoPath,
82
88
  status: "proposed",
83
89
  confidence: 0.3,
90
+ evidenceKind: "sourced",
84
91
  externalId,
92
+ contentAsOf: decision.proposedAt,
85
93
  };
86
94
  if (title)
87
95
  input.title = title;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -26,7 +26,7 @@
26
26
  "@typescript-eslint/typescript-estree": "^8.61.1",
27
27
  "commander": "^12.1.0",
28
28
  "kodingo-agent": "^0.2.0",
29
- "kodingo-core": "^0.2.1",
29
+ "kodingo-core": "^0.3.1",
30
30
  "open": "^11.0.0",
31
31
  "simple-git": "^3.30.0",
32
32
  "tree-sitter-wasms": "^0.1.13",