weavatrix 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -39
- package/SECURITY.md +2 -2
- package/docs/releases/v0.2.14.md +93 -0
- package/package.json +2 -2
- package/skill/SKILL.md +57 -38
- package/src/analysis/architecture/contract-graph.js +119 -0
- package/src/analysis/architecture/contract-schema.js +110 -0
- package/src/analysis/architecture/contract-storage.js +35 -0
- package/src/analysis/architecture/contract-verification.js +168 -0
- package/src/analysis/architecture-contract.js +16 -343
- package/src/analysis/change-classification/diff-parser.js +103 -0
- package/src/analysis/change-classification/options.js +40 -0
- package/src/analysis/change-classification/symbol-classifier.js +177 -0
- package/src/analysis/change-classification.js +120 -519
- package/src/analysis/dead-code-review/policy.js +23 -0
- package/src/analysis/dead-code-review.js +9 -19
- package/src/analysis/dep-check.js +10 -57
- package/src/analysis/dep-rules.js +10 -303
- package/src/analysis/dependency/scoped-dependencies.js +40 -0
- package/src/analysis/endpoints/common.js +62 -0
- package/src/analysis/endpoints/extract.js +107 -0
- package/src/analysis/endpoints/inventory.js +84 -0
- package/src/analysis/endpoints/mounts.js +129 -0
- package/src/analysis/endpoints-java.js +80 -3
- package/src/analysis/endpoints.js +3 -448
- package/src/analysis/git-history/analytics.js +177 -0
- package/src/analysis/git-history/collector.js +109 -0
- package/src/analysis/git-history/options.js +68 -0
- package/src/analysis/git-history.js +128 -577
- package/src/analysis/health-capabilities.js +82 -0
- package/src/analysis/http-contracts/analysis.js +169 -0
- package/src/analysis/http-contracts/client-call-detection.js +81 -0
- package/src/analysis/http-contracts/client-call-parser.js +255 -0
- package/src/analysis/http-contracts/graph-context.js +143 -0
- package/src/analysis/http-contracts/matching.js +61 -0
- package/src/analysis/http-contracts/shared.js +86 -0
- package/src/analysis/http-contracts.js +6 -822
- package/src/analysis/internal-audit/dependency-health.js +111 -0
- package/src/analysis/internal-audit/python-manifests.js +65 -0
- package/src/analysis/internal-audit/repo-files.js +55 -0
- package/src/analysis/internal-audit/supply-chain.js +132 -0
- package/src/analysis/internal-audit.collect.js +5 -106
- package/src/analysis/internal-audit.run.js +113 -200
- package/src/analysis/jvm-dependency-evidence.js +69 -0
- package/src/analysis/source-correctness/retry-patterns.js +93 -0
- package/src/analysis/source-correctness.js +225 -0
- package/src/analysis/structure/dependency-graph.js +156 -0
- package/src/analysis/structure/findings.js +142 -0
- package/src/graph/builder/go-receiver-resolution.js +112 -0
- package/src/graph/builder/js/queries.js +48 -0
- package/src/graph/builder/lang-go.js +89 -4
- package/src/graph/builder/lang-js.js +4 -44
- package/src/graph/builder/pass2-resolution.js +68 -0
- package/src/graph/freshness-probe.js +2 -1
- package/src/graph/incremental-refresh.js +3 -2
- package/src/graph/internal-builder.build.js +22 -183
- package/src/graph/internal-builder.pass2.js +161 -0
- package/src/graph/internal-builder.resolvers.js +2 -105
- package/src/graph/resolvers/rust.js +117 -0
- package/src/mcp/actions/advisories.mjs +18 -0
- package/src/mcp/actions/graph-lifecycle.mjs +148 -0
- package/src/mcp/actions/graph-sync.mjs +195 -0
- package/src/mcp/actions/hosted-architecture.mjs +57 -0
- package/src/mcp/architecture-bootstrap.mjs +168 -0
- package/src/mcp/architecture-starter.mjs +234 -0
- package/src/mcp/catalog.mjs +20 -6
- package/src/mcp/evidence/bun-lock-graph.mjs +209 -0
- package/src/mcp/evidence/package-graph-common.mjs +115 -0
- package/src/mcp/evidence/package-lock-graph.mjs +92 -0
- package/src/mcp/evidence-snapshot.package-graph.mjs +5 -474
- package/src/mcp/graph/context-core.mjs +111 -0
- package/src/mcp/graph/context-seeds.mjs +208 -0
- package/src/mcp/graph/context-state.mjs +86 -0
- package/src/mcp/graph/tools-core.mjs +143 -0
- package/src/mcp/graph/tools-query.mjs +223 -0
- package/src/mcp/graph-context.mjs +4 -496
- package/src/mcp/health/audit-format.mjs +172 -0
- package/src/mcp/health/audit.mjs +171 -0
- package/src/mcp/health/dead-code.mjs +110 -0
- package/src/mcp/health/duplicates.mjs +83 -0
- package/src/mcp/health/endpoints.mjs +42 -0
- package/src/mcp/health/structure.mjs +219 -0
- package/src/mcp/runtime-version.mjs +32 -0
- package/src/mcp/server/auto-refresh.mjs +66 -0
- package/src/mcp/server/runtime-config.mjs +43 -0
- package/src/mcp/server/shutdown.mjs +40 -0
- package/src/mcp/sync/evidence-architecture.mjs +54 -0
- package/src/mcp/sync/evidence-common.mjs +88 -0
- package/src/mcp/sync/evidence-duplicates.mjs +100 -0
- package/src/mcp/sync/evidence-health.mjs +56 -0
- package/src/mcp/sync/evidence-packages.mjs +95 -0
- package/src/mcp/sync/payload-common.mjs +97 -0
- package/src/mcp/sync/payload-v2.mjs +53 -0
- package/src/mcp/sync/payload-v3.mjs +79 -0
- package/src/mcp/sync-evidence.mjs +7 -402
- package/src/mcp/sync-payload.mjs +10 -244
- package/src/mcp/tools-actions.mjs +18 -414
- package/src/mcp/tools-architecture.mjs +18 -70
- package/src/mcp/tools-context.mjs +56 -15
- package/src/mcp/tools-endpoints.mjs +4 -1
- package/src/mcp/tools-graph.mjs +17 -331
- package/src/mcp/tools-health.mjs +24 -705
- package/src/mcp-server.mjs +35 -146
- package/src/precision/lsp-client/constants.js +12 -0
- package/src/precision/lsp-client/environment.js +20 -0
- package/src/precision/lsp-client/errors.js +15 -0
- package/src/precision/lsp-client/lifecycle.js +127 -0
- package/src/precision/lsp-client/message-parser.js +81 -0
- package/src/precision/lsp-client/protocol.js +212 -0
- package/src/precision/lsp-client/registry.js +58 -0
- package/src/precision/lsp-client/repo-uri.js +60 -0
- package/src/precision/lsp-client/stdio-client.js +151 -0
- package/src/precision/lsp-client.js +10 -682
- package/src/precision/lsp-overlay/build.js +238 -0
- package/src/precision/lsp-overlay/contract.js +61 -0
- package/src/precision/lsp-overlay/reference-results.js +108 -0
- package/src/precision/lsp-overlay/semantic-inputs.js +62 -0
- package/src/precision/lsp-overlay/source-session.js +134 -0
- package/src/precision/lsp-overlay/store.js +150 -0
- package/src/precision/lsp-overlay/target-index.js +147 -0
- package/src/precision/lsp-overlay/target-query.js +55 -0
- package/src/precision/lsp-overlay.js +11 -868
- package/src/precision/typescript-lsp-provider.js +12 -682
- package/src/precision/typescript-provider/client.js +69 -0
- package/src/precision/typescript-provider/discovery.js +124 -0
- package/src/precision/typescript-provider/project-host.js +249 -0
- package/src/precision/typescript-provider/project-safety.js +161 -0
- package/src/precision/typescript-provider/reference-usage.js +53 -0
- package/src/version.js +7 -0
- package/docs/releases/v0.2.13.md +0 -48
|
@@ -1,534 +1,135 @@
|
|
|
1
|
-
// Symbol-aware git-diff classification for change_impact.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
maxLineLength: boundedNumber(value.maxLineLength, DEFAULT_LIMITS.maxLineLength, 80, DEFAULT_LIMITS.maxLineLength),
|
|
41
|
-
maxSymbolsPerFile: boundedNumber(value.maxSymbolsPerFile, DEFAULT_LIMITS.maxSymbolsPerFile, 1, DEFAULT_LIMITS.maxSymbolsPerFile),
|
|
42
|
-
maxSeeds: boundedNumber(value.maxSeeds, DEFAULT_LIMITS.maxSeeds, 1, DEFAULT_LIMITS.maxSeeds),
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function decodeGitQuoted(value) {
|
|
47
|
-
const input = String(value || "").trim();
|
|
48
|
-
if (!input.startsWith('"')) return input.split("\t", 1)[0];
|
|
49
|
-
try { return JSON.parse(input); } catch { /* Git also emits octal escapes, which JSON rejects. */ }
|
|
50
|
-
const bytes = [];
|
|
51
|
-
for (let index = 1; index < input.length - 1; index += 1) {
|
|
52
|
-
const char = input[index];
|
|
53
|
-
if (char !== "\\") {
|
|
54
|
-
bytes.push(...Buffer.from(char));
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const next = input[++index] || "";
|
|
58
|
-
if (/[0-7]/.test(next)) {
|
|
59
|
-
let octal = next;
|
|
60
|
-
while (octal.length < 3 && /[0-7]/.test(input[index + 1] || "")) octal += input[++index];
|
|
61
|
-
bytes.push(parseInt(octal, 8));
|
|
62
|
-
} else {
|
|
63
|
-
const escapes = { n: 10, r: 13, t: 9, b: 8, f: 12, v: 11, "\\": 92, '"': 34 };
|
|
64
|
-
bytes.push(escapes[next] ?? next.charCodeAt(0));
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return Buffer.from(bytes).toString("utf8");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function diffPath(raw, prefix) {
|
|
71
|
-
const decoded = decodeGitQuoted(raw);
|
|
72
|
-
if (!decoded || decoded === "/dev/null") return null;
|
|
73
|
-
return normalizePath(decoded.startsWith(`${prefix}/`) ? decoded.slice(2) : decoded);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function headerPaths(line) {
|
|
77
|
-
const match = /^diff --git ("(?:\\.|[^"])*"|\S+) ("(?:\\.|[^"])*"|\S+)$/.exec(line);
|
|
78
|
-
if (!match) return { oldPath: null, newPath: null };
|
|
79
|
-
return { oldPath: diffPath(match[1], "a"), newPath: diffPath(match[2], "b") };
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const emptyFile = (paths = {}) => ({
|
|
83
|
-
oldPath: paths.oldPath || null,
|
|
84
|
-
newPath: paths.newPath || null,
|
|
85
|
-
newFile: false,
|
|
86
|
-
deletedFile: false,
|
|
87
|
-
renamed: false,
|
|
88
|
-
binary: false,
|
|
89
|
-
hunks: [],
|
|
90
|
-
additions: [],
|
|
91
|
-
removals: [],
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Parse ordinary unified output, expecting --unified=0 but safely accepting context lines too.
|
|
95
|
-
export function parseZeroContextDiff(diffText, options = {}) {
|
|
96
|
-
const limits = limitsOf(options);
|
|
97
|
-
const original = String(diffText ?? "");
|
|
98
|
-
const byteLength = Buffer.byteLength(original);
|
|
99
|
-
const oversized = byteLength > limits.maxDiffBytes;
|
|
100
|
-
const text = oversized ? original.slice(0, limits.maxDiffBytes) : original;
|
|
101
|
-
const files = [];
|
|
102
|
-
let file = null;
|
|
103
|
-
let hunk = null;
|
|
104
|
-
let changedLines = 0;
|
|
105
|
-
let truncated = oversized;
|
|
106
|
-
const finish = () => {
|
|
107
|
-
if (!file) return;
|
|
108
|
-
if ((file.oldPath || file.newPath) && files.length < limits.maxFiles) files.push(file);
|
|
109
|
-
else if (files.length >= limits.maxFiles) truncated = true;
|
|
110
|
-
file = null;
|
|
111
|
-
hunk = null;
|
|
112
|
-
};
|
|
113
|
-
const addChange = (kind, line, oldLine, newLine, mappedNewLine) => {
|
|
114
|
-
changedLines++;
|
|
115
|
-
if (changedLines > limits.maxChangedLines) { truncated = true; return; }
|
|
116
|
-
const change = {
|
|
117
|
-
kind,
|
|
118
|
-
text: String(line).slice(0, limits.maxLineLength),
|
|
119
|
-
...(oldLine != null ? { oldLine } : {}),
|
|
120
|
-
...(newLine != null ? { newLine } : {}),
|
|
121
|
-
mappedNewLine,
|
|
122
|
-
};
|
|
123
|
-
(kind === "added" ? file.additions : file.removals).push(change);
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
for (const line of text.split(/\r?\n/)) {
|
|
127
|
-
if (line.startsWith("diff --git ")) {
|
|
128
|
-
finish();
|
|
129
|
-
file = emptyFile(headerPaths(line));
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (!file && line.startsWith("--- ")) file = emptyFile();
|
|
133
|
-
if (!file) continue;
|
|
134
|
-
if (line.startsWith("new file mode ")) { file.newFile = true; continue; }
|
|
135
|
-
if (line.startsWith("deleted file mode ")) { file.deletedFile = true; continue; }
|
|
136
|
-
if (line.startsWith("rename from ")) { file.oldPath = normalizePath(decodeGitQuoted(line.slice(12))); file.renamed = true; continue; }
|
|
137
|
-
if (line.startsWith("rename to ")) { file.newPath = normalizePath(decodeGitQuoted(line.slice(10))); file.renamed = true; continue; }
|
|
138
|
-
if (line.startsWith("Binary files ") || line === "GIT binary patch") { file.binary = true; continue; }
|
|
139
|
-
if (!hunk && line.startsWith("--- ")) {
|
|
140
|
-
file.oldPath = diffPath(line.slice(4), "a");
|
|
141
|
-
if (!file.oldPath) file.newFile = true;
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (!hunk && line.startsWith("+++ ")) {
|
|
145
|
-
file.newPath = diffPath(line.slice(4), "b");
|
|
146
|
-
if (!file.newPath) file.deletedFile = true;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
const match = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/.exec(line);
|
|
150
|
-
if (match) {
|
|
151
|
-
hunk = {
|
|
152
|
-
oldStart: Number(match[1]), oldCount: match[2] == null ? 1 : Number(match[2]),
|
|
153
|
-
newStart: Number(match[3]), newCount: match[4] == null ? 1 : Number(match[4]),
|
|
154
|
-
oldCursor: Number(match[1]), newCursor: Number(match[3]),
|
|
155
|
-
};
|
|
156
|
-
file.hunks.push({ oldStart: hunk.oldStart, oldCount: hunk.oldCount, newStart: hunk.newStart, newCount: hunk.newCount });
|
|
157
|
-
continue;
|
|
1
|
+
// Symbol-aware git-diff classification for change_impact.
|
|
2
|
+
import {spawnSync} from 'node:child_process'
|
|
3
|
+
import {childProcessEnv} from '../child-env.js'
|
|
4
|
+
import {createPathClassifier} from '../path-classification.js'
|
|
5
|
+
import {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
6
|
+
import {
|
|
7
|
+
CHANGE_CLASSIFICATION_LIMITS,
|
|
8
|
+
CHANGE_CLASS_RANK,
|
|
9
|
+
changeLimits,
|
|
10
|
+
normalizeChangePath,
|
|
11
|
+
uniqueChangeSeeds,
|
|
12
|
+
} from './change-classification/options.js'
|
|
13
|
+
import {
|
|
14
|
+
analyzeParsedFile,
|
|
15
|
+
classifyTestSurface,
|
|
16
|
+
indexChangeGraph,
|
|
17
|
+
unknownChangedFile,
|
|
18
|
+
} from './change-classification/symbol-classifier.js'
|
|
19
|
+
|
|
20
|
+
export {parseZeroContextDiff} from './change-classification/diff-parser.js'
|
|
21
|
+
export {CHANGE_CLASSIFICATION_LIMITS} from './change-classification/options.js'
|
|
22
|
+
|
|
23
|
+
const VERDICT_RANK = Object.freeze({LOW: 0, MEDIUM: 1, HIGH: 2})
|
|
24
|
+
|
|
25
|
+
function runGitDiff(repoRoot, base, limits) {
|
|
26
|
+
const args = ['-C', repoRoot, 'diff', '--no-ext-diff', '--find-renames', '--no-color', '--unified=0', String(base), '--']
|
|
27
|
+
const result = spawnSync('git', args, {
|
|
28
|
+
encoding: 'utf8', windowsHide: true, timeout: 12_000,
|
|
29
|
+
maxBuffer: limits.maxDiffBytes + 1, env: childProcessEnv(),
|
|
30
|
+
})
|
|
31
|
+
if (result.status === 0) return {available: true, text: String(result.stdout || ''), error: null}
|
|
32
|
+
const oversized = result.error?.code === 'ENOBUFS' || Buffer.byteLength(String(result.stdout || '')) > limits.maxDiffBytes
|
|
33
|
+
return {
|
|
34
|
+
available: false,
|
|
35
|
+
text: String(result.stdout || ''),
|
|
36
|
+
oversized,
|
|
37
|
+
error: oversized
|
|
38
|
+
? 'git diff exceeded the byte limit'
|
|
39
|
+
: String(result.stderr || result.error?.message || 'git diff unavailable').trim(),
|
|
158
40
|
}
|
|
159
|
-
if (!hunk || line.startsWith("\\ No newline")) continue;
|
|
160
|
-
if (line.startsWith("+")) {
|
|
161
|
-
addChange("added", line.slice(1), null, hunk.newCursor, hunk.newCursor);
|
|
162
|
-
hunk.newCursor++;
|
|
163
|
-
} else if (line.startsWith("-")) {
|
|
164
|
-
addChange("removed", line.slice(1), hunk.oldCursor, null, hunk.newCursor);
|
|
165
|
-
hunk.oldCursor++;
|
|
166
|
-
} else {
|
|
167
|
-
hunk.oldCursor++;
|
|
168
|
-
hunk.newCursor++;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
finish();
|
|
172
|
-
return {
|
|
173
|
-
files,
|
|
174
|
-
byteLength,
|
|
175
|
-
changedLines: Math.min(changedLines, limits.maxChangedLines),
|
|
176
|
-
truncated,
|
|
177
|
-
oversized,
|
|
178
|
-
limits,
|
|
179
|
-
};
|
|
180
41
|
}
|
|
181
42
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (!start) continue;
|
|
193
|
-
record.symbols.push({
|
|
194
|
-
id: String(node.id),
|
|
195
|
-
label: String(node.label || node.id),
|
|
196
|
-
start,
|
|
197
|
-
end: lineNumber(node.source_end),
|
|
198
|
-
exported: node.exported === true,
|
|
199
|
-
symbolKind: node.symbol_kind || null,
|
|
200
|
-
});
|
|
43
|
+
const validExplicitFiles = (files) => [...new Set((Array.isArray(files) ? files : [])
|
|
44
|
+
.map(normalizeChangePath)
|
|
45
|
+
.filter((file) => file && !file.startsWith('../') && !file.includes('/../') && !file.startsWith('-')))]
|
|
46
|
+
.sort((a, b) => a.localeCompare(b))
|
|
47
|
+
|
|
48
|
+
function collectDiffInput({diffText, repoRoot, base, explicitFiles, limits}) {
|
|
49
|
+
if (typeof diffText === 'string') return {source: 'provided-diff', available: true, text: diffText, reason: '', oversized: false}
|
|
50
|
+
if (repoRoot && base) {
|
|
51
|
+
const result = runGitDiff(repoRoot, base, limits)
|
|
52
|
+
return {source: 'git-diff', available: result.available, text: result.text, reason: result.error || '', oversized: result.oversized === true}
|
|
201
53
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
for (let index = 0; index < record.symbols.length; index += 1) {
|
|
206
|
-
const symbol = record.symbols[index];
|
|
207
|
-
if (!symbol.end || symbol.end < symbol.start) {
|
|
208
|
-
const next = record.symbols[index + 1]?.start;
|
|
209
|
-
symbol.end = next ? Math.max(symbol.start, next - 1) : symbol.start + 400;
|
|
210
|
-
}
|
|
211
|
-
symbol.end = Math.min(symbol.end, symbol.start + 2_000);
|
|
54
|
+
return {
|
|
55
|
+
source: 'files-only', available: false, text: '', oversized: false,
|
|
56
|
+
reason: 'no unified diff was provided and no repoRoot/base pair was available',
|
|
212
57
|
}
|
|
213
|
-
}
|
|
214
|
-
return byFile;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function isMetadataLine(text) {
|
|
218
|
-
const value = String(text || "").trim();
|
|
219
|
-
return !value || /^(?:\/\/|\/\*|\*|\*\/|#(?!include\b)|<!--|-->|"""|''')/.test(value);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
function signatureText(text, symbol) {
|
|
223
|
-
const value = String(text || "").trim();
|
|
224
|
-
const name = bareLabel(symbol.label);
|
|
225
|
-
const hasName = name && new RegExp(`\\b${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "i").test(value);
|
|
226
|
-
return hasName && /\b(?:export|default|declare|abstract|async|function|class|interface|type|enum|const|let|var|def|func|fn|struct|trait|impl|public|private|protected|static)\b/.test(value);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function signaturePosition(change, symbol) {
|
|
230
|
-
const candidates = change.kind === "removed" ? [change.mappedNewLine, change.oldLine] : [change.newLine];
|
|
231
|
-
if (candidates.some((line) => line === symbol.start)) return true;
|
|
232
|
-
const nearStart = candidates.some((line) => Number.isFinite(line) && line > symbol.start && line <= symbol.start + 4);
|
|
233
|
-
if (!nearStart) return false;
|
|
234
|
-
const value = String(change.text || "").trim();
|
|
235
|
-
// Multiline TS/Python/Java/Go parameter/type clauses. Keep statement keywords out so an ordinary
|
|
236
|
-
// first body line is not promoted to a public-contract change.
|
|
237
|
-
if (/^(?:return|throw|yield|if|for|while|switch|match|const|let|var|this\.|self\.|[A-Za-z_$][\w$]*\s*=)/.test(value)) return false;
|
|
238
|
-
return /^(?:[A-Za-z_$][\w$]*\??\s*:\s*[^;]+[,)]?|[A-Za-z_$][\w$<>,.?\[\] :*&]+\s+[A-Za-z_$][\w$]*\s*[,)]|[),:<>{}\[\]|&?]+)$/.test(value);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function moduleSignatureText(text) {
|
|
242
|
-
return /^\s*(?:import\b|export\s+(?:\*|\{)|(?:const|let|var)\s+\w+\s*=\s*require\b|using\b|package\b|#include\b|mod\b|pub\s+use\b)/.test(String(text || ""));
|
|
243
58
|
}
|
|
244
59
|
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return candidates.sort((a, b) => Number(b.mentions) - Number(a.mentions) || a.span - b.span || a.symbol.id.localeCompare(b.symbol.id))[0]?.symbol || null;
|
|
60
|
+
function classificationReasons(counts, {available, unavailableReason, truncated, includeAddedSeeds}) {
|
|
61
|
+
const reasons = []
|
|
62
|
+
if (!available) reasons.push(`Diff unavailable: ${unavailableReason || 'unknown error'}; using conservative file/symbol seeds.`)
|
|
63
|
+
if (truncated) reasons.push('Diff exceeded a safety bound; incomplete evidence is classified HIGH/unknown.')
|
|
64
|
+
if (counts.removed || counts['signature-changed']) reasons.push(`${counts.removed} removed and ${counts['signature-changed']} signature/module-surface file change(s) can break existing callers.`)
|
|
65
|
+
if (counts['body-changed']) reasons.push(`${counts['body-changed']} file(s) contain mapped executable body changes.`)
|
|
66
|
+
if (counts.added && !includeAddedSeeds) reasons.push(`${counts.added} purely additive file change(s) create no dependent seeds by default.`)
|
|
67
|
+
if (counts['metadata-only']) reasons.push(`${counts['metadata-only']} metadata-only file change(s) create no dependent seeds.`)
|
|
68
|
+
if (counts['test-only']) reasons.push(`${counts['test-only']} test-only file change(s) are labelled explicitly and create no product blast-radius seeds.`)
|
|
69
|
+
if (counts.unknown) reasons.push(`${counts.unknown} file change(s) remain unknown and are seeded conservatively.`)
|
|
70
|
+
if (!reasons.length) reasons.push('No changed files were present in the supplied diff.')
|
|
71
|
+
return reasons
|
|
258
72
|
}
|
|
259
73
|
|
|
260
|
-
function strongest(classes) {
|
|
261
|
-
return [...classes].sort((a, b) => (CLASS_RANK[b] ?? 5) - (CLASS_RANK[a] ?? 5) || a.localeCompare(b))[0] || "metadata-only";
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function uniqueSorted(values, limit) {
|
|
265
|
-
const all = [...new Set(values.filter(Boolean).map(String))].sort((a, b) => a.localeCompare(b));
|
|
266
|
-
return { items: all.slice(0, limit), truncated: all.length > limit, total: all.length };
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
function analyzeParsedFile(parsed, indexed, { includeAddedSeeds }) {
|
|
270
|
-
const path = parsed.newPath || parsed.oldPath || "(unknown)";
|
|
271
|
-
const record = indexed.get(parsed.newPath) || indexed.get(parsed.oldPath) || null;
|
|
272
|
-
const grouped = new Map();
|
|
273
|
-
const unmapped = [];
|
|
274
|
-
for (const change of [...parsed.additions, ...parsed.removals]) {
|
|
275
|
-
const symbol = chooseSymbol(record, change);
|
|
276
|
-
if (!symbol) { unmapped.push(change); continue; }
|
|
277
|
-
if (!grouped.has(symbol.id)) grouped.set(symbol.id, { symbol, additions: [], removals: [] });
|
|
278
|
-
grouped.get(symbol.id)[change.kind === "added" ? "additions" : "removals"].push(change);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
const symbols = [];
|
|
282
|
-
const seedIds = [];
|
|
283
|
-
for (const group of [...grouped.values()].sort((a, b) => a.symbol.start - b.symbol.start || a.symbol.id.localeCompare(b.symbol.id))) {
|
|
284
|
-
const addedCode = group.additions.filter((change) => !isMetadataLine(change.text));
|
|
285
|
-
const removedCode = group.removals.filter((change) => !isMetadataLine(change.text));
|
|
286
|
-
const addedDeclaration = addedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol));
|
|
287
|
-
const removedDeclaration = removedCode.some((change) => signaturePosition(change, group.symbol) || signatureText(change.text, group.symbol));
|
|
288
|
-
let classification;
|
|
289
|
-
if (parsed.newFile || (addedDeclaration && !removedCode.length)) classification = "added";
|
|
290
|
-
else if (parsed.deletedFile || (removedDeclaration && !addedCode.length)) classification = "removed";
|
|
291
|
-
else if (addedDeclaration || removedDeclaration) classification = "signature-changed";
|
|
292
|
-
else if (addedCode.length || removedCode.length) classification = "body-changed";
|
|
293
|
-
else classification = "metadata-only";
|
|
294
|
-
|
|
295
|
-
const reasons = {
|
|
296
|
-
added: "new declaration; existing callers cannot depend on it yet",
|
|
297
|
-
removed: "declaration removed; existing callers may break",
|
|
298
|
-
"signature-changed": "declaration/signature line changed",
|
|
299
|
-
"body-changed": "executable lines changed inside the symbol body",
|
|
300
|
-
"metadata-only": "only comment/blank lines changed in this symbol",
|
|
301
|
-
};
|
|
302
|
-
const symbolSeeds = [];
|
|
303
|
-
if (classification === "added") {
|
|
304
|
-
if (includeAddedSeeds) symbolSeeds.push(group.symbol.id);
|
|
305
|
-
} else if (classification !== "metadata-only") {
|
|
306
|
-
symbolSeeds.push(group.symbol.id);
|
|
307
|
-
if ((classification === "removed" || classification === "signature-changed") && group.symbol.exported && record?.fileNodeId) symbolSeeds.push(record.fileNodeId);
|
|
308
|
-
}
|
|
309
|
-
seedIds.push(...symbolSeeds);
|
|
310
|
-
symbols.push({
|
|
311
|
-
id: group.symbol.id,
|
|
312
|
-
label: group.symbol.label,
|
|
313
|
-
start: group.symbol.start,
|
|
314
|
-
end: group.symbol.end,
|
|
315
|
-
exported: group.symbol.exported,
|
|
316
|
-
classification,
|
|
317
|
-
reason: reasons[classification],
|
|
318
|
-
addedLines: group.additions.map((change) => change.newLine).filter(Boolean),
|
|
319
|
-
removedLines: group.removals.map((change) => change.oldLine).filter(Boolean),
|
|
320
|
-
seedIds: [...new Set(symbolSeeds)].sort(),
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const unmappedCode = unmapped.filter((change) => !isMetadataLine(change.text));
|
|
325
|
-
const unmappedMetadata = unmapped.length > 0 && !unmappedCode.length;
|
|
326
|
-
let classification;
|
|
327
|
-
let reason;
|
|
328
|
-
if (parsed.binary) {
|
|
329
|
-
classification = "unknown";
|
|
330
|
-
reason = "binary diff has no line-level evidence";
|
|
331
|
-
} else if (parsed.deletedFile) {
|
|
332
|
-
classification = "removed";
|
|
333
|
-
reason = "file removed";
|
|
334
|
-
} else if (parsed.renamed) {
|
|
335
|
-
classification = "signature-changed";
|
|
336
|
-
reason = "file rename changes module identity";
|
|
337
|
-
} else if (parsed.newFile) {
|
|
338
|
-
classification = "added";
|
|
339
|
-
reason = "new file; no existing dependent can target it yet";
|
|
340
|
-
} else if (unmappedCode.some((change) => moduleSignatureText(change.text))) {
|
|
341
|
-
classification = "signature-changed";
|
|
342
|
-
reason = "module import/export surface changed outside a mapped symbol";
|
|
343
|
-
} else if (unmappedCode.length) {
|
|
344
|
-
classification = "unknown";
|
|
345
|
-
reason = "executable diff lines could not be mapped to a graph symbol";
|
|
346
|
-
} else if (symbols.length) {
|
|
347
|
-
classification = strongest(symbols.map((symbol) => symbol.classification));
|
|
348
|
-
reason = symbols.length === 1 ? symbols[0].reason : `${symbols.length} mapped symbols; strongest change is ${classification}`;
|
|
349
|
-
} else if (unmappedMetadata || parsed.hunks.length || parsed.additions.length || parsed.removals.length) {
|
|
350
|
-
classification = "metadata-only";
|
|
351
|
-
reason = "only comment/blank metadata changed outside symbols";
|
|
352
|
-
} else {
|
|
353
|
-
classification = "metadata-only";
|
|
354
|
-
reason = "file metadata changed without textual hunks";
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
if (parsed.binary || parsed.deletedFile || parsed.renamed || classification === "unknown") {
|
|
358
|
-
if (record?.fileNodeId) seedIds.push(record.fileNodeId);
|
|
359
|
-
if (parsed.binary || parsed.deletedFile || classification === "unknown") seedIds.push(...(record?.symbols || []).map((symbol) => symbol.id));
|
|
360
|
-
} else if (classification === "signature-changed" && !symbols.length && record?.fileNodeId) {
|
|
361
|
-
seedIds.push(record.fileNodeId);
|
|
362
|
-
} else if (classification === "added" && includeAddedSeeds) {
|
|
363
|
-
if (record?.fileNodeId) seedIds.push(record.fileNodeId);
|
|
364
|
-
seedIds.push(...symbols.map((symbol) => symbol.id));
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
return {
|
|
368
|
-
path,
|
|
369
|
-
oldPath: parsed.oldPath,
|
|
370
|
-
newPath: parsed.newPath,
|
|
371
|
-
classification,
|
|
372
|
-
reason,
|
|
373
|
-
binary: parsed.binary,
|
|
374
|
-
renamed: parsed.renamed,
|
|
375
|
-
addedLines: parsed.additions.length,
|
|
376
|
-
removedLines: parsed.removals.length,
|
|
377
|
-
symbols,
|
|
378
|
-
seedIds: [...new Set(seedIds)].sort(),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function unknownFile(path, indexed, reason) {
|
|
383
|
-
const normalized = normalizePath(path);
|
|
384
|
-
const record = indexed.get(normalized);
|
|
385
|
-
return {
|
|
386
|
-
path: normalized,
|
|
387
|
-
oldPath: normalized,
|
|
388
|
-
newPath: normalized,
|
|
389
|
-
classification: "unknown",
|
|
390
|
-
reason,
|
|
391
|
-
binary: false,
|
|
392
|
-
renamed: false,
|
|
393
|
-
addedLines: 0,
|
|
394
|
-
removedLines: 0,
|
|
395
|
-
symbols: [],
|
|
396
|
-
seedIds: [record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id)].filter(Boolean).sort(),
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function classifyTestSurface(file, pathClassifier) {
|
|
401
|
-
const explanation = pathClassifier.explain(file.path);
|
|
402
|
-
if (!hasPathClass(explanation, "test", "e2e")) return file;
|
|
403
|
-
const surface = explanation.classes.includes("e2e") ? "e2e" : "test";
|
|
404
|
-
return {
|
|
405
|
-
...file,
|
|
406
|
-
classification: "test-only",
|
|
407
|
-
changeClassification: file.classification,
|
|
408
|
-
reason: `${surface} path; excluded from the product blast-radius seed set`,
|
|
409
|
-
pathClasses: explanation.classes,
|
|
410
|
-
seedIds: [],
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
function runGitDiff(repoRoot, base, _files, limits) {
|
|
415
|
-
const args = ["-C", repoRoot, "diff", "--no-ext-diff", "--find-renames", "--no-color", "--unified=0", String(base), "--"];
|
|
416
|
-
const result = spawnSync("git", args, {
|
|
417
|
-
encoding: "utf8",
|
|
418
|
-
windowsHide: true,
|
|
419
|
-
timeout: 12_000,
|
|
420
|
-
maxBuffer: limits.maxDiffBytes + 1,
|
|
421
|
-
env: childProcessEnv(),
|
|
422
|
-
});
|
|
423
|
-
if (result.status === 0) return { available: true, text: String(result.stdout || ""), error: null };
|
|
424
|
-
const oversized = result.error?.code === "ENOBUFS" || Buffer.byteLength(String(result.stdout || "")) > limits.maxDiffBytes;
|
|
425
|
-
return { available: false, text: String(result.stdout || ""), oversized, error: oversized ? "git diff exceeded the byte limit" : String(result.stderr || result.error?.message || "git diff unavailable").trim() };
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
const validExplicitFiles = (files) => [...new Set((Array.isArray(files) ? files : [])
|
|
429
|
-
.map(normalizePath)
|
|
430
|
-
.filter((file) => file && !file.startsWith("../") && !file.includes("/../") && !file.startsWith("-")))]
|
|
431
|
-
.sort((a, b) => a.localeCompare(b));
|
|
432
|
-
|
|
433
74
|
export function classifyChangeImpact({
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
base = "",
|
|
437
|
-
diffText,
|
|
438
|
-
files = [],
|
|
439
|
-
includeAddedSeeds = false,
|
|
440
|
-
limits: requestedLimits = {},
|
|
75
|
+
repoRoot = '', graph = {}, base = '', diffText, files = [], includeAddedSeeds = false,
|
|
76
|
+
limits: requestedLimits = {},
|
|
441
77
|
} = {}) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
));
|
|
468
|
-
const represented = new Set(analyzed.flatMap((file) => [file.oldPath, file.newPath].filter(Boolean)));
|
|
469
|
-
for (const file of explicitFiles) {
|
|
470
|
-
if (!represented.has(file)) analyzed.push(classifyTestSurface(
|
|
471
|
-
unknownFile(file, indexed, available ? "explicitly changed file had no textual hunk" : unavailableReason),
|
|
472
|
-
pathClassifier,
|
|
473
|
-
));
|
|
474
|
-
}
|
|
475
|
-
if (!available && !explicitFiles.length) analyzed.push(unknownFile("(diff unavailable)", indexed, unavailableReason));
|
|
476
|
-
analyzed.sort((a, b) => a.path.localeCompare(b.path));
|
|
78
|
+
const limits = changeLimits(requestedLimits)
|
|
79
|
+
const explicitFiles = validExplicitFiles(files)
|
|
80
|
+
const input = collectDiffInput({diffText, repoRoot, base, explicitFiles, limits})
|
|
81
|
+
const indexed = indexChangeGraph(graph, limits)
|
|
82
|
+
const pathClassifier = createPathClassifier(repoRoot)
|
|
83
|
+
const parsed = input.available
|
|
84
|
+
? parseZeroContextDiff(input.text, limits)
|
|
85
|
+
: {files: [], changedLines: 0, byteLength: 0, truncated: input.oversized, oversized: input.oversized, limits}
|
|
86
|
+
const analyzed = parsed.files.map((file) => classifyTestSurface(analyzeParsedFile(file, indexed, {includeAddedSeeds}), pathClassifier))
|
|
87
|
+
const represented = new Set(analyzed.flatMap((file) => [file.oldPath, file.newPath].filter(Boolean)))
|
|
88
|
+
for (const file of explicitFiles) if (!represented.has(file)) analyzed.push(classifyTestSurface(
|
|
89
|
+
unknownChangedFile(file, indexed, input.available ? 'explicitly changed file had no textual hunk' : input.reason),
|
|
90
|
+
pathClassifier,
|
|
91
|
+
))
|
|
92
|
+
if (!input.available && !explicitFiles.length) analyzed.push(unknownChangedFile('(diff unavailable)', indexed, input.reason))
|
|
93
|
+
analyzed.sort((a, b) => a.path.localeCompare(b.path))
|
|
94
|
+
|
|
95
|
+
const incomplete = parsed.truncated || parsed.oversized || input.oversized
|
|
96
|
+
if (incomplete) for (const file of analyzed) {
|
|
97
|
+
if (file.classification === 'test-only') continue
|
|
98
|
+
file.classification = 'unknown'
|
|
99
|
+
file.reason = 'diff was truncated/oversized; symbol-level classification is incomplete'
|
|
100
|
+
const record = indexed.get(file.newPath) || indexed.get(file.oldPath)
|
|
101
|
+
file.seedIds = [...new Set([record?.fileNodeId, ...(record?.symbols || []).map((symbol) => symbol.id), ...file.seedIds].filter(Boolean))].sort()
|
|
102
|
+
}
|
|
477
103
|
|
|
478
|
-
|
|
104
|
+
const seeds = uniqueChangeSeeds(analyzed.flatMap((file) => file.seedIds), limits.maxSeeds)
|
|
105
|
+
let verdict = 'LOW'
|
|
479
106
|
for (const file of analyzed) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
107
|
+
const next = ['removed', 'signature-changed', 'unknown'].includes(file.classification)
|
|
108
|
+
? 'HIGH' : file.classification === 'body-changed' ? 'MEDIUM' : 'LOW'
|
|
109
|
+
if (VERDICT_RANK[next] > VERDICT_RANK[verdict]) verdict = next
|
|
110
|
+
}
|
|
111
|
+
if (!input.available || parsed.truncated || seeds.truncated) verdict = 'HIGH'
|
|
112
|
+
const counts = Object.fromEntries(Object.keys(CHANGE_CLASS_RANK)
|
|
113
|
+
.map((name) => [name, analyzed.filter((file) => file.classification === name).length]))
|
|
114
|
+
return {
|
|
115
|
+
ok: input.available && !parsed.truncated && !seeds.truncated,
|
|
116
|
+
source: input.source,
|
|
117
|
+
verdict,
|
|
118
|
+
reasons: classificationReasons(counts, {
|
|
119
|
+
available: input.available,
|
|
120
|
+
unavailableReason: input.reason,
|
|
121
|
+
truncated: incomplete,
|
|
122
|
+
includeAddedSeeds,
|
|
123
|
+
}),
|
|
124
|
+
seedIds: seeds.items,
|
|
125
|
+
files: analyzed,
|
|
126
|
+
summary: {
|
|
127
|
+
files: analyzed.length,
|
|
128
|
+
symbols: analyzed.reduce((sum, file) => sum + file.symbols.length, 0),
|
|
129
|
+
counts,
|
|
130
|
+
seeds: seeds.items.length,
|
|
131
|
+
totalSeedsBeforeCap: seeds.total,
|
|
132
|
+
},
|
|
133
|
+
bounds: {...limits, diffBytes: parsed.byteLength, changedLines: parsed.changedLines, truncated: !!parsed.truncated || seeds.truncated},
|
|
485
134
|
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const rawSeeds = analyzed.flatMap((file) => file.seedIds);
|
|
489
|
-
const seeds = uniqueSorted(rawSeeds, limits.maxSeeds);
|
|
490
|
-
let verdict = "LOW";
|
|
491
|
-
for (const file of analyzed) {
|
|
492
|
-
const next = ["removed", "signature-changed", "unknown"].includes(file.classification)
|
|
493
|
-
? "HIGH"
|
|
494
|
-
: file.classification === "body-changed" ? "MEDIUM" : "LOW";
|
|
495
|
-
if (VERDICT_RANK[next] > VERDICT_RANK[verdict]) verdict = next;
|
|
496
|
-
}
|
|
497
|
-
if (!available || parsed.truncated || seeds.truncated) verdict = "HIGH";
|
|
498
|
-
|
|
499
|
-
const counts = Object.fromEntries(Object.keys(CLASS_RANK).map((name) => [name, analyzed.filter((file) => file.classification === name).length]));
|
|
500
|
-
const reasons = [];
|
|
501
|
-
if (!available) reasons.push(`Diff unavailable: ${unavailableReason || "unknown error"}; using conservative file/symbol seeds.`);
|
|
502
|
-
if (parsed.truncated || parsed.oversized || gitOversized) reasons.push("Diff exceeded a safety bound; incomplete evidence is classified HIGH/unknown.");
|
|
503
|
-
if (counts.removed || counts["signature-changed"]) reasons.push(`${counts.removed} removed and ${counts["signature-changed"]} signature/module-surface file change(s) can break existing callers.`);
|
|
504
|
-
if (counts["body-changed"]) reasons.push(`${counts["body-changed"]} file(s) contain mapped executable body changes.`);
|
|
505
|
-
if (counts.added && !includeAddedSeeds) reasons.push(`${counts.added} purely additive file change(s) create no dependent seeds by default.`);
|
|
506
|
-
if (counts["metadata-only"]) reasons.push(`${counts["metadata-only"]} metadata-only file change(s) create no dependent seeds.`);
|
|
507
|
-
if (counts["test-only"]) reasons.push(`${counts["test-only"]} test-only file change(s) are labelled explicitly and create no product blast-radius seeds.`);
|
|
508
|
-
if (counts.unknown) reasons.push(`${counts.unknown} file change(s) remain unknown and are seeded conservatively.`);
|
|
509
|
-
if (!reasons.length) reasons.push("No changed files were present in the supplied diff.");
|
|
510
|
-
|
|
511
|
-
return {
|
|
512
|
-
ok: available && !parsed.truncated && !seeds.truncated,
|
|
513
|
-
source,
|
|
514
|
-
verdict,
|
|
515
|
-
reasons,
|
|
516
|
-
seedIds: seeds.items,
|
|
517
|
-
files: analyzed,
|
|
518
|
-
summary: {
|
|
519
|
-
files: analyzed.length,
|
|
520
|
-
symbols: analyzed.reduce((sum, file) => sum + file.symbols.length, 0),
|
|
521
|
-
counts,
|
|
522
|
-
seeds: seeds.items.length,
|
|
523
|
-
totalSeedsBeforeCap: seeds.total,
|
|
524
|
-
},
|
|
525
|
-
bounds: {
|
|
526
|
-
...limits,
|
|
527
|
-
diffBytes: parsed.byteLength,
|
|
528
|
-
changedLines: parsed.changedLines,
|
|
529
|
-
truncated: !!parsed.truncated || seeds.truncated,
|
|
530
|
-
},
|
|
531
|
-
};
|
|
532
135
|
}
|
|
533
|
-
|
|
534
|
-
export { DEFAULT_LIMITS as CHANGE_CLASSIFICATION_LIMITS };
|