@mainahq/core 1.0.2 → 1.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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/src/ai/__tests__/availability.test.ts +131 -0
  3. package/src/ai/__tests__/delegation.test.ts +55 -1
  4. package/src/ai/availability.ts +23 -0
  5. package/src/ai/delegation.ts +5 -3
  6. package/src/context/__tests__/budget.test.ts +29 -6
  7. package/src/context/__tests__/engine.test.ts +1 -0
  8. package/src/context/__tests__/selector.test.ts +23 -3
  9. package/src/context/__tests__/wiki.test.ts +349 -0
  10. package/src/context/budget.ts +12 -8
  11. package/src/context/engine.ts +37 -0
  12. package/src/context/selector.ts +30 -4
  13. package/src/context/wiki.ts +296 -0
  14. package/src/db/index.ts +12 -0
  15. package/src/feedback/__tests__/capture.test.ts +166 -0
  16. package/src/feedback/__tests__/signals.test.ts +144 -0
  17. package/src/feedback/__tests__/tmp-capture-1775575256633-lah0etnzlj/feedback.db +0 -0
  18. package/src/feedback/__tests__/tmp-capture-1775575256640-2xmjme4qraa/feedback.db +0 -0
  19. package/src/feedback/capture.ts +102 -0
  20. package/src/feedback/signals.ts +68 -0
  21. package/src/index.ts +108 -1
  22. package/src/init/__tests__/init.test.ts +477 -18
  23. package/src/init/index.ts +419 -13
  24. package/src/language/__tests__/__fixtures__/detect/composer.lock +1 -0
  25. package/src/prompts/defaults/index.ts +3 -1
  26. package/src/prompts/defaults/wiki-compile.md +20 -0
  27. package/src/prompts/defaults/wiki-query.md +18 -0
  28. package/src/stats/__tests__/tool-usage.test.ts +133 -0
  29. package/src/stats/tracker.ts +92 -0
  30. package/src/verify/__tests__/builtin.test.ts +270 -0
  31. package/src/verify/__tests__/pipeline.test.ts +11 -8
  32. package/src/verify/builtin.ts +350 -0
  33. package/src/verify/pipeline.ts +32 -2
  34. package/src/verify/tools/__tests__/wiki-lint.test.ts +784 -0
  35. package/src/verify/tools/wiki-lint-runner.ts +38 -0
  36. package/src/verify/tools/wiki-lint.ts +898 -0
  37. package/src/wiki/__tests__/compiler.test.ts +389 -0
  38. package/src/wiki/__tests__/extractors/code.test.ts +99 -0
  39. package/src/wiki/__tests__/extractors/decision.test.ts +323 -0
  40. package/src/wiki/__tests__/extractors/feature.test.ts +186 -0
  41. package/src/wiki/__tests__/extractors/workflow.test.ts +131 -0
  42. package/src/wiki/__tests__/graph.test.ts +344 -0
  43. package/src/wiki/__tests__/hooks.test.ts +119 -0
  44. package/src/wiki/__tests__/indexer.test.ts +285 -0
  45. package/src/wiki/__tests__/linker.test.ts +230 -0
  46. package/src/wiki/__tests__/louvain.test.ts +229 -0
  47. package/src/wiki/__tests__/query.test.ts +316 -0
  48. package/src/wiki/__tests__/schema.test.ts +114 -0
  49. package/src/wiki/__tests__/signals.test.ts +474 -0
  50. package/src/wiki/__tests__/state.test.ts +168 -0
  51. package/src/wiki/__tests__/tracking.test.ts +118 -0
  52. package/src/wiki/__tests__/types.test.ts +387 -0
  53. package/src/wiki/compiler.ts +1075 -0
  54. package/src/wiki/extractors/code.ts +90 -0
  55. package/src/wiki/extractors/decision.ts +217 -0
  56. package/src/wiki/extractors/feature.ts +206 -0
  57. package/src/wiki/extractors/workflow.ts +112 -0
  58. package/src/wiki/graph.ts +445 -0
  59. package/src/wiki/hooks.ts +49 -0
  60. package/src/wiki/indexer.ts +105 -0
  61. package/src/wiki/linker.ts +117 -0
  62. package/src/wiki/louvain.ts +190 -0
  63. package/src/wiki/prompts/compile-architecture.md +59 -0
  64. package/src/wiki/prompts/compile-decision.md +66 -0
  65. package/src/wiki/prompts/compile-entity.md +56 -0
  66. package/src/wiki/prompts/compile-feature.md +60 -0
  67. package/src/wiki/prompts/compile-module.md +42 -0
  68. package/src/wiki/prompts/wiki-query.md +25 -0
  69. package/src/wiki/query.ts +338 -0
  70. package/src/wiki/schema.ts +111 -0
  71. package/src/wiki/signals.ts +368 -0
  72. package/src/wiki/state.ts +89 -0
  73. package/src/wiki/tracking.ts +30 -0
  74. package/src/wiki/types.ts +169 -0
  75. package/src/workflow/context.ts +26 -0
@@ -0,0 +1,350 @@
1
+ /**
2
+ * Built-in Verify Checks — pure-function checks that always run.
3
+ *
4
+ * These provide baseline verification without requiring external linters.
5
+ * Each check is a pure function: (filePath, content) => Finding[].
6
+ * No I/O, no side effects, no subprocess spawns.
7
+ */
8
+
9
+ import type { Finding } from "./diff-filter";
10
+
11
+ // ─── Helpers ─────────────────────────────────────────────────────────────
12
+
13
+ function isTestFile(filePath: string): boolean {
14
+ return (
15
+ filePath.endsWith(".test.ts") ||
16
+ filePath.endsWith(".test.tsx") ||
17
+ filePath.endsWith(".test.js") ||
18
+ filePath.endsWith(".test.jsx") ||
19
+ filePath.endsWith(".spec.ts") ||
20
+ filePath.endsWith(".spec.tsx") ||
21
+ filePath.endsWith(".spec.js") ||
22
+ filePath.endsWith(".spec.jsx") ||
23
+ filePath.includes("__tests__/")
24
+ );
25
+ }
26
+
27
+ function isDeclarationFile(filePath: string): boolean {
28
+ return filePath.endsWith(".d.ts");
29
+ }
30
+
31
+ function isTypeScriptFile(filePath: string): boolean {
32
+ return (
33
+ filePath.endsWith(".ts") ||
34
+ filePath.endsWith(".tsx") ||
35
+ filePath.endsWith(".mts") ||
36
+ filePath.endsWith(".cts")
37
+ );
38
+ }
39
+
40
+ // ─── Check 1: console.log in non-test files ─────────────────────────────
41
+
42
+ /**
43
+ * Detect console.log/warn/error/debug/info calls in production code.
44
+ * Test files are excluded since console usage is acceptable there.
45
+ */
46
+ export function checkConsoleLogs(filePath: string, content: string): Finding[] {
47
+ if (isTestFile(filePath)) return [];
48
+
49
+ const findings: Finding[] = [];
50
+ const lines = content.split("\n");
51
+ const consolePattern = /\bconsole\.(log|warn|error|debug|info)\s*\(/;
52
+
53
+ for (const [i, line] of lines.entries()) {
54
+ if (consolePattern.test(line)) {
55
+ findings.push({
56
+ tool: "builtin",
57
+ file: filePath,
58
+ line: i + 1,
59
+ message: `console.${line.match(consolePattern)?.[1]} found in production code`,
60
+ severity: "warning",
61
+ ruleId: "no-console-log",
62
+ });
63
+ }
64
+ }
65
+
66
+ return findings;
67
+ }
68
+
69
+ // ─── Check 2: Unused imports ─────────────────────────────────────────────
70
+
71
+ /**
72
+ * Best-effort regex check for unused named imports.
73
+ * Looks for `import { X, Y }` where identifiers don't appear
74
+ * elsewhere in the file. Prefers false negatives over false positives.
75
+ */
76
+ export function checkUnusedImports(
77
+ filePath: string,
78
+ content: string,
79
+ ): Finding[] {
80
+ const findings: Finding[] = [];
81
+ const lines = content.split("\n");
82
+
83
+ // Match named imports: import { A, B } from "..." or import type { A } from "..."
84
+ const importLinePattern =
85
+ /^import\s+(?:type\s+)?{([^}]+)}\s+from\s+["'][^"']+["'];?\s*$/;
86
+
87
+ for (const [i, line] of lines.entries()) {
88
+ const match = line.match(importLinePattern);
89
+ if (!match) continue;
90
+
91
+ // Check if this is a type-only import (import type { ... })
92
+ const isTypeImport = /^import\s+type\s+\{/.test(line);
93
+
94
+ const rawNames = match[1]?.split(",") ?? [];
95
+ const importedNames: string[] = [];
96
+ for (const raw of rawNames) {
97
+ // Handle "X as Y" — the local name is Y
98
+ const parts = raw.trim().split(/\s+as\s+/);
99
+ const resolved = (parts.length > 1 ? parts[1] : parts[0])?.trim() ?? "";
100
+ if (resolved.length > 0) {
101
+ importedNames.push(resolved);
102
+ }
103
+ }
104
+
105
+ // Get the rest of the file content (excluding import lines)
106
+ const restOfFile = lines
107
+ .filter((l) => !importLinePattern.test(l))
108
+ .join("\n");
109
+
110
+ for (const name of importedNames) {
111
+ // Check if the identifier appears in the rest of the file
112
+ // Use word boundary to avoid matching substrings
113
+ const usagePattern = new RegExp(`\\b${escapeRegex(name)}\\b`);
114
+ if (!usagePattern.test(restOfFile)) {
115
+ findings.push({
116
+ tool: "builtin",
117
+ file: filePath,
118
+ line: i + 1,
119
+ message: `Import '${name}' appears unused${isTypeImport ? " (type import)" : ""}`,
120
+ severity: "warning",
121
+ ruleId: "unused-import",
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ return findings;
128
+ }
129
+
130
+ function escapeRegex(str: string): string {
131
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
132
+ }
133
+
134
+ // ─── Check 3: TODO/FIXME/HACK comments ──────────────────────────────────
135
+
136
+ /**
137
+ * Count and report TODO, FIXME, and HACK markers.
138
+ * These are informational — they don't block verification.
139
+ */
140
+ export function checkTodoComments(
141
+ filePath: string,
142
+ content: string,
143
+ ): Finding[] {
144
+ const findings: Finding[] = [];
145
+ const lines = content.split("\n");
146
+ const todoPattern = /\b(TODO|FIXME|HACK)\b/;
147
+
148
+ for (const [i, line] of lines.entries()) {
149
+ const match = line.match(todoPattern);
150
+ if (match) {
151
+ findings.push({
152
+ tool: "builtin",
153
+ file: filePath,
154
+ line: i + 1,
155
+ message: `${match[1]} comment found: ${line.trim()}`,
156
+ severity: "info",
157
+ ruleId: "todo-comment",
158
+ });
159
+ }
160
+ }
161
+
162
+ return findings;
163
+ }
164
+
165
+ // ─── Check 4: File size ──────────────────────────────────────────────────
166
+
167
+ /**
168
+ * Flag files exceeding 500 lines. Large files are harder to review
169
+ * and maintain — consider splitting them.
170
+ */
171
+ export function checkFileSize(filePath: string, content: string): Finding[] {
172
+ const lineCount = content.split("\n").length;
173
+
174
+ if (lineCount > 500) {
175
+ return [
176
+ {
177
+ tool: "builtin",
178
+ file: filePath,
179
+ line: 1,
180
+ message: `File has ${lineCount} lines (exceeds 500 line limit). Consider splitting.`,
181
+ severity: "warning",
182
+ ruleId: "file-too-long",
183
+ },
184
+ ];
185
+ }
186
+
187
+ return [];
188
+ }
189
+
190
+ // ─── Check 5: Secrets patterns ──────────────────────────────────────────
191
+
192
+ /**
193
+ * Detect hardcoded secrets: password=, secret=, token=, api_key=
194
+ * followed by a quoted or literal non-empty value (not a variable reference).
195
+ */
196
+ export function checkSecrets(filePath: string, content: string): Finding[] {
197
+ const findings: Finding[] = [];
198
+ const lines = content.split("\n");
199
+
200
+ // Patterns: key followed by = and a hardcoded value (quoted string or bare literal)
201
+ // Does NOT match variable references like process.env.X, ${VAR}, etc.
202
+ const secretPattern =
203
+ /\b(password|secret|token|api_key|apikey|api_secret|private_key|auth_token)\s*[=:]\s*["'`]([^"'`\s$]{2,})["'`]/i;
204
+
205
+ for (const [i, line] of lines.entries()) {
206
+ const match = line.match(secretPattern);
207
+ if (match) {
208
+ findings.push({
209
+ tool: "builtin",
210
+ file: filePath,
211
+ line: i + 1,
212
+ message: `Possible hardcoded ${match[1]} detected`,
213
+ severity: "error",
214
+ ruleId: "hardcoded-secret",
215
+ });
216
+ }
217
+ }
218
+
219
+ return findings;
220
+ }
221
+
222
+ // ─── Check 6: Empty catch blocks ─────────────────────────────────────────
223
+
224
+ /**
225
+ * Detect empty catch blocks (no statements, no comments).
226
+ * A catch with only whitespace is still flagged.
227
+ * A catch with a comment is considered intentional and allowed.
228
+ */
229
+ export function checkEmptyCatch(filePath: string, content: string): Finding[] {
230
+ const findings: Finding[] = [];
231
+ const lines = content.split("\n");
232
+
233
+ for (const [i, line] of lines.entries()) {
234
+ // Match catch on same line: catch (e) {}
235
+ // or catch (e) { } (with just whitespace)
236
+ const inlineMatch = line.match(/\bcatch\s*\([^)]*\)\s*\{\s*\}\s*$/);
237
+ if (inlineMatch) {
238
+ findings.push({
239
+ tool: "builtin",
240
+ file: filePath,
241
+ line: i + 1,
242
+ message: "Empty catch block — errors are silently swallowed",
243
+ severity: "warning",
244
+ ruleId: "empty-catch",
245
+ });
246
+ continue;
247
+ }
248
+
249
+ // Multi-line catch: catch (e) { on this line, } on a later line
250
+ const catchOpenMatch = line.match(/\bcatch\s*\([^)]*\)\s*\{\s*$/);
251
+ if (catchOpenMatch) {
252
+ // Look ahead for the closing brace
253
+ let blockContent = "";
254
+ let closingLine = -1;
255
+ for (let j = i + 1; j < lines.length && j < i + 20; j++) {
256
+ const nextLine = lines[j] ?? "";
257
+ if (nextLine.trim() === "}") {
258
+ closingLine = j;
259
+ break;
260
+ }
261
+ blockContent += nextLine;
262
+ }
263
+
264
+ if (closingLine !== -1) {
265
+ const trimmed = blockContent.trim();
266
+ // Empty or whitespace-only is flagged
267
+ // Comments are intentional — not flagged
268
+ if (trimmed === "") {
269
+ findings.push({
270
+ tool: "builtin",
271
+ file: filePath,
272
+ line: i + 1,
273
+ message: "Empty catch block — errors are silently swallowed",
274
+ severity: "warning",
275
+ ruleId: "empty-catch",
276
+ });
277
+ }
278
+ // If it contains a comment (// or /* or *), it's intentional
279
+ // If it contains actual code, it's not empty
280
+ // Either way, no finding needed
281
+ }
282
+ }
283
+ }
284
+
285
+ return findings;
286
+ }
287
+
288
+ // ─── Check 7: `any` type usage ───────────────────────────────────────────
289
+
290
+ /**
291
+ * Detect `any` type annotations in TypeScript files.
292
+ * Skips .d.ts files where `any` is sometimes necessary.
293
+ * Avoids false positives on words containing "any" (e.g., "many", "company")
294
+ * and on comments/strings.
295
+ */
296
+ export function checkAnyType(filePath: string, content: string): Finding[] {
297
+ if (!isTypeScriptFile(filePath)) return [];
298
+ if (isDeclarationFile(filePath)) return [];
299
+
300
+ const findings: Finding[] = [];
301
+ const lines = content.split("\n");
302
+
303
+ // Match `: any`, `as any`, `<any>`, `any[]`, `any,`, `any)`, `any;`
304
+ // — basically `any` used as a type annotation, not as a substring in identifiers
305
+ const anyTypePattern =
306
+ /(?::\s*any\b|(?:as|extends|implements)\s+any\b|<any\b|any\s*[[\]>,);|&])/;
307
+
308
+ for (const [i, line] of lines.entries()) {
309
+ // Skip comment lines
310
+ const trimmed = line.trim();
311
+ if (trimmed.startsWith("//") || trimmed.startsWith("*")) continue;
312
+
313
+ // Skip lines where 'any' only appears in a string literal
314
+ // Simple heuristic: remove string contents and check again
315
+ const withoutStrings = line
316
+ .replace(/"(?:[^"\\]|\\.)*"/g, '""')
317
+ .replace(/'(?:[^'\\]|\\.)*'/g, "''")
318
+ .replace(/`(?:[^`\\]|\\.)*`/g, "``");
319
+
320
+ if (anyTypePattern.test(withoutStrings)) {
321
+ findings.push({
322
+ tool: "builtin",
323
+ file: filePath,
324
+ line: i + 1,
325
+ message: "Usage of 'any' type — prefer explicit types or 'unknown'",
326
+ severity: "warning",
327
+ ruleId: "no-any-type",
328
+ });
329
+ }
330
+ }
331
+
332
+ return findings;
333
+ }
334
+
335
+ // ─── Aggregator ──────────────────────────────────────────────────────────
336
+
337
+ /**
338
+ * Run all built-in checks on a single file and return aggregated findings.
339
+ */
340
+ export function runBuiltinChecks(filePath: string, content: string): Finding[] {
341
+ return [
342
+ ...checkConsoleLogs(filePath, content),
343
+ ...checkUnusedImports(filePath, content),
344
+ ...checkTodoComments(filePath, content),
345
+ ...checkFileSize(filePath, content),
346
+ ...checkSecrets(filePath, content),
347
+ ...checkEmptyCatch(filePath, content),
348
+ ...checkAnyType(filePath, content),
349
+ ];
350
+ }
@@ -5,7 +5,7 @@
5
5
  * 1. Get files to check (staged files, or provided list)
6
6
  * 2. Run syntax guard FIRST — abort immediately if it fails
7
7
  * 3. Auto-detect available tools
8
- * 4. Run all available tools in PARALLEL (slop, semgrep, trivy, secretlint)
8
+ * 4. Run all available tools in PARALLEL (slop, builtin, semgrep, trivy, secretlint)
9
9
  * 5. Collect all findings
10
10
  * 6. Apply diff-only filter (unless diffOnly === false)
11
11
  * 7. Determine pass/fail: passed = no error-severity findings
@@ -19,6 +19,7 @@ import { detectLanguages } from "../language/detect";
19
19
  import type { LanguageId } from "../language/profile";
20
20
  import { getProfile } from "../language/profile";
21
21
  import { type AIReviewResult, runAIReview } from "./ai-review";
22
+ import { runBuiltinChecks } from "./builtin";
22
23
  import { checkConsistency } from "./consistency";
23
24
  import { runCoverage } from "./coverage";
24
25
  import type { DetectedTool } from "./detect";
@@ -32,6 +33,7 @@ import { detectSlop } from "./slop";
32
33
  import { runSonar } from "./sonar";
33
34
  import type { SyntaxDiagnostic } from "./syntax-guard";
34
35
  import { syntaxGuard } from "./syntax-guard";
36
+ import { runWikiLintTool } from "./tools/wiki-lint-runner";
35
37
  import { runTrivy } from "./trivy";
36
38
  import { runTypecheck } from "./typecheck";
37
39
 
@@ -240,10 +242,38 @@ export async function runPipeline(
240
242
  }),
241
243
  );
242
244
 
245
+ // Built-in checks (always run, pure functions, no external dependencies)
246
+ toolPromises.push(
247
+ runToolWithTiming("builtin", async () => {
248
+ const findings: Finding[] = [];
249
+ for (const file of files) {
250
+ try {
251
+ const fullPath = file.startsWith("/") ? file : `${cwd}/${file}`;
252
+ const text = await Bun.file(fullPath).text();
253
+ findings.push(...runBuiltinChecks(file, text));
254
+ } catch {
255
+ // File read failure should not block pipeline
256
+ }
257
+ }
258
+ return { findings, skipped: false };
259
+ }),
260
+ );
261
+
262
+ // Wiki lint — only runs if .maina/wiki/ exists (auto-skips otherwise)
263
+ toolPromises.push(
264
+ runToolWithTiming("wiki-lint", () => runWikiLintTool({ cwd, mainaDir })),
265
+ );
266
+
243
267
  const toolReports = await Promise.all(toolPromises);
244
268
 
245
269
  // ── Step 4b: Warn if all external tools were skipped ─────────────────
246
- const builtInTools = new Set(["slop", "typecheck", "consistency"]);
270
+ const builtInTools = new Set([
271
+ "slop",
272
+ "typecheck",
273
+ "consistency",
274
+ "builtin",
275
+ "wiki-lint",
276
+ ]);
247
277
  const externalTools = toolReports.filter((r) => !builtInTools.has(r.tool));
248
278
  const allExternalSkipped =
249
279
  externalTools.length > 0 && externalTools.every((r) => r.skipped);