@getmikk/core 2.0.12 → 2.0.14
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 +12 -3
- package/package.json +1 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +272 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/adr-manager.ts +5 -4
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-writer.ts +3 -2
- package/src/contract/lock-compiler.ts +34 -0
- package/src/contract/lock-reader.ts +62 -5
- package/src/contract/schema.ts +10 -0
- package/src/index.ts +14 -1
- package/src/parser/error-recovery.ts +646 -0
- package/src/parser/index.ts +330 -74
- package/src/parser/oxc-parser.ts +3 -2
- package/src/parser/tree-sitter/parser.ts +59 -9
- package/src/parser/tree-sitter/queries.ts +27 -0
- package/src/parser/types.ts +1 -1
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +176 -0
- package/src/utils/atomic-write.ts +131 -0
- package/src/utils/fs.ts +76 -25
- package/src/utils/language-registry.ts +95 -0
- package/src/utils/minimatch.ts +49 -6
- package/tests/adr-manager.test.ts +6 -0
- package/tests/artifact-transaction.test.ts +73 -0
- package/tests/contract.test.ts +12 -0
- package/tests/dead-code.test.ts +12 -0
- package/tests/esm-resolver.test.ts +6 -0
- package/tests/fs.test.ts +22 -1
- package/tests/fuzzy-match.test.ts +6 -0
- package/tests/go-parser.test.ts +7 -0
- package/tests/graph.test.ts +10 -0
- package/tests/hash.test.ts +6 -0
- package/tests/impact-classified.test.ts +13 -0
- package/tests/js-parser.test.ts +10 -0
- package/tests/language-registry.test.ts +64 -0
- package/tests/parse-diagnostics.test.ts +115 -0
- package/tests/parser.test.ts +36 -0
- package/tests/tree-sitter-parser.test.ts +201 -0
- package/tests/ts-parser.test.ts +6 -0
package/src/utils/fs.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'node:fs/promises'
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
import fg from 'fast-glob'
|
|
4
|
+
import { getDiscoveryExtensions } from './language-registry.js'
|
|
4
5
|
|
|
5
6
|
// --- Well-known patterns for schema/config/route files ---------------------
|
|
6
7
|
// These are structural files an AI agent needs but aren't source code.
|
|
@@ -275,7 +276,7 @@ function inferContextFileType(filePath: string): ContextFileType {
|
|
|
275
276
|
}
|
|
276
277
|
|
|
277
278
|
/** Recognised project language */
|
|
278
|
-
export type ProjectLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' | 'java' | 'ruby' | 'php' | 'csharp' | 'c' | 'cpp' | 'unknown'
|
|
279
|
+
export type ProjectLanguage = 'typescript' | 'javascript' | 'python' | 'go' | 'rust' | 'java' | 'swift' | 'ruby' | 'php' | 'csharp' | 'c' | 'cpp' | 'unknown' | 'polyglot'
|
|
279
280
|
|
|
280
281
|
/** Auto-detect the project's primary language from manifest files */
|
|
281
282
|
export async function detectProjectLanguage(projectRoot: string): Promise<ProjectLanguage> {
|
|
@@ -293,6 +294,7 @@ export async function detectProjectLanguage(projectRoot: string): Promise<Projec
|
|
|
293
294
|
if (await exists('pyproject.toml') || await exists('setup.py') || await exists('requirements.txt')) return 'python'
|
|
294
295
|
if (await exists('Gemfile')) return 'ruby'
|
|
295
296
|
if (await exists('pom.xml') || await exists('build.gradle') || await exists('build.gradle.kts')) return 'java'
|
|
297
|
+
if (await exists('Package.swift')) return 'swift'
|
|
296
298
|
if (await exists('composer.json')) return 'php'
|
|
297
299
|
if (await hasGlob('*.csproj') || await hasGlob('*.sln')) return 'csharp'
|
|
298
300
|
if (await hasGlob('CMakeLists.txt') || await hasGlob('**/*.cmake') || await hasGlob('*.cpp')) return 'cpp'
|
|
@@ -306,68 +308,95 @@ export function getDiscoveryPatterns(language: ProjectLanguage): { patterns: str
|
|
|
306
308
|
const commonIgnore = [
|
|
307
309
|
'**/.mikk/**', '**/.git/**', '**/coverage/**', '**/build/**',
|
|
308
310
|
]
|
|
311
|
+
|
|
312
|
+
const toPatterns = (lang: ProjectLanguage): string[] => {
|
|
313
|
+
if (lang === 'polyglot') {
|
|
314
|
+
// For polyglot, use LANGUAGE_EXTENSIONS.polyglot directly
|
|
315
|
+
return getDiscoveryExtensions('polyglot' as any).map(ext => `**/*${ext}`)
|
|
316
|
+
}
|
|
317
|
+
return getDiscoveryExtensions(lang as any).map(ext => `**/*${ext}`)
|
|
318
|
+
}
|
|
319
|
+
|
|
309
320
|
switch (language) {
|
|
310
321
|
case 'typescript':
|
|
311
322
|
return {
|
|
312
|
-
patterns:
|
|
313
|
-
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}'],
|
|
323
|
+
patterns: toPatterns(language),
|
|
324
|
+
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
|
|
314
325
|
}
|
|
315
326
|
case 'javascript':
|
|
316
327
|
return {
|
|
317
|
-
patterns:
|
|
318
|
-
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}'],
|
|
328
|
+
patterns: toPatterns(language),
|
|
329
|
+
ignore: [...commonIgnore, '**/node_modules/**', '**/dist/**', '**/.next/**', '**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}', '**/venv/**', '**/.venv/**'],
|
|
319
330
|
}
|
|
320
331
|
case 'python':
|
|
321
332
|
return {
|
|
322
|
-
patterns:
|
|
323
|
-
ignore: [...commonIgnore, '**/__pycache__/**', '**/venv/**', '**/.venv/**', '**/.tox/**', '**/test_*.py', '**/*_test.py'],
|
|
333
|
+
patterns: toPatterns(language),
|
|
334
|
+
ignore: [...commonIgnore, '**/__pycache__/**', '**/venv/**', '**/.venv/**', '**/.tox/**', '**/test_*.py', '**/*_test.py', '**/lib/site-packages/**'],
|
|
324
335
|
}
|
|
325
336
|
case 'go':
|
|
326
337
|
return {
|
|
327
|
-
patterns:
|
|
338
|
+
patterns: toPatterns(language),
|
|
328
339
|
ignore: [...commonIgnore, '**/vendor/**', '**/*_test.go'],
|
|
329
340
|
}
|
|
330
341
|
case 'rust':
|
|
331
342
|
return {
|
|
332
|
-
patterns:
|
|
343
|
+
patterns: toPatterns(language),
|
|
333
344
|
ignore: [...commonIgnore, '**/target/**'],
|
|
334
345
|
}
|
|
335
346
|
case 'java':
|
|
336
347
|
return {
|
|
337
|
-
patterns:
|
|
348
|
+
patterns: toPatterns(language),
|
|
338
349
|
ignore: [...commonIgnore, '**/target/**', '**/.gradle/**', '**/Test*.java', '**/*Test.java'],
|
|
339
350
|
}
|
|
351
|
+
case 'swift':
|
|
352
|
+
return {
|
|
353
|
+
patterns: toPatterns(language),
|
|
354
|
+
ignore: [...commonIgnore, '**/.build/**', '**/Tests/**'],
|
|
355
|
+
}
|
|
340
356
|
case 'ruby':
|
|
341
357
|
return {
|
|
342
|
-
patterns:
|
|
343
|
-
ignore: [...commonIgnore, '**/vendor/**', '
|
|
358
|
+
patterns: toPatterns(language),
|
|
359
|
+
ignore: [...commonIgnore, '**/vendor/**', '**/*.gemspec'],
|
|
344
360
|
}
|
|
345
361
|
case 'php':
|
|
346
362
|
return {
|
|
347
|
-
patterns:
|
|
348
|
-
ignore: [...commonIgnore, '**/vendor/**', '
|
|
363
|
+
patterns: toPatterns(language),
|
|
364
|
+
ignore: [...commonIgnore, '**/vendor/**', '**/tests/**', '**/Test*.php'],
|
|
349
365
|
}
|
|
350
366
|
case 'csharp':
|
|
351
367
|
return {
|
|
352
|
-
patterns:
|
|
353
|
-
ignore: [...commonIgnore, '**/bin/**', '**/obj/**'],
|
|
368
|
+
patterns: toPatterns(language),
|
|
369
|
+
ignore: [...commonIgnore, '**/bin/**', '**/obj/**', '**/*Test.cs'],
|
|
370
|
+
}
|
|
371
|
+
case 'c':
|
|
372
|
+
return {
|
|
373
|
+
patterns: toPatterns(language),
|
|
374
|
+
ignore: [...commonIgnore, '**/*.h'],
|
|
354
375
|
}
|
|
355
376
|
case 'cpp':
|
|
356
377
|
return {
|
|
357
|
-
patterns:
|
|
358
|
-
ignore: [...commonIgnore, '**/build/**', '
|
|
378
|
+
patterns: toPatterns(language),
|
|
379
|
+
ignore: [...commonIgnore, '**/build/**', '**/*.hpp'],
|
|
359
380
|
}
|
|
360
|
-
case '
|
|
381
|
+
case 'polyglot':
|
|
361
382
|
return {
|
|
362
|
-
patterns:
|
|
363
|
-
ignore: [
|
|
383
|
+
patterns: toPatterns(language),
|
|
384
|
+
ignore: [
|
|
385
|
+
...commonIgnore,
|
|
386
|
+
'**/node_modules/**', '**/dist/**', '**/.next/**', '**/.nuxt/**', '**/.svelte-kit/**',
|
|
387
|
+
'**/__pycache__/**', '**/venv/**', '**/.venv/**', '**/.tox/**', '**/lib/site-packages/**',
|
|
388
|
+
'**/vendor/**', '**/target/**', '**/.gradle/**', '**/.build/**', '**/bin/**', '**/obj/**',
|
|
389
|
+
'**/*.d.ts', '**/*.test.{ts,js,tsx,jsx}', '**/*.spec.{ts,js,tsx,jsx}',
|
|
390
|
+
'**/test_*.py', '**/*_test.py', '**/Test*.java', '**/*Test.java', '**/*Test.cs',
|
|
391
|
+
],
|
|
364
392
|
}
|
|
365
|
-
|
|
366
|
-
// Fallback: discover JS/TS (most common)
|
|
393
|
+
case 'unknown':
|
|
367
394
|
return {
|
|
368
|
-
patterns: ['**/*.ts
|
|
369
|
-
ignore: [...commonIgnore, '**/node_modules/**'
|
|
395
|
+
patterns: ['**/*.{ts,tsx,js,jsx}'],
|
|
396
|
+
ignore: [...commonIgnore, '**/node_modules/**'],
|
|
370
397
|
}
|
|
398
|
+
default:
|
|
399
|
+
return { patterns: [], ignore: commonIgnore }
|
|
371
400
|
}
|
|
372
401
|
}
|
|
373
402
|
|
|
@@ -550,6 +579,14 @@ const LANGUAGE_IGNORE_TEMPLATES: Record<ProjectLanguage, string[]> = {
|
|
|
550
579
|
'gradle/',
|
|
551
580
|
'',
|
|
552
581
|
],
|
|
582
|
+
swift: [
|
|
583
|
+
'# Swift artifacts',
|
|
584
|
+
'.build/',
|
|
585
|
+
'.swiftpm/',
|
|
586
|
+
'Packages/',
|
|
587
|
+
'Tests/',
|
|
588
|
+
'',
|
|
589
|
+
],
|
|
553
590
|
ruby: [
|
|
554
591
|
'# Test files',
|
|
555
592
|
'*_spec.rb',
|
|
@@ -605,6 +642,20 @@ const LANGUAGE_IGNORE_TEMPLATES: Record<ProjectLanguage, string[]> = {
|
|
|
605
642
|
'__tests__/',
|
|
606
643
|
'',
|
|
607
644
|
],
|
|
645
|
+
polyglot: [
|
|
646
|
+
'# Multi-language project',
|
|
647
|
+
'**/node_modules/**',
|
|
648
|
+
'**/venv/**',
|
|
649
|
+
'**/.venv/**',
|
|
650
|
+
'**/__pycache__/**',
|
|
651
|
+
'**/site-packages/**',
|
|
652
|
+
'**/vendor/**',
|
|
653
|
+
'**/target/**',
|
|
654
|
+
'**/build/**',
|
|
655
|
+
'**/dist/**',
|
|
656
|
+
'**/.next/**',
|
|
657
|
+
'',
|
|
658
|
+
],
|
|
608
659
|
}
|
|
609
660
|
|
|
610
661
|
/**
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type ParserKind = 'oxc' | 'go' | 'tree-sitter' | 'unknown'
|
|
2
|
+
|
|
3
|
+
export type RegistryLanguage =
|
|
4
|
+
| 'typescript'
|
|
5
|
+
| 'javascript'
|
|
6
|
+
| 'python'
|
|
7
|
+
| 'go'
|
|
8
|
+
| 'rust'
|
|
9
|
+
| 'java'
|
|
10
|
+
| 'kotlin'
|
|
11
|
+
| 'swift'
|
|
12
|
+
| 'ruby'
|
|
13
|
+
| 'php'
|
|
14
|
+
| 'csharp'
|
|
15
|
+
| 'c'
|
|
16
|
+
| 'cpp'
|
|
17
|
+
| 'polyglot'
|
|
18
|
+
| 'unknown'
|
|
19
|
+
|
|
20
|
+
const OXC_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'] as const
|
|
21
|
+
const GO_EXTENSIONS = ['.go'] as const
|
|
22
|
+
const TREE_SITTER_EXTENSIONS = [
|
|
23
|
+
'.py', '.java', '.kt', '.kts', '.swift',
|
|
24
|
+
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
|
|
25
|
+
'.cs', '.rs', '.php', '.rb',
|
|
26
|
+
] as const
|
|
27
|
+
|
|
28
|
+
const PARSER_EXTENSIONS: Record<Exclude<ParserKind, 'unknown'>, readonly string[]> = {
|
|
29
|
+
oxc: OXC_EXTENSIONS,
|
|
30
|
+
go: GO_EXTENSIONS,
|
|
31
|
+
'tree-sitter': TREE_SITTER_EXTENSIONS,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const LANGUAGE_EXTENSIONS: Record<RegistryLanguage, readonly string[]> = {
|
|
35
|
+
typescript: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
36
|
+
javascript: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'],
|
|
37
|
+
python: ['.py'],
|
|
38
|
+
go: ['.go'],
|
|
39
|
+
rust: ['.rs'],
|
|
40
|
+
kotlin: ['.kt', '.kts'],
|
|
41
|
+
java: ['.java', '.kt', '.kts'],
|
|
42
|
+
swift: ['.swift'],
|
|
43
|
+
ruby: ['.rb'],
|
|
44
|
+
php: ['.php'],
|
|
45
|
+
csharp: ['.cs'],
|
|
46
|
+
c: ['.c', '.h'],
|
|
47
|
+
cpp: ['.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh', '.h'],
|
|
48
|
+
polyglot: [
|
|
49
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
50
|
+
'.py',
|
|
51
|
+
'.go',
|
|
52
|
+
'.rs',
|
|
53
|
+
'.java', '.kt', '.kts',
|
|
54
|
+
'.swift',
|
|
55
|
+
'.rb',
|
|
56
|
+
'.php',
|
|
57
|
+
'.cs',
|
|
58
|
+
'.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hxx', '.hh',
|
|
59
|
+
],
|
|
60
|
+
unknown: ['.ts', '.tsx', '.js', '.jsx'],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EXT_TO_PARSER = new Map<string, ParserKind>()
|
|
64
|
+
for (const ext of OXC_EXTENSIONS) EXT_TO_PARSER.set(ext, 'oxc')
|
|
65
|
+
for (const ext of GO_EXTENSIONS) EXT_TO_PARSER.set(ext, 'go')
|
|
66
|
+
for (const ext of TREE_SITTER_EXTENSIONS) EXT_TO_PARSER.set(ext, 'tree-sitter')
|
|
67
|
+
|
|
68
|
+
const EXT_TO_LANGUAGE = new Map<string, RegistryLanguage>()
|
|
69
|
+
for (const [language, extensions] of Object.entries(LANGUAGE_EXTENSIONS)) {
|
|
70
|
+
for (const ext of extensions) {
|
|
71
|
+
if (!EXT_TO_LANGUAGE.has(ext)) {
|
|
72
|
+
EXT_TO_LANGUAGE.set(ext, language as RegistryLanguage)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parserKindForExtension(ext: string): ParserKind {
|
|
78
|
+
return EXT_TO_PARSER.get(ext.toLowerCase()) ?? 'unknown'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function languageForExtension(ext: string): RegistryLanguage {
|
|
82
|
+
return EXT_TO_LANGUAGE.get(ext.toLowerCase()) ?? 'unknown'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getParserExtensions(kind: Exclude<ParserKind, 'unknown'>): readonly string[] {
|
|
86
|
+
return PARSER_EXTENSIONS[kind]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getDiscoveryExtensions(language: RegistryLanguage): readonly string[] {
|
|
90
|
+
return LANGUAGE_EXTENSIONS[language]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isTreeSitterExtension(ext: string): boolean {
|
|
94
|
+
return parserKindForExtension(ext) === 'tree-sitter'
|
|
95
|
+
}
|
package/src/utils/minimatch.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Rules:
|
|
5
5
|
* - Pattern with no glob chars (*, ?, {, [) → directory prefix match
|
|
6
6
|
* "src/auth" matches "src/auth/jwt.ts" and "src/auth" itself
|
|
7
|
-
* - "**" matches any depth
|
|
7
|
+
* - "**" matches any depth (zero or more directory segments)
|
|
8
8
|
* - "*" matches within a single directory segment
|
|
9
9
|
*/
|
|
10
10
|
export function minimatch(filePath: string, pattern: string): boolean {
|
|
@@ -17,12 +17,55 @@ export function minimatch(filePath: string, pattern: string): boolean {
|
|
|
17
17
|
return normalizedPath === bare || normalizedPath.startsWith(bare + '/')
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
//
|
|
21
|
-
|
|
20
|
+
// Handle patterns that start with ** - these should match anywhere in the path
|
|
21
|
+
// e.g., **/venv/** should match if there's a /venv/ segment anywhere
|
|
22
|
+
if (normalizedPattern.startsWith('**/')) {
|
|
23
|
+
const rest = normalizedPattern.slice(3) // Remove **/
|
|
24
|
+
// Check if the rest of the pattern appears as a path segment
|
|
25
|
+
// For **/venv/**, check if /venv/ is in the path
|
|
26
|
+
// For **/node_modules/**, check if /node_modules/ is in the path
|
|
27
|
+
const segments = normalizedPath.split('/')
|
|
28
|
+
const patternSegments = rest.split('/').filter(Boolean)
|
|
29
|
+
|
|
30
|
+
// Check if pattern segments appear consecutively in path
|
|
31
|
+
for (let i = 0; i <= segments.length - patternSegments.length; i++) {
|
|
32
|
+
let match = true
|
|
33
|
+
for (let j = 0; j < patternSegments.length; j++) {
|
|
34
|
+
const pseg = patternSegments[j].replace(/\*/g, '[^/]*')
|
|
35
|
+
if (!new RegExp('^' + pseg + '$', 'i').test(segments[i + j])) {
|
|
36
|
+
match = false
|
|
37
|
+
break
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (match) return true
|
|
41
|
+
}
|
|
42
|
+
return false
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Convert glob to regex (for patterns not starting with **)
|
|
46
|
+
let regexStr = normalizedPattern
|
|
47
|
+
.replace(/\[/g, '\\[')
|
|
48
|
+
.replace(/\]/g, '\\]')
|
|
22
49
|
.replace(/\./g, '\\.')
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
|
|
51
|
+
// Replace **/ at end with (?:[^/]+/)* - matches zero or more dir segments ending with /
|
|
52
|
+
// But we need to handle path/** specifically - matching path/file, path/dir/file, etc.
|
|
53
|
+
|
|
54
|
+
// Handle trailing /** specifically - should match path itself and anything under it
|
|
55
|
+
if (normalizedPattern.endsWith('/**')) {
|
|
56
|
+
const base = normalizedPattern.slice(0, -3) // Remove /**
|
|
57
|
+
// Match either exact base or base + anything
|
|
58
|
+
return normalizedPath === base || normalizedPath.startsWith(base + '/')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Replace **/ with (?:[^/]+/)* - matches zero or more directory segments
|
|
62
|
+
regexStr = regexStr.replace(/\*\*\//g, '(?:[^/]+/)*')
|
|
63
|
+
// Replace trailing ** with (?:[^/]+/)*[^/]+ - matches zero or more at end
|
|
64
|
+
regexStr = regexStr.replace(/\*\*$/g, '(?:[^/]+/)*[^/]+')
|
|
65
|
+
// Standalone **
|
|
66
|
+
regexStr = regexStr.replace(/\*\*/g, '(?:[^/]+/)*[^/]+')
|
|
67
|
+
// Single * matches any characters except slash
|
|
68
|
+
regexStr = regexStr.replace(/\*/g, '[^/]*')
|
|
26
69
|
|
|
27
70
|
return new RegExp(`^${regexStr}$`, 'i').test(normalizedPath)
|
|
28
71
|
}
|
|
@@ -94,4 +94,10 @@ describe('AdrManager', () => {
|
|
|
94
94
|
const decisions = await manager.list()
|
|
95
95
|
expect(decisions).toHaveLength(0)
|
|
96
96
|
})
|
|
97
|
+
|
|
98
|
+
it('returns false when removing a missing decision', async () => {
|
|
99
|
+
const manager = new AdrManager(CONTRACT_PATH)
|
|
100
|
+
const success = await manager.remove('ADR-404')
|
|
101
|
+
expect(success).toBe(false)
|
|
102
|
+
})
|
|
97
103
|
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
import * as os from 'node:os'
|
|
5
|
+
import {
|
|
6
|
+
runArtifactWriteTransaction,
|
|
7
|
+
recoverArtifactWriteTransactions,
|
|
8
|
+
} from '../src/utils/artifact-transaction'
|
|
9
|
+
|
|
10
|
+
describe('artifact write transactions', () => {
|
|
11
|
+
it('commits grouped writes atomically', async () => {
|
|
12
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mikk-tx-'))
|
|
13
|
+
const aPath = path.join(root, 'mikk.lock.json')
|
|
14
|
+
const bPath = path.join(root, 'claude.md')
|
|
15
|
+
|
|
16
|
+
await runArtifactWriteTransaction(root, 'commit-test', [
|
|
17
|
+
{ targetPath: aPath, content: '{"ok":true}' },
|
|
18
|
+
{ targetPath: bPath, content: '# context' },
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
expect(await fs.readFile(aPath, 'utf-8')).toContain('ok')
|
|
22
|
+
expect(await fs.readFile(bPath, 'utf-8')).toContain('context')
|
|
23
|
+
|
|
24
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('rolls back staged writes after pre-commit crash', async () => {
|
|
28
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mikk-tx-'))
|
|
29
|
+
const lockPath = path.join(root, 'mikk.lock.json')
|
|
30
|
+
|
|
31
|
+
await expect(
|
|
32
|
+
runArtifactWriteTransaction(
|
|
33
|
+
root,
|
|
34
|
+
'rollback-test',
|
|
35
|
+
[{ targetPath: lockPath, content: '{"v":1}' }],
|
|
36
|
+
{ simulateCrashAt: 'after-stage' },
|
|
37
|
+
),
|
|
38
|
+
).rejects.toThrow('Simulated crash after stage')
|
|
39
|
+
|
|
40
|
+
const summary = await recoverArtifactWriteTransactions(root)
|
|
41
|
+
expect(summary.rolledBack).toBeGreaterThanOrEqual(1)
|
|
42
|
+
|
|
43
|
+
let exists = true
|
|
44
|
+
try {
|
|
45
|
+
await fs.access(lockPath)
|
|
46
|
+
} catch {
|
|
47
|
+
exists = false
|
|
48
|
+
}
|
|
49
|
+
expect(exists).toBe(false)
|
|
50
|
+
|
|
51
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('recovers commit-ready journals after post-commit-marker crash', async () => {
|
|
55
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'mikk-tx-'))
|
|
56
|
+
const lockPath = path.join(root, 'mikk.lock.json')
|
|
57
|
+
|
|
58
|
+
await expect(
|
|
59
|
+
runArtifactWriteTransaction(
|
|
60
|
+
root,
|
|
61
|
+
'recovery-test',
|
|
62
|
+
[{ targetPath: lockPath, content: '{"v":2}' }],
|
|
63
|
+
{ simulateCrashAt: 'after-commit-marker' },
|
|
64
|
+
),
|
|
65
|
+
).rejects.toThrow('Simulated crash after commit marker')
|
|
66
|
+
|
|
67
|
+
const summary = await recoverArtifactWriteTransactions(root)
|
|
68
|
+
expect(summary.recovered).toBeGreaterThanOrEqual(1)
|
|
69
|
+
expect(await fs.readFile(lockPath, 'utf-8')).toContain('"v":2')
|
|
70
|
+
|
|
71
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
72
|
+
})
|
|
73
|
+
})
|
package/tests/contract.test.ts
CHANGED
|
@@ -49,6 +49,18 @@ describe('MikkContractSchema', () => {
|
|
|
49
49
|
expect(result.data.overwrite.mode).toBe('never')
|
|
50
50
|
}
|
|
51
51
|
})
|
|
52
|
+
|
|
53
|
+
it('rejects contract with invalid declared.modules type', () => {
|
|
54
|
+
const bad = {
|
|
55
|
+
...validContract,
|
|
56
|
+
declared: {
|
|
57
|
+
...validContract.declared,
|
|
58
|
+
modules: 'not-an-array',
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
const result = MikkContractSchema.safeParse(bad)
|
|
62
|
+
expect(result.success).toBe(false)
|
|
63
|
+
})
|
|
52
64
|
})
|
|
53
65
|
|
|
54
66
|
describe('LockCompiler', () => {
|
package/tests/dead-code.test.ts
CHANGED
|
@@ -131,4 +131,16 @@ describe('DeadCodeDetector', () => {
|
|
|
131
131
|
|
|
132
132
|
expect(result.deadFunctions).toHaveLength(0) // InternalHelper is called by exported fn in same file
|
|
133
133
|
})
|
|
134
|
+
|
|
135
|
+
it('keeps deadCount aligned with deadFunctions length', () => {
|
|
136
|
+
const graph = buildTestGraph([
|
|
137
|
+
['A', 'nothing'],
|
|
138
|
+
['B', 'nothing'],
|
|
139
|
+
])
|
|
140
|
+
const lock = generateDummyLock(graph.nodes)
|
|
141
|
+
|
|
142
|
+
const detector = new DeadCodeDetector(graph, lock)
|
|
143
|
+
const result = detector.detect()
|
|
144
|
+
expect(result.deadCount).toBe(result.deadFunctions.length)
|
|
145
|
+
})
|
|
134
146
|
})
|
|
@@ -73,4 +73,10 @@ describe('OxcResolver - ESM and CJS Resolution', () => {
|
|
|
73
73
|
const res = await resolver.resolve('./local', path.join(FIXTURE_DIR, 'index.ts'))
|
|
74
74
|
expect(res).toContain('.test-fixture-esm/local.ts')
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
it('does not throw for missing package imports', async () => {
|
|
78
|
+
const resolver = new OxcResolver(FIXTURE_DIR)
|
|
79
|
+
const res = await resolver.resolve('totally-missing-pkg', path.join(FIXTURE_DIR, 'index.ts'))
|
|
80
|
+
expect(typeof res).toBe('string')
|
|
81
|
+
})
|
|
76
82
|
})
|
package/tests/fs.test.ts
CHANGED
|
@@ -75,6 +75,12 @@ describe('detectProjectLanguage', () => {
|
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
it('detects Swift from Package.swift', async () => {
|
|
79
|
+
await withFile('Package.swift', async () => {
|
|
80
|
+
expect(await detectProjectLanguage(tmpDir)).toBe('swift')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
78
84
|
it('detects C# from .csproj file', async () => {
|
|
79
85
|
await withFile('MyApp.csproj', async () => {
|
|
80
86
|
expect(await detectProjectLanguage(tmpDir)).toBe('csharp')
|
|
@@ -116,7 +122,7 @@ describe('detectProjectLanguage', () => {
|
|
|
116
122
|
describe('getDiscoveryPatterns', () => {
|
|
117
123
|
const languages: ProjectLanguage[] = [
|
|
118
124
|
'typescript', 'javascript', 'python', 'go', 'rust',
|
|
119
|
-
'java', 'ruby', 'php', 'csharp', 'unknown',
|
|
125
|
+
'java', 'swift', 'ruby', 'php', 'csharp', 'unknown',
|
|
120
126
|
]
|
|
121
127
|
|
|
122
128
|
for (const lang of languages) {
|
|
@@ -143,6 +149,21 @@ describe('getDiscoveryPatterns', () => {
|
|
|
143
149
|
expect(patterns).toContain('**/*.py')
|
|
144
150
|
})
|
|
145
151
|
|
|
152
|
+
it('java discovery patterns include Kotlin scripts for mixed JVM codebases', () => {
|
|
153
|
+
const { patterns } = getDiscoveryPatterns('java')
|
|
154
|
+
expect(patterns).toContain('**/*.kts')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('swift patterns include .swift', () => {
|
|
158
|
+
const { patterns } = getDiscoveryPatterns('swift')
|
|
159
|
+
expect(patterns).toContain('**/*.swift')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('cpp patterns include .hh headers', () => {
|
|
163
|
+
const { patterns } = getDiscoveryPatterns('cpp')
|
|
164
|
+
expect(patterns).toContain('**/*.hh')
|
|
165
|
+
})
|
|
166
|
+
|
|
146
167
|
it('all languages ignore .mikk and .git', () => {
|
|
147
168
|
for (const lang of languages) {
|
|
148
169
|
const { ignore } = getDiscoveryPatterns(lang)
|
|
@@ -61,6 +61,12 @@ describe('levenshtein', () => {
|
|
|
61
61
|
test('insertion', () => {
|
|
62
62
|
expect(levenshtein('test', 'tests')).toBe(1)
|
|
63
63
|
})
|
|
64
|
+
|
|
65
|
+
test('distance is symmetric', () => {
|
|
66
|
+
expect(levenshtein('verifyToken', 'tokenVerify')).toBe(
|
|
67
|
+
levenshtein('tokenVerify', 'verifyToken'),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
64
70
|
})
|
|
65
71
|
|
|
66
72
|
describe('splitCamelCase', () => {
|
package/tests/go-parser.test.ts
CHANGED
|
@@ -363,4 +363,11 @@ describe('GoParser', () => {
|
|
|
363
363
|
const resolved = await parser.resolveImports(files, '/tmp/no-gomod-' + Date.now())
|
|
364
364
|
expect(resolved.length).toBe(1)
|
|
365
365
|
})
|
|
366
|
+
|
|
367
|
+
test('parse keeps deterministic hash for identical input', async () => {
|
|
368
|
+
const parser = new GoParser()
|
|
369
|
+
const a = await parser.parse('auth/service.go', SIMPLE_GO)
|
|
370
|
+
const b = await parser.parse('auth/service.go', SIMPLE_GO)
|
|
371
|
+
expect(a.hash).toBe(b.hash)
|
|
372
|
+
})
|
|
366
373
|
})
|
package/tests/graph.test.ts
CHANGED
|
@@ -167,4 +167,14 @@ describe('ClusterDetector', () => {
|
|
|
167
167
|
expect(score).toBeGreaterThanOrEqual(0)
|
|
168
168
|
expect(score).toBeLessThanOrEqual(1)
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
it('keeps distinct function nodes for same function names in different files', () => {
|
|
172
|
+
const files = [
|
|
173
|
+
mockParsedFile('src/auth/a.ts', [mockFunction('shared', [], 'src/auth/a.ts')]),
|
|
174
|
+
mockParsedFile('src/db/b.ts', [mockFunction('shared', [], 'src/db/b.ts')]),
|
|
175
|
+
]
|
|
176
|
+
const graph = new GraphBuilder().build(files)
|
|
177
|
+
expect(graph.nodes.has('fn:src/auth/a.ts:shared')).toBe(true)
|
|
178
|
+
expect(graph.nodes.has('fn:src/db/b.ts:shared')).toBe(true)
|
|
179
|
+
})
|
|
170
180
|
})
|
package/tests/hash.test.ts
CHANGED
|
@@ -46,4 +46,10 @@ describe('computeRootHash', () => {
|
|
|
46
46
|
const hash2 = computeRootHash({ payments: 'def', auth: 'abc' })
|
|
47
47
|
expect(hash1).toBe(hash2)
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
it('changes when a module hash changes', () => {
|
|
51
|
+
const base = computeRootHash({ auth: 'abc', payments: 'def' })
|
|
52
|
+
const changed = computeRootHash({ auth: 'abc', payments: 'xyz' })
|
|
53
|
+
expect(base).not.toBe(changed)
|
|
54
|
+
})
|
|
49
55
|
})
|
|
@@ -71,4 +71,17 @@ describe('ImpactAnalyzer - Classified', () => {
|
|
|
71
71
|
expect(result.classified.low).toHaveLength(1)
|
|
72
72
|
expect(result.classified.low[0].nodeId).toBe('fn:src/highriskauthservice.ts:highriskauthservice')
|
|
73
73
|
})
|
|
74
|
+
|
|
75
|
+
it('deduplicates impacted nodes when changed list includes duplicates', () => {
|
|
76
|
+
const graph = buildTestGraph([
|
|
77
|
+
['A', 'B'],
|
|
78
|
+
['B', 'C'],
|
|
79
|
+
['C', 'nothing'],
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const analyzer = new ImpactAnalyzer(graph)
|
|
83
|
+
const result = analyzer.analyze(['fn:src/c.ts:c', 'fn:src/c.ts:c'])
|
|
84
|
+
const unique = new Set(result.impacted)
|
|
85
|
+
expect(unique.size).toBe(result.impacted.length)
|
|
86
|
+
})
|
|
74
87
|
})
|
package/tests/js-parser.test.ts
CHANGED
|
@@ -1042,6 +1042,16 @@ describe('JavaScript - Additional Edge Cases', () => {
|
|
|
1042
1042
|
})
|
|
1043
1043
|
})
|
|
1044
1044
|
|
|
1045
|
+
describe('Deterministic parsing', () => {
|
|
1046
|
+
test('produces identical file hash for identical source', async () => {
|
|
1047
|
+
const parser = new JavaScriptParser()
|
|
1048
|
+
const source = 'export function stable() { return 1 }'
|
|
1049
|
+
const a = await parser.parse('src/stable.js', source)
|
|
1050
|
+
const b = await parser.parse('src/stable.js', source)
|
|
1051
|
+
expect(a.hash).toBe(b.hash)
|
|
1052
|
+
})
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1045
1055
|
describe('Chained Methods', () => {
|
|
1046
1056
|
test('handles method chaining', () => {
|
|
1047
1057
|
const src = `
|