kodingo-cli 1.3.0 → 1.3.2
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/__tests__/csharpParser.test.js +187 -0
- package/dist/adapters/cloud-adapter/cloud-persistence.js +4 -0
- package/dist/adapters/db-adapter/memory-persistence.js +22 -10
- package/dist/commands/scan-repo.js +11 -1
- package/dist/ports/cloud-event-port.js +1 -0
- package/dist/ports/db-event-port.js +5 -1
- package/dist/utils/ast-parser.js +1 -1
- package/dist/utils/graph-builder.js +1 -1
- package/dist/utils/tree-sitter-parser.js +26 -0
- package/package.json +3 -3
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real tests for C#/.NET support in tree-sitter-parser.ts / ast-parser.ts —
|
|
4
|
+
* build brief "Add real C#/.NET support to the CLI's parser" (2026-09-20).
|
|
5
|
+
*
|
|
6
|
+
* The real bar per the brief: not just "C# symbols get extracted", but
|
|
7
|
+
* genuine, stable deduplication — the same, unchanged function produces a
|
|
8
|
+
* byte-for-byte identical fingerprint across repeated runs, and a genuinely
|
|
9
|
+
* changed function's fingerprint correctly changes.
|
|
10
|
+
*
|
|
11
|
+
* Uses Node's built-in test runner (node:test) — this repo has no existing
|
|
12
|
+
* test framework to inherit (confirmed: package.json's own "test" script
|
|
13
|
+
* was a stub), and node:test needs zero new dependencies. Compiled and run
|
|
14
|
+
* via `npm test` (tsc then `node --test dist/__tests__`).
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
const node_test_1 = require("node:test");
|
|
21
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
22
|
+
const tree_sitter_parser_1 = require("../utils/tree-sitter-parser");
|
|
23
|
+
const ast_parser_1 = require("../utils/ast-parser");
|
|
24
|
+
const REAL_CSHARP_SOURCE = `
|
|
25
|
+
using System;
|
|
26
|
+
using System.Threading.Tasks;
|
|
27
|
+
|
|
28
|
+
namespace Acme.Orders
|
|
29
|
+
{
|
|
30
|
+
public class OrderService : IOrderService
|
|
31
|
+
{
|
|
32
|
+
private readonly IRepository _repo;
|
|
33
|
+
|
|
34
|
+
public OrderService(IRepository repo)
|
|
35
|
+
{
|
|
36
|
+
_repo = repo;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public async Task<Order> GetOrderAsync(int id)
|
|
40
|
+
{
|
|
41
|
+
var order = await _repo.FindAsync(id);
|
|
42
|
+
LogAccess(id);
|
|
43
|
+
return order;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private void LogAccess(int id)
|
|
47
|
+
{
|
|
48
|
+
Console.WriteLine($"Accessed order {id}");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static int Add(int a, int b)
|
|
52
|
+
{
|
|
53
|
+
if (a > 0)
|
|
54
|
+
{
|
|
55
|
+
return a + b;
|
|
56
|
+
}
|
|
57
|
+
return b;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public interface IOrderService
|
|
62
|
+
{
|
|
63
|
+
Task<Order> GetOrderAsync(int id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public struct Point
|
|
67
|
+
{
|
|
68
|
+
public int Sum(int x, int y) => x + y;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public record OrderDto(int Id, string Name)
|
|
72
|
+
{
|
|
73
|
+
public string Describe() => $"{Id}: {Name}";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public class Order { }
|
|
77
|
+
}
|
|
78
|
+
`;
|
|
79
|
+
// A real, structural change to GetOrderAsync — a distinct code path added,
|
|
80
|
+
// not a cosmetic rename/whitespace/comment tweak (those are what
|
|
81
|
+
// computeFingerprint is specifically designed to SURVIVE).
|
|
82
|
+
const CHANGED_CSHARP_SOURCE = REAL_CSHARP_SOURCE.replace(`public async Task<Order> GetOrderAsync(int id)
|
|
83
|
+
{
|
|
84
|
+
var order = await _repo.FindAsync(id);
|
|
85
|
+
LogAccess(id);
|
|
86
|
+
return order;
|
|
87
|
+
}`, `public async Task<Order> GetOrderAsync(int id)
|
|
88
|
+
{
|
|
89
|
+
var order = await _repo.FindAsync(id);
|
|
90
|
+
LogAccess(id);
|
|
91
|
+
if (order == null) throw new InvalidOperationException("not found");
|
|
92
|
+
return order;
|
|
93
|
+
}`);
|
|
94
|
+
(0, node_test_1.describe)("parseWithTreeSitter — real C# extraction", () => {
|
|
95
|
+
(0, node_test_1.it)("extracts every real method, constructor, and local function with class-qualified names", async () => {
|
|
96
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
97
|
+
const names = symbols.map(s => s.name).sort();
|
|
98
|
+
strict_1.default.deepEqual(names, [
|
|
99
|
+
"IOrderService.GetOrderAsync",
|
|
100
|
+
"OrderDto.Describe",
|
|
101
|
+
"OrderService.Add",
|
|
102
|
+
"OrderService.GetOrderAsync",
|
|
103
|
+
"OrderService.LogAccess",
|
|
104
|
+
"OrderService.OrderService",
|
|
105
|
+
"Point.Sum",
|
|
106
|
+
].sort());
|
|
107
|
+
});
|
|
108
|
+
(0, node_test_1.it)("real, correct method vs constructor typing", async () => {
|
|
109
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
110
|
+
const ctor = symbols.find(s => s.name === "OrderService.OrderService");
|
|
111
|
+
const method = symbols.find(s => s.name === "OrderService.GetOrderAsync");
|
|
112
|
+
strict_1.default.equal(ctor?.type, "method");
|
|
113
|
+
strict_1.default.equal(method?.type, "method");
|
|
114
|
+
});
|
|
115
|
+
(0, node_test_1.it)("real call extraction trims member-access callees to their trailing identifier", async () => {
|
|
116
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
117
|
+
const method = symbols.find(s => s.name === "OrderService.GetOrderAsync");
|
|
118
|
+
strict_1.default.ok(method);
|
|
119
|
+
strict_1.default.ok(method.calls.includes("FindAsync"), `expected FindAsync in ${JSON.stringify(method.calls)}`);
|
|
120
|
+
strict_1.default.ok(method.calls.includes("LogAccess"), `expected LogAccess in ${JSON.stringify(method.calls)}`);
|
|
121
|
+
});
|
|
122
|
+
(0, node_test_1.it)("real using-directive extraction", async () => {
|
|
123
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
124
|
+
strict_1.default.ok(symbols[0].imports.some(i => i.source.includes("using System;")));
|
|
125
|
+
strict_1.default.ok(symbols[0].imports.some(i => i.source.includes("using System.Threading.Tasks;")));
|
|
126
|
+
});
|
|
127
|
+
(0, node_test_1.it)("returns [] for an unrecognized extension rather than throwing", async () => {
|
|
128
|
+
const symbols = await (0, tree_sitter_parser_1.parseWithTreeSitter)("readme.md", "# not code");
|
|
129
|
+
strict_1.default.deepEqual(symbols, []);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
(0, node_test_1.describe)("real deduplication — the actual bar this brief is judged against", () => {
|
|
133
|
+
(0, node_test_1.it)("the SAME, unchanged C# function produces a byte-for-byte identical fingerprint across repeated runs", async () => {
|
|
134
|
+
const run1 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
135
|
+
const run2 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
136
|
+
const run3 = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
137
|
+
const fp1 = run1.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
138
|
+
const fp2 = run2.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
139
|
+
const fp3 = run3.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
140
|
+
strict_1.default.ok(fp1);
|
|
141
|
+
strict_1.default.equal(fp1, fp2);
|
|
142
|
+
strict_1.default.equal(fp2, fp3);
|
|
143
|
+
// Every single real symbol in the file, not just one — genuine, stable
|
|
144
|
+
// dedup across the whole file, not a cherry-picked case.
|
|
145
|
+
for (let i = 0; i < run1.length; i++) {
|
|
146
|
+
strict_1.default.equal(run1[i].fingerprint, run2[i].fingerprint, `symbol ${run1[i].name} fingerprint drifted across runs`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
(0, node_test_1.it)("via the real, full parseFile dispatch (the actual path scan-repo.ts calls) — same stability", async () => {
|
|
150
|
+
const run1 = await (0, ast_parser_1.parseFile)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
151
|
+
const run2 = await (0, ast_parser_1.parseFile)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
152
|
+
strict_1.default.ok(run1.length > 0);
|
|
153
|
+
strict_1.default.deepEqual(run1.map(s => s.fingerprint), run2.map(s => s.fingerprint));
|
|
154
|
+
});
|
|
155
|
+
(0, node_test_1.it)("cosmetic-only changes (comments, whitespace) do NOT change the fingerprint — by design", async () => {
|
|
156
|
+
const cosmetic = REAL_CSHARP_SOURCE.replace(`public static int Add(int a, int b)`, `// adds two numbers\n public static int Add(int a, int b)`);
|
|
157
|
+
const before = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
158
|
+
const after = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", cosmetic);
|
|
159
|
+
const fpBefore = before.find(s => s.name === "OrderService.Add")?.fingerprint;
|
|
160
|
+
const fpAfter = after.find(s => s.name === "OrderService.Add")?.fingerprint;
|
|
161
|
+
strict_1.default.equal(fpBefore, fpAfter);
|
|
162
|
+
});
|
|
163
|
+
(0, node_test_1.it)("a genuinely CHANGED function's fingerprint correctly changes — an edit is not invisible", async () => {
|
|
164
|
+
const before = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", REAL_CSHARP_SOURCE);
|
|
165
|
+
const after = await (0, tree_sitter_parser_1.parseWithTreeSitter)("OrderService.cs", CHANGED_CSHARP_SOURCE);
|
|
166
|
+
const fpBefore = before.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
167
|
+
const fpAfter = after.find(s => s.name === "OrderService.GetOrderAsync")?.fingerprint;
|
|
168
|
+
strict_1.default.ok(fpBefore);
|
|
169
|
+
strict_1.default.ok(fpAfter);
|
|
170
|
+
strict_1.default.notEqual(fpBefore, fpAfter);
|
|
171
|
+
// Every OTHER real symbol in the file is untouched by the edit and must
|
|
172
|
+
// keep its own stable fingerprint — proving the change is properly
|
|
173
|
+
// scoped to the actual edited function, not a global reshuffle.
|
|
174
|
+
for (const name of ["OrderService.Add", "OrderService.LogAccess", "OrderService.OrderService", "Point.Sum"]) {
|
|
175
|
+
const b = before.find(s => s.name === name)?.fingerprint;
|
|
176
|
+
const a = after.find(s => s.name === name)?.fingerprint;
|
|
177
|
+
strict_1.default.equal(b, a, `unrelated symbol ${name} should not have changed`);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
(0, node_test_1.describe)("computeFingerprint — sanity on real C# body text", () => {
|
|
182
|
+
(0, node_test_1.it)("normalizes C# string interpolation/comments deterministically", () => {
|
|
183
|
+
const a = (0, ast_parser_1.computeFingerprint)(`Console.WriteLine($"Accessed order {id}"); // log it`);
|
|
184
|
+
const b = (0, ast_parser_1.computeFingerprint)(`Console.WriteLine($"Accessed order {id}");`);
|
|
185
|
+
strict_1.default.equal(a, b);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -119,6 +119,8 @@ function mapApiRecord(r) {
|
|
|
119
119
|
record.fingerprint = r.fingerprint;
|
|
120
120
|
if (r.deniedAt)
|
|
121
121
|
record.deniedAt = new Date(r.deniedAt);
|
|
122
|
+
if (r.contentAsOf)
|
|
123
|
+
record.contentAsOf = new Date(r.contentAsOf);
|
|
122
124
|
return record;
|
|
123
125
|
}
|
|
124
126
|
// ── Public API (mirrors memory-persistence.ts) ────────────────────────────────
|
|
@@ -161,6 +163,8 @@ async function saveMemory(input) {
|
|
|
161
163
|
body.correctsId = input.correctsId;
|
|
162
164
|
if (input.correctedById)
|
|
163
165
|
body.correctedById = input.correctedById;
|
|
166
|
+
if (input.contentAsOf)
|
|
167
|
+
body.contentAsOf = input.contentAsOf.toISOString();
|
|
164
168
|
// Attribution
|
|
165
169
|
try {
|
|
166
170
|
const config = (0, persistence_config_1.readConfig)();
|
|
@@ -61,6 +61,11 @@ async function initDb() {
|
|
|
61
61
|
END IF;
|
|
62
62
|
END $$;
|
|
63
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
|
+
|
|
64
69
|
-- Indexes
|
|
65
70
|
CREATE INDEX IF NOT EXISTS kodingo_memory_created_at_idx
|
|
66
71
|
ON kodingo_memory (created_at);
|
|
@@ -128,7 +133,7 @@ async function saveMemory(input) {
|
|
|
128
133
|
WITH inserted AS (
|
|
129
134
|
INSERT INTO kodingo_memory (
|
|
130
135
|
id, type, title, content, tags, repo_path,
|
|
131
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
136
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
132
137
|
corrects_id, corrected_by_id
|
|
133
138
|
)
|
|
134
139
|
VALUES (
|
|
@@ -143,12 +148,13 @@ async function saveMemory(input) {
|
|
|
143
148
|
${sqlNullableLiteral(input.symbol)},
|
|
144
149
|
${sqlNullableLiteral(externalId)},
|
|
145
150
|
${sqlLiteral(input.evidenceKind)},
|
|
151
|
+
${sqlNullableTimestampLiteral(input.contentAsOf)},
|
|
146
152
|
${sqlNullableUuidLiteral(input.correctsId)},
|
|
147
153
|
${sqlNullableUuidLiteral(input.correctedById)}
|
|
148
154
|
)
|
|
149
155
|
RETURNING
|
|
150
156
|
id, type, title, content, tags, repo_path,
|
|
151
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
157
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
152
158
|
corrects_id, corrected_by_id,
|
|
153
159
|
created_at
|
|
154
160
|
)
|
|
@@ -165,7 +171,7 @@ async function getMemoryById(id) {
|
|
|
165
171
|
SELECT row_to_json(result) FROM (
|
|
166
172
|
SELECT
|
|
167
173
|
id, type, title, content, tags, repo_path,
|
|
168
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
174
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
169
175
|
corrects_id, corrected_by_id,
|
|
170
176
|
created_at
|
|
171
177
|
FROM kodingo_memory
|
|
@@ -193,7 +199,7 @@ async function updateMemoryLifecycle(params) {
|
|
|
193
199
|
WHERE id = ${sqlLiteral(params.id)}
|
|
194
200
|
RETURNING
|
|
195
201
|
id, type, title, content, tags, repo_path,
|
|
196
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
202
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
197
203
|
corrects_id, corrected_by_id,
|
|
198
204
|
created_at
|
|
199
205
|
)
|
|
@@ -246,7 +252,7 @@ async function getMemoryByExternalId(externalId, repoPath) {
|
|
|
246
252
|
SELECT row_to_json(result) FROM (
|
|
247
253
|
SELECT
|
|
248
254
|
id, type, title, content, tags, repo_path,
|
|
249
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
255
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
250
256
|
corrects_id, corrected_by_id,
|
|
251
257
|
created_at
|
|
252
258
|
FROM kodingo_memory
|
|
@@ -271,7 +277,7 @@ async function getProposedOrIgnoredRecordBySymbol(params) {
|
|
|
271
277
|
SELECT row_to_json(result) FROM (
|
|
272
278
|
SELECT
|
|
273
279
|
id, type, title, content, tags, repo_path,
|
|
274
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
280
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
275
281
|
corrects_id, corrected_by_id,
|
|
276
282
|
created_at
|
|
277
283
|
FROM kodingo_memory
|
|
@@ -303,7 +309,7 @@ async function updateMemoryConfidence(params) {
|
|
|
303
309
|
WHERE id = ${sqlLiteral(params.id)}
|
|
304
310
|
RETURNING
|
|
305
311
|
id, type, title, content, tags, repo_path,
|
|
306
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
312
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
307
313
|
corrects_id, corrected_by_id,
|
|
308
314
|
created_at
|
|
309
315
|
)
|
|
@@ -346,7 +352,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
|
|
|
346
352
|
SELECT row_to_json(result) FROM (
|
|
347
353
|
SELECT DISTINCT ON (coalesce(symbol, id::text))
|
|
348
354
|
id, type, title, content, tags, repo_path,
|
|
349
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
355
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
350
356
|
corrects_id, corrected_by_id,
|
|
351
357
|
created_at
|
|
352
358
|
FROM kodingo_memory
|
|
@@ -377,7 +383,7 @@ async function queryMemory(text, limit = 10, repoPath, opts = {}) {
|
|
|
377
383
|
SELECT row_to_json(result) FROM (
|
|
378
384
|
SELECT
|
|
379
385
|
id, type, title, content, tags, repo_path,
|
|
380
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
386
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
381
387
|
corrects_id, corrected_by_id,
|
|
382
388
|
created_at,
|
|
383
389
|
CASE
|
|
@@ -441,7 +447,7 @@ async function queryMemoryBySymbol(params) {
|
|
|
441
447
|
SELECT row_to_json(result) FROM (
|
|
442
448
|
SELECT
|
|
443
449
|
id, type, title, content, tags, repo_path,
|
|
444
|
-
status, confidence, symbol, external_id, evidence_kind,
|
|
450
|
+
status, confidence, symbol, external_id, evidence_kind, content_as_of,
|
|
445
451
|
corrects_id, corrected_by_id,
|
|
446
452
|
created_at
|
|
447
453
|
FROM kodingo_memory
|
|
@@ -473,6 +479,7 @@ function mapRow(row) {
|
|
|
473
479
|
symbol: row.symbol ?? null,
|
|
474
480
|
externalId: row.external_id ?? null,
|
|
475
481
|
evidenceKind: row.evidence_kind ?? "sourced",
|
|
482
|
+
contentAsOf: row.content_as_of ? new Date(row.content_as_of) : null,
|
|
476
483
|
correctsId: row.corrects_id ?? null,
|
|
477
484
|
correctedById: row.corrected_by_id ?? null,
|
|
478
485
|
createdAt: new Date(row.created_at),
|
|
@@ -538,6 +545,11 @@ function sqlNullableUuidLiteral(value) {
|
|
|
538
545
|
return "NULL";
|
|
539
546
|
return `${sqlLiteral(value)}::uuid`;
|
|
540
547
|
}
|
|
548
|
+
function sqlNullableTimestampLiteral(value) {
|
|
549
|
+
if (!value)
|
|
550
|
+
return "NULL";
|
|
551
|
+
return `${sqlLiteral(value.toISOString())}::timestamptz`;
|
|
552
|
+
}
|
|
541
553
|
function sqlArrayLiteral(values) {
|
|
542
554
|
if (!values || values.length === 0)
|
|
543
555
|
return "NULL";
|
|
@@ -61,7 +61,7 @@ const API_BASE = "https://kodingo-api.onrender.com";
|
|
|
61
61
|
const SUPPORTED_EXTENSIONS = [
|
|
62
62
|
".ts", ".tsx", ".js", ".jsx",
|
|
63
63
|
".py", ".go", ".rs", ".php",
|
|
64
|
-
".java", ".kt", ".rb", ".c", ".cpp",
|
|
64
|
+
".java", ".kt", ".rb", ".c", ".cpp", ".cs",
|
|
65
65
|
];
|
|
66
66
|
const IGNORED_DIRS = [
|
|
67
67
|
"node_modules", ".git", ".kortex", "dist", "build",
|
|
@@ -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 {
|
|
@@ -201,6 +203,14 @@ async function inferAndCapture(token, symbol, repo, graph, repoRoot) {
|
|
|
201
203
|
// — Kortex's own conclusion, not traceable to one real external
|
|
202
204
|
// event, same class as kodingo-api's githubScanner.ts.
|
|
203
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("/"),
|
|
204
214
|
}),
|
|
205
215
|
});
|
|
206
216
|
return saveRes.ok;
|
|
@@ -77,7 +77,10 @@ class DbEventPort {
|
|
|
77
77
|
}
|
|
78
78
|
// No existing proposed/ignored record — insert new record as usual.
|
|
79
79
|
// Git-signal-derived captures are genuinely sourced — externalId here is
|
|
80
|
-
// a real git:<commitHash> reference (see the caller).
|
|
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).
|
|
81
84
|
const input = {
|
|
82
85
|
type: "decision",
|
|
83
86
|
content,
|
|
@@ -86,6 +89,7 @@ class DbEventPort {
|
|
|
86
89
|
confidence: 0.3,
|
|
87
90
|
evidenceKind: "sourced",
|
|
88
91
|
externalId,
|
|
92
|
+
contentAsOf: decision.proposedAt,
|
|
89
93
|
};
|
|
90
94
|
if (title)
|
|
91
95
|
input.title = title;
|
package/dist/utils/ast-parser.js
CHANGED
|
@@ -417,7 +417,7 @@ const KOTLIN_PATTERNS = [
|
|
|
417
417
|
/^(?:fun|override\s+fun|suspend\s+fun|private\s+fun|public\s+fun)\s+(\w+)/,
|
|
418
418
|
/^(?:class|data class|object|interface)\s+(\w+)/,
|
|
419
419
|
];
|
|
420
|
-
const TREE_SITTER_EXTENSIONS = new Set([".py", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".cpp"]);
|
|
420
|
+
const TREE_SITTER_EXTENSIONS = new Set([".py", ".go", ".rs", ".java", ".kt", ".rb", ".php", ".c", ".cpp", ".cs"]);
|
|
421
421
|
async function parseFile(filePath, source) {
|
|
422
422
|
const ext = path.extname(filePath).toLowerCase();
|
|
423
423
|
switch (ext) {
|
|
@@ -64,7 +64,7 @@ const ast_parser_1 = require("./ast-parser");
|
|
|
64
64
|
const SUPPORTED_EXTENSIONS = [
|
|
65
65
|
".ts", ".tsx", ".js", ".jsx",
|
|
66
66
|
".py", ".go", ".rs", ".php",
|
|
67
|
-
".java", ".kt", ".rb", ".c", ".cpp",
|
|
67
|
+
".java", ".kt", ".rb", ".c", ".cpp", ".cs",
|
|
68
68
|
];
|
|
69
69
|
const TS_JS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
70
70
|
const IGNORED_DIRS = [
|
|
@@ -104,6 +104,31 @@ const LANGUAGE_CONFIGS = {
|
|
|
104
104
|
nameFieldCandidates: [], // handled via declarator fallback
|
|
105
105
|
importTypes: ["preproc_include"],
|
|
106
106
|
},
|
|
107
|
+
// C# — build brief "Add real C#/.NET support to the CLI's parser"
|
|
108
|
+
// (2026-09-20). Real node types confirmed directly against the bundled
|
|
109
|
+
// tree-sitter-c_sharp.wasm grammar (tree-sitter-wasms package, already a
|
|
110
|
+
// real dependency here — no new native package needed, see this brief's
|
|
111
|
+
// own final report for why that matters). method_declaration,
|
|
112
|
+
// constructor_declaration, and local_function_statement all carry a real
|
|
113
|
+
// `name` field; invocation_expression carries a real `function` field
|
|
114
|
+
// whose text (e.g. "_repo.FindAsync", "Console.WriteLine") is trimmed to
|
|
115
|
+
// its trailing identifier by the same extractTrailingIdentifier() already
|
|
116
|
+
// proven for PHP's "$this->service->method" shape — no C#-specific call
|
|
117
|
+
// handling needed. Deliberately excludes operator_declaration,
|
|
118
|
+
// destructor_declaration, and property accessors — real, distinct node
|
|
119
|
+
// shapes with no `name` field (or none at all) that no other language
|
|
120
|
+
// here gets special-cased treatment for either; kept proportionate to the
|
|
121
|
+
// existing five languages' own level of coverage rather than gold-plating
|
|
122
|
+
// C# beyond them.
|
|
123
|
+
csharp: {
|
|
124
|
+
wasmFile: "tree-sitter-c_sharp.wasm",
|
|
125
|
+
functionTypes: ["method_declaration", "constructor_declaration", "local_function_statement"],
|
|
126
|
+
classTypes: ["class_declaration", "interface_declaration", "struct_declaration", "record_declaration"],
|
|
127
|
+
callTypes: ["invocation_expression"],
|
|
128
|
+
calleeFieldCandidates: ["function"],
|
|
129
|
+
nameFieldCandidates: ["name"],
|
|
130
|
+
importTypes: ["using_directive"],
|
|
131
|
+
},
|
|
107
132
|
};
|
|
108
133
|
const EXT_TO_LANGUAGE = {
|
|
109
134
|
".py": "python",
|
|
@@ -115,6 +140,7 @@ const EXT_TO_LANGUAGE = {
|
|
|
115
140
|
".php": "php",
|
|
116
141
|
".c": "c",
|
|
117
142
|
".cpp": "cpp",
|
|
143
|
+
".cs": "csharp",
|
|
118
144
|
};
|
|
119
145
|
let initialised = false;
|
|
120
146
|
const languageCache = new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kodingo-cli",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"description": "Kodingo CLI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
17
|
"dev": "tsc -p tsconfig.json --watch",
|
|
18
18
|
"start": "node dist/cli.js",
|
|
19
|
-
"test": "
|
|
19
|
+
"test": "npm run build && node --test dist/__tests__/*.js",
|
|
20
20
|
"prepare": "npm run build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
@@ -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.3.
|
|
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",
|