@getmikk/core 1.2.0 → 1.3.1

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 (44) hide show
  1. package/README.md +431 -0
  2. package/package.json +6 -2
  3. package/src/contract/contract-generator.ts +85 -85
  4. package/src/contract/contract-reader.ts +28 -28
  5. package/src/contract/contract-writer.ts +114 -114
  6. package/src/contract/index.ts +12 -12
  7. package/src/contract/lock-compiler.ts +221 -221
  8. package/src/contract/lock-reader.ts +34 -34
  9. package/src/contract/schema.ts +147 -147
  10. package/src/graph/cluster-detector.ts +312 -312
  11. package/src/graph/graph-builder.ts +211 -211
  12. package/src/graph/impact-analyzer.ts +55 -55
  13. package/src/graph/index.ts +4 -4
  14. package/src/graph/types.ts +59 -59
  15. package/src/hash/file-hasher.ts +30 -30
  16. package/src/hash/hash-store.ts +119 -119
  17. package/src/hash/index.ts +3 -3
  18. package/src/hash/tree-hasher.ts +20 -20
  19. package/src/index.ts +12 -12
  20. package/src/parser/base-parser.ts +16 -16
  21. package/src/parser/boundary-checker.ts +211 -211
  22. package/src/parser/index.ts +46 -46
  23. package/src/parser/types.ts +90 -90
  24. package/src/parser/typescript/ts-extractor.ts +543 -543
  25. package/src/parser/typescript/ts-parser.ts +41 -41
  26. package/src/parser/typescript/ts-resolver.ts +86 -86
  27. package/src/utils/errors.ts +42 -42
  28. package/src/utils/fs.ts +75 -75
  29. package/src/utils/fuzzy-match.ts +186 -186
  30. package/src/utils/logger.ts +36 -36
  31. package/src/utils/minimatch.ts +19 -19
  32. package/tests/contract.test.ts +134 -134
  33. package/tests/fixtures/simple-api/package.json +5 -5
  34. package/tests/fixtures/simple-api/src/auth/middleware.ts +9 -9
  35. package/tests/fixtures/simple-api/src/auth/verify.ts +6 -6
  36. package/tests/fixtures/simple-api/src/index.ts +9 -9
  37. package/tests/fixtures/simple-api/src/utils/jwt.ts +3 -3
  38. package/tests/fixtures/simple-api/tsconfig.json +8 -8
  39. package/tests/fuzzy-match.test.ts +142 -142
  40. package/tests/graph.test.ts +169 -169
  41. package/tests/hash.test.ts +49 -49
  42. package/tests/helpers.ts +83 -83
  43. package/tests/parser.test.ts +218 -218
  44. package/tsconfig.json +15 -15
@@ -1,41 +1,41 @@
1
- import { BaseParser } from '../base-parser.js'
2
- import { TypeScriptExtractor } from './ts-extractor.js'
3
- import { TypeScriptResolver } from './ts-resolver.js'
4
- import { hashContent } from '../../hash/file-hasher.js'
5
- import type { ParsedFile } from '../types.js'
6
-
7
- /**
8
- * TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
9
- * and extract structured data (functions, classes, imports, exports).
10
- */
11
- export class TypeScriptParser extends BaseParser {
12
- /** Parse a single TypeScript file */
13
- parse(filePath: string, content: string): ParsedFile {
14
- const extractor = new TypeScriptExtractor(filePath, content)
15
- return {
16
- path: filePath,
17
- language: 'typescript',
18
- functions: extractor.extractFunctions(),
19
- classes: extractor.extractClasses(),
20
- generics: extractor.extractGenerics(),
21
- imports: extractor.extractImports(),
22
- exports: extractor.extractExports(),
23
- hash: hashContent(content),
24
- parsedAt: Date.now(),
25
- }
26
- }
27
-
28
- /** Resolve all import paths in parsed files to absolute project paths */
29
- resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
30
- const resolver = new TypeScriptResolver(projectRoot)
31
- const allFilePaths = files.map(f => f.path)
32
- return files.map(file => ({
33
- ...file,
34
- imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
35
- }))
36
- }
37
-
38
- getSupportedExtensions(): string[] {
39
- return ['.ts', '.tsx']
40
- }
41
- }
1
+ import { BaseParser } from '../base-parser.js'
2
+ import { TypeScriptExtractor } from './ts-extractor.js'
3
+ import { TypeScriptResolver } from './ts-resolver.js'
4
+ import { hashContent } from '../../hash/file-hasher.js'
5
+ import type { ParsedFile } from '../types.js'
6
+
7
+ /**
8
+ * TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
9
+ * and extract structured data (functions, classes, imports, exports).
10
+ */
11
+ export class TypeScriptParser extends BaseParser {
12
+ /** Parse a single TypeScript file */
13
+ parse(filePath: string, content: string): ParsedFile {
14
+ const extractor = new TypeScriptExtractor(filePath, content)
15
+ return {
16
+ path: filePath,
17
+ language: 'typescript',
18
+ functions: extractor.extractFunctions(),
19
+ classes: extractor.extractClasses(),
20
+ generics: extractor.extractGenerics(),
21
+ imports: extractor.extractImports(),
22
+ exports: extractor.extractExports(),
23
+ hash: hashContent(content),
24
+ parsedAt: Date.now(),
25
+ }
26
+ }
27
+
28
+ /** Resolve all import paths in parsed files to absolute project paths */
29
+ resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
30
+ const resolver = new TypeScriptResolver(projectRoot)
31
+ const allFilePaths = files.map(f => f.path)
32
+ return files.map(file => ({
33
+ ...file,
34
+ imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
35
+ }))
36
+ }
37
+
38
+ getSupportedExtensions(): string[] {
39
+ return ['.ts', '.tsx']
40
+ }
41
+ }
@@ -1,86 +1,86 @@
1
- import * as path from 'node:path'
2
- import type { ParsedImport } from '../types.js'
3
-
4
- interface TSConfigPaths {
5
- [alias: string]: string[]
6
- }
7
-
8
- /**
9
- * Resolves TypeScript import paths to absolute project-relative paths.
10
- * Handles: relative imports, path aliases, index files, extension inference.
11
- */
12
- export class TypeScriptResolver {
13
- private aliases: TSConfigPaths
14
-
15
- constructor(
16
- private projectRoot: string,
17
- tsConfigPaths?: TSConfigPaths
18
- ) {
19
- this.aliases = tsConfigPaths || {}
20
- }
21
-
22
- /** Resolve a single import relative to the importing file */
23
- resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
24
- // Skip external packages (no relative path prefix, no alias match)
25
- if (!imp.source.startsWith('.') && !imp.source.startsWith('/') && !this.matchesAlias(imp.source)) {
26
- return { ...imp, resolvedPath: '' }
27
- }
28
-
29
- const resolved = this.resolvePath(imp.source, fromFile, allProjectFiles)
30
- return { ...imp, resolvedPath: resolved }
31
- }
32
-
33
- private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
34
- let resolvedSource = source
35
-
36
- // 1. Handle path aliases: @/utils/jwt → src/utils/jwt
37
- for (const [alias, targets] of Object.entries(this.aliases)) {
38
- const aliasPrefix = alias.replace('/*', '')
39
- if (source.startsWith(aliasPrefix)) {
40
- const suffix = source.slice(aliasPrefix.length)
41
- const target = targets[0].replace('/*', '')
42
- resolvedSource = target + suffix
43
- break
44
- }
45
- }
46
-
47
- // 2. Handle relative paths
48
- let resolved: string
49
- if (resolvedSource.startsWith('.')) {
50
- const fromDir = path.dirname(fromFile)
51
- resolved = path.posix.normalize(path.posix.join(fromDir, resolvedSource))
52
- } else {
53
- resolved = resolvedSource
54
- }
55
-
56
- // Normalize to posix
57
- resolved = resolved.replace(/\\/g, '/')
58
-
59
- // 3. Try to find exact match with extensions
60
- const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx']
61
-
62
- // If the path already has an extension, return it
63
- if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
64
- return resolved
65
- }
66
-
67
- // Try adding extensions to find matching file
68
- for (const ext of extensions) {
69
- const candidate = resolved + ext
70
- if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
71
- return candidate
72
- }
73
- }
74
-
75
- // Fallback: just add .ts
76
- return resolved + '.ts'
77
- }
78
-
79
- private matchesAlias(source: string): boolean {
80
- for (const alias of Object.keys(this.aliases)) {
81
- const prefix = alias.replace('/*', '')
82
- if (source.startsWith(prefix)) return true
83
- }
84
- return false
85
- }
86
- }
1
+ import * as path from 'node:path'
2
+ import type { ParsedImport } from '../types.js'
3
+
4
+ interface TSConfigPaths {
5
+ [alias: string]: string[]
6
+ }
7
+
8
+ /**
9
+ * Resolves TypeScript import paths to absolute project-relative paths.
10
+ * Handles: relative imports, path aliases, index files, extension inference.
11
+ */
12
+ export class TypeScriptResolver {
13
+ private aliases: TSConfigPaths
14
+
15
+ constructor(
16
+ private projectRoot: string,
17
+ tsConfigPaths?: TSConfigPaths
18
+ ) {
19
+ this.aliases = tsConfigPaths || {}
20
+ }
21
+
22
+ /** Resolve a single import relative to the importing file */
23
+ resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
24
+ // Skip external packages (no relative path prefix, no alias match)
25
+ if (!imp.source.startsWith('.') && !imp.source.startsWith('/') && !this.matchesAlias(imp.source)) {
26
+ return { ...imp, resolvedPath: '' }
27
+ }
28
+
29
+ const resolved = this.resolvePath(imp.source, fromFile, allProjectFiles)
30
+ return { ...imp, resolvedPath: resolved }
31
+ }
32
+
33
+ private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
34
+ let resolvedSource = source
35
+
36
+ // 1. Handle path aliases: @/utils/jwt → src/utils/jwt
37
+ for (const [alias, targets] of Object.entries(this.aliases)) {
38
+ const aliasPrefix = alias.replace('/*', '')
39
+ if (source.startsWith(aliasPrefix)) {
40
+ const suffix = source.slice(aliasPrefix.length)
41
+ const target = targets[0].replace('/*', '')
42
+ resolvedSource = target + suffix
43
+ break
44
+ }
45
+ }
46
+
47
+ // 2. Handle relative paths
48
+ let resolved: string
49
+ if (resolvedSource.startsWith('.')) {
50
+ const fromDir = path.dirname(fromFile)
51
+ resolved = path.posix.normalize(path.posix.join(fromDir, resolvedSource))
52
+ } else {
53
+ resolved = resolvedSource
54
+ }
55
+
56
+ // Normalize to posix
57
+ resolved = resolved.replace(/\\/g, '/')
58
+
59
+ // 3. Try to find exact match with extensions
60
+ const extensions = ['.ts', '.tsx', '/index.ts', '/index.tsx']
61
+
62
+ // If the path already has an extension, return it
63
+ if (resolved.endsWith('.ts') || resolved.endsWith('.tsx')) {
64
+ return resolved
65
+ }
66
+
67
+ // Try adding extensions to find matching file
68
+ for (const ext of extensions) {
69
+ const candidate = resolved + ext
70
+ if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
71
+ return candidate
72
+ }
73
+ }
74
+
75
+ // Fallback: just add .ts
76
+ return resolved + '.ts'
77
+ }
78
+
79
+ private matchesAlias(source: string): boolean {
80
+ for (const alias of Object.keys(this.aliases)) {
81
+ const prefix = alias.replace('/*', '')
82
+ if (source.startsWith(prefix)) return true
83
+ }
84
+ return false
85
+ }
86
+ }
@@ -1,42 +1,42 @@
1
- export class MikkError extends Error {
2
- constructor(message: string, public code: string) {
3
- super(message)
4
- this.name = 'MikkError'
5
- }
6
- }
7
-
8
- export class ParseError extends MikkError {
9
- constructor(file: string, cause: string) {
10
- super(`Failed to parse ${file}: ${cause}`, 'PARSE_ERROR')
11
- }
12
- }
13
-
14
- export class ContractNotFoundError extends MikkError {
15
- constructor(path: string) {
16
- super(`No mikk.json found at ${path}. Run 'mikk init' first.`, 'CONTRACT_NOT_FOUND')
17
- }
18
- }
19
-
20
- export class LockNotFoundError extends MikkError {
21
- constructor() {
22
- super(`No mikk.lock.json found. Run 'mikk analyze' first.`, 'LOCK_NOT_FOUND')
23
- }
24
- }
25
-
26
- export class UnsupportedLanguageError extends MikkError {
27
- constructor(ext: string) {
28
- super(`Unsupported file extension: ${ext}`, 'UNSUPPORTED_LANGUAGE')
29
- }
30
- }
31
-
32
- export class OverwritePermissionError extends MikkError {
33
- constructor() {
34
- super(`Overwrite mode is 'never'. Change to 'ask' or 'explicit' to allow updates.`, 'OVERWRITE_DENIED')
35
- }
36
- }
37
-
38
- export class SyncStateError extends MikkError {
39
- constructor(status: string) {
40
- super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
41
- }
42
- }
1
+ export class MikkError extends Error {
2
+ constructor(message: string, public code: string) {
3
+ super(message)
4
+ this.name = 'MikkError'
5
+ }
6
+ }
7
+
8
+ export class ParseError extends MikkError {
9
+ constructor(file: string, cause: string) {
10
+ super(`Failed to parse ${file}: ${cause}`, 'PARSE_ERROR')
11
+ }
12
+ }
13
+
14
+ export class ContractNotFoundError extends MikkError {
15
+ constructor(path: string) {
16
+ super(`No mikk.json found at ${path}. Run 'mikk init' first.`, 'CONTRACT_NOT_FOUND')
17
+ }
18
+ }
19
+
20
+ export class LockNotFoundError extends MikkError {
21
+ constructor() {
22
+ super(`No mikk.lock.json found. Run 'mikk analyze' first.`, 'LOCK_NOT_FOUND')
23
+ }
24
+ }
25
+
26
+ export class UnsupportedLanguageError extends MikkError {
27
+ constructor(ext: string) {
28
+ super(`Unsupported file extension: ${ext}`, 'UNSUPPORTED_LANGUAGE')
29
+ }
30
+ }
31
+
32
+ export class OverwritePermissionError extends MikkError {
33
+ constructor() {
34
+ super(`Overwrite mode is 'never'. Change to 'ask' or 'explicit' to allow updates.`, 'OVERWRITE_DENIED')
35
+ }
36
+ }
37
+
38
+ export class SyncStateError extends MikkError {
39
+ constructor(status: string) {
40
+ super(`Mikk is in ${status} state. Run 'mikk analyze' to sync.`, 'SYNC_STATE_ERROR')
41
+ }
42
+ }
package/src/utils/fs.ts CHANGED
@@ -1,75 +1,75 @@
1
- import * as fs from 'node:fs/promises'
2
- import * as path from 'node:path'
3
- import fg from 'fast-glob'
4
-
5
- /**
6
- * Discover all source files in a project directory.
7
- * Respects common ignore patterns (node_modules, dist, .mikk, etc.)
8
- */
9
- export async function discoverFiles(
10
- projectRoot: string,
11
- patterns: string[] = ['**/*.ts', '**/*.tsx'],
12
- ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
13
- ): Promise<string[]> {
14
- const files = await fg(patterns, {
15
- cwd: projectRoot,
16
- ignore,
17
- absolute: false,
18
- onlyFiles: true,
19
- })
20
- return files.map(f => f.replace(/\\/g, '/'))
21
- }
22
-
23
- /**
24
- * Reads a file and returns its content as a UTF-8 string.
25
- */
26
- export async function readFileContent(filePath: string): Promise<string> {
27
- return fs.readFile(filePath, 'utf-8')
28
- }
29
-
30
- /**
31
- * Writes content to a file, creating parent directories if needed.
32
- */
33
- export async function writeFileContent(filePath: string, content: string): Promise<void> {
34
- await fs.mkdir(path.dirname(filePath), { recursive: true })
35
- await fs.writeFile(filePath, content, 'utf-8')
36
- }
37
-
38
- /**
39
- * Check if a file exists.
40
- */
41
- export async function fileExists(filePath: string): Promise<boolean> {
42
- try {
43
- await fs.access(filePath)
44
- return true
45
- } catch {
46
- return false
47
- }
48
- }
49
-
50
- /**
51
- * Set up the .mikk directory structure in a project root.
52
- */
53
- export async function setupMikkDirectory(projectRoot: string): Promise<void> {
54
- const dirs = [
55
- '.mikk',
56
- '.mikk/fragments',
57
- '.mikk/diagrams',
58
- '.mikk/diagrams/modules',
59
- '.mikk/diagrams/capsules',
60
- '.mikk/diagrams/flows',
61
- '.mikk/diagrams/impact',
62
- '.mikk/diagrams/exposure',
63
- '.mikk/intent',
64
- '.mikk/cache',
65
- ]
66
- for (const dir of dirs) {
67
- await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
68
- }
69
-
70
- // Create .gitkeep in impact dir
71
- const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
72
- if (!await fileExists(impactKeep)) {
73
- await fs.writeFile(impactKeep, '', 'utf-8')
74
- }
75
- }
1
+ import * as fs from 'node:fs/promises'
2
+ import * as path from 'node:path'
3
+ import fg from 'fast-glob'
4
+
5
+ /**
6
+ * Discover all source files in a project directory.
7
+ * Respects common ignore patterns (node_modules, dist, .mikk, etc.)
8
+ */
9
+ export async function discoverFiles(
10
+ projectRoot: string,
11
+ patterns: string[] = ['**/*.ts', '**/*.tsx'],
12
+ ignore: string[] = ['**/node_modules/**', '**/dist/**', '**/.mikk/**', '**/coverage/**', '**/*.d.ts', '**/*.test.ts', '**/*.spec.ts']
13
+ ): Promise<string[]> {
14
+ const files = await fg(patterns, {
15
+ cwd: projectRoot,
16
+ ignore,
17
+ absolute: false,
18
+ onlyFiles: true,
19
+ })
20
+ return files.map(f => f.replace(/\\/g, '/'))
21
+ }
22
+
23
+ /**
24
+ * Reads a file and returns its content as a UTF-8 string.
25
+ */
26
+ export async function readFileContent(filePath: string): Promise<string> {
27
+ return fs.readFile(filePath, 'utf-8')
28
+ }
29
+
30
+ /**
31
+ * Writes content to a file, creating parent directories if needed.
32
+ */
33
+ export async function writeFileContent(filePath: string, content: string): Promise<void> {
34
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
35
+ await fs.writeFile(filePath, content, 'utf-8')
36
+ }
37
+
38
+ /**
39
+ * Check if a file exists.
40
+ */
41
+ export async function fileExists(filePath: string): Promise<boolean> {
42
+ try {
43
+ await fs.access(filePath)
44
+ return true
45
+ } catch {
46
+ return false
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Set up the .mikk directory structure in a project root.
52
+ */
53
+ export async function setupMikkDirectory(projectRoot: string): Promise<void> {
54
+ const dirs = [
55
+ '.mikk',
56
+ '.mikk/fragments',
57
+ '.mikk/diagrams',
58
+ '.mikk/diagrams/modules',
59
+ '.mikk/diagrams/capsules',
60
+ '.mikk/diagrams/flows',
61
+ '.mikk/diagrams/impact',
62
+ '.mikk/diagrams/exposure',
63
+ '.mikk/intent',
64
+ '.mikk/cache',
65
+ ]
66
+ for (const dir of dirs) {
67
+ await fs.mkdir(path.join(projectRoot, dir), { recursive: true })
68
+ }
69
+
70
+ // Create .gitkeep in impact dir
71
+ const impactKeep = path.join(projectRoot, '.mikk/diagrams/impact/.gitkeep')
72
+ if (!await fileExists(impactKeep)) {
73
+ await fs.writeFile(impactKeep, '', 'utf-8')
74
+ }
75
+ }