@lbroth/rothunter 1.0.0-rc.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/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/adapters/llm.d.ts +68 -0
- package/dist/adapters/llm.d.ts.map +1 -0
- package/dist/adapters/llm.js +189 -0
- package/dist/adapters/llm.js.map +1 -0
- package/dist/config.d.ts +37 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +81 -0
- package/dist/config.js.map +1 -0
- package/dist/detector-registry.d.ts +32 -0
- package/dist/detector-registry.d.ts.map +1 -0
- package/dist/detector-registry.js +74 -0
- package/dist/detector-registry.js.map +1 -0
- package/dist/detectors/api-race.d.ts +6 -0
- package/dist/detectors/api-race.d.ts.map +1 -0
- package/dist/detectors/api-race.js +222 -0
- package/dist/detectors/api-race.js.map +1 -0
- package/dist/detectors/bad-config.d.ts +6 -0
- package/dist/detectors/bad-config.d.ts.map +1 -0
- package/dist/detectors/bad-config.js +529 -0
- package/dist/detectors/bad-config.js.map +1 -0
- package/dist/detectors/console-log-prod.d.ts +6 -0
- package/dist/detectors/console-log-prod.d.ts.map +1 -0
- package/dist/detectors/console-log-prod.js +72 -0
- package/dist/detectors/console-log-prod.js.map +1 -0
- package/dist/detectors/dead-api.d.ts +10 -0
- package/dist/detectors/dead-api.d.ts.map +1 -0
- package/dist/detectors/dead-api.js +115 -0
- package/dist/detectors/dead-api.js.map +1 -0
- package/dist/detectors/dead-export.d.ts +12 -0
- package/dist/detectors/dead-export.d.ts.map +1 -0
- package/dist/detectors/dead-export.js +140 -0
- package/dist/detectors/dead-export.js.map +1 -0
- package/dist/detectors/dead-handler.d.ts +12 -0
- package/dist/detectors/dead-handler.d.ts.map +1 -0
- package/dist/detectors/dead-handler.js +40 -0
- package/dist/detectors/dead-handler.js.map +1 -0
- package/dist/detectors/dead-module.d.ts +14 -0
- package/dist/detectors/dead-module.d.ts.map +1 -0
- package/dist/detectors/dead-module.js +50 -0
- package/dist/detectors/dead-module.js.map +1 -0
- package/dist/detectors/deep-nesting.d.ts +12 -0
- package/dist/detectors/deep-nesting.d.ts.map +1 -0
- package/dist/detectors/deep-nesting.js +133 -0
- package/dist/detectors/deep-nesting.js.map +1 -0
- package/dist/detectors/duplicate-function.d.ts +9 -0
- package/dist/detectors/duplicate-function.d.ts.map +1 -0
- package/dist/detectors/duplicate-function.js +199 -0
- package/dist/detectors/duplicate-function.js.map +1 -0
- package/dist/detectors/duplicate-type.d.ts +9 -0
- package/dist/detectors/duplicate-type.d.ts.map +1 -0
- package/dist/detectors/duplicate-type.js +166 -0
- package/dist/detectors/duplicate-type.js.map +1 -0
- package/dist/detectors/hot-hub-file.d.ts +11 -0
- package/dist/detectors/hot-hub-file.d.ts.map +1 -0
- package/dist/detectors/hot-hub-file.js +42 -0
- package/dist/detectors/hot-hub-file.js.map +1 -0
- package/dist/detectors/long-file.d.ts +12 -0
- package/dist/detectors/long-file.d.ts.map +1 -0
- package/dist/detectors/long-file.js +82 -0
- package/dist/detectors/long-file.js.map +1 -0
- package/dist/detectors/long-function.d.ts +12 -0
- package/dist/detectors/long-function.d.ts.map +1 -0
- package/dist/detectors/long-function.js +45 -0
- package/dist/detectors/long-function.js.map +1 -0
- package/dist/detectors/magic-numbers.d.ts +10 -0
- package/dist/detectors/magic-numbers.d.ts.map +1 -0
- package/dist/detectors/magic-numbers.js +332 -0
- package/dist/detectors/magic-numbers.js.map +1 -0
- package/dist/detectors/mutable-globals.d.ts +6 -0
- package/dist/detectors/mutable-globals.d.ts.map +1 -0
- package/dist/detectors/mutable-globals.js +95 -0
- package/dist/detectors/mutable-globals.js.map +1 -0
- package/dist/detectors/mutation.d.ts +11 -0
- package/dist/detectors/mutation.d.ts.map +1 -0
- package/dist/detectors/mutation.js +397 -0
- package/dist/detectors/mutation.js.map +1 -0
- package/dist/detectors/public-any.d.ts +6 -0
- package/dist/detectors/public-any.d.ts.map +1 -0
- package/dist/detectors/public-any.js +52 -0
- package/dist/detectors/public-any.js.map +1 -0
- package/dist/detectors/race-condition.d.ts +6 -0
- package/dist/detectors/race-condition.d.ts.map +1 -0
- package/dist/detectors/race-condition.js +608 -0
- package/dist/detectors/race-condition.js.map +1 -0
- package/dist/detectors/shared-db-write.d.ts +6 -0
- package/dist/detectors/shared-db-write.d.ts.map +1 -0
- package/dist/detectors/shared-db-write.js +656 -0
- package/dist/detectors/shared-db-write.js.map +1 -0
- package/dist/detectors/silent-catch.d.ts +6 -0
- package/dist/detectors/silent-catch.d.ts.map +1 -0
- package/dist/detectors/silent-catch.js +167 -0
- package/dist/detectors/silent-catch.js.map +1 -0
- package/dist/detectors/similar-functions.d.ts +15 -0
- package/dist/detectors/similar-functions.d.ts.map +1 -0
- package/dist/detectors/similar-functions.js +334 -0
- package/dist/detectors/similar-functions.js.map +1 -0
- package/dist/detectors/skip-tests.d.ts +6 -0
- package/dist/detectors/skip-tests.d.ts.map +1 -0
- package/dist/detectors/skip-tests.js +69 -0
- package/dist/detectors/skip-tests.js.map +1 -0
- package/dist/detectors/todo-comments.d.ts +29 -0
- package/dist/detectors/todo-comments.d.ts.map +1 -0
- package/dist/detectors/todo-comments.js +154 -0
- package/dist/detectors/todo-comments.js.map +1 -0
- package/dist/detectors/unused-deps.d.ts +8 -0
- package/dist/detectors/unused-deps.d.ts.map +1 -0
- package/dist/detectors/unused-deps.js +115 -0
- package/dist/detectors/unused-deps.js.map +1 -0
- package/dist/extraction/api-race-confirmer.d.ts +31 -0
- package/dist/extraction/api-race-confirmer.d.ts.map +1 -0
- package/dist/extraction/api-race-confirmer.js +110 -0
- package/dist/extraction/api-race-confirmer.js.map +1 -0
- package/dist/extraction/llm-confirmer.d.ts +25 -0
- package/dist/extraction/llm-confirmer.d.ts.map +1 -0
- package/dist/extraction/llm-confirmer.js +118 -0
- package/dist/extraction/llm-confirmer.js.map +1 -0
- package/dist/extraction/mutation-confirmer.d.ts +30 -0
- package/dist/extraction/mutation-confirmer.d.ts.map +1 -0
- package/dist/extraction/mutation-confirmer.js +73 -0
- package/dist/extraction/mutation-confirmer.js.map +1 -0
- package/dist/extraction/prompt-chunking.d.ts +37 -0
- package/dist/extraction/prompt-chunking.d.ts.map +1 -0
- package/dist/extraction/prompt-chunking.js +61 -0
- package/dist/extraction/prompt-chunking.js.map +1 -0
- package/dist/extraction/race-confirmer.d.ts +28 -0
- package/dist/extraction/race-confirmer.d.ts.map +1 -0
- package/dist/extraction/race-confirmer.js +68 -0
- package/dist/extraction/race-confirmer.js.map +1 -0
- package/dist/extraction/shared-db-write-confirmer.d.ts +31 -0
- package/dist/extraction/shared-db-write-confirmer.d.ts.map +1 -0
- package/dist/extraction/shared-db-write-confirmer.js +141 -0
- package/dist/extraction/shared-db-write-confirmer.js.map +1 -0
- package/dist/extraction/triage-confirmer.d.ts +59 -0
- package/dist/extraction/triage-confirmer.d.ts.map +1 -0
- package/dist/extraction/triage-confirmer.js +104 -0
- package/dist/extraction/triage-confirmer.js.map +1 -0
- package/dist/graph/cfg.d.ts +45 -0
- package/dist/graph/cfg.d.ts.map +1 -0
- package/dist/graph/cfg.js +198 -0
- package/dist/graph/cfg.js.map +1 -0
- package/dist/graph/decorator-entries.d.ts +2 -0
- package/dist/graph/decorator-entries.d.ts.map +1 -0
- package/dist/graph/decorator-entries.js +89 -0
- package/dist/graph/decorator-entries.js.map +1 -0
- package/dist/graph/entry-points.d.ts +12 -0
- package/dist/graph/entry-points.d.ts.map +1 -0
- package/dist/graph/entry-points.js +282 -0
- package/dist/graph/entry-points.js.map +1 -0
- package/dist/graph/handler-conventions.d.ts +2 -0
- package/dist/graph/handler-conventions.d.ts.map +1 -0
- package/dist/graph/handler-conventions.js +26 -0
- package/dist/graph/handler-conventions.js.map +1 -0
- package/dist/graph/iac-entries.d.ts +2 -0
- package/dist/graph/iac-entries.d.ts.map +1 -0
- package/dist/graph/iac-entries.js +123 -0
- package/dist/graph/iac-entries.js.map +1 -0
- package/dist/graph/import-graph.d.ts +48 -0
- package/dist/graph/import-graph.d.ts.map +1 -0
- package/dist/graph/import-graph.js +86 -0
- package/dist/graph/import-graph.js.map +1 -0
- package/dist/graph/monorepo-detect.d.ts +3 -0
- package/dist/graph/monorepo-detect.d.ts.map +1 -0
- package/dist/graph/monorepo-detect.js +166 -0
- package/dist/graph/monorepo-detect.js.map +1 -0
- package/dist/graph/tsconfig-paths.d.ts +23 -0
- package/dist/graph/tsconfig-paths.d.ts.map +1 -0
- package/dist/graph/tsconfig-paths.js +217 -0
- package/dist/graph/tsconfig-paths.js.map +1 -0
- package/dist/multi-workspace-scanner.d.ts +13 -0
- package/dist/multi-workspace-scanner.d.ts.map +1 -0
- package/dist/multi-workspace-scanner.js +130 -0
- package/dist/multi-workspace-scanner.js.map +1 -0
- package/dist/normalizers/type-normalizer.d.ts +16 -0
- package/dist/normalizers/type-normalizer.d.ts.map +1 -0
- package/dist/normalizers/type-normalizer.js +189 -0
- package/dist/normalizers/type-normalizer.js.map +1 -0
- package/dist/parsers/typescript-parser.d.ts +57 -0
- package/dist/parsers/typescript-parser.d.ts.map +1 -0
- package/dist/parsers/typescript-parser.js +502 -0
- package/dist/parsers/typescript-parser.js.map +1 -0
- package/dist/reporter/json-reporter.d.ts +12 -0
- package/dist/reporter/json-reporter.d.ts.map +1 -0
- package/dist/reporter/json-reporter.js +28 -0
- package/dist/reporter/json-reporter.js.map +1 -0
- package/dist/reporter/markdown-reporter.d.ts +11 -0
- package/dist/reporter/markdown-reporter.d.ts.map +1 -0
- package/dist/reporter/markdown-reporter.js +77 -0
- package/dist/reporter/markdown-reporter.js.map +1 -0
- package/dist/rothunter.d.ts +125 -0
- package/dist/rothunter.d.ts.map +1 -0
- package/dist/rothunter.js +1038 -0
- package/dist/rothunter.js.map +1 -0
- package/dist/server/false-positives.d.ts +34 -0
- package/dist/server/false-positives.d.ts.map +1 -0
- package/dist/server/false-positives.js +85 -0
- package/dist/server/false-positives.js.map +1 -0
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1529 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/marked-to-fix.d.ts +16 -0
- package/dist/server/marked-to-fix.d.ts.map +1 -0
- package/dist/server/marked-to-fix.js +36 -0
- package/dist/server/marked-to-fix.js.map +1 -0
- package/dist/server/scan-store.d.ts +147 -0
- package/dist/server/scan-store.d.ts.map +1 -0
- package/dist/server/scan-store.js +291 -0
- package/dist/server/scan-store.js.map +1 -0
- package/dist/server/settings-store.d.ts +28 -0
- package/dist/server/settings-store.d.ts.map +1 -0
- package/dist/server/settings-store.js +46 -0
- package/dist/server/settings-store.js.map +1 -0
- package/dist/server/workspace-store.d.ts +39 -0
- package/dist/server/workspace-store.d.ts.map +1 -0
- package/dist/server/workspace-store.js +108 -0
- package/dist/server/workspace-store.js.map +1 -0
- package/dist/types/detector-input.d.ts +37 -0
- package/dist/types/detector-input.d.ts.map +1 -0
- package/dist/types/detector-input.js +2 -0
- package/dist/types/detector-input.js.map +1 -0
- package/dist/types.d.ts +110 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/clustering.d.ts +14 -0
- package/dist/utils/clustering.d.ts.map +1 -0
- package/dist/utils/clustering.js +56 -0
- package/dist/utils/clustering.js.map +1 -0
- package/dist/utils/gitignore.d.ts +32 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +122 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/hash.d.ts +11 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +14 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/ignore-annotation.d.ts +28 -0
- package/dist/utils/ignore-annotation.d.ts.map +1 -0
- package/dist/utils/ignore-annotation.js +46 -0
- package/dist/utils/ignore-annotation.js.map +1 -0
- package/dist/utils/llm-json.d.ts +2 -0
- package/dist/utils/llm-json.d.ts.map +1 -0
- package/dist/utils/llm-json.js +53 -0
- package/dist/utils/llm-json.js.map +1 -0
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +4 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/project-conventions.d.ts +2 -0
- package/dist/utils/project-conventions.d.ts.map +1 -0
- package/dist/utils/project-conventions.js +108 -0
- package/dist/utils/project-conventions.js.map +1 -0
- package/dist/utils/regex.d.ts +9 -0
- package/dist/utils/regex.d.ts.map +1 -0
- package/dist/utils/regex.js +11 -0
- package/dist/utils/regex.js.map +1 -0
- package/dist/utils/snippet.d.ts +20 -0
- package/dist/utils/snippet.d.ts.map +1 -0
- package/dist/utils/snippet.js +28 -0
- package/dist/utils/snippet.js.map +1 -0
- package/dist/utils/source-reader.d.ts +19 -0
- package/dist/utils/source-reader.d.ts.map +1 -0
- package/dist/utils/source-reader.js +32 -0
- package/dist/utils/source-reader.js.map +1 -0
- package/logo.png +0 -0
- package/package.json +92 -0
- package/scripts/start-llm.mjs +161 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createDefaultLlmClient } from '../adapters/llm.js';
|
|
3
|
+
import { parseLlmJsonResponse } from '../utils/llm-json.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const ConfirmationSchema = z.object({
|
|
6
|
+
same_concept: z.boolean(),
|
|
7
|
+
confidence: z.number().min(0).max(1),
|
|
8
|
+
reason: z.string(),
|
|
9
|
+
});
|
|
10
|
+
// Anchored on fields, not names. Smaller LLMs default to "different concept"
|
|
11
|
+
// when names differ, even with identical fields — counter that here.
|
|
12
|
+
const PROMPT = `You are deciding whether two TypeScript types are duplicates that should be unified.
|
|
13
|
+
|
|
14
|
+
CRITICAL: A static analyzer ALREADY proved their structures match. Field names will often differ — that is the WHOLE POINT of running this check. Different names with matching structures usually mean a duplicate that drifted under different aliases, NOT two different concepts. Default to "same_concept: true" unless the shape is so small (≤2 generic primitives) that the match is coincidental AND the field names also differ.
|
|
15
|
+
|
|
16
|
+
If field NAMES match exactly AND the type names also relate to the same concept, this is same_concept: true.
|
|
17
|
+
If field NAMES match exactly BUT the type names point to clearly unrelated subsystems (e.g. \`Template\` from a cloud-provider DTO vs \`Document\` from a test fixture), the field overlap is coincidental — same_concept: false.
|
|
18
|
+
If field TYPES match but names differ AND the shape has ≥4 fields with mixed types, this is same_concept: true.
|
|
19
|
+
Tiny generic shapes ({id, name}, {x, y}, {key, value}, {host, port}) across unrelated type names are ALWAYS coincidental → same_concept: false. The type names are the only evidence here — use them.
|
|
20
|
+
|
|
21
|
+
Output exactly ONE JSON object, then STOP:
|
|
22
|
+
{"same_concept": <boolean>, "confidence": <0..1>, "reason": "<max 12 words>"}
|
|
23
|
+
|
|
24
|
+
Worked examples (study these — they show the expected behavior):
|
|
25
|
+
|
|
26
|
+
Example 1 (rename + retype-equivalent):
|
|
27
|
+
A: interface ShipmentEntry { trackingId: string; weightKg: number; delivered: boolean; shippedOn: string; }
|
|
28
|
+
B: interface ParcelRow { code: string; mass: number; arrived: boolean; dispatchDate: string; }
|
|
29
|
+
→ {"same_concept": true, "confidence": 0.9, "reason": "1:1 field map under rename — same shipment row"}
|
|
30
|
+
|
|
31
|
+
Example 2 (identical fields, different type names):
|
|
32
|
+
A: interface InboundEvent { kind: string; payload: string; }
|
|
33
|
+
B: interface OutboundFrame { kind: string; payload: string; }
|
|
34
|
+
→ {"same_concept": true, "confidence": 0.95, "reason": "Identical fields, both model a message frame"}
|
|
35
|
+
|
|
36
|
+
Example 3 (snake ↔ camel + common synonyms):
|
|
37
|
+
A: interface CustomerSnake { cust_id: string; phone_number: string; signup_date: string; }
|
|
38
|
+
B: interface CustomerCamel { customerId: string; phone: string; createdAt: string; }
|
|
39
|
+
→ {"same_concept": true, "confidence": 0.9, "reason": "Snake↔camel + synonyms (phone, createdAt) — same customer"}
|
|
40
|
+
|
|
41
|
+
Example 4 (REJECT — too generic):
|
|
42
|
+
A: interface MenuItem { id: string; name: string; }
|
|
43
|
+
B: interface ApiToken { id: string; name: string; }
|
|
44
|
+
→ {"same_concept": false, "confidence": 0.85, "reason": "Tiny {id,name} shape across unrelated domains"}
|
|
45
|
+
|
|
46
|
+
Example 5 (REJECT — same shape, different semantics):
|
|
47
|
+
A: interface Vector { x: number; y: number; z: number; }
|
|
48
|
+
B: interface RGB { r: number; g: number; b: number; }
|
|
49
|
+
→ {"same_concept": false, "confidence": 0.9, "reason": "Same {number×3} but vector vs color semantics"}
|
|
50
|
+
|
|
51
|
+
Now decide for the real pair:
|
|
52
|
+
{{PROJECT_CONVENTIONS}}
|
|
53
|
+
Type A ({{FILE_A}}):
|
|
54
|
+
\`\`\`typescript
|
|
55
|
+
{{SOURCE_A}}
|
|
56
|
+
\`\`\`
|
|
57
|
+
|
|
58
|
+
Type B ({{FILE_B}}):
|
|
59
|
+
\`\`\`typescript
|
|
60
|
+
{{SOURCE_B}}
|
|
61
|
+
\`\`\`
|
|
62
|
+
`;
|
|
63
|
+
export class LlmConfirmer {
|
|
64
|
+
llm;
|
|
65
|
+
cache = new Map();
|
|
66
|
+
constructor(llm) {
|
|
67
|
+
this.llm = llm ?? createDefaultLlmClient();
|
|
68
|
+
}
|
|
69
|
+
async confirmSameConcept(a, b,
|
|
70
|
+
/**
|
|
71
|
+
* Project conventions text (CLAUDE.md / AGENTS.md / etc., already
|
|
72
|
+
* concatenated and truncated). When present, the verdict weighs
|
|
73
|
+
* project-stated rules — e.g. "three similar lines is better than
|
|
74
|
+
* premature abstraction" turns this into `same_concept: false` even
|
|
75
|
+
* on a tight skeleton match.
|
|
76
|
+
*/
|
|
77
|
+
projectConventions) {
|
|
78
|
+
const cacheKey = pairKey(a, b);
|
|
79
|
+
const cached = this.cache.get(cacheKey);
|
|
80
|
+
if (cached)
|
|
81
|
+
return cached;
|
|
82
|
+
// Local llama.cpp default context is 4096 tokens. Two SymbolRecord
|
|
83
|
+
// sources from heavy React components (Outline's Icon / SharePopover)
|
|
84
|
+
// routinely overflow. If we can't fit both sources + the prompt
|
|
85
|
+
// overhead in the budget, skip the LLM verdict and let the deterministic
|
|
86
|
+
// finding stand at its Tier-1 confidence. Truncating type bodies loses
|
|
87
|
+
// the field signal the verdict needs, so we prefer "no verdict" over
|
|
88
|
+
// "verdict on a truncated body".
|
|
89
|
+
const SOURCE_BUDGET_CHARS = 2500;
|
|
90
|
+
if (a.source.length + b.source.length > SOURCE_BUDGET_CHARS) {
|
|
91
|
+
logger.info({ a: a.name, b: b.name, sizeA: a.source.length, sizeB: b.source.length }, 'LLM confirmer: pair too large for local context, skipping verdict');
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const conventionsBlock = projectConventions
|
|
95
|
+
? `\nProject conventions (treat as authoritative — override generic best-practice when in conflict):\n${projectConventions}\n`
|
|
96
|
+
: '';
|
|
97
|
+
const prompt = PROMPT.replace('{{FILE_A}}', a.file)
|
|
98
|
+
.replace('{{SOURCE_A}}', a.source)
|
|
99
|
+
.replace('{{FILE_B}}', b.file)
|
|
100
|
+
.replace('{{SOURCE_B}}', b.source)
|
|
101
|
+
.replace('{{PROJECT_CONVENTIONS}}', conventionsBlock);
|
|
102
|
+
try {
|
|
103
|
+
const raw = await this.llm.chat([{ role: 'user', content: prompt }], { temperature: 0, json: true });
|
|
104
|
+
const parsed = parseLlmJsonResponse(raw);
|
|
105
|
+
const result = ConfirmationSchema.parse(parsed);
|
|
106
|
+
this.cache.set(cacheKey, result);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
logger.warn({ err: err.message, a: a.name, b: b.name }, 'LLM confirmer failed; returning null');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function pairKey(a, b) {
|
|
116
|
+
return [a.hashStrict ?? a.id, b.hashStrict ?? b.id].sort().join('|');
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=llm-confirmer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"llm-confirmer.js","sourceRoot":"","sources":["../../src/extraction/llm-confirmer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAa,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAG5C,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC;IAClC,YAAY,EAAE,CAAC,CAAC,OAAO,EAAE;IACzB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAIH,6EAA6E;AAC7E,qEAAqE;AACrE,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDd,CAAC;AAEF,MAAM,OAAO,YAAY;IACf,GAAG,CAAY;IACf,KAAK,GAAG,IAAI,GAAG,EAA8B,CAAC;IAEtD,YAAY,GAAe;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,IAAI,sBAAsB,EAAE,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,kBAAkB,CACtB,CAAe,EACf,CAAe;IACf;;;;;;OAMG;IACH,kBAA2B;QAE3B,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC/B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,mEAAmE;QACnE,sEAAsE;QACtE,gEAAgE;QAChE,yEAAyE;QACzE,uEAAuE;QACvE,qEAAqE;QACrE,iCAAiC;QACjC,MAAM,mBAAmB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,mBAAmB,EAAE,CAAC;YAC5D,MAAM,CAAC,IAAI,CACT,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,EACxE,mEAAmE,CACpE,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,gBAAgB,GAAG,kBAAkB;YACzC,CAAC,CAAC,sGAAsG,kBAAkB,IAAI;YAC9H,CAAC,CAAC,EAAE,CAAC;QACP,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC;aAChD,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC;aACjC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC;aAC7B,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,MAAM,CAAC;aACjC,OAAO,CAAC,yBAAyB,EAAE,gBAAgB,CAAC,CAAC;QAExD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAC7B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EACnC,EAAE,WAAW,EAAE,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAC/B,CAAC;YACF,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAChD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YACjC,OAAO,MAAM,CAAC;QAChB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CACT,EAAE,GAAG,EAAG,GAAa,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,EACrD,sCAAsC,CACvC,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF;AAED,SAAS,OAAO,CAAC,CAAe,EAAE,CAAe;IAC/C,OAAO,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LlmClient } from '../adapters/llm.js';
|
|
3
|
+
declare const VerdictSchema: z.ZodObject<{
|
|
4
|
+
intentional: z.ZodBoolean;
|
|
5
|
+
confidence: z.ZodNumber;
|
|
6
|
+
reason: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type MutationVerdict = z.infer<typeof VerdictSchema>;
|
|
9
|
+
export interface MutationCheckInput {
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
/** Free-text pattern label: 'array-mutator' / 'object-assign' / etc. */
|
|
13
|
+
pattern: string;
|
|
14
|
+
/** Whether the deterministic layer detected escape. */
|
|
15
|
+
escapes: boolean;
|
|
16
|
+
/** The flagged mutation code (e.g. `record.tag = tag`). */
|
|
17
|
+
snippet: string;
|
|
18
|
+
/** Surrounding function source, trimmed to a few lines around the mutation. */
|
|
19
|
+
enclosingSource: string;
|
|
20
|
+
/** Optional function or method name for context. */
|
|
21
|
+
enclosingName?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare class MutationConfirmer {
|
|
24
|
+
private llm;
|
|
25
|
+
private cache;
|
|
26
|
+
constructor(llm?: LlmClient);
|
|
27
|
+
confirm(input: MutationCheckInput): Promise<MutationVerdict | null>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
30
|
+
//# sourceMappingURL=mutation-confirmer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-confirmer.d.ts","sourceRoot":"","sources":["../../src/extraction/mutation-confirmer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAA0B,MAAM,oBAAoB,CAAC;AAIvE,QAAA,MAAM,aAAa;;;;iBAIjB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,OAAO,EAAE,OAAO,CAAC;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA0BD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,KAAK,CAAsC;gBAEvC,GAAG,CAAC,EAAE,SAAS;IAIrB,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;CA8C1E"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createDefaultLlmClient } from '../adapters/llm.js';
|
|
3
|
+
import { parseLlmJsonResponse } from '../utils/llm-json.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const VerdictSchema = z.object({
|
|
6
|
+
intentional: z.boolean(),
|
|
7
|
+
confidence: z.number().min(0).max(1),
|
|
8
|
+
reason: z.string(),
|
|
9
|
+
});
|
|
10
|
+
const PROMPT = `You are reviewing a TypeScript mutation that a static analyzer flagged. Decide: is this mutation INTENTIONAL (builder pattern, accumulator, documented in-place transform, framework-required convention) or a BUG (accidental shared-state corruption / surprise side effect)?
|
|
11
|
+
|
|
12
|
+
Output ONE compact JSON object and STOP:
|
|
13
|
+
{"intentional": boolean, "confidence": <0..1>, "reason": "<max 15 words>"}
|
|
14
|
+
|
|
15
|
+
Decision rules (apply in order):
|
|
16
|
+
1. Accumulator / builder pattern. Function name contains build/collect/accumulate/append; or parameter name is one of \`out\`, \`acc\`, \`result\`, \`into\`, \`buf\` — intentional, confidence ≥ 0.85.
|
|
17
|
+
2. Framework convention: Fastify/Express \`req.<prop> = value\` to decorate the request, NestJS interceptors, JSX prop spread builders — intentional, confidence ≥ 0.8.
|
|
18
|
+
3. In-place transform whose name signals mutation: \`mutate\`, \`update\`, \`assign\`, \`patch\`, \`apply\`, \`reset\`, \`init\`, \`load\` — intentional, confidence ≥ 0.8.
|
|
19
|
+
4. Escapes via return AND function name sounds pure (compute*, calc*, derive*, format*, transform*, build*Pure) — bug, confidence ≥ 0.8.
|
|
20
|
+
5. delete on an arg that looks like a DTO returned to a caller — bug.
|
|
21
|
+
6. Module-scope state write (\`shared-state-write\` pattern) outside a class boundary — borderline; mark intentional only if there is a clear init/setup function name.
|
|
22
|
+
7. When genuinely unclear → \`intentional: false, confidence: 0.5\` (the deterministic finding stays at the original severity).
|
|
23
|
+
|
|
24
|
+
Pattern: {{PATTERN}}{{ESCAPE_TAG}}
|
|
25
|
+
Flagged code: {{SNIPPET}}
|
|
26
|
+
{{ENCLOSING_NAME_LINE}}
|
|
27
|
+
|
|
28
|
+
Enclosing function ({{FILE}}:{{LINE}}):
|
|
29
|
+
\`\`\`typescript
|
|
30
|
+
{{ENCLOSING}}
|
|
31
|
+
\`\`\`
|
|
32
|
+
`;
|
|
33
|
+
export class MutationConfirmer {
|
|
34
|
+
llm;
|
|
35
|
+
cache = new Map();
|
|
36
|
+
constructor(llm) {
|
|
37
|
+
this.llm = llm ?? createDefaultLlmClient();
|
|
38
|
+
}
|
|
39
|
+
async confirm(input) {
|
|
40
|
+
const cacheKey = `${input.file}:${input.line}:${input.pattern}:${input.snippet}`;
|
|
41
|
+
const cached = this.cache.get(cacheKey);
|
|
42
|
+
if (cached)
|
|
43
|
+
return cached;
|
|
44
|
+
// Skip the verdict when the enclosing source is too large for the local
|
|
45
|
+
// 4096-token context. Truncating the body strips the surrounding code
|
|
46
|
+
// that determines intentional-vs-bug, so a partial verdict is worse
|
|
47
|
+
// than no verdict — let the deterministic Tier-1 finding stand.
|
|
48
|
+
const ENCLOSING_BUDGET_CHARS = 2500;
|
|
49
|
+
if (input.enclosingSource.length > ENCLOSING_BUDGET_CHARS) {
|
|
50
|
+
logger.info({ file: input.file, line: input.line, size: input.enclosingSource.length }, 'MutationConfirmer: enclosing source too large for local context, skipping verdict');
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const prompt = PROMPT.replace('{{PATTERN}}', input.pattern)
|
|
54
|
+
.replace('{{ESCAPE_TAG}}', input.escapes ? ' (escapes via return / this / call)' : '')
|
|
55
|
+
.replace('{{SNIPPET}}', input.snippet)
|
|
56
|
+
.replace('{{ENCLOSING_NAME_LINE}}', input.enclosingName ? `Enclosing identifier: \`${input.enclosingName}\`` : '')
|
|
57
|
+
.replace('{{FILE}}', input.file)
|
|
58
|
+
.replace('{{LINE}}', String(input.line))
|
|
59
|
+
.replace('{{ENCLOSING}}', input.enclosingSource);
|
|
60
|
+
try {
|
|
61
|
+
const raw = await this.llm.chat([{ role: 'user', content: prompt }], { temperature: 0, maxTokens: 96 });
|
|
62
|
+
const parsed = parseLlmJsonResponse(raw);
|
|
63
|
+
const verdict = VerdictSchema.parse(parsed);
|
|
64
|
+
this.cache.set(cacheKey, verdict);
|
|
65
|
+
return verdict;
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
logger.warn({ err: err.message, file: input.file, line: input.line }, 'MutationConfirmer failed; returning null');
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=mutation-confirmer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mutation-confirmer.js","sourceRoot":"","sources":["../../src/extraction/mutation-confirmer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export declare const MAX_SITES_PER_CALL = 8;
|
|
2
|
+
export declare const PROMPT_BUDGET_CHARS = 3500;
|
|
3
|
+
export declare const CHUNK_SITE_LIMIT = 2;
|
|
4
|
+
export interface ClusterSite {
|
|
5
|
+
file: string;
|
|
6
|
+
line: number;
|
|
7
|
+
enclosingName?: string;
|
|
8
|
+
enclosingSource: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function prepareSites<T extends ClusterSite>(sites: ReadonlyArray<T>): T[];
|
|
11
|
+
/**
|
|
12
|
+
* Approximate prompt size in characters. Conservative — does not attempt
|
|
13
|
+
* tokenizer-accurate counting.
|
|
14
|
+
*/
|
|
15
|
+
export declare function estimatePromptChars(promptTemplate: string, sites: ReadonlyArray<ClusterSite>): number;
|
|
16
|
+
/**
|
|
17
|
+
* Consecutive partition into chunks of <= `CHUNK_SITE_LIMIT` sites.
|
|
18
|
+
* Function bodies are preserved in full — we trade more LLM calls for
|
|
19
|
+
* loss-free context.
|
|
20
|
+
*/
|
|
21
|
+
export declare function splitIntoChunks<T extends ClusterSite>(sites: ReadonlyArray<T>): T[][];
|
|
22
|
+
export interface ChunkVerdict {
|
|
23
|
+
race: boolean;
|
|
24
|
+
confidence: number;
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Aggregate per-chunk verdicts. Race wins.
|
|
29
|
+
*
|
|
30
|
+
* - If ANY chunk says race with confidence ≥ 0.7 → cluster is race.
|
|
31
|
+
* Confidence = MAX across the race verdicts. Reason cites the chunk
|
|
32
|
+
* count when multiple chunks fired.
|
|
33
|
+
* - Otherwise → cluster is safe. Confidence = MIN across safe verdicts
|
|
34
|
+
* (conservative — the weakest safe verdict caps the cluster).
|
|
35
|
+
*/
|
|
36
|
+
export declare function aggregateChunkVerdicts(verdicts: ReadonlyArray<ChunkVerdict>): ChunkVerdict;
|
|
37
|
+
//# sourceMappingURL=prompt-chunking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-chunking.d.ts","sourceRoot":"","sources":["../../src/extraction/prompt-chunking.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,kBAAkB,IAAI,CAAC;AACpC,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,CAEhF;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,CAAC,WAAW,CAAC,GAAG,MAAM,CAKrG;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,WAAW,EAAE,KAAK,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAMrF;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,aAAa,CAAC,YAAY,CAAC,GAAG,YAAY,CAa1F"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Prompt chunking for cluster confirmers. Never truncate bodies — split
|
|
2
|
+
// into consecutive pairs (2 sites/call), aggregate per-chunk verdicts.
|
|
3
|
+
// Race wins: cluster is race if any chunk says race ≥ 0.7.
|
|
4
|
+
// Caller code (rothunter.ts) feeds confirmers up to 8 call sites per
|
|
5
|
+
// cluster. Keep this cap aligned with that slice so the LLM sees the
|
|
6
|
+
// whole cluster — earlier versions silently dropped sites 5..8. The
|
|
7
|
+
// single-call vs chunked decision is delegated to estimatePromptChars
|
|
8
|
+
// + PROMPT_BUDGET_CHARS — small clusters still fit one prompt, large
|
|
9
|
+
// ones split into CHUNK_SITE_LIMIT-sized chunks.
|
|
10
|
+
export const MAX_SITES_PER_CALL = 8;
|
|
11
|
+
export const PROMPT_BUDGET_CHARS = 3500;
|
|
12
|
+
export const CHUNK_SITE_LIMIT = 2;
|
|
13
|
+
export function prepareSites(sites) {
|
|
14
|
+
return sites.slice(0, MAX_SITES_PER_CALL);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Approximate prompt size in characters. Conservative — does not attempt
|
|
18
|
+
* tokenizer-accurate counting.
|
|
19
|
+
*/
|
|
20
|
+
export function estimatePromptChars(promptTemplate, sites) {
|
|
21
|
+
const sitesBlock = sites
|
|
22
|
+
.map((s, i) => `[${i + 1}] ${s.file}:${s.line}\n${s.enclosingSource}`)
|
|
23
|
+
.join('\n---\n');
|
|
24
|
+
return promptTemplate.length + sitesBlock.length;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Consecutive partition into chunks of <= `CHUNK_SITE_LIMIT` sites.
|
|
28
|
+
* Function bodies are preserved in full — we trade more LLM calls for
|
|
29
|
+
* loss-free context.
|
|
30
|
+
*/
|
|
31
|
+
export function splitIntoChunks(sites) {
|
|
32
|
+
const out = [];
|
|
33
|
+
for (let i = 0; i < sites.length; i += CHUNK_SITE_LIMIT) {
|
|
34
|
+
out.push(sites.slice(i, i + CHUNK_SITE_LIMIT));
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Aggregate per-chunk verdicts. Race wins.
|
|
40
|
+
*
|
|
41
|
+
* - If ANY chunk says race with confidence ≥ 0.7 → cluster is race.
|
|
42
|
+
* Confidence = MAX across the race verdicts. Reason cites the chunk
|
|
43
|
+
* count when multiple chunks fired.
|
|
44
|
+
* - Otherwise → cluster is safe. Confidence = MIN across safe verdicts
|
|
45
|
+
* (conservative — the weakest safe verdict caps the cluster).
|
|
46
|
+
*/
|
|
47
|
+
export function aggregateChunkVerdicts(verdicts) {
|
|
48
|
+
const races = verdicts.filter((v) => v.race && v.confidence >= 0.7);
|
|
49
|
+
if (races.length > 0) {
|
|
50
|
+
const best = races.reduce((a, b) => (b.confidence > a.confidence ? b : a));
|
|
51
|
+
return {
|
|
52
|
+
race: true,
|
|
53
|
+
confidence: best.confidence,
|
|
54
|
+
reason: races.length > 1 ? `${best.reason} (across ${races.length} chunks)` : best.reason,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
const minConf = verdicts.reduce((m, v) => Math.min(m, v.confidence), 1);
|
|
58
|
+
const reason = verdicts[0]?.reason ?? 'safe';
|
|
59
|
+
return { race: false, confidence: minConf, reason };
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=prompt-chunking.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt-chunking.js","sourceRoot":"","sources":["../../src/extraction/prompt-chunking.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,uEAAuE;AACvE,2DAA2D;AAE3D,qEAAqE;AACrE,qEAAqE;AACrE,oEAAoE;AACpE,sEAAsE;AACtE,qEAAqE;AACrE,iDAAiD;AACjD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC;AACpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,IAAI,CAAC;AACxC,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AASlC,MAAM,UAAU,YAAY,CAAwB,KAAuB;IACzE,OAAO,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;AAC5C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,mBAAmB,CAAC,cAAsB,EAAE,KAAiC;IAC3F,MAAM,UAAU,GAAG,KAAK;SACrB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,eAAe,EAAE,CAAC;SACrE,IAAI,CAAC,SAAS,CAAC,CAAC;IACnB,OAAO,cAAc,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;AACnD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAwB,KAAuB;IAC5E,MAAM,GAAG,GAAU,EAAE,CAAC;IACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACxD,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAQD;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,QAAqC;IAC1E,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,UAAU,IAAI,GAAG,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACrB,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC3E,OAAO;YACL,IAAI,EAAE,IAAI;YACV,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,YAAY,KAAK,CAAC,MAAM,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM;SAC1F,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IACxE,MAAM,MAAM,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,MAAM,CAAC;IAC7C,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACtD,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LlmClient } from '../adapters/llm.js';
|
|
3
|
+
declare const VerdictSchema: z.ZodObject<{
|
|
4
|
+
race: z.ZodBoolean;
|
|
5
|
+
confidence: z.ZodNumber;
|
|
6
|
+
reason: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type RaceVerdict = z.infer<typeof VerdictSchema>;
|
|
9
|
+
export interface RaceCheckInput {
|
|
10
|
+
file: string;
|
|
11
|
+
line: number;
|
|
12
|
+
/** Detection pattern: read-modify-write | promise-all | emitter-handler. */
|
|
13
|
+
pattern: 'read-modify-write' | 'promise-all' | 'emitter-handler';
|
|
14
|
+
/** Canonical shared target (e.g. `this.tally`, `userCache`). */
|
|
15
|
+
target: string;
|
|
16
|
+
/** Surrounding function source — trimmed to ≤ 42 lines by the detector. */
|
|
17
|
+
enclosingSource: string;
|
|
18
|
+
/** Optional function or method name for context. */
|
|
19
|
+
enclosingName?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare class RaceConfirmer {
|
|
22
|
+
private llm;
|
|
23
|
+
private cache;
|
|
24
|
+
constructor(llm?: LlmClient);
|
|
25
|
+
confirm(input: RaceCheckInput): Promise<RaceVerdict | null>;
|
|
26
|
+
}
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=race-confirmer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"race-confirmer.d.ts","sourceRoot":"","sources":["../../src/extraction/race-confirmer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAA0B,MAAM,oBAAoB,CAAC;AAIvE,QAAA,MAAM,aAAa;;;;iBAIjB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,4EAA4E;IAC5E,OAAO,EAAE,mBAAmB,GAAG,aAAa,GAAG,iBAAiB,CAAC;IACjE,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA+BD,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,KAAK,CAAkC;gBAEnC,GAAG,CAAC,EAAE,SAAS;IAIrB,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;CAgClE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createDefaultLlmClient } from '../adapters/llm.js';
|
|
3
|
+
import { parseLlmJsonResponse } from '../utils/llm-json.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
const VerdictSchema = z.object({
|
|
6
|
+
race: z.boolean(),
|
|
7
|
+
confidence: z.number().min(0).max(1),
|
|
8
|
+
reason: z.string(),
|
|
9
|
+
});
|
|
10
|
+
const PROMPT = `You are reviewing a TypeScript race-condition finding from a static analyzer. Decide: is this a REAL race (two concurrent callers can produce a lost update / corrupted state) or SAFE (the apparent race is neutralised by scope, mutex, idempotency, or single-flight)?
|
|
11
|
+
|
|
12
|
+
Output ONE compact JSON object and STOP:
|
|
13
|
+
{"race": boolean, "confidence": <0..1>, "reason": "<max 18 words>"}
|
|
14
|
+
|
|
15
|
+
Calibration:
|
|
16
|
+
- Unambiguous classic race (\`this.<field>\` or module-scope \`let\`/\`var\` with read → await → write back, no mutex, no guard, no idempotency) → confidence ≥ 0.85.
|
|
17
|
+
- \`Promise.all\` siblings both writing the same \`this.<field>\` or module mutable → confidence ≥ 0.9 (no timing window required).
|
|
18
|
+
- Unambiguous safe pattern (function-local \`let\` / mutex-wrapped / single-flight guard / distinct read+write targets / idempotent constant assignment) → confidence ≥ 0.85.
|
|
19
|
+
- Genuinely unclear → \`race: true, confidence: 0.6\` (keep the deterministic finding at low confidence).
|
|
20
|
+
|
|
21
|
+
Decision rules (apply in order):
|
|
22
|
+
1. Shared target is a FUNCTION-LOCAL \`let\`/\`var\` declared inside the same function (not a parameter, not module-scope, not \`this.<field>\`) — safe. Per-caller copy; no concurrent sharing.
|
|
23
|
+
2. Write assigns a constant / boolean literal and no later read depends on the value — safe. Idempotent.
|
|
24
|
+
3. Critical section wrapped in a mutex / lock / single-flight (\`await acquire()\` / \`mutex.lock()\` / guard \`if (this.X !== null) return this.X\` before the first await) — safe.
|
|
25
|
+
4. Read target and write target differ (e.g. read \`this.raw\`, write \`this.parsed\`) — safe. No lost-update.
|
|
26
|
+
5. Pattern is \`promise-all\` but callback only reads shared state — safe.
|
|
27
|
+
6. Otherwise: \`this.<field>\` or module-scope mutable + read → await → write back to the same target → race.
|
|
28
|
+
|
|
29
|
+
Pattern: {{PATTERN}}
|
|
30
|
+
Shared target: \`{{TARGET}}\`
|
|
31
|
+
{{ENCLOSING_NAME_LINE}}
|
|
32
|
+
|
|
33
|
+
Enclosing function ({{FILE}}:{{LINE}}):
|
|
34
|
+
\`\`\`typescript
|
|
35
|
+
{{ENCLOSING}}
|
|
36
|
+
\`\`\`
|
|
37
|
+
`;
|
|
38
|
+
export class RaceConfirmer {
|
|
39
|
+
llm;
|
|
40
|
+
cache = new Map();
|
|
41
|
+
constructor(llm) {
|
|
42
|
+
this.llm = llm ?? createDefaultLlmClient();
|
|
43
|
+
}
|
|
44
|
+
async confirm(input) {
|
|
45
|
+
const cacheKey = `${input.file}:${input.line}:${input.pattern}:${input.target}`;
|
|
46
|
+
const cached = this.cache.get(cacheKey);
|
|
47
|
+
if (cached)
|
|
48
|
+
return cached;
|
|
49
|
+
const prompt = PROMPT.replace('{{PATTERN}}', input.pattern)
|
|
50
|
+
.replace('{{TARGET}}', input.target)
|
|
51
|
+
.replace('{{ENCLOSING_NAME_LINE}}', input.enclosingName ? `Enclosing identifier: \`${input.enclosingName}\`` : '')
|
|
52
|
+
.replace('{{FILE}}', input.file)
|
|
53
|
+
.replace('{{LINE}}', String(input.line))
|
|
54
|
+
.replace('{{ENCLOSING}}', input.enclosingSource);
|
|
55
|
+
try {
|
|
56
|
+
const raw = await this.llm.chat([{ role: 'user', content: prompt }], { temperature: 0, maxTokens: 96 });
|
|
57
|
+
const parsed = parseLlmJsonResponse(raw);
|
|
58
|
+
const verdict = VerdictSchema.parse(parsed);
|
|
59
|
+
this.cache.set(cacheKey, verdict);
|
|
60
|
+
return verdict;
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
logger.warn({ err: err.message, file: input.file, line: input.line }, 'RaceConfirmer failed; returning null');
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=race-confirmer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"race-confirmer.js","sourceRoot":"","sources":["../../src/extraction/race-confirmer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAa,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC5D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,aAAa,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,IAAI,EAAE,CAAC,CAAC,OAAO,EAAE;IACjB,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE;CACnB,CAAC,CAAC;AAiBH,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2Bd,CAAC;AAEF,MAAM,OAAO,aAAa;IAChB,GAAG,CAAY;IACf,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,YAAY,GAAe;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,IAAI,sBAAsB,EAAE,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,OAAO,CAAC,KAAqB;QACjC,MAAM,QAAQ,GAAG,GAAG,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;QAChF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAE1B,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,aAAa,EAAE,KAAK,CAAC,OAAO,CAAC;aACxD,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,MAAM,CAAC;aACnC,OAAO,CACN,yBAAyB,EACzB,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,2BAA2B,KAAK,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,EAAE,CAC9E;aACA,OAAO,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,CAAC;aAC/B,OAAO,CAAC,UAAU,EAAE,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;aACvC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,eAAe,CAAC,CAAC;QAEnD,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,IAAI,CAC7B,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EACnC,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,EAAE,EAAE,CAClC,CAAC;YACF,MAAM,MAAM,GAAG,oBAAoB,CAAC,GAAG,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAClC,OAAO,OAAO,CAAC;QACjB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CACT,EAAE,GAAG,EAAG,GAAa,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,EACnE,sCAAsC,CACvC,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { LlmClient } from '../adapters/llm.js';
|
|
3
|
+
declare const VerdictSchema: z.ZodObject<{
|
|
4
|
+
race: z.ZodBoolean;
|
|
5
|
+
confidence: z.ZodNumber;
|
|
6
|
+
reason: z.ZodString;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type SharedDbVerdict = z.infer<typeof VerdictSchema>;
|
|
9
|
+
export interface SharedDbCheckInput {
|
|
10
|
+
entity: string;
|
|
11
|
+
column: string;
|
|
12
|
+
/** ORM adapter mix detected on the cluster (e.g. `prisma+sequelize`). */
|
|
13
|
+
adapters: string;
|
|
14
|
+
/** Up to 8 call sites — each one is a small enclosing-function source slice. */
|
|
15
|
+
sites: ReadonlyArray<{
|
|
16
|
+
file: string;
|
|
17
|
+
line: number;
|
|
18
|
+
enclosingName?: string;
|
|
19
|
+
enclosingSource: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export declare class SharedDbWriteConfirmer {
|
|
23
|
+
private llm;
|
|
24
|
+
private cache;
|
|
25
|
+
constructor(llm?: LlmClient);
|
|
26
|
+
confirm(input: SharedDbCheckInput): Promise<SharedDbVerdict | null>;
|
|
27
|
+
private callOnce;
|
|
28
|
+
private callChunked;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=shared-db-write-confirmer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shared-db-write-confirmer.d.ts","sourceRoot":"","sources":["../../src/extraction/shared-db-write-confirmer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,SAAS,EAA0B,MAAM,oBAAoB,CAAC;AAYvE,QAAA,MAAM,aAAa;;;;iBAIjB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE5D,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,MAAM,CAAC;IACjB,gFAAgF;IAChF,KAAK,EAAE,aAAa,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC,CAAC;CACJ;AA6ED,qBAAa,sBAAsB;IACjC,OAAO,CAAC,GAAG,CAAY;IACvB,OAAO,CAAC,KAAK,CAAsC;gBAEvC,GAAG,CAAC,EAAE,SAAS;IAIrB,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC;YAuB3D,QAAQ;YAwBR,WAAW;CAa1B"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { createDefaultLlmClient } from '../adapters/llm.js';
|
|
3
|
+
import { parseLlmJsonResponse } from '../utils/llm-json.js';
|
|
4
|
+
import { logger } from '../utils/logger.js';
|
|
5
|
+
import { prepareSites, estimatePromptChars, splitIntoChunks, aggregateChunkVerdicts, PROMPT_BUDGET_CHARS, } from './prompt-chunking.js';
|
|
6
|
+
const VerdictSchema = z.object({
|
|
7
|
+
race: z.boolean(),
|
|
8
|
+
confidence: z.number().min(0).max(1),
|
|
9
|
+
reason: z.string(),
|
|
10
|
+
});
|
|
11
|
+
const PROMPT = `You are reviewing a static-analysis finding about a database column written by multiple TypeScript functions across different files. Decide: is this a REAL cross-flow race (two independent flows can concurrently overwrite the same column, producing a lost update) or SAFE (the writes are owned by one component, wrapped in a transaction, init-only, or idempotent)?
|
|
12
|
+
|
|
13
|
+
Output ONE compact JSON object and STOP:
|
|
14
|
+
{"race": boolean, "confidence": <0..1>, "reason": "<max 22 words>"}
|
|
15
|
+
|
|
16
|
+
Decision rules (apply IN ORDER — first match wins):
|
|
17
|
+
|
|
18
|
+
1. **Test-file callers** — STRICT trigger. Check each call-site header line (e.g. \`[1] src/foo/bar.ts:42 — myFn\`). At least one header must satisfy ONE of:
|
|
19
|
+
- Path contains the literal substring \`__tests__/\`
|
|
20
|
+
- Path contains the literal substring \`/tests/\`
|
|
21
|
+
- Path ends in \`.test.ts\` or \`.test.tsx\`
|
|
22
|
+
- Path ends in \`.spec.ts\` or \`.spec.tsx\`
|
|
23
|
+
- Function name (after \`—\`) starts with \`test_\` / \`it_\` / \`describe_\`
|
|
24
|
+
|
|
25
|
+
POSITIVE example — rule 1 fires:
|
|
26
|
+
[1] src/users/users.service.ts:42 — createUser
|
|
27
|
+
[2] tests/users.test.ts:18 — test_creates_user
|
|
28
|
+
→ safe (one caller is in tests/, function name is \`test_creates_user\`)
|
|
29
|
+
|
|
30
|
+
NEGATIVE example — rule 1 does NOT fire (this is the dominant case):
|
|
31
|
+
[1] src/__fixtures__/shared-db/case_03_typeorm_status_api.ts:5 — markOrderPaidFromApi
|
|
32
|
+
[2] src/__fixtures__/shared-db/case_03_typeorm_status_consumer.ts:5 — refundOrderFromKafkaConsumer
|
|
33
|
+
→ NOT safe via rule 1. Paths contain \`__fixtures__\` which is NOT \`__tests__\` and NOT \`.test.ts\`. Owner suffixes \`_api\` / \`_consumer\` go to rule 7.
|
|
34
|
+
|
|
35
|
+
NEGATIVE example #2 — rule 1 also does NOT fire on production paths:
|
|
36
|
+
[1] src/api/users/handler.ts:12 — updateUserHandler
|
|
37
|
+
[2] src/workers/sync.ts:34 — syncUserWorker
|
|
38
|
+
→ NOT safe via rule 1. Owner suffixes \`handler\` / \`worker\` go to rule 7.
|
|
39
|
+
|
|
40
|
+
The substring check is LITERAL. \`__fixtures__\` is NOT \`__tests__\`. \`fixtures/\` is NOT \`tests/\`. \`case_*\` is NOT \`test_*\`.
|
|
41
|
+
|
|
42
|
+
When this rule fires → safe, confidence ≥ 0.85.
|
|
43
|
+
|
|
44
|
+
2. **INSERT-only cluster** — every caller uses an INSERT method (\`.create(\` without \`upsert\`, \`.bulkCreate(\`, \`.insertMany(\`, \`.createMany(\`, \`INSERT INTO …\` raw SQL). INSERTs create NEW rows — concurrent inserts do not overwrite each other (a unique-constraint violation surfaces as an error, not a lost update). → safe, confidence ≥ 0.85. Caveat: if even ONE caller uses \`.update / .updateOne / .findOneAndUpdate / .upsert / .set\` the cluster is NOT insert-only.
|
|
45
|
+
|
|
46
|
+
3. **Idempotent / commutative value** — all writers assign \`new Date()\`, \`Date.now()\`, a monotonic clock, a boolean constant (\`true\`/\`false\`), or a literal that does not depend on the prior column value. No read of the column precedes the write. → safe, confidence ≥ 0.85. "Last writer wins" IS the intended semantics; concurrent writes lose no information.
|
|
47
|
+
|
|
48
|
+
4. **Per-tenant / per-plugin row isolation** — each caller targets a DIFFERENT row of the table identified by a constant key in the WHERE clause. Look for snippets where the filter object hard-codes a different value of the same key across callers, e.g. \`{ service: 'figma' }\` in figma.ts, \`{ service: 'gitlab' }\` in gitlab.ts, \`{ service: 'linear' }\` in linear.ts. Distinct rows = no overwrite race even when the same column is touched. Common shapes: OAuth plugin handlers, multi-tenant integrations, per-provider auth tables. → safe, confidence ≥ 0.8.
|
|
49
|
+
|
|
50
|
+
5. **Transaction-wrapped** — every writer is nested INSIDE the same outer \`$transaction(async (tx) => { ... })\` / \`unitOfWork.run(...)\` callback, OR every writer reads its \`tx\` from a single shared function parameter that is documented as the transaction handle (function name like \`applyXInsideTransaction*\` / \`*InTx\`). Caveat: simply passing \`{ transaction: tx }\` as an OPTION to a Sequelize/TypeORM call does NOT qualify — the caller can still invoke A and B from two independent flows. → safe, confidence ≥ 0.85.
|
|
51
|
+
|
|
52
|
+
6. **Same single-owner component** — all call sites are helpers inside the same service / module / class. Function names share a common prefix (e.g. \`FooService_step1\` + \`FooService_step2\`) or both files live under \`services/foo/\`. → safe, confidence ≥ 0.8.
|
|
53
|
+
|
|
54
|
+
7. **Init-only / one-shot scripts** — all writers are bootstrap / seed / migration / backfill / one-shot import scripts. Function names contain \`seed\`, \`import\`, \`backfill\`, \`migrate\`. → safe, confidence ≥ 0.8.
|
|
55
|
+
|
|
56
|
+
8. **Independent flows** — names / paths show two unrelated owners (HTTP handler + worker, two webhook handlers, API + cron, GraphQL mutation + bot, checkout + sync worker) writing the SAME column via UPDATE-style methods. The written value depends on prior state or external input (no idempotency). → race, confidence ≥ 0.85.
|
|
57
|
+
|
|
58
|
+
9. **Default** — UPDATE-style ORM write (\`.update / .upsert / .updateOne / .findOneAndUpdate / .set\`) with no transaction, no shared owner, no idempotency. → race, confidence ≥ 0.8.
|
|
59
|
+
|
|
60
|
+
10. Unclear → \`race: true, confidence: 0.6\`. Preserve the deterministic finding at low confidence.
|
|
61
|
+
|
|
62
|
+
Notes:
|
|
63
|
+
- Rule 1 has top priority. If ANY snippet is from a test file, the cluster is safe — tests do not race with production.
|
|
64
|
+
- Rule 2: INSERT methods create new rows and cannot lose information across concurrent callers. UPDATE methods overwrite an existing row's column and CAN lose information. The distinction is critical — most Sequelize/Mongoose codebases have many \`Model.create(...)\` cross-file calls (factories, lifecycle hooks, test fixtures) that the deterministic detector clusters together. They are not races.
|
|
65
|
+
- Rule 3 takes priority over rules 5 and 7. An audit-trail \`lastSeenAt = new Date()\` written by an HTTP handler AND a background ping is **safe** — both writers compute the same kind of value.
|
|
66
|
+
- Rule 4 takes priority over rule 7. Two flows sharing a transaction are safe even if they look independent.
|
|
67
|
+
- For rule 7: examine function and file names — \`handler\`, \`webhook\`, \`cron\`, \`worker\`, \`consumer\`, \`bot\`, \`api\`, \`route\` are independent-flow signals.
|
|
68
|
+
|
|
69
|
+
Cluster: \`{{ENTITY}}.{{COLUMN}}\` (adapters: {{ADAPTERS}})
|
|
70
|
+
|
|
71
|
+
Call sites:
|
|
72
|
+
\`\`\`
|
|
73
|
+
{{SITES}}
|
|
74
|
+
\`\`\`
|
|
75
|
+
`;
|
|
76
|
+
function renderSites(sites) {
|
|
77
|
+
return sites
|
|
78
|
+
.map((s, i) => {
|
|
79
|
+
const header = `[${i + 1}] ${s.file}:${s.line}${s.enclosingName ? ` — ${s.enclosingName}` : ''}`;
|
|
80
|
+
return `${header}\n${s.enclosingSource}`;
|
|
81
|
+
})
|
|
82
|
+
.join('\n---\n');
|
|
83
|
+
}
|
|
84
|
+
export class SharedDbWriteConfirmer {
|
|
85
|
+
llm;
|
|
86
|
+
cache = new Map();
|
|
87
|
+
constructor(llm) {
|
|
88
|
+
this.llm = llm ?? createDefaultLlmClient();
|
|
89
|
+
}
|
|
90
|
+
async confirm(input) {
|
|
91
|
+
const cacheKey = `${input.entity}.${input.column}::${input.adapters}::${input.sites
|
|
92
|
+
.map((s) => `${s.file}:${s.line}`)
|
|
93
|
+
.sort()
|
|
94
|
+
.join(',')}`;
|
|
95
|
+
const cached = this.cache.get(cacheKey);
|
|
96
|
+
if (cached)
|
|
97
|
+
return cached;
|
|
98
|
+
const sites = prepareSites(input.sites);
|
|
99
|
+
// If even the truncated single-call prompt exceeds the context budget,
|
|
100
|
+
// split the sites into chunks and aggregate verdicts.
|
|
101
|
+
const singleCallChars = estimatePromptChars(PROMPT, sites);
|
|
102
|
+
let verdict;
|
|
103
|
+
if (singleCallChars <= PROMPT_BUDGET_CHARS) {
|
|
104
|
+
verdict = await this.callOnce(input, sites);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
verdict = await this.callChunked(input, sites);
|
|
108
|
+
}
|
|
109
|
+
if (verdict)
|
|
110
|
+
this.cache.set(cacheKey, verdict);
|
|
111
|
+
return verdict;
|
|
112
|
+
}
|
|
113
|
+
async callOnce(input, sites) {
|
|
114
|
+
const prompt = PROMPT.replace('{{ENTITY}}', input.entity)
|
|
115
|
+
.replace('{{COLUMN}}', input.column)
|
|
116
|
+
.replace('{{ADAPTERS}}', input.adapters)
|
|
117
|
+
.replace('{{SITES}}', renderSites(sites));
|
|
118
|
+
try {
|
|
119
|
+
const raw = await this.llm.chat([{ role: 'user', content: prompt }], { temperature: 0, maxTokens: 128 });
|
|
120
|
+
const parsed = parseLlmJsonResponse(raw);
|
|
121
|
+
return VerdictSchema.parse(parsed);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
logger.warn({ err: err.message, entity: input.entity, column: input.column }, 'SharedDbWriteConfirmer failed; returning null');
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async callChunked(input, sites) {
|
|
129
|
+
const chunks = splitIntoChunks(sites);
|
|
130
|
+
const chunkVerdicts = [];
|
|
131
|
+
for (const chunk of chunks) {
|
|
132
|
+
const v = await this.callOnce(input, chunk);
|
|
133
|
+
if (v)
|
|
134
|
+
chunkVerdicts.push(v);
|
|
135
|
+
}
|
|
136
|
+
if (chunkVerdicts.length === 0)
|
|
137
|
+
return null;
|
|
138
|
+
return aggregateChunkVerdicts(chunkVerdicts);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=shared-db-write-confirmer.js.map
|