@getmikk/watcher 1.8.0 → 1.9.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.
- package/package.json +2 -2
- package/src/incremental-analyzer.ts +90 -95
- package/tests/analyzer.test.ts +119 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getmikk/watcher",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"dev": "tsc --watch"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@getmikk/core": "^1.
|
|
24
|
+
"@getmikk/core": "^1.9.1",
|
|
25
25
|
"chokidar": "^4.0.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises'
|
|
|
2
2
|
import * as path from 'node:path'
|
|
3
3
|
import {
|
|
4
4
|
getParser, GraphBuilder, ImpactAnalyzer, LockCompiler, hashFile,
|
|
5
|
-
type ParsedFile, type DependencyGraph, type MikkLock, type MikkContract, type ImpactResult
|
|
5
|
+
type ParsedFile, type DependencyGraph, type MikkLock, type MikkContract, type ImpactResult, type GraphEdge
|
|
6
6
|
} from '@getmikk/core'
|
|
7
7
|
import type { FileChangeEvent } from './types.js'
|
|
8
8
|
|
|
@@ -13,14 +13,9 @@ const FULL_ANALYSIS_THRESHOLD = 15
|
|
|
13
13
|
const MAX_RETRIES = 3
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* IncrementalAnalyzer — re-parses only changed files,
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* Supports batch analysis: if > 15 files change at once (e.g. git checkout),
|
|
20
|
-
* runs a full re-analysis instead of incremental.
|
|
21
|
-
*
|
|
22
|
-
* Race condition handling: after parsing, re-hashes the file and re-parses
|
|
23
|
-
* if the content changed during parsing (up to 3 retries).
|
|
16
|
+
* IncrementalAnalyzer — re-parses only changed files, performs a surgical
|
|
17
|
+
* graph update (removes stale nodes/edges, inserts new ones), then runs
|
|
18
|
+
* impact analysis over the affected subgraph.
|
|
24
19
|
*/
|
|
25
20
|
export class IncrementalAnalyzer {
|
|
26
21
|
private parsedFiles: Map<string, ParsedFile> = new Map()
|
|
@@ -30,59 +25,96 @@ export class IncrementalAnalyzer {
|
|
|
30
25
|
private lock: MikkLock,
|
|
31
26
|
private contract: MikkContract,
|
|
32
27
|
private projectRoot: string
|
|
33
|
-
) {
|
|
28
|
+
) {
|
|
29
|
+
if (!this.graph.outEdges) this.graph.outEdges = new Map()
|
|
30
|
+
if (!this.graph.inEdges) this.graph.inEdges = new Map()
|
|
31
|
+
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
public get fileCount(): number {
|
|
34
|
+
return this.parsedFiles.size
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Handle a batch of file change events */
|
|
36
38
|
async analyzeBatch(events: FileChangeEvent[]): Promise<{
|
|
37
39
|
graph: DependencyGraph
|
|
38
40
|
lock: MikkLock
|
|
39
41
|
impactResult: ImpactResult
|
|
40
42
|
mode: 'incremental' | 'full'
|
|
41
43
|
}> {
|
|
42
|
-
// If too many changes at once, run full analysis
|
|
43
44
|
if (events.length > FULL_ANALYSIS_THRESHOLD) {
|
|
44
45
|
return this.runFullAnalysis(events)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
// Incremental: process each event, collecting changed file paths
|
|
48
48
|
const changedFilePaths: string[] = []
|
|
49
49
|
|
|
50
50
|
for (const event of events) {
|
|
51
51
|
if (event.type === 'deleted') {
|
|
52
52
|
this.parsedFiles.delete(event.path)
|
|
53
|
-
changedFilePaths.push(event.path)
|
|
54
53
|
} else {
|
|
55
54
|
const parsed = await this.parseWithRaceCheck(event.path)
|
|
56
|
-
if (parsed)
|
|
57
|
-
this.parsedFiles.set(event.path, parsed)
|
|
58
|
-
}
|
|
59
|
-
changedFilePaths.push(event.path)
|
|
55
|
+
if (parsed) this.parsedFiles.set(event.path, parsed)
|
|
60
56
|
}
|
|
57
|
+
changedFilePaths.push(event.path)
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
// --- Surgical graph update ---
|
|
61
|
+
const staleNodeIds = new Set<string>(
|
|
62
|
+
changedFilePaths.flatMap(fp => this.findNodeIdsForFile(fp))
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
for (const nodeId of staleNodeIds) {
|
|
66
|
+
this.graph.nodes.delete(nodeId)
|
|
67
|
+
}
|
|
68
|
+
for (const fp of changedFilePaths) {
|
|
69
|
+
this.graph.nodes.delete(fp)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const allStaleIds = new Set([...staleNodeIds, ...changedFilePaths])
|
|
73
|
+
this.graph.edges = this.graph.edges.filter(
|
|
74
|
+
edge => !allStaleIds.has(edge.from) && !allStaleIds.has(edge.to)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
const changedParsedFiles = changedFilePaths
|
|
78
|
+
.map(fp => this.parsedFiles.get(fp))
|
|
79
|
+
.filter((f): f is ParsedFile => f !== undefined)
|
|
80
|
+
|
|
81
|
+
if (changedParsedFiles.length > 0) {
|
|
82
|
+
const miniBuilder = new GraphBuilder()
|
|
83
|
+
const miniGraph = miniBuilder.build(changedParsedFiles)
|
|
84
|
+
|
|
85
|
+
for (const [id, node] of miniGraph.nodes) {
|
|
86
|
+
this.graph.nodes.set(id, node)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const edge of miniGraph.edges) {
|
|
90
|
+
this.graph.edges.push(edge)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Rebuild adjacency maps
|
|
95
|
+
this.graph.outEdges = new Map()
|
|
96
|
+
this.graph.inEdges = new Map()
|
|
97
|
+
for (const edge of this.graph.edges) {
|
|
98
|
+
if (!this.graph.outEdges.has(edge.from)) this.graph.outEdges.set(edge.from, [])
|
|
99
|
+
this.graph.outEdges.get(edge.from)!.push(edge)
|
|
100
|
+
if (!this.graph.inEdges.has(edge.to)) this.graph.inEdges.set(edge.to, [])
|
|
101
|
+
this.graph.inEdges.get(edge.to)!.push(edge)
|
|
102
|
+
}
|
|
68
103
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
)]
|
|
104
|
+
const changedNodeIds = [
|
|
105
|
+
...new Set(changedFilePaths.flatMap(fp => this.findNodeIdsForFile(fp)))
|
|
106
|
+
]
|
|
73
107
|
|
|
74
|
-
// Run impact analysis on all changed nodes
|
|
75
108
|
const analyzer = new ImpactAnalyzer(this.graph)
|
|
76
109
|
const impactResult = analyzer.analyze(changedNodeIds)
|
|
77
110
|
|
|
78
|
-
|
|
111
|
+
const allParsedFiles = [...this.parsedFiles.values()]
|
|
79
112
|
const compiler = new LockCompiler()
|
|
80
|
-
this.lock = compiler.compile(this.graph, this.contract, allParsedFiles)
|
|
113
|
+
this.lock = compiler.compile(this.graph, this.contract, allParsedFiles, undefined, this.projectRoot)
|
|
81
114
|
|
|
82
115
|
return { graph: this.graph, lock: this.lock, impactResult, mode: 'incremental' }
|
|
83
116
|
}
|
|
84
117
|
|
|
85
|
-
/** Handle a single file change event */
|
|
86
118
|
async analyze(event: FileChangeEvent): Promise<{
|
|
87
119
|
graph: DependencyGraph
|
|
88
120
|
lock: MikkLock
|
|
@@ -92,112 +124,75 @@ export class IncrementalAnalyzer {
|
|
|
92
124
|
return { graph: result.graph, lock: result.lock, impactResult: result.impactResult }
|
|
93
125
|
}
|
|
94
126
|
|
|
95
|
-
/** Add a parsed file to the tracker */
|
|
96
127
|
addParsedFile(file: ParsedFile): void {
|
|
97
128
|
this.parsedFiles.set(file.path, file)
|
|
98
129
|
}
|
|
99
130
|
|
|
100
|
-
/** Get the current parsed file count */
|
|
101
|
-
get fileCount(): number {
|
|
102
|
-
return this.parsedFiles.size
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// ─── Private ──────────────────────────────────────────────────
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Parse a file with race-condition detection.
|
|
109
|
-
* After parsing, re-hash the file. If the hash differs from what we started with,
|
|
110
|
-
* the file changed during parsing — re-parse (up to MAX_RETRIES).
|
|
111
|
-
*/
|
|
112
131
|
private async parseWithRaceCheck(changedFile: string): Promise<ParsedFile | null> {
|
|
113
132
|
const fullPath = path.join(this.projectRoot, changedFile)
|
|
114
|
-
|
|
115
133
|
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
116
134
|
try {
|
|
117
135
|
const content = await fs.readFile(fullPath, 'utf-8')
|
|
118
136
|
const parser = getParser(changedFile)
|
|
119
137
|
const parsedFile = await parser.parse(changedFile, content)
|
|
120
138
|
|
|
121
|
-
// Race condition check: re-hash after parse
|
|
122
139
|
try {
|
|
123
140
|
const postParseHash = await hashFile(fullPath)
|
|
124
|
-
if (postParseHash === parsedFile.hash)
|
|
125
|
-
return parsedFile // Content stable
|
|
126
|
-
}
|
|
127
|
-
// Content changed during parse — retry
|
|
141
|
+
if (postParseHash === parsedFile.hash) return parsedFile
|
|
128
142
|
} catch {
|
|
129
|
-
return parsedFile
|
|
143
|
+
return parsedFile
|
|
130
144
|
}
|
|
131
145
|
} catch {
|
|
132
|
-
return null
|
|
146
|
+
return null
|
|
133
147
|
}
|
|
134
148
|
}
|
|
135
|
-
|
|
136
|
-
// Exhausted retries — parse one final time and accept
|
|
137
|
-
try {
|
|
138
|
-
const content = await fs.readFile(fullPath, 'utf-8')
|
|
139
|
-
const parser = getParser(changedFile)
|
|
140
|
-
return await parser.parse(changedFile, content)
|
|
141
|
-
} catch {
|
|
142
|
-
return null
|
|
143
|
-
}
|
|
149
|
+
return null
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
/** Run a full re-analysis (for large batches like git checkout) */
|
|
147
152
|
private async runFullAnalysis(events: FileChangeEvent[]): Promise<{
|
|
148
153
|
graph: DependencyGraph
|
|
149
154
|
lock: MikkLock
|
|
150
155
|
impactResult: ImpactResult
|
|
151
156
|
mode: 'full'
|
|
152
157
|
}> {
|
|
153
|
-
// Remove deleted files
|
|
154
158
|
for (const event of events) {
|
|
155
|
-
if (event.type === 'deleted')
|
|
156
|
-
this.parsedFiles.delete(event.path)
|
|
157
|
-
}
|
|
159
|
+
if (event.type === 'deleted') this.parsedFiles.delete(event.path)
|
|
158
160
|
}
|
|
159
161
|
|
|
160
|
-
// Re-parse all non-deleted changed files
|
|
161
162
|
const nonDeleted = events.filter(e => e.type !== 'deleted')
|
|
162
163
|
await Promise.all(nonDeleted.map(async (event) => {
|
|
163
164
|
const parsed = await this.parseWithRaceCheck(event.path)
|
|
164
|
-
if (parsed)
|
|
165
|
-
this.parsedFiles.set(event.path, parsed)
|
|
166
|
-
}
|
|
165
|
+
if (parsed) this.parsedFiles.set(event.path, parsed)
|
|
167
166
|
}))
|
|
168
167
|
|
|
169
|
-
// Full rebuild
|
|
170
168
|
const allParsedFiles = [...this.parsedFiles.values()]
|
|
171
169
|
const builder = new GraphBuilder()
|
|
172
170
|
this.graph = builder.build(allParsedFiles)
|
|
173
171
|
|
|
174
172
|
const compiler = new LockCompiler()
|
|
175
|
-
this.lock = compiler.compile(this.graph, this.contract, allParsedFiles)
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
medium: [],
|
|
189
|
-
low: [],
|
|
190
|
-
},
|
|
191
|
-
depth: 0,
|
|
192
|
-
confidence: 'low', // Full rebuild = can't determine precise impact
|
|
193
|
-
},
|
|
194
|
-
mode: 'full',
|
|
173
|
+
this.lock = compiler.compile(this.graph, this.contract, allParsedFiles, undefined, this.projectRoot)
|
|
174
|
+
|
|
175
|
+
const impactResult: ImpactResult = {
|
|
176
|
+
changed: events.map(e => e.path),
|
|
177
|
+
impacted: [],
|
|
178
|
+
allImpacted: [],
|
|
179
|
+
depth: 0,
|
|
180
|
+
entryPoints: [],
|
|
181
|
+
criticalModules: [],
|
|
182
|
+
paths: [],
|
|
183
|
+
confidence: 1.0,
|
|
184
|
+
riskScore: 0,
|
|
185
|
+
classified: { critical: [], high: [], medium: [], low: [] }
|
|
195
186
|
}
|
|
187
|
+
|
|
188
|
+
return { graph: this.graph, lock: this.lock, impactResult, mode: 'full' }
|
|
196
189
|
}
|
|
197
190
|
|
|
198
|
-
private
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
191
|
+
private findNodeIdsForFile(filePath: string): string[] {
|
|
192
|
+
const ids: string[] = []
|
|
193
|
+
for (const [id, node] of this.graph.nodes) {
|
|
194
|
+
if (node.file === filePath) ids.push(id)
|
|
195
|
+
}
|
|
196
|
+
return ids
|
|
202
197
|
}
|
|
203
198
|
}
|
package/tests/analyzer.test.ts
CHANGED
|
@@ -1,91 +1,158 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test'
|
|
2
2
|
import { IncrementalAnalyzer } from '../src/incremental-analyzer.js'
|
|
3
|
-
import type { MikkLock,
|
|
3
|
+
import type { MikkLock, DependencyGraph, MikkContract } from '@getmikk/core'
|
|
4
|
+
import type { FileChangeEvent } from '../src/types.js'
|
|
4
5
|
|
|
5
6
|
describe('IncrementalAnalyzer', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
7
|
+
const mockGraph = (): DependencyGraph => ({
|
|
8
|
+
nodes: new Map(),
|
|
9
|
+
edges: [],
|
|
10
|
+
outEdges: new Map(),
|
|
11
|
+
inEdges: new Map()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const mockLock: MikkLock = {
|
|
15
|
+
version: '2.0.0',
|
|
16
|
+
generatedAt: new Date().toISOString(),
|
|
17
|
+
generatorVersion: '1.0.0',
|
|
18
|
+
projectRoot: '/project',
|
|
19
|
+
syncState: {
|
|
20
|
+
status: 'clean',
|
|
21
|
+
lastSyncAt: new Date().toISOString(),
|
|
22
|
+
lockHash: 'abc',
|
|
23
|
+
contractHash: 'xyz'
|
|
24
|
+
},
|
|
25
|
+
files: {
|
|
26
|
+
'src/index.ts': {
|
|
27
|
+
path: 'src/index.ts',
|
|
28
|
+
hash: 'abc',
|
|
29
|
+
moduleId: 'root',
|
|
30
|
+
lastModified: new Date().toISOString(),
|
|
31
|
+
imports: []
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
functions: {},
|
|
35
|
+
classes: {},
|
|
36
|
+
modules: {},
|
|
37
|
+
graph: {
|
|
38
|
+
nodes: 1,
|
|
39
|
+
edges: 0,
|
|
40
|
+
rootHash: 'abc'
|
|
23
41
|
}
|
|
42
|
+
}
|
|
24
43
|
|
|
44
|
+
const contract: MikkContract = {
|
|
45
|
+
version: '2.0.0',
|
|
46
|
+
project: {
|
|
47
|
+
name: 'test',
|
|
48
|
+
description: 'test project',
|
|
49
|
+
language: 'typescript',
|
|
50
|
+
framework: 'none',
|
|
51
|
+
entryPoints: []
|
|
52
|
+
},
|
|
53
|
+
declared: {
|
|
54
|
+
modules: [],
|
|
55
|
+
constraints: [],
|
|
56
|
+
decisions: []
|
|
57
|
+
},
|
|
58
|
+
overwrite: {
|
|
59
|
+
mode: 'never',
|
|
60
|
+
requireConfirmation: false
|
|
61
|
+
},
|
|
62
|
+
policies: {
|
|
63
|
+
maxRiskScore: 70,
|
|
64
|
+
maxImpactNodes: 10,
|
|
65
|
+
protectedModules: [],
|
|
66
|
+
enforceStrictBoundaries: true,
|
|
67
|
+
requireReasoningForCritical: true
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
it('detects changes correctly without throwing', async () => {
|
|
25
72
|
const analyzer = new IncrementalAnalyzer(
|
|
26
|
-
|
|
73
|
+
mockGraph(),
|
|
27
74
|
mockLock,
|
|
28
|
-
|
|
29
|
-
project: { name: 'test', language: 'typescript', framework: null },
|
|
30
|
-
declared: { modules: [], constraints: [], decisions: [] },
|
|
31
|
-
overwrite: { mode: 'never', requireConfirmation: false }
|
|
32
|
-
},
|
|
75
|
+
contract,
|
|
33
76
|
'/project'
|
|
34
77
|
)
|
|
35
78
|
|
|
36
|
-
|
|
37
|
-
|
|
79
|
+
const event: FileChangeEvent = {
|
|
80
|
+
path: 'src/index.ts',
|
|
81
|
+
type: 'changed',
|
|
82
|
+
oldHash: 'old',
|
|
83
|
+
newHash: 'abc',
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
affectedModuleIds: []
|
|
86
|
+
}
|
|
87
|
+
const result = await analyzer.analyze(event)
|
|
38
88
|
expect(result.graph).toBeDefined()
|
|
39
89
|
expect(result.lock).toBeDefined()
|
|
40
90
|
expect(result.impactResult).toBeDefined()
|
|
41
91
|
})
|
|
42
92
|
|
|
43
93
|
describe('Edge Cases and Batch Processing', () => {
|
|
44
|
-
const mockLock: MikkLock = {
|
|
45
|
-
version: '1',
|
|
46
|
-
lastUpdated: new Date().toISOString(),
|
|
47
|
-
files: {}, functions: {}, classes: {}, modules: {}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const contract = {
|
|
51
|
-
project: { name: 'test', language: 'typescript', framework: null },
|
|
52
|
-
declared: { modules: [], constraints: [], decisions: [] },
|
|
53
|
-
overwrite: { mode: 'never', requireConfirmation: false }
|
|
54
|
-
}
|
|
55
|
-
|
|
56
94
|
it('handles file deletions by removing nodes from graph and lock', async () => {
|
|
57
|
-
const analyzer = new IncrementalAnalyzer(
|
|
95
|
+
const analyzer = new IncrementalAnalyzer(mockGraph(), mockLock, contract, '/project')
|
|
58
96
|
// First add it
|
|
59
|
-
analyzer.addParsedFile({
|
|
97
|
+
analyzer.addParsedFile({
|
|
98
|
+
path: 'src/to-delete.ts',
|
|
99
|
+
language: 'typescript',
|
|
100
|
+
hash: 'foo',
|
|
101
|
+
parsedAt: Date.now(),
|
|
102
|
+
functions: [],
|
|
103
|
+
classes: [],
|
|
104
|
+
imports: [],
|
|
105
|
+
exports: [],
|
|
106
|
+
routes: [],
|
|
107
|
+
variables: [],
|
|
108
|
+
generics: [],
|
|
109
|
+
calls: []
|
|
110
|
+
})
|
|
60
111
|
expect(analyzer.fileCount).toBe(1)
|
|
61
112
|
|
|
62
113
|
// Now send a deleted event
|
|
63
|
-
|
|
114
|
+
const event: FileChangeEvent = {
|
|
115
|
+
path: 'src/to-delete.ts',
|
|
116
|
+
type: 'deleted',
|
|
117
|
+
oldHash: 'foo',
|
|
118
|
+
newHash: '',
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
affectedModuleIds: []
|
|
121
|
+
}
|
|
122
|
+
await analyzer.analyze(event)
|
|
64
123
|
expect(analyzer.fileCount).toBe(0)
|
|
65
124
|
})
|
|
66
125
|
|
|
67
126
|
it('survives analyze events on completely non-existent OS files gracefully', async () => {
|
|
68
|
-
const analyzer = new IncrementalAnalyzer(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
127
|
+
const analyzer = new IncrementalAnalyzer(mockGraph(), mockLock, contract, '/project')
|
|
128
|
+
const event: FileChangeEvent = {
|
|
129
|
+
path: 'does/not/exist.ts',
|
|
130
|
+
type: 'changed',
|
|
131
|
+
oldHash: '',
|
|
132
|
+
newHash: 'new',
|
|
133
|
+
timestamp: Date.now(),
|
|
134
|
+
affectedModuleIds: []
|
|
135
|
+
}
|
|
136
|
+
const result = await analyzer.analyzeBatch([event])
|
|
137
|
+
expect(result.mode).toBe('incremental')
|
|
72
138
|
expect(result.impactResult).toBeDefined()
|
|
73
|
-
// Should not have crashed the analyzer
|
|
74
139
|
expect(analyzer.fileCount).toBe(0)
|
|
75
140
|
})
|
|
76
141
|
|
|
77
142
|
it('triggers a full re-analysis if file batch exceeds FULL_ANALYSIS_THRESHOLD (15)', async () => {
|
|
78
|
-
const analyzer = new IncrementalAnalyzer(
|
|
79
|
-
const events = Array.from({ length: 16 }).map((_, i) => ({
|
|
143
|
+
const analyzer = new IncrementalAnalyzer(mockGraph(), mockLock, contract, '/project')
|
|
144
|
+
const events: FileChangeEvent[] = Array.from({ length: 16 }).map((_, i) => ({
|
|
80
145
|
path: `src/file_${i}.ts`,
|
|
81
|
-
type: '
|
|
146
|
+
type: 'changed',
|
|
147
|
+
oldHash: '',
|
|
148
|
+
newHash: `hash_${i}`,
|
|
149
|
+
timestamp: Date.now(),
|
|
150
|
+
affectedModuleIds: []
|
|
82
151
|
}))
|
|
83
152
|
|
|
84
153
|
const result = await analyzer.analyzeBatch(events)
|
|
85
|
-
// It should have hit runFullAnalysis, which returns mode: 'full'
|
|
86
154
|
expect(result.mode).toBe('full')
|
|
87
|
-
|
|
88
|
-
expect(result.impactResult.confidence).toBe('low')
|
|
155
|
+
expect(result.impactResult.confidence).toBe(1.0)
|
|
89
156
|
})
|
|
90
157
|
})
|
|
91
158
|
})
|