tryassay 0.22.2 → 0.25.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/dist/cli.js +38 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/assess.js +73 -6
- package/dist/commands/assess.js.map +1 -1
- package/dist/commands/suppress.d.ts +17 -0
- package/dist/commands/suppress.js +119 -0
- package/dist/commands/suppress.js.map +1 -0
- package/dist/lib/__tests__/claim-id.test.d.ts +1 -0
- package/dist/lib/__tests__/claim-id.test.js +24 -0
- package/dist/lib/__tests__/claim-id.test.js.map +1 -0
- package/dist/lib/__tests__/gap-matcher.test.d.ts +1 -0
- package/dist/lib/__tests__/gap-matcher.test.js +291 -0
- package/dist/lib/__tests__/gap-matcher.test.js.map +1 -0
- package/dist/lib/__tests__/gap-suppression.test.d.ts +1 -0
- package/dist/lib/__tests__/gap-suppression.test.js +168 -0
- package/dist/lib/__tests__/gap-suppression.test.js.map +1 -0
- package/dist/lib/__tests__/output-dir-migration.test.d.ts +1 -0
- package/dist/lib/__tests__/output-dir-migration.test.js +33 -0
- package/dist/lib/__tests__/output-dir-migration.test.js.map +1 -0
- package/dist/lib/__tests__/overrides.test.d.ts +1 -0
- package/dist/lib/__tests__/overrides.test.js +55 -0
- package/dist/lib/__tests__/overrides.test.js.map +1 -0
- package/dist/lib/assessment-reporter.js +4 -1
- package/dist/lib/assessment-reporter.js.map +1 -1
- package/dist/lib/claim-id.d.ts +1 -0
- package/dist/lib/claim-id.js +6 -0
- package/dist/lib/claim-id.js.map +1 -0
- package/dist/lib/gap-matcher.d.ts +63 -0
- package/dist/lib/gap-matcher.js +268 -0
- package/dist/lib/gap-matcher.js.map +1 -0
- package/dist/lib/intent-extractor.d.ts +5 -0
- package/dist/lib/intent-extractor.js +18 -3
- package/dist/lib/intent-extractor.js.map +1 -1
- package/dist/lib/intent-types.d.ts +11 -0
- package/dist/lib/llm-provider.js +36 -7
- package/dist/lib/llm-provider.js.map +1 -1
- package/dist/lib/output-dir.d.ts +4 -0
- package/dist/lib/output-dir.js +19 -0
- package/dist/lib/output-dir.js.map +1 -0
- package/dist/lib/overrides.d.ts +27 -0
- package/dist/lib/overrides.js +81 -0
- package/dist/lib/overrides.js.map +1 -0
- package/dist/types.d.ts +12 -0
- package/package.json +3 -2
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { extractAnchors, simpleStem, extractTokens, tokensOverlap, matchGapToSuppression, } from '../gap-matcher.js';
|
|
3
|
+
// ── extractAnchors ───────────────────────────────────────────────────
|
|
4
|
+
describe('extractAnchors', () => {
|
|
5
|
+
it('extracts file paths', () => {
|
|
6
|
+
const anchors = extractAnchors('Missing src/routes/auth.ts implementation');
|
|
7
|
+
expect(anchors).toContain('src/routes/auth.ts');
|
|
8
|
+
});
|
|
9
|
+
it('extracts nested file paths', () => {
|
|
10
|
+
const anchors = extractAnchors('The file src/lib/gmail/handler.ts is referenced');
|
|
11
|
+
expect(anchors).toContain('src/lib/gmail/handler.ts');
|
|
12
|
+
});
|
|
13
|
+
it('extracts API route patterns', () => {
|
|
14
|
+
const anchors = extractAnchors('POST /api/users endpoint not found');
|
|
15
|
+
expect(anchors).toContain('/api/users');
|
|
16
|
+
});
|
|
17
|
+
it('extracts route-like paths without HTTP method', () => {
|
|
18
|
+
const anchors = extractAnchors('The /api/v2/auth/login route');
|
|
19
|
+
expect(anchors).toContain('/api/v2/auth/login');
|
|
20
|
+
});
|
|
21
|
+
it('extracts camelCase identifiers >= 6 chars', () => {
|
|
22
|
+
const anchors = extractAnchors('The handleAuth function is missing');
|
|
23
|
+
expect(anchors).toContain('handleauth');
|
|
24
|
+
});
|
|
25
|
+
it('extracts snake_case identifiers >= 6 chars', () => {
|
|
26
|
+
const anchors = extractAnchors('Missing user_profile_handler implementation');
|
|
27
|
+
expect(anchors).toContain('user_profile_handler');
|
|
28
|
+
});
|
|
29
|
+
it('ignores short identifiers', () => {
|
|
30
|
+
const anchors = extractAnchors('The foo_bar variable');
|
|
31
|
+
// foo_bar is 7 chars, should be included
|
|
32
|
+
expect(anchors).toContain('foo_bar');
|
|
33
|
+
});
|
|
34
|
+
it('returns empty array for empty input', () => {
|
|
35
|
+
expect(extractAnchors('')).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
it('returns empty array for text without anchors', () => {
|
|
38
|
+
const anchors = extractAnchors('This is a plain sentence');
|
|
39
|
+
expect(anchors).toEqual([]);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
// ── simpleStem ───────────────────────────────────────────────────────
|
|
43
|
+
describe('simpleStem', () => {
|
|
44
|
+
it('strips -ing suffix', () => {
|
|
45
|
+
expect(simpleStem('handling')).toBe('handl');
|
|
46
|
+
});
|
|
47
|
+
it('strips -tion suffix', () => {
|
|
48
|
+
// 'authentication' hits -ation first (longer match) → 'authentic'
|
|
49
|
+
expect(simpleStem('authentication')).toBe('authentic');
|
|
50
|
+
// 'production' hits -tion → 'produc'
|
|
51
|
+
expect(simpleStem('production')).toBe('produc');
|
|
52
|
+
});
|
|
53
|
+
it('strips -ation suffix', () => {
|
|
54
|
+
expect(simpleStem('validation')).toBe('valid');
|
|
55
|
+
});
|
|
56
|
+
it('strips -ed suffix', () => {
|
|
57
|
+
expect(simpleStem('configured')).toBe('configur');
|
|
58
|
+
});
|
|
59
|
+
it('strips trailing -s', () => {
|
|
60
|
+
expect(simpleStem('handlers')).toBe('handler');
|
|
61
|
+
});
|
|
62
|
+
it('does not strip -ss words', () => {
|
|
63
|
+
expect(simpleStem('process')).toBe('process');
|
|
64
|
+
});
|
|
65
|
+
it('protects short words (4 chars or less)', () => {
|
|
66
|
+
expect(simpleStem('auth')).toBe('auth');
|
|
67
|
+
expect(simpleStem('file')).toBe('file');
|
|
68
|
+
expect(simpleStem('code')).toBe('code');
|
|
69
|
+
});
|
|
70
|
+
it('lowercases input', () => {
|
|
71
|
+
expect(simpleStem('GMAIL')).toBe('gmail');
|
|
72
|
+
});
|
|
73
|
+
it('strips -ment suffix', () => {
|
|
74
|
+
expect(simpleStem('management')).toBe('manage');
|
|
75
|
+
});
|
|
76
|
+
it('strips -ness suffix', () => {
|
|
77
|
+
expect(simpleStem('completeness')).toBe('complete');
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// ── extractTokens ────────────────────────────────────────────────────
|
|
81
|
+
describe('extractTokens', () => {
|
|
82
|
+
it('removes general stop words', () => {
|
|
83
|
+
const tokens = extractTokens('the handler is not working for the user', []);
|
|
84
|
+
expect(tokens).not.toContain('the');
|
|
85
|
+
expect(tokens).not.toContain('is');
|
|
86
|
+
expect(tokens).not.toContain('not');
|
|
87
|
+
expect(tokens).not.toContain('for');
|
|
88
|
+
});
|
|
89
|
+
it('removes domain stop stems', () => {
|
|
90
|
+
const tokens = extractTokens('missing file implementation documented', []);
|
|
91
|
+
// "miss", "file", "implement", "document" are all domain stop stems
|
|
92
|
+
expect(tokens).toEqual([]);
|
|
93
|
+
});
|
|
94
|
+
it('excludes anchor strings from tokens', () => {
|
|
95
|
+
const anchors = ['src/routes/auth.ts'];
|
|
96
|
+
const tokens = extractTokens('The src/routes/auth.ts file has gmail logic', anchors);
|
|
97
|
+
// "auth" and "ts" should be excluded since they're part of the anchor path
|
|
98
|
+
// "gmail" should remain
|
|
99
|
+
expect(tokens).toContain('gmail');
|
|
100
|
+
});
|
|
101
|
+
it('returns stemmed tokens', () => {
|
|
102
|
+
const tokens = extractTokens('authentication handling validation', []);
|
|
103
|
+
expect(tokens).toContain('authentic');
|
|
104
|
+
expect(tokens).toContain('handl');
|
|
105
|
+
expect(tokens).toContain('valid');
|
|
106
|
+
});
|
|
107
|
+
it('returns empty for empty input', () => {
|
|
108
|
+
expect(extractTokens('', [])).toEqual([]);
|
|
109
|
+
});
|
|
110
|
+
it('deduplicates tokens', () => {
|
|
111
|
+
const tokens = extractTokens('gmail gmail gmail calendar', []);
|
|
112
|
+
const gmailCount = tokens.filter(t => t === 'gmail').length;
|
|
113
|
+
expect(gmailCount).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
// ── tokensOverlap ────────────────────────────────────────────────────
|
|
117
|
+
describe('tokensOverlap', () => {
|
|
118
|
+
it('finds exact matches', () => {
|
|
119
|
+
const overlap = tokensOverlap(['gmail', 'calendar'], ['gmail', 'webhook']);
|
|
120
|
+
expect(overlap).toContain('gmail');
|
|
121
|
+
expect(overlap).not.toContain('calendar');
|
|
122
|
+
expect(overlap).not.toContain('webhook');
|
|
123
|
+
});
|
|
124
|
+
it('finds prefix matches (4+ chars)', () => {
|
|
125
|
+
const overlap = tokensOverlap(['auth'], ['authent']);
|
|
126
|
+
expect(overlap).toContain('auth');
|
|
127
|
+
});
|
|
128
|
+
it('prefix matching works in both directions', () => {
|
|
129
|
+
const overlap = tokensOverlap(['authent'], ['auth']);
|
|
130
|
+
expect(overlap).toContain('auth');
|
|
131
|
+
});
|
|
132
|
+
it('rejects prefix matches shorter than 4 chars', () => {
|
|
133
|
+
const overlap = tokensOverlap(['foo'], ['foobar']);
|
|
134
|
+
expect(overlap).toEqual([]);
|
|
135
|
+
});
|
|
136
|
+
it('returns empty for no overlap', () => {
|
|
137
|
+
const overlap = tokensOverlap(['gmail', 'calendar'], ['webhook', 'cron']);
|
|
138
|
+
expect(overlap).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
it('handles empty inputs', () => {
|
|
141
|
+
expect(tokensOverlap([], ['gmail'])).toEqual([]);
|
|
142
|
+
expect(tokensOverlap(['gmail'], [])).toEqual([]);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
// ── matchGapToSuppression ────────────────────────────────────────────
|
|
146
|
+
describe('matchGapToSuppression', () => {
|
|
147
|
+
it('returns null on type mismatch', () => {
|
|
148
|
+
const gap = { type: 'missing-flow', description: 'No auth flow', gapId: 'abc123' };
|
|
149
|
+
const supp = {
|
|
150
|
+
gapId: 'abc123',
|
|
151
|
+
type: 'dead-code',
|
|
152
|
+
description: 'No auth flow',
|
|
153
|
+
reason: 'test',
|
|
154
|
+
suppressedAt: '',
|
|
155
|
+
};
|
|
156
|
+
expect(matchGapToSuppression(gap, supp)).toBeNull();
|
|
157
|
+
});
|
|
158
|
+
it('returns exact confidence on ID match', () => {
|
|
159
|
+
const gap = { type: 'missing-flow', description: 'anything', gapId: 'abc12345' };
|
|
160
|
+
const supp = {
|
|
161
|
+
gapId: 'abc12345',
|
|
162
|
+
type: 'missing-flow',
|
|
163
|
+
description: 'completely different text',
|
|
164
|
+
reason: 'test',
|
|
165
|
+
suppressedAt: '',
|
|
166
|
+
};
|
|
167
|
+
const result = matchGapToSuppression(gap, supp);
|
|
168
|
+
expect(result).not.toBeNull();
|
|
169
|
+
expect(result.confidence).toBe('exact');
|
|
170
|
+
expect(result.score).toBe(1.0);
|
|
171
|
+
});
|
|
172
|
+
it('matches Gmail gap rephrasing (real claudeclaw data)', () => {
|
|
173
|
+
// Run 1 suppressed gap
|
|
174
|
+
const supp = {
|
|
175
|
+
gapId: '870b77bf',
|
|
176
|
+
type: 'incomplete-flow',
|
|
177
|
+
description: 'Gmail and Calendar scripts referenced in .env.example but no corresponding implementation files found in source directory',
|
|
178
|
+
evidence: 'GMAIL_CREDENTIALS_PATH and GOOGLE_CALENDAR_ID referenced in .env.example but no gmail.ts or calendar.ts found in src/',
|
|
179
|
+
reason: 'User-installed skill dependencies, not core application code',
|
|
180
|
+
suppressedAt: '2026-03-07T14:20:00Z',
|
|
181
|
+
};
|
|
182
|
+
// Run 2 rephrased gap (different description, different gapId)
|
|
183
|
+
const gap = {
|
|
184
|
+
type: 'incomplete-flow',
|
|
185
|
+
gapId: '09fddfd4',
|
|
186
|
+
description: 'Gmail/Calendar integration scripts missing despite environment configuration referencing them',
|
|
187
|
+
evidence: '.env.example defines GMAIL_CREDENTIALS_PATH and GOOGLE_CALENDAR_ID but src/ contains no gmail or calendar implementation modules',
|
|
188
|
+
};
|
|
189
|
+
const result = matchGapToSuppression(gap, supp);
|
|
190
|
+
expect(result).not.toBeNull();
|
|
191
|
+
expect(result.confidence).toBe('medium');
|
|
192
|
+
expect(result.score).toBeGreaterThanOrEqual(0.55);
|
|
193
|
+
});
|
|
194
|
+
it('does not match different gaps about the same file', () => {
|
|
195
|
+
const supp = {
|
|
196
|
+
gapId: 'aaa11111',
|
|
197
|
+
type: 'incomplete-flow',
|
|
198
|
+
description: 'Error handling in src/routes/auth.ts is incomplete — no try-catch around token verification',
|
|
199
|
+
evidence: 'src/routes/auth.ts:45-60 has unprotected JWT verification call',
|
|
200
|
+
reason: 'Known, tracked separately',
|
|
201
|
+
suppressedAt: '',
|
|
202
|
+
};
|
|
203
|
+
const gap = {
|
|
204
|
+
type: 'incomplete-flow',
|
|
205
|
+
gapId: 'bbb22222',
|
|
206
|
+
description: 'Input validation missing in src/routes/auth.ts — no schema validation for login request body',
|
|
207
|
+
evidence: 'src/routes/auth.ts:20-30 accepts req.body without zod/joi validation',
|
|
208
|
+
};
|
|
209
|
+
// These share the file path anchor but describe different issues.
|
|
210
|
+
// The token content should be different enough to prevent matching.
|
|
211
|
+
const result = matchGapToSuppression(gap, supp);
|
|
212
|
+
// They might match on high anchor overlap alone if only 1 anchor each,
|
|
213
|
+
// but should not reach the threshold since tokens differ significantly
|
|
214
|
+
if (result !== null) {
|
|
215
|
+
// If it matches, the score should be below threshold or have too few shared items
|
|
216
|
+
// This test is defensive — we want no match ideally
|
|
217
|
+
expect(result.score).toBeLessThan(0.55);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
it('does not match completely different gaps', () => {
|
|
221
|
+
const supp = {
|
|
222
|
+
gapId: 'xxx11111',
|
|
223
|
+
type: 'missing-flow',
|
|
224
|
+
description: 'No password reset flow exists for user accounts',
|
|
225
|
+
evidence: 'No reset endpoint or email sending logic found in auth module',
|
|
226
|
+
reason: 'Planned for v2',
|
|
227
|
+
suppressedAt: '',
|
|
228
|
+
};
|
|
229
|
+
const gap = {
|
|
230
|
+
type: 'missing-flow',
|
|
231
|
+
gapId: 'yyy22222',
|
|
232
|
+
description: 'WebSocket disconnection handling is absent from the chat module',
|
|
233
|
+
evidence: 'src/chat/websocket.ts has no onclose or reconnection logic',
|
|
234
|
+
};
|
|
235
|
+
expect(matchGapToSuppression(gap, supp)).toBeNull();
|
|
236
|
+
});
|
|
237
|
+
it('matches short descriptions with shared concept', () => {
|
|
238
|
+
const supp = {
|
|
239
|
+
gapId: 'short1',
|
|
240
|
+
type: 'incomplete-flow',
|
|
241
|
+
description: 'Gradium TTS provider lacks dedicated implementation file',
|
|
242
|
+
evidence: 'GRADIUM_API_KEY referenced in .env.example but no gradium.ts module',
|
|
243
|
+
reason: 'Consolidated in voice.ts',
|
|
244
|
+
suppressedAt: '',
|
|
245
|
+
};
|
|
246
|
+
const gap = {
|
|
247
|
+
type: 'incomplete-flow',
|
|
248
|
+
gapId: 'short2',
|
|
249
|
+
description: 'No dedicated Gradium TTS implementation module exists',
|
|
250
|
+
evidence: 'GRADIUM_API_KEY and GRADIUM_VOICE_ID in .env.example without corresponding gradium provider file',
|
|
251
|
+
};
|
|
252
|
+
const result = matchGapToSuppression(gap, supp);
|
|
253
|
+
expect(result).not.toBeNull();
|
|
254
|
+
expect(result.score).toBeGreaterThanOrEqual(0.55);
|
|
255
|
+
});
|
|
256
|
+
it('uses pre-computed matchAnchors and matchTokens', () => {
|
|
257
|
+
const supp = {
|
|
258
|
+
gapId: 'pre1',
|
|
259
|
+
type: 'incomplete-flow',
|
|
260
|
+
description: 'Gmail integration scripts missing',
|
|
261
|
+
evidence: 'src/gmail.ts not found',
|
|
262
|
+
reason: 'test',
|
|
263
|
+
suppressedAt: '',
|
|
264
|
+
matchAnchors: ['src/gmail.ts'],
|
|
265
|
+
matchTokens: ['gmail', 'integr', 'script'],
|
|
266
|
+
};
|
|
267
|
+
const gap = {
|
|
268
|
+
type: 'incomplete-flow',
|
|
269
|
+
gapId: 'pre2',
|
|
270
|
+
description: 'Gmail integration modules absent from source',
|
|
271
|
+
evidence: 'Expected src/gmail.ts but not present',
|
|
272
|
+
};
|
|
273
|
+
const result = matchGapToSuppression(gap, supp);
|
|
274
|
+
expect(result).not.toBeNull();
|
|
275
|
+
});
|
|
276
|
+
it('returns null when gap has no gapId and no text overlap', () => {
|
|
277
|
+
const supp = {
|
|
278
|
+
gapId: 'id1',
|
|
279
|
+
type: 'dead-code',
|
|
280
|
+
description: 'Unused helper function in utils',
|
|
281
|
+
reason: 'test',
|
|
282
|
+
suppressedAt: '',
|
|
283
|
+
};
|
|
284
|
+
const gap = {
|
|
285
|
+
type: 'dead-code',
|
|
286
|
+
description: 'Orphaned database migration script',
|
|
287
|
+
};
|
|
288
|
+
expect(matchGapToSuppression(gap, supp)).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
//# sourceMappingURL=gap-matcher.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gap-matcher.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/gap-matcher.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EACL,cAAc,EACd,UAAU,EACV,aAAa,EACb,aAAa,EACb,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAG3B,wEAAwE;AAExE,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,OAAO,GAAG,cAAc,CAAC,2CAA2C,CAAC,CAAC;QAC5E,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,OAAO,GAAG,cAAc,CAAC,iDAAiD,CAAC,CAAC;QAClF,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,OAAO,GAAG,cAAc,CAAC,oCAAoC,CAAC,CAAC;QACrE,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,OAAO,GAAG,cAAc,CAAC,8BAA8B,CAAC,CAAC;QAC/D,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,OAAO,GAAG,cAAc,CAAC,oCAAoC,CAAC,CAAC;QACrE,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,GAAG,EAAE;QACpD,MAAM,OAAO,GAAG,cAAc,CAAC,6CAA6C,CAAC,CAAC;QAC9E,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,sBAAsB,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,OAAO,GAAG,cAAc,CAAC,sBAAsB,CAAC,CAAC;QACvD,yCAAyC;QACzC,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACvC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,OAAO,GAAG,cAAc,CAAC,0BAA0B,CAAC,CAAC;QAC3D,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,wEAAwE;AAExE,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,kEAAkE;QAClE,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;QACvD,qCAAqC;QACrC,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0BAA0B,EAAE,GAAG,EAAE;QAClC,MAAM,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAChD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACxC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC1C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kBAAkB,EAAE,GAAG,EAAE;QAC1B,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,wEAAwE;AAExE,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,MAAM,GAAG,aAAa,CAAC,yCAAyC,EAAE,EAAE,CAAC,CAAC;QAC5E,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACpC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,MAAM,GAAG,aAAa,CAAC,wCAAwC,EAAE,EAAE,CAAC,CAAC;QAC3E,oEAAoE;QACpE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,OAAO,GAAG,CAAC,oBAAoB,CAAC,CAAC;QACvC,MAAM,MAAM,GAAG,aAAa,CAAC,6CAA6C,EAAE,OAAO,CAAC,CAAC;QACrF,2EAA2E;QAC3E,wBAAwB;QACxB,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,MAAM,GAAG,aAAa,CAAC,oCAAoC,EAAE,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC;QACtC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QAClC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,MAAM,GAAG,aAAa,CAAC,4BAA4B,EAAE,EAAE,CAAC,CAAC;QAC/D,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;QAC5D,MAAM,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,wEAAwE;AAExE,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,qBAAqB,EAAE,GAAG,EAAE;QAC7B,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC;QAC3E,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;QACrD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC;QACrD,MAAM,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;QACnD,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,OAAO,GAAG,aAAa,CAAC,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;QAC1E,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC9B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sBAAsB,EAAE,GAAG,EAAE;QAC9B,MAAM,CAAC,aAAa,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,wEAAwE;AAExE,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,cAAuB,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAC5F,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,WAAW;YACjB,WAAW,EAAE,cAAc;YAC3B,MAAM,EAAE,MAAM;YACd,YAAY,EAAE,EAAE;SACjB,CAAC;QACF,MAAM,CAAC,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,GAAG,GAAG,EAAE,IAAI,EAAE,cAAuB,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC;QAC1F,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,cAAc;YACpB,WAAW,EAAE,2BAA2B;YACxC,MAAM,EAAE,MAAM;YACd,YAAY,EAAE,EAAE;SACjB,CAAC;QACF,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACzC,MAAM,CAAC,MAAO,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,uBAAuB;QACvB,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,iBAAiB;YACvB,WAAW,EAAE,2HAA2H;YACxI,QAAQ,EAAE,uHAAuH;YACjI,MAAM,EAAE,8DAA8D;YACtE,YAAY,EAAE,sBAAsB;SACrC,CAAC;QAEF,+DAA+D;QAC/D,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,iBAA0B;YAChC,KAAK,EAAE,UAAU;YACjB,WAAW,EAAE,+FAA+F;YAC5G,QAAQ,EAAE,kIAAkI;SAC7I,CAAC;QAEF,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC1C,MAAM,CAAC,MAAO,CAAC,KAAK,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,mDAAmD,EAAE,GAAG,EAAE;QAC3D,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,iBAAiB;YACvB,WAAW,EAAE,6FAA6F;YAC1G,QAAQ,EAAE,gEAAgE;YAC1E,MAAM,EAAE,2BAA2B;YACnC,YAAY,EAAE,EAAE;SACjB,CAAC;QAEF,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,iBAA0B;YAChC,KAAK,EAAE,UAAU;YACjB,WAAW,EAAE,8FAA8F;YAC3G,QAAQ,EAAE,sEAAsE;SACjF,CAAC;QAEF,kEAAkE;QAClE,oEAAoE;QACpE,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChD,uEAAuE;QACvE,uEAAuE;QACvE,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,kFAAkF;YAClF,oDAAoD;YACpD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,UAAU;YACjB,IAAI,EAAE,cAAc;YACpB,WAAW,EAAE,iDAAiD;YAC9D,QAAQ,EAAE,+DAA+D;YACzE,MAAM,EAAE,gBAAgB;YACxB,YAAY,EAAE,EAAE;SACjB,CAAC;QAEF,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,cAAuB;YAC7B,KAAK,EAAE,UAAU;YACjB,WAAW,EAAE,iEAAiE;YAC9E,QAAQ,EAAE,4DAA4D;SACvE,CAAC;QAEF,MAAM,CAAC,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,iBAAiB;YACvB,WAAW,EAAE,0DAA0D;YACvE,QAAQ,EAAE,qEAAqE;YAC/E,MAAM,EAAE,0BAA0B;YAClC,YAAY,EAAE,EAAE;SACjB,CAAC;QAEF,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,iBAA0B;YAChC,KAAK,EAAE,QAAQ;YACf,WAAW,EAAE,uDAAuD;YACpE,QAAQ,EAAE,kGAAkG;SAC7G,CAAC;QAEF,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC9B,MAAM,CAAC,MAAO,CAAC,KAAK,CAAC,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,MAAM;YACb,IAAI,EAAE,iBAAiB;YACvB,WAAW,EAAE,mCAAmC;YAChD,QAAQ,EAAE,wBAAwB;YAClC,MAAM,EAAE,MAAM;YACd,YAAY,EAAE,EAAE;YAChB,YAAY,EAAE,CAAC,cAAc,CAAC;YAC9B,WAAW,EAAE,CAAC,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC;SAC3C,CAAC;QAEF,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,iBAA0B;YAChC,KAAK,EAAE,MAAM;YACb,WAAW,EAAE,8CAA8C;YAC3D,QAAQ,EAAE,uCAAuC;SAClD,CAAC;QAEF,MAAM,MAAM,GAAG,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAChD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,IAAI,GAAyB;YACjC,KAAK,EAAE,KAAK;YACZ,IAAI,EAAE,WAAW;YACjB,WAAW,EAAE,iCAAiC;YAC9C,MAAM,EAAE,MAAM;YACd,YAAY,EAAE,EAAE;SACjB,CAAC;QAEF,MAAM,GAAG,GAAG;YACV,IAAI,EAAE,WAAoB;YAC1B,WAAW,EAAE,oCAAoC;SAClD,CAAC;QAEF,MAAM,CAAC,qBAAqB,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACtD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { generateGapId } from '../intent-extractor.js';
|
|
6
|
+
import { addGapSuppression, buildSuppressedGapIdSet, loadOverrides, isGapSuppressed } from '../overrides.js';
|
|
7
|
+
describe('generateGapId', () => {
|
|
8
|
+
it('produces deterministic 8-char hex', () => {
|
|
9
|
+
const id1 = generateGapId('missing-flow', 'No password reset flow');
|
|
10
|
+
const id2 = generateGapId('missing-flow', 'No password reset flow');
|
|
11
|
+
expect(id1).toBe(id2);
|
|
12
|
+
expect(id1).toMatch(/^[0-9a-f]{8}$/);
|
|
13
|
+
});
|
|
14
|
+
it('produces different IDs for different inputs', () => {
|
|
15
|
+
const id1 = generateGapId('missing-flow', 'No password reset');
|
|
16
|
+
const id2 = generateGapId('dead-code', 'Unused helper function');
|
|
17
|
+
expect(id1).not.toBe(id2);
|
|
18
|
+
});
|
|
19
|
+
it('produces different IDs when type differs but description is same', () => {
|
|
20
|
+
const id1 = generateGapId('missing-flow', 'same description');
|
|
21
|
+
const id2 = generateGapId('dead-code', 'same description');
|
|
22
|
+
expect(id1).not.toBe(id2);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe('gap suppression storage', () => {
|
|
26
|
+
let tempDir;
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
tempDir = await mkdtemp(join(tmpdir(), 'assay-gap-test-'));
|
|
29
|
+
await mkdir(join(tempDir, '.assay'), { recursive: true });
|
|
30
|
+
});
|
|
31
|
+
it('adds a gap suppression to overrides', async () => {
|
|
32
|
+
await addGapSuppression(tempDir, {
|
|
33
|
+
gapId: 'abc12345',
|
|
34
|
+
type: 'missing-flow',
|
|
35
|
+
description: 'No password reset flow',
|
|
36
|
+
reason: 'Handled by external auth service',
|
|
37
|
+
});
|
|
38
|
+
const overrides = await loadOverrides(tempDir);
|
|
39
|
+
expect(overrides.gapSuppressions).toHaveLength(1);
|
|
40
|
+
expect(overrides.gapSuppressions[0].gapId).toBe('abc12345');
|
|
41
|
+
expect(overrides.gapSuppressions[0].reason).toBe('Handled by external auth service');
|
|
42
|
+
});
|
|
43
|
+
it('does not add duplicate gap suppressions', async () => {
|
|
44
|
+
await addGapSuppression(tempDir, {
|
|
45
|
+
gapId: 'abc12345',
|
|
46
|
+
type: 'missing-flow',
|
|
47
|
+
description: 'test',
|
|
48
|
+
reason: 'reason 1',
|
|
49
|
+
});
|
|
50
|
+
await addGapSuppression(tempDir, {
|
|
51
|
+
gapId: 'abc12345',
|
|
52
|
+
type: 'missing-flow',
|
|
53
|
+
description: 'test',
|
|
54
|
+
reason: 'reason 2',
|
|
55
|
+
});
|
|
56
|
+
const overrides = await loadOverrides(tempDir);
|
|
57
|
+
expect(overrides.gapSuppressions).toHaveLength(1);
|
|
58
|
+
});
|
|
59
|
+
it('preserves existing claim suppressions when adding gap suppression', async () => {
|
|
60
|
+
// Write existing overrides with claim suppressions
|
|
61
|
+
await writeFile(join(tempDir, '.assay', 'overrides.json'), JSON.stringify({
|
|
62
|
+
suppressions: [{
|
|
63
|
+
claimId: 'claim001',
|
|
64
|
+
file: 'a.ts',
|
|
65
|
+
claim: 'test claim',
|
|
66
|
+
reason: 'fp',
|
|
67
|
+
suppressedAt: '2026-01-01T00:00:00Z',
|
|
68
|
+
suppressedBy: 'manual',
|
|
69
|
+
}],
|
|
70
|
+
}));
|
|
71
|
+
await addGapSuppression(tempDir, {
|
|
72
|
+
gapId: 'gap001',
|
|
73
|
+
type: 'dead-code',
|
|
74
|
+
description: 'Unused helper',
|
|
75
|
+
reason: 'Intentional',
|
|
76
|
+
});
|
|
77
|
+
const overrides = await loadOverrides(tempDir);
|
|
78
|
+
expect(overrides.suppressions).toHaveLength(1);
|
|
79
|
+
expect(overrides.gapSuppressions).toHaveLength(1);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('buildSuppressedGapIdSet', () => {
|
|
83
|
+
it('returns empty set for empty overrides', () => {
|
|
84
|
+
const set = buildSuppressedGapIdSet({ suppressions: [] });
|
|
85
|
+
expect(set.size).toBe(0);
|
|
86
|
+
});
|
|
87
|
+
it('returns empty set when gapSuppressions is undefined (backward compat)', () => {
|
|
88
|
+
const set = buildSuppressedGapIdSet({ suppressions: [] });
|
|
89
|
+
expect(set.size).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
it('returns set of suppressed gap IDs', () => {
|
|
92
|
+
const set = buildSuppressedGapIdSet({
|
|
93
|
+
suppressions: [],
|
|
94
|
+
gapSuppressions: [
|
|
95
|
+
{ gapId: 'aaa', type: 'missing-flow', description: 'x', reason: 'y', suppressedAt: '' },
|
|
96
|
+
{ gapId: 'bbb', type: 'dead-code', description: 'x', reason: 'y', suppressedAt: '' },
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
expect(set.size).toBe(2);
|
|
100
|
+
expect(set.has('aaa')).toBe(true);
|
|
101
|
+
expect(set.has('bbb')).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe('isGapSuppressed', () => {
|
|
105
|
+
it('returns exact match for matching gap ID', () => {
|
|
106
|
+
const result = isGapSuppressed({ type: 'missing-flow', description: 'test', gapId: 'abc123' }, {
|
|
107
|
+
suppressions: [],
|
|
108
|
+
gapSuppressions: [
|
|
109
|
+
{ gapId: 'abc123', type: 'missing-flow', description: 'test', reason: 'r', suppressedAt: '' },
|
|
110
|
+
],
|
|
111
|
+
});
|
|
112
|
+
expect(result).not.toBeNull();
|
|
113
|
+
expect(result.confidence).toBe('exact');
|
|
114
|
+
});
|
|
115
|
+
it('returns null when no suppressions exist', () => {
|
|
116
|
+
const result = isGapSuppressed({ type: 'missing-flow', description: 'test', gapId: 'abc123' }, { suppressions: [] });
|
|
117
|
+
expect(result).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
it('backward compat: works with old suppressions missing matchAnchors/matchTokens', () => {
|
|
120
|
+
// Old-format suppression without matchAnchors, matchTokens, or evidence
|
|
121
|
+
const result = isGapSuppressed({
|
|
122
|
+
type: 'incomplete-flow',
|
|
123
|
+
gapId: 'new-id',
|
|
124
|
+
description: 'Gmail and Calendar scripts referenced in .env.example but no implementation files found',
|
|
125
|
+
evidence: 'GMAIL_CREDENTIALS_PATH and GOOGLE_CALENDAR_ID in .env.example',
|
|
126
|
+
}, {
|
|
127
|
+
suppressions: [],
|
|
128
|
+
gapSuppressions: [
|
|
129
|
+
{
|
|
130
|
+
gapId: 'old-id',
|
|
131
|
+
type: 'incomplete-flow',
|
|
132
|
+
description: 'Gmail/Calendar integration scripts missing despite environment configuration',
|
|
133
|
+
reason: 'User-installed skills',
|
|
134
|
+
suppressedAt: '',
|
|
135
|
+
// NO evidence, matchAnchors, or matchTokens — old format
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
});
|
|
139
|
+
// Should still attempt pattern matching using lazy computation
|
|
140
|
+
// Whether it matches depends on token overlap — the key test is that it doesn't crash
|
|
141
|
+
expect(result === null || result.confidence === 'medium').toBe(true);
|
|
142
|
+
});
|
|
143
|
+
it('pattern-matches rephrased gap when exact ID fails', () => {
|
|
144
|
+
const result = isGapSuppressed({
|
|
145
|
+
type: 'incomplete-flow',
|
|
146
|
+
gapId: 'different-id',
|
|
147
|
+
description: 'Gradium TTS provider lacks dedicated implementation module',
|
|
148
|
+
evidence: 'GRADIUM_API_KEY and GRADIUM_VOICE_ID in .env.example without gradium.ts',
|
|
149
|
+
}, {
|
|
150
|
+
suppressions: [],
|
|
151
|
+
gapSuppressions: [
|
|
152
|
+
{
|
|
153
|
+
gapId: 'original-id',
|
|
154
|
+
type: 'incomplete-flow',
|
|
155
|
+
description: 'No dedicated Gradium TTS implementation file exists',
|
|
156
|
+
evidence: 'GRADIUM_API_KEY referenced in .env.example but no gradium.ts module found',
|
|
157
|
+
reason: 'Consolidated in voice.ts',
|
|
158
|
+
suppressedAt: '',
|
|
159
|
+
matchAnchors: ['gradium.ts'],
|
|
160
|
+
matchTokens: ['gradium', 'tts', 'provid', 'dedic', 'lack'],
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
expect(result).not.toBeNull();
|
|
165
|
+
expect(result.confidence).toBe('medium');
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
//# sourceMappingURL=gap-suppression.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gap-suppression.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/gap-suppression.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile, readFile, stat } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { migrateOutputDir } from '../output-dir.js';
|
|
6
|
+
describe('migrateOutputDir', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
tempDir = await mkdtemp(join(tmpdir(), 'assay-test-'));
|
|
10
|
+
});
|
|
11
|
+
it('migrates .assay-assessment/ to .assay/ when only old dir exists', async () => {
|
|
12
|
+
const oldDir = join(tempDir, '.assay-assessment');
|
|
13
|
+
await mkdir(oldDir, { recursive: true });
|
|
14
|
+
await writeFile(join(oldDir, 'assessment-summary.json'), '{"test":true}');
|
|
15
|
+
const result = await migrateOutputDir(tempDir);
|
|
16
|
+
expect(result.migrated).toBe(true);
|
|
17
|
+
const content = await readFile(join(tempDir, '.assay', 'assessment-summary.json'), 'utf-8');
|
|
18
|
+
expect(JSON.parse(content)).toEqual({ test: true });
|
|
19
|
+
await expect(stat(oldDir)).rejects.toThrow();
|
|
20
|
+
});
|
|
21
|
+
it('does nothing when .assay/ already exists', async () => {
|
|
22
|
+
const newDir = join(tempDir, '.assay');
|
|
23
|
+
await mkdir(newDir, { recursive: true });
|
|
24
|
+
await writeFile(join(newDir, 'data.json'), '{"existing":true}');
|
|
25
|
+
const result = await migrateOutputDir(tempDir);
|
|
26
|
+
expect(result.migrated).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
it('does nothing when neither directory exists', async () => {
|
|
29
|
+
const result = await migrateOutputDir(tempDir);
|
|
30
|
+
expect(result.migrated).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
//# sourceMappingURL=output-dir-migration.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output-dir-migration.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/output-dir-migration.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC1D,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,IAAI,OAAe,CAAC;IAEpB,UAAU,CAAC,KAAK,IAAI,EAAE;QACpB,OAAO,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,aAAa,CAAC,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,mBAAmB,CAAC,CAAC;QAClD,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,yBAAyB,CAAC,EAAE,eAAe,CAAC,CAAC;QAC1E,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,yBAAyB,CAAC,EAAE,OAAO,CAAC,CAAC;QAC5F,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,0CAA0C,EAAE,KAAK,IAAI,EAAE;QACxD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;QACvC,MAAM,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACzC,MAAM,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,EAAE,mBAAmB,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;QAC1D,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAC/C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { loadOverrides, addSuppression } from '../overrides.js';
|
|
6
|
+
describe('overrides', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
let assayDir;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await mkdtemp(join(tmpdir(), 'assay-test-'));
|
|
11
|
+
assayDir = join(tempDir, '.assay');
|
|
12
|
+
await mkdir(assayDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
it('returns empty suppressions when no overrides file exists', async () => {
|
|
15
|
+
const overrides = await loadOverrides(tempDir);
|
|
16
|
+
expect(overrides.suppressions).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
it('loads existing overrides file', async () => {
|
|
19
|
+
const data = {
|
|
20
|
+
suppressions: [
|
|
21
|
+
{
|
|
22
|
+
claimId: 'abc12345',
|
|
23
|
+
file: 'a.ts',
|
|
24
|
+
claim: 'test',
|
|
25
|
+
reason: 'false positive',
|
|
26
|
+
suppressedAt: '2026-01-01T00:00:00Z',
|
|
27
|
+
suppressedBy: 'manual',
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
await writeFile(join(assayDir, 'overrides.json'), JSON.stringify(data));
|
|
32
|
+
const overrides = await loadOverrides(tempDir);
|
|
33
|
+
expect(overrides.suppressions).toHaveLength(1);
|
|
34
|
+
expect(overrides.suppressions[0].claimId).toBe('abc12345');
|
|
35
|
+
});
|
|
36
|
+
it('adds a suppression and saves', async () => {
|
|
37
|
+
await addSuppression(tempDir, {
|
|
38
|
+
claimId: 'def67890',
|
|
39
|
+
file: 'src/route.ts',
|
|
40
|
+
claim: 'Route requires auth',
|
|
41
|
+
reason: 'Intentionally public',
|
|
42
|
+
});
|
|
43
|
+
const overrides = await loadOverrides(tempDir);
|
|
44
|
+
expect(overrides.suppressions).toHaveLength(1);
|
|
45
|
+
expect(overrides.suppressions[0].claimId).toBe('def67890');
|
|
46
|
+
expect(overrides.suppressions[0].suppressedBy).toBe('manual');
|
|
47
|
+
});
|
|
48
|
+
it('does not add duplicate suppressions', async () => {
|
|
49
|
+
await addSuppression(tempDir, { claimId: 'aaa11111', file: 'a.ts', claim: 'x', reason: 'y' });
|
|
50
|
+
await addSuppression(tempDir, { claimId: 'aaa11111', file: 'a.ts', claim: 'x', reason: 'z' });
|
|
51
|
+
const overrides = await loadOverrides(tempDir);
|
|
52
|
+
expect(overrides.suppressions).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
//# sourceMappingURL=overrides.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"overrides.test.js","sourceRoot":"","sources":["../../../src/lib/__tests__/overrides.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { MODEL } from './anthropic.js';
|
|
5
5
|
import { getProvider } from './llm-provider.js';
|
|
6
|
+
import { generateClaimId } from './claim-id.js';
|
|
6
7
|
// ── Severity ordering for sort ──────────────────────────────────────
|
|
7
8
|
const SEVERITY_ORDER = {
|
|
8
9
|
critical: 0,
|
|
@@ -38,6 +39,7 @@ function collectBugEntries(routeResults, severityMap) {
|
|
|
38
39
|
entries.push({
|
|
39
40
|
bug,
|
|
40
41
|
severity: getBugSeverity(bug, severityMap),
|
|
42
|
+
flowId: route.requirement.id,
|
|
41
43
|
route: route.requirement.route,
|
|
42
44
|
domain: route.requirement.domain,
|
|
43
45
|
category: claim?.category ?? 'functionality',
|
|
@@ -79,10 +81,11 @@ function generateBugReport(entries, repoName, bugCounts) {
|
|
|
79
81
|
for (const entry of sevEntries) {
|
|
80
82
|
bugNumber++;
|
|
81
83
|
const bugId = `BUG-${String(bugNumber).padStart(3, '0')}`;
|
|
84
|
+
const claimId = generateClaimId(entry.flowId, entry.bug.claim, entry.bug.evidence[0]?.file ?? '');
|
|
82
85
|
const evidenceLines = entry.bug.evidence.length > 0
|
|
83
86
|
? entry.bug.evidence.map((e) => ` - \`${e.file}${e.lineNumber ? ':' + e.lineNumber : ''}\` -- ${e.snippet}`)
|
|
84
87
|
: [' - No specific file evidence'];
|
|
85
|
-
lines.push(`### ${bugId}: ${entry.bug.claim}`, `- **Route:** ${entry.route}`, `- **Domain:** ${entry.domain}`, `- **Category:** ${entry.category}`, `- **Verdict:** ${entry.bug.verdict}`, `- **Evidence:**`, ...evidenceLines, `- **Reasoning:** ${entry.bug.reasoning}`, ``);
|
|
88
|
+
lines.push(`### ${bugId} [ID: ${claimId}]: ${entry.bug.claim}`, `- **Route:** ${entry.route}`, `- **Domain:** ${entry.domain}`, `- **Category:** ${entry.category}`, `- **Verdict:** ${entry.bug.verdict}`, `- **Suppress:** \`npx tryassay suppress ${claimId}\``, `- **Evidence:**`, ...evidenceLines, `- **Reasoning:** ${entry.bug.reasoning}`, ``);
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
if (entries.length === 0) {
|