@aprimediet/codewalker 1.3.0 → 1.4.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 +19 -4
- package/package.json +1 -1
- package/prompts/codewalker.md +6 -1
- package/skills/codewalker/SKILL.md +54 -7
- package/src/analyze/analyzer.test.ts +214 -0
- package/src/analyze/analyzer.ts +290 -0
- package/src/analyze/cards.test.ts +156 -0
- package/src/analyze/cards.ts +110 -0
- package/src/analyze/coverage.test.ts +158 -0
- package/src/analyze/coverage.ts +98 -0
- package/src/analyze/debt.test.ts +111 -0
- package/src/analyze/debt.ts +180 -0
- package/src/analyze/review.test.ts +127 -0
- package/src/analyze/review.ts +127 -0
- package/src/db.test.ts +223 -3
- package/src/db.ts +191 -1
- package/src/format.test.ts +97 -0
- package/src/format.ts +8 -0
- package/src/index.contract.test.ts +31 -0
- package/src/index.ts +227 -14
- package/src/notes-cards.ts +1 -1
- package/src/notes.ts +6 -0
- package/src/project.test.ts +9 -0
- package/src/project.ts +5 -1
- package/src/query.test.ts +76 -1
- package/src/query.ts +11 -6
- package/src/types.ts +31 -3
package/src/db.ts
CHANGED
|
@@ -28,7 +28,7 @@ export function openDb(dbPath: string): DatabaseType {
|
|
|
28
28
|
/** Bootstrap DDL — idempotent (all CREATE use IF NOT EXISTS). */
|
|
29
29
|
export function bootstrapDb(db: DatabaseType): void {
|
|
30
30
|
db.exec(`
|
|
31
|
-
PRAGMA user_version =
|
|
31
|
+
PRAGMA user_version = 4;
|
|
32
32
|
|
|
33
33
|
CREATE TABLE IF NOT EXISTS files (
|
|
34
34
|
path TEXT PRIMARY KEY,
|
|
@@ -155,6 +155,45 @@ export function bootstrapDb(db: DatabaseType): void {
|
|
|
155
155
|
INSERT INTO notes_fts(rowid, title, body, tags)
|
|
156
156
|
VALUES (new.id, new.title, new.body, new.tags);
|
|
157
157
|
END;
|
|
158
|
+
|
|
159
|
+
-- v1.4: Analysis table for coverage/debt/practice findings
|
|
160
|
+
CREATE TABLE IF NOT EXISTS analysis (
|
|
161
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
162
|
+
finding_kind TEXT NOT NULL,
|
|
163
|
+
title TEXT NOT NULL,
|
|
164
|
+
severity TEXT,
|
|
165
|
+
file_path TEXT,
|
|
166
|
+
line_start INTEGER,
|
|
167
|
+
line_end INTEGER,
|
|
168
|
+
metric TEXT,
|
|
169
|
+
body TEXT,
|
|
170
|
+
related TEXT,
|
|
171
|
+
card_path TEXT,
|
|
172
|
+
created_at TEXT
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS analysis_fts USING fts5(
|
|
176
|
+
title, body, metric,
|
|
177
|
+
content='analysis', content_rowid='id',
|
|
178
|
+
tokenize='unicode61 remove_diacritics 2'
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
CREATE TRIGGER IF NOT EXISTS analysis_ai AFTER INSERT ON analysis BEGIN
|
|
182
|
+
INSERT INTO analysis_fts(rowid, title, body, metric)
|
|
183
|
+
VALUES (new.id, new.title, new.body, new.metric);
|
|
184
|
+
END;
|
|
185
|
+
|
|
186
|
+
CREATE TRIGGER IF NOT EXISTS analysis_ad AFTER DELETE ON analysis BEGIN
|
|
187
|
+
INSERT INTO analysis_fts(analysis_fts, rowid, title, body, metric)
|
|
188
|
+
VALUES ('delete', old.id, old.title, old.body, old.metric);
|
|
189
|
+
END;
|
|
190
|
+
|
|
191
|
+
CREATE TRIGGER IF NOT EXISTS analysis_au AFTER UPDATE ON analysis BEGIN
|
|
192
|
+
INSERT INTO analysis_fts(analysis_fts, rowid, title, body, metric)
|
|
193
|
+
VALUES ('delete', old.id, old.title, old.body, old.metric);
|
|
194
|
+
INSERT INTO analysis_fts(rowid, title, body, metric)
|
|
195
|
+
VALUES (new.id, new.title, new.body, new.metric);
|
|
196
|
+
END;
|
|
158
197
|
`);
|
|
159
198
|
}
|
|
160
199
|
|
|
@@ -171,6 +210,7 @@ export function rebuildFtsIndexes(db: DatabaseType): void {
|
|
|
171
210
|
db.exec("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild')");
|
|
172
211
|
db.exec("INSERT INTO lib_symbols_fts(lib_symbols_fts) VALUES('rebuild')");
|
|
173
212
|
db.exec("INSERT INTO notes_fts(notes_fts) VALUES('rebuild')");
|
|
213
|
+
db.exec("INSERT INTO analysis_fts(analysis_fts) VALUES('rebuild')");
|
|
174
214
|
}
|
|
175
215
|
|
|
176
216
|
/** Upsert a file record. */
|
|
@@ -378,6 +418,156 @@ export function searchNotes(
|
|
|
378
418
|
}));
|
|
379
419
|
}
|
|
380
420
|
|
|
421
|
+
// ── Analysis CRUD ────────────────────────────────────────────
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Allowed finding kinds.
|
|
425
|
+
*/
|
|
426
|
+
const VALID_FINDING_KINDS = new Set(["coverage", "debt", "practice"]);
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Upsert a finding keyed on (finding_kind, file_path, title).
|
|
430
|
+
* Returns the row id.
|
|
431
|
+
*/
|
|
432
|
+
export function upsertFinding(
|
|
433
|
+
db: DatabaseType,
|
|
434
|
+
finding: {
|
|
435
|
+
finding_kind: string;
|
|
436
|
+
title: string;
|
|
437
|
+
severity?: string;
|
|
438
|
+
file_path?: string;
|
|
439
|
+
line_start?: number;
|
|
440
|
+
line_end?: number;
|
|
441
|
+
metric?: string;
|
|
442
|
+
body?: string;
|
|
443
|
+
related?: string;
|
|
444
|
+
card_path?: string;
|
|
445
|
+
},
|
|
446
|
+
): number {
|
|
447
|
+
if (!VALID_FINDING_KINDS.has(finding.finding_kind)) {
|
|
448
|
+
throw new Error(`Invalid finding_kind "${finding.finding_kind}". Must be coverage, debt, or practice.`);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const existing = db.prepare(
|
|
452
|
+
"SELECT id FROM analysis WHERE finding_kind = ? AND file_path = ? AND title = ?",
|
|
453
|
+
).get(finding.finding_kind, finding.file_path ?? "", finding.title) as { id: number } | undefined;
|
|
454
|
+
|
|
455
|
+
if (existing) {
|
|
456
|
+
db.prepare(
|
|
457
|
+
`UPDATE analysis SET severity=?, line_start=?, line_end=?, metric=?, body=?, related=?, card_path=?, created_at=COALESCE(created_at, datetime('now'))
|
|
458
|
+
WHERE id = ?`,
|
|
459
|
+
).run(
|
|
460
|
+
finding.severity ?? null, finding.line_start ?? null, finding.line_end ?? null,
|
|
461
|
+
finding.metric ?? null, finding.body ?? null, finding.related ?? null,
|
|
462
|
+
finding.card_path ?? null, existing.id,
|
|
463
|
+
);
|
|
464
|
+
return existing.id;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const result = db.prepare(
|
|
468
|
+
`INSERT INTO analysis (finding_kind, title, severity, file_path, line_start, line_end, metric, body, related, card_path, created_at)
|
|
469
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))`,
|
|
470
|
+
).run(
|
|
471
|
+
finding.finding_kind, finding.title, finding.severity ?? null, finding.file_path ?? "",
|
|
472
|
+
finding.line_start ?? null, finding.line_end ?? null, finding.metric ?? null,
|
|
473
|
+
finding.body ?? null, finding.related ?? null, finding.card_path ?? null,
|
|
474
|
+
);
|
|
475
|
+
return Number(result.lastInsertRowid);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Delete all findings for a given file of a given kind.
|
|
480
|
+
* The analysis_ad trigger removes the FTS rows.
|
|
481
|
+
*/
|
|
482
|
+
export function deleteFindingsForFile(
|
|
483
|
+
db: DatabaseType,
|
|
484
|
+
findingKind: string,
|
|
485
|
+
filePath: string,
|
|
486
|
+
): void {
|
|
487
|
+
db.prepare("DELETE FROM analysis WHERE finding_kind = ? AND file_path = ?").run(findingKind, filePath);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Search findings via FTS5 MATCH, ranked by bm25.
|
|
492
|
+
* Empty query returns all findings ordered by severity, title.
|
|
493
|
+
* Each result is shaped as a QueryResultRow with source:'analysis'.
|
|
494
|
+
*/
|
|
495
|
+
export function searchFindings(
|
|
496
|
+
db: DatabaseType,
|
|
497
|
+
query: string,
|
|
498
|
+
kindFilter?: string,
|
|
499
|
+
limit = 10,
|
|
500
|
+
): Array<{
|
|
501
|
+
id: number;
|
|
502
|
+
name: string;
|
|
503
|
+
kind: string;
|
|
504
|
+
summary: string;
|
|
505
|
+
score: number;
|
|
506
|
+
source: "analysis";
|
|
507
|
+
finding_kind: string;
|
|
508
|
+
severity: string | null;
|
|
509
|
+
metric: string | null;
|
|
510
|
+
file_path: string;
|
|
511
|
+
line_start: number;
|
|
512
|
+
line_end: number;
|
|
513
|
+
}> {
|
|
514
|
+
if (!query.trim()) {
|
|
515
|
+
let sql = `
|
|
516
|
+
SELECT a.id, a.title as name, a.finding_kind as kind, a.body as summary,
|
|
517
|
+
a.severity, a.metric, a.file_path, a.line_start, a.line_end, 0.0 as score
|
|
518
|
+
FROM analysis a
|
|
519
|
+
`;
|
|
520
|
+
const params: unknown[] = [];
|
|
521
|
+
if (kindFilter) {
|
|
522
|
+
sql += " WHERE a.finding_kind = ?";
|
|
523
|
+
params.push(kindFilter);
|
|
524
|
+
}
|
|
525
|
+
sql += " ORDER BY CASE a.severity WHEN 'high' THEN 1 WHEN 'warn' THEN 2 WHEN 'info' THEN 3 ELSE 4 END, a.title LIMIT ?";
|
|
526
|
+
params.push(limit);
|
|
527
|
+
const rows = db.prepare(sql).all(...params) as any[];
|
|
528
|
+
return rows.map((r) => ({
|
|
529
|
+
...r,
|
|
530
|
+
source: "analysis" as const,
|
|
531
|
+
finding_kind: r.kind as string,
|
|
532
|
+
file_path: r.file_path ?? "",
|
|
533
|
+
line_start: r.line_start ?? 0,
|
|
534
|
+
line_end: r.line_end ?? 0,
|
|
535
|
+
severity: r.severity ?? null,
|
|
536
|
+
metric: r.metric ?? null,
|
|
537
|
+
}));
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
let sql = `
|
|
541
|
+
SELECT a.id, a.title as name, a.finding_kind as kind, a.body as summary,
|
|
542
|
+
a.severity, a.metric, a.file_path, a.line_start, a.line_end,
|
|
543
|
+
bm25(analysis_fts, 10.0, 5.0, 3.0) as score
|
|
544
|
+
FROM analysis_fts
|
|
545
|
+
JOIN analysis a ON a.id = analysis_fts.rowid
|
|
546
|
+
WHERE analysis_fts MATCH ?
|
|
547
|
+
`;
|
|
548
|
+
const params: unknown[] = [query];
|
|
549
|
+
|
|
550
|
+
if (kindFilter) {
|
|
551
|
+
sql += " AND a.finding_kind = ?";
|
|
552
|
+
params.push(kindFilter);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
sql += " ORDER BY CASE a.severity WHEN 'high' THEN 1 WHEN 'warn' THEN 2 WHEN 'info' THEN 3 ELSE 4 END, score LIMIT ?";
|
|
556
|
+
params.push(limit);
|
|
557
|
+
|
|
558
|
+
const rows = db.prepare(sql).all(...params) as any[];
|
|
559
|
+
return rows.map((r) => ({
|
|
560
|
+
...r,
|
|
561
|
+
source: "analysis" as const,
|
|
562
|
+
finding_kind: r.kind as string,
|
|
563
|
+
file_path: r.file_path ?? "",
|
|
564
|
+
line_start: r.line_start ?? 0,
|
|
565
|
+
line_end: r.line_end ?? 0,
|
|
566
|
+
severity: r.severity ?? null,
|
|
567
|
+
metric: r.metric ?? null,
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
|
|
381
571
|
// ── Enrichment helpers ──────────────────────────────────────────
|
|
382
572
|
|
|
383
573
|
/**
|
package/src/format.test.ts
CHANGED
|
@@ -166,6 +166,103 @@ describe('formatCompact with note rows', () => {
|
|
|
166
166
|
});
|
|
167
167
|
});
|
|
168
168
|
|
|
169
|
+
describe('formatCompact with analysis rows', () => {
|
|
170
|
+
it('renders a coverage finding row with [coverage] prefix and severity', () => {
|
|
171
|
+
const rows: QueryResultRow[] = [{
|
|
172
|
+
id: 1, name: 'Low coverage: src/auth/token.ts', kind: 'coverage',
|
|
173
|
+
file_path: 'src/auth/token.ts', line_start: 0, line_end: 0,
|
|
174
|
+
signature: '', summary: 'Token path is under-tested.',
|
|
175
|
+
score: 0.5, source: 'analysis', finding_kind: 'coverage',
|
|
176
|
+
severity: 'warn', metric: '38% (24/63 lines)',
|
|
177
|
+
}];
|
|
178
|
+
const result = formatCompact(rows, null);
|
|
179
|
+
expect(result).toContain('Low coverage: src/auth/token.ts');
|
|
180
|
+
expect(result).toContain('[coverage]');
|
|
181
|
+
expect(result).toContain('warn');
|
|
182
|
+
expect(result).toContain('38%');
|
|
183
|
+
expect(result).toContain('token.ts');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('renders a debt finding row with [debt] prefix', () => {
|
|
187
|
+
const rows: QueryResultRow[] = [{
|
|
188
|
+
id: 1, name: 'TODO: fix this', kind: 'debt',
|
|
189
|
+
file_path: 'src/a.ts', line_start: 5, line_end: 5,
|
|
190
|
+
signature: '', summary: 'Need to handle edge case',
|
|
191
|
+
score: 0.5, source: 'analysis', finding_kind: 'debt',
|
|
192
|
+
severity: 'info', metric: 'TODO',
|
|
193
|
+
}];
|
|
194
|
+
const result = formatCompact(rows, null);
|
|
195
|
+
expect(result).toContain('[debt]');
|
|
196
|
+
expect(result).toContain('TODO: fix this');
|
|
197
|
+
expect(result).toContain('info');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('renders a practice finding row with [practice] prefix', () => {
|
|
201
|
+
const rows: QueryResultRow[] = [{
|
|
202
|
+
id: 1, name: 'Missing error handling', kind: 'practice',
|
|
203
|
+
file_path: 'src/api/route.ts', line_start: 15, line_end: 15,
|
|
204
|
+
signature: '', summary: 'No error handling in this function.',
|
|
205
|
+
score: 0.5, source: 'analysis', finding_kind: 'practice',
|
|
206
|
+
severity: 'high', metric: '',
|
|
207
|
+
}];
|
|
208
|
+
const result = formatCompact(rows, null);
|
|
209
|
+
expect(result).toContain('[practice]');
|
|
210
|
+
expect(result).toContain('high');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('renders mixed code + analysis rows', () => {
|
|
214
|
+
const codeRow: QueryResultRow = {
|
|
215
|
+
id: 1, name: 'myFunc', kind: 'function',
|
|
216
|
+
file_path: 'src/util/helper.ts', line_start: 10, line_end: 20,
|
|
217
|
+
signature: '', summary: 'Does something', score: 0.5,
|
|
218
|
+
};
|
|
219
|
+
const analysisRow: QueryResultRow = {
|
|
220
|
+
id: 2, name: 'Low coverage', kind: 'coverage',
|
|
221
|
+
file_path: 'src/util/helper.ts', line_start: 0, line_end: 0,
|
|
222
|
+
signature: '', summary: 'Low coverage.',
|
|
223
|
+
score: 0.3, source: 'analysis', finding_kind: 'coverage',
|
|
224
|
+
severity: 'warn', metric: '30%',
|
|
225
|
+
};
|
|
226
|
+
const result = formatCompact([codeRow, analysisRow], null);
|
|
227
|
+
expect(result).toContain('helper.ts:10-20');
|
|
228
|
+
expect(result).toContain('[coverage]');
|
|
229
|
+
expect(result.split('\n')).toHaveLength(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('renders mixed code + lib + note + analysis rows', () => {
|
|
233
|
+
const codeRow: QueryResultRow = {
|
|
234
|
+
id: 1, name: 'myFunc', kind: 'function',
|
|
235
|
+
file_path: 'src/util/helper.ts', line_start: 10, line_end: 20,
|
|
236
|
+
signature: '', summary: 'Does something', score: 0.5,
|
|
237
|
+
};
|
|
238
|
+
const libRow: QueryResultRow = {
|
|
239
|
+
id: 100, name: 'createMiddleware', kind: 'function',
|
|
240
|
+
file_path: 'hono/dist/helper.d.ts', line_start: 0, line_end: 0,
|
|
241
|
+
signature: '', summary: 'Define a typed middleware handler.',
|
|
242
|
+
score: 0.3, source: 'lib', lib: 'hono', version: '4.6.3',
|
|
243
|
+
};
|
|
244
|
+
const noteRow: QueryResultRow = {
|
|
245
|
+
id: 1, name: 'Retry Key', kind: 'glossary',
|
|
246
|
+
file_path: '', line_start: 0, line_end: 0,
|
|
247
|
+
signature: '', summary: 'Key for idempotent retries.',
|
|
248
|
+
score: 0.5, source: 'note', note_kind: 'glossary', tags: '',
|
|
249
|
+
};
|
|
250
|
+
const analysisRow: QueryResultRow = {
|
|
251
|
+
id: 2, name: 'Low coverage', kind: 'coverage',
|
|
252
|
+
file_path: 'src/util/helper.ts', line_start: 0, line_end: 0,
|
|
253
|
+
signature: '', summary: 'Low coverage.',
|
|
254
|
+
score: 0.3, source: 'analysis', finding_kind: 'coverage',
|
|
255
|
+
severity: 'warn', metric: '30%',
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = formatCompact([codeRow, libRow, noteRow, analysisRow], null);
|
|
259
|
+
expect(result).toContain('[coverage]');
|
|
260
|
+
expect(result).toContain('[hono@4.6.3]');
|
|
261
|
+
expect(result).toContain('[glossary]');
|
|
262
|
+
expect(result.split('\n')).toHaveLength(4);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
169
266
|
describe('formatCardBody', () => {
|
|
170
267
|
it('returns the card body text', () => {
|
|
171
268
|
const body = '# myFunc\n\nDoes something.\n';
|
package/src/format.ts
CHANGED
|
@@ -32,6 +32,14 @@ export function formatCompact(
|
|
|
32
32
|
const summary = truncate(row.summary || "", SUMMARY_MAX);
|
|
33
33
|
return `${row.name} · ${row.note_kind} · ${prefix} · ${summary}`;
|
|
34
34
|
}
|
|
35
|
+
if (row.source === "analysis") {
|
|
36
|
+
const kindTag = `[${row.finding_kind || "finding"}]`;
|
|
37
|
+
const sev = row.severity ? ` · ${row.severity}` : "";
|
|
38
|
+
const metric = row.metric ? ` · ${row.metric}` : "";
|
|
39
|
+
const loc = row.file_path ? ` · ${basename(row.file_path)}` : "";
|
|
40
|
+
const summary = truncate(row.summary || row.name || "", SUMMARY_MAX);
|
|
41
|
+
return `${row.name}${loc}${sev}${metric} · ${kindTag} · ${summary}`;
|
|
42
|
+
}
|
|
35
43
|
if (row.source === "lib" && row.lib && row.version) {
|
|
36
44
|
const origin = `[${row.lib}@${row.version}]`;
|
|
37
45
|
const summary = truncate(row.summary || "", SUMMARY_MAX);
|
|
@@ -78,6 +78,15 @@ describe('index.ts contract', () => {
|
|
|
78
78
|
expect(noteParams.properties).toHaveProperty('title');
|
|
79
79
|
expect(noteParams.properties).toHaveProperty('body');
|
|
80
80
|
|
|
81
|
+
// Check v1.4 tools
|
|
82
|
+
const findingTool = stub.tools.find(t => t.name === 'codewalker_finding');
|
|
83
|
+
expect(findingTool).toBeDefined();
|
|
84
|
+
expect(findingTool!.description).toContain('finding');
|
|
85
|
+
const findingParams = (findingTool!.parameters as any);
|
|
86
|
+
expect(findingParams.properties).toHaveProperty('kind');
|
|
87
|
+
expect(findingParams.properties).toHaveProperty('title');
|
|
88
|
+
expect(findingParams.properties).toHaveProperty('body');
|
|
89
|
+
|
|
81
90
|
// Check tool has a source parameter
|
|
82
91
|
const toolParams = (queryTool!.parameters as any);
|
|
83
92
|
expect(toolParams.properties).toHaveProperty('source');
|
|
@@ -173,4 +182,26 @@ describe('index.ts contract', () => {
|
|
|
173
182
|
expect(cmd.description).toContain('glossary');
|
|
174
183
|
expect(cmd.description).toContain('decisions');
|
|
175
184
|
});
|
|
185
|
+
|
|
186
|
+
it('command description includes v1.4 subcommands (analyze, review, findings, conventions)', async () => {
|
|
187
|
+
const mod = await import('./index.ts');
|
|
188
|
+
const stub = createPiStub();
|
|
189
|
+
mod.default(stub.api as any);
|
|
190
|
+
|
|
191
|
+
const cmd = stub.commands.find(c => c.name === 'codewalker')!;
|
|
192
|
+
expect(cmd.description).toContain('analyze');
|
|
193
|
+
expect(cmd.description).toContain('review');
|
|
194
|
+
expect(cmd.description).toContain('findings');
|
|
195
|
+
expect(cmd.description).toContain('conventions');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('codewalker_note tool accepts type="convention"', async () => {
|
|
199
|
+
const mod = await import('./index.ts');
|
|
200
|
+
const stub = createPiStub();
|
|
201
|
+
mod.default(stub.api as any);
|
|
202
|
+
|
|
203
|
+
const noteTool = stub.tools.find(t => t.name === 'codewalker_note')!;
|
|
204
|
+
const noteParams = (noteTool!.parameters as any);
|
|
205
|
+
expect(noteParams.properties.type.description).toContain('convention');
|
|
206
|
+
});
|
|
176
207
|
});
|