@aprimediet/codewalker 1.2.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 +45 -5
- package/package.json +1 -1
- package/prompts/codewalker.md +8 -1
- package/skills/codewalker/SKILL.md +165 -28
- 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/cards.test.ts +123 -1
- package/src/cards.ts +53 -0
- package/src/db.test.ts +484 -8
- package/src/db.ts +398 -2
- package/src/enrich.test.ts +102 -0
- package/src/enrich.ts +107 -0
- package/src/format.test.ts +148 -0
- package/src/format.ts +13 -0
- package/src/index.contract.test.ts +62 -0
- package/src/index.ts +427 -9
- package/src/indexer.heal.test.ts +90 -0
- package/src/indexer.ts +9 -1
- package/src/notes-cards.test.ts +99 -0
- package/src/notes-cards.ts +92 -0
- package/src/notes.test.ts +172 -0
- package/src/notes.ts +151 -0
- package/src/project.test.ts +21 -1
- package/src/project.ts +9 -1
- package/src/query.test.ts +152 -1
- package/src/query.ts +15 -6
- package/src/types.ts +46 -2
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review helpers for codewalker v1.4.
|
|
3
|
+
*
|
|
4
|
+
* Pure/lightweight helpers for the agent-driven best-practice review workflow:
|
|
5
|
+
* - `validateReviewPath()` — ensures path is provided
|
|
6
|
+
* - `checkReviewCap()` — enforces the review cap guardrail
|
|
7
|
+
* - `selectFilesForReview()` — selects files under a path prefix with cap
|
|
8
|
+
* - `formatReviewWorklist()` — formats worklist items for agent display
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the shape of enrich.ts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Default maximum files per review command. */
|
|
14
|
+
export const DEFAULT_REVIEW_CAP = 25;
|
|
15
|
+
|
|
16
|
+
/** Result of validateReviewPath. */
|
|
17
|
+
export interface PathValidation {
|
|
18
|
+
valid: boolean;
|
|
19
|
+
error?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Result of checkReviewCap. */
|
|
23
|
+
export interface CapCheck {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
count: number;
|
|
26
|
+
/** Number of files over the cap (only when !ok). */
|
|
27
|
+
skipped: number;
|
|
28
|
+
error?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate that a review path was provided.
|
|
33
|
+
*/
|
|
34
|
+
export function validateReviewPath(path: string | undefined | null): PathValidation {
|
|
35
|
+
const trimmed = (path ?? "").trim();
|
|
36
|
+
if (!trimmed) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
error: 'Specify a path, e.g. src/auth. Bare /codewalker review with no path is not allowed.',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return { valid: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check whether a count of files exceeds the cap.
|
|
47
|
+
* If ok is false, the caller should refuse and tell the user to narrow the path.
|
|
48
|
+
*
|
|
49
|
+
* @param count - Number of files found.
|
|
50
|
+
* @param cap - Maximum allowed (default: DEFAULT_REVIEW_CAP = 25).
|
|
51
|
+
*/
|
|
52
|
+
export function checkReviewCap(count: number, cap: number = DEFAULT_REVIEW_CAP): CapCheck {
|
|
53
|
+
if (count > cap) {
|
|
54
|
+
const skipped = count - cap;
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
count,
|
|
58
|
+
skipped,
|
|
59
|
+
error: `Refusing to review ${count} files (cap ${cap}). ` +
|
|
60
|
+
`Narrow your path or raise the cap with --max=${count}.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { ok: true, count, skipped: 0 };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Select files under a path prefix, respecting the cap.
|
|
68
|
+
* Files not matching the prefix are excluded.
|
|
69
|
+
*
|
|
70
|
+
* @param files - Full list of source file paths.
|
|
71
|
+
* @param pathPrefix - The path prefix to filter by (empty string = all files).
|
|
72
|
+
* @param cap - Maximum number to select.
|
|
73
|
+
* @returns Array of file paths matching the prefix, up to cap.
|
|
74
|
+
*/
|
|
75
|
+
export function selectFilesForReview(
|
|
76
|
+
files: string[],
|
|
77
|
+
pathPrefix: string,
|
|
78
|
+
cap: number,
|
|
79
|
+
): string[] {
|
|
80
|
+
const matching = pathPrefix
|
|
81
|
+
? files.filter(f => f.startsWith(pathPrefix))
|
|
82
|
+
: files;
|
|
83
|
+
return matching.slice(0, cap);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format a review worklist for the agent to process.
|
|
88
|
+
* Each line shows one file to review. Includes instructions about grounding
|
|
89
|
+
* findings in the project's own conventions and decisions.
|
|
90
|
+
*/
|
|
91
|
+
export function formatReviewWorklist(
|
|
92
|
+
files: string[],
|
|
93
|
+
pathPrefix: string,
|
|
94
|
+
): string {
|
|
95
|
+
if (files.length === 0) {
|
|
96
|
+
return `No files found under "${pathPrefix}" to review.`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const lines: string[] = [
|
|
100
|
+
`Found ${files.length} file(s) under "${pathPrefix}" selected for review:`,
|
|
101
|
+
"",
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
lines.push(` ${file}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push(
|
|
109
|
+
"",
|
|
110
|
+
"Instructions:",
|
|
111
|
+
` For each file above, read its content and judge it against the project's`,
|
|
112
|
+
` conventions and decisions. Before starting, query for relevant context:`,
|
|
113
|
+
` codewalker_query source:'all' query:'conventions'`,
|
|
114
|
+
` codewalker_query source:'all' query:'decisions'`,
|
|
115
|
+
"",
|
|
116
|
+
` For each issue found, call \`codewalker_finding\` with kind='practice',`,
|
|
117
|
+
` an appropriate severity, and a body grounded in a specific convention or`,
|
|
118
|
+
` decision. Avoid generic style nits — only flag what the project's own`,
|
|
119
|
+
` conventions would flag.`,
|
|
120
|
+
"",
|
|
121
|
+
` Example: \`codewalker_finding { kind: "practice", title: "...",`,
|
|
122
|
+
` file: "src/auth/token.ts", severity: "warn",`,
|
|
123
|
+
` body: "Convention X says Y, but this code does Z." }\``,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
package/src/cards.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderCard, parseCard, cardHead } from './cards.ts';
|
|
2
|
+
import { renderCard, parseCard, cardHead, updateCardSummary } from './cards.ts';
|
|
3
3
|
import type { Symbol } from './types.ts';
|
|
4
4
|
|
|
5
5
|
function makeSymbol(overrides: Partial<Symbol> = {}): Symbol {
|
|
@@ -71,6 +71,128 @@ describe('parseCard', () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
describe('updateCardSummary', () => {
|
|
75
|
+
it('replaces frontmatter summary line and appends ## What it does section', () => {
|
|
76
|
+
const card = `---
|
|
77
|
+
name: probeCompat
|
|
78
|
+
kind: function
|
|
79
|
+
signature: (cwd: string) => CompatResult
|
|
80
|
+
location: compat.ts:201-243
|
|
81
|
+
summary: Old summary
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
# probeCompat
|
|
85
|
+
|
|
86
|
+
Some body text here.
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
const updated = updateCardSummary(card, 'Detect whether minion & memory are active.');
|
|
90
|
+
|
|
91
|
+
// Frontmatter summary updated
|
|
92
|
+
expect(updated).toContain('summary: Detect whether minion & memory are active.');
|
|
93
|
+
expect(updated).not.toContain('summary: Old summary');
|
|
94
|
+
|
|
95
|
+
// ## What it does section added
|
|
96
|
+
expect(updated).toContain('## What it does');
|
|
97
|
+
expect(updated).toContain('Detect whether minion & memory are active.');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('is idempotent — second apply does not stack duplicates', () => {
|
|
101
|
+
const card = `---
|
|
102
|
+
name: probeCompat
|
|
103
|
+
kind: function
|
|
104
|
+
location: compat.ts:201-243
|
|
105
|
+
summary: Old
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
# probeCompat
|
|
109
|
+
`;
|
|
110
|
+
|
|
111
|
+
const once = updateCardSummary(card, 'First summary.');
|
|
112
|
+
const twice = updateCardSummary(once, 'Second summary.');
|
|
113
|
+
|
|
114
|
+
// Has the new summary
|
|
115
|
+
expect(twice).toContain('summary: Second summary.');
|
|
116
|
+
expect(twice).not.toContain('summary: First summary.');
|
|
117
|
+
|
|
118
|
+
// Only one ## What it does section
|
|
119
|
+
const matches = twice.match(/## What it does/g);
|
|
120
|
+
expect(matches).toHaveLength(1);
|
|
121
|
+
|
|
122
|
+
// Only one summary in frontmatter
|
|
123
|
+
const summaryMatches = twice.match(/^summary:/gm);
|
|
124
|
+
expect(summaryMatches).toHaveLength(1);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('handles empty summary in input', () => {
|
|
128
|
+
const card = `---
|
|
129
|
+
name: foo
|
|
130
|
+
kind: function
|
|
131
|
+
location: foo.ts:1-10
|
|
132
|
+
summary:
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
# foo
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
const updated = updateCardSummary(card, 'New summary.');
|
|
139
|
+
expect(updated).toContain('summary: New summary.');
|
|
140
|
+
expect(updated).toContain('## What it does');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('handles card with no existing body', () => {
|
|
144
|
+
const card = `---
|
|
145
|
+
name: bar
|
|
146
|
+
kind: const
|
|
147
|
+
location: bar.ts:5-5
|
|
148
|
+
summary:
|
|
149
|
+
---
|
|
150
|
+
`;
|
|
151
|
+
|
|
152
|
+
const updated = updateCardSummary(card, 'Just a constant.');
|
|
153
|
+
expect(updated).toContain('summary: Just a constant.');
|
|
154
|
+
expect(updated).toContain('## What it does');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('does not break frontmatter for cards with tags field', () => {
|
|
158
|
+
const card = `---
|
|
159
|
+
name: myFunc
|
|
160
|
+
kind: function
|
|
161
|
+
location: a.ts:1-10
|
|
162
|
+
tags: alpha, beta
|
|
163
|
+
summary:
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
# myFunc
|
|
167
|
+
`;
|
|
168
|
+
|
|
169
|
+
const updated = updateCardSummary(card, 'A function that does something.');
|
|
170
|
+
const parsed = parseCard(updated);
|
|
171
|
+
expect(parsed).not.toBeNull();
|
|
172
|
+
expect(parsed!.head.name).toBe('myFunc');
|
|
173
|
+
expect(parsed!.head.summary).toBe('A function that does something.');
|
|
174
|
+
expect(parsed!.head.tags).toEqual(['alpha', 'beta']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('round-trips through parseCard', () => {
|
|
178
|
+
const card = `---
|
|
179
|
+
name: probeCompat
|
|
180
|
+
kind: function
|
|
181
|
+
location: compat.ts:201-243
|
|
182
|
+
summary: Old
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
# probeCompat
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
const updated = updateCardSummary(card, 'A function to detect integrations.');
|
|
189
|
+
const parsed = parseCard(updated);
|
|
190
|
+
expect(parsed).not.toBeNull();
|
|
191
|
+
expect(parsed!.head.summary).toBe('A function to detect integrations.');
|
|
192
|
+
expect(parsed!.body).toContain('A function to detect integrations.');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
74
196
|
describe('cardHead', () => {
|
|
75
197
|
it('returns only the frontmatter head from a card', () => {
|
|
76
198
|
const sym = makeSymbol();
|
package/src/cards.ts
CHANGED
|
@@ -76,6 +76,59 @@ export function parseCard(text: string): { head: CardHead; body: string } | null
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Pure: rewrite a card's frontmatter summary: line and upsert a ## What it does body section.
|
|
81
|
+
* Idempotent — enriching twice yields the same card (replaces the section, doesn't stack duplicates).
|
|
82
|
+
*/
|
|
83
|
+
export function updateCardSummary(cardText: string, summary: string): string {
|
|
84
|
+
const trimmed = cardText.trim();
|
|
85
|
+
|
|
86
|
+
// Split into frontmatter and body
|
|
87
|
+
const endOfFm = trimmed.indexOf("\n---", 3);
|
|
88
|
+
if (endOfFm === -1) return cardText; // invalid card, return as-is
|
|
89
|
+
|
|
90
|
+
const fmRaw = trimmed.slice(3, endOfFm).trim();
|
|
91
|
+
const bodyRaw = trimmed.slice(endOfFm + 4).trim();
|
|
92
|
+
|
|
93
|
+
// Rebuild frontmatter, replacing summary: line
|
|
94
|
+
const fmLines = fmRaw.split("\n");
|
|
95
|
+
const newFmLines: string[] = [];
|
|
96
|
+
let summaryReplaced = false;
|
|
97
|
+
|
|
98
|
+
for (const line of fmLines) {
|
|
99
|
+
const sep = line.indexOf(":");
|
|
100
|
+
if (sep > 0) {
|
|
101
|
+
const key = line.slice(0, sep).trim();
|
|
102
|
+
if (key === "summary") {
|
|
103
|
+
newFmLines.push(`summary: ${summary}`);
|
|
104
|
+
summaryReplaced = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
newFmLines.push(line);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!summaryReplaced) {
|
|
112
|
+
newFmLines.push(`summary: ${summary}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const newFrontmatter = `---\n${newFmLines.join("\n")}\n---`;
|
|
116
|
+
|
|
117
|
+
// Build body — replace existing ## What it does section if present
|
|
118
|
+
let body = bodyRaw;
|
|
119
|
+
const whatItDoesRegex = /## What it does[\s\S]*?(?=\n## |$)/;
|
|
120
|
+
const whatItDoesSection = `## What it does\n\n${summary}`;
|
|
121
|
+
|
|
122
|
+
if (whatItDoesRegex.test(body)) {
|
|
123
|
+
body = body.replace(whatItDoesRegex, whatItDoesSection);
|
|
124
|
+
} else {
|
|
125
|
+
// Append after existing body (or replace empty body)
|
|
126
|
+
body = body ? `${body}\n\n${whatItDoesSection}` : whatItDoesSection;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return `${newFrontmatter}\n\n${body}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
79
132
|
/**
|
|
80
133
|
* Extract only the frontmatter head from a card — the compact, agent-cheap view.
|
|
81
134
|
* Returns null if the card is invalid.
|