@neurcode-ai/cli 0.9.44 → 0.9.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/contract.js +47 -0
- package/dist/commands/plan.js +40 -0
- package/dist/commands/verify.d.ts +2 -0
- package/dist/commands/verify.js +240 -114
- package/dist/index.js +41 -5
- package/dist/utils/advisory-signals.d.ts +20 -0
- package/dist/utils/advisory-signals.js +177 -0
- package/dist/utils/change-contract.d.ts +105 -1
- package/dist/utils/change-contract.js +685 -12
- package/dist/utils/diff-symbols.d.ts +10 -0
- package/dist/utils/diff-symbols.js +218 -0
- package/dist/utils/plan-symbols.d.ts +17 -0
- package/dist/utils/plan-symbols.js +209 -0
- package/package.json +6 -14
- package/LICENSE +0 -201
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js.map +0 -1
- package/dist/commands/allow.d.ts.map +0 -1
- package/dist/commands/allow.js.map +0 -1
- package/dist/commands/apply.d.ts.map +0 -1
- package/dist/commands/apply.js.map +0 -1
- package/dist/commands/approve.d.ts.map +0 -1
- package/dist/commands/approve.js.map +0 -1
- package/dist/commands/ask.d.ts.map +0 -1
- package/dist/commands/ask.js.map +0 -1
- package/dist/commands/audit.d.ts.map +0 -1
- package/dist/commands/audit.js.map +0 -1
- package/dist/commands/bootstrap.d.ts.map +0 -1
- package/dist/commands/bootstrap.js.map +0 -1
- package/dist/commands/brain.d.ts.map +0 -1
- package/dist/commands/brain.js.map +0 -1
- package/dist/commands/check.d.ts.map +0 -1
- package/dist/commands/check.js.map +0 -1
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/contract.d.ts.map +0 -1
- package/dist/commands/contract.js.map +0 -1
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/feedback.d.ts.map +0 -1
- package/dist/commands/feedback.js.map +0 -1
- package/dist/commands/guard.d.ts.map +0 -1
- package/dist/commands/guard.js.map +0 -1
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/map.d.ts.map +0 -1
- package/dist/commands/map.js.map +0 -1
- package/dist/commands/plan-slo.d.ts.map +0 -1
- package/dist/commands/plan-slo.js.map +0 -1
- package/dist/commands/plan.d.ts.map +0 -1
- package/dist/commands/plan.js.map +0 -1
- package/dist/commands/policy.d.ts.map +0 -1
- package/dist/commands/policy.js.map +0 -1
- package/dist/commands/prompt.d.ts.map +0 -1
- package/dist/commands/prompt.js.map +0 -1
- package/dist/commands/refactor.d.ts.map +0 -1
- package/dist/commands/refactor.js.map +0 -1
- package/dist/commands/remediate.d.ts.map +0 -1
- package/dist/commands/remediate.js.map +0 -1
- package/dist/commands/repo.d.ts.map +0 -1
- package/dist/commands/repo.js.map +0 -1
- package/dist/commands/revert.d.ts.map +0 -1
- package/dist/commands/revert.js.map +0 -1
- package/dist/commands/security.d.ts.map +0 -1
- package/dist/commands/security.js.map +0 -1
- package/dist/commands/session.d.ts.map +0 -1
- package/dist/commands/session.js.map +0 -1
- package/dist/commands/ship.d.ts.map +0 -1
- package/dist/commands/ship.js.map +0 -1
- package/dist/commands/simulate.d.ts.map +0 -1
- package/dist/commands/simulate.js.map +0 -1
- package/dist/commands/verify.d.ts.map +0 -1
- package/dist/commands/verify.js.map +0 -1
- package/dist/commands/watch.d.ts.map +0 -1
- package/dist/commands/watch.js.map +0 -1
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js.map +0 -1
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/rules.d.ts.map +0 -1
- package/dist/rules.js.map +0 -1
- package/dist/services/integrations/TicketService.d.ts.map +0 -1
- package/dist/services/integrations/TicketService.js.map +0 -1
- package/dist/services/mapper/ProjectScanner.d.ts.map +0 -1
- package/dist/services/mapper/ProjectScanner.js.map +0 -1
- package/dist/services/project-knowledge-service.d.ts.map +0 -1
- package/dist/services/project-knowledge-service.js.map +0 -1
- package/dist/services/security/SecurityGuard.d.ts.map +0 -1
- package/dist/services/security/SecurityGuard.js.map +0 -1
- package/dist/services/toolbox-service.d.ts.map +0 -1
- package/dist/services/toolbox-service.js.map +0 -1
- package/dist/services/watch/BlobStore.d.ts.map +0 -1
- package/dist/services/watch/BlobStore.js.map +0 -1
- package/dist/services/watch/CommandPoller.d.ts.map +0 -1
- package/dist/services/watch/CommandPoller.js.map +0 -1
- package/dist/services/watch/Journal.d.ts.map +0 -1
- package/dist/services/watch/Journal.js.map +0 -1
- package/dist/services/watch/Sentinel.d.ts.map +0 -1
- package/dist/services/watch/Sentinel.js.map +0 -1
- package/dist/services/watch/Syncer.d.ts.map +0 -1
- package/dist/services/watch/Syncer.js.map +0 -1
- package/dist/utils/ROILogger.d.ts.map +0 -1
- package/dist/utils/ROILogger.js.map +0 -1
- package/dist/utils/RelevanceScorer.d.ts.map +0 -1
- package/dist/utils/RelevanceScorer.js.map +0 -1
- package/dist/utils/ai-debt-budget.d.ts.map +0 -1
- package/dist/utils/ai-debt-budget.js.map +0 -1
- package/dist/utils/artifact-signature.d.ts.map +0 -1
- package/dist/utils/artifact-signature.js.map +0 -1
- package/dist/utils/ask-cache.d.ts.map +0 -1
- package/dist/utils/ask-cache.js.map +0 -1
- package/dist/utils/box.d.ts.map +0 -1
- package/dist/utils/box.js.map +0 -1
- package/dist/utils/brain-context.d.ts.map +0 -1
- package/dist/utils/brain-context.js.map +0 -1
- package/dist/utils/breakage-simulator.d.ts.map +0 -1
- package/dist/utils/breakage-simulator.js.map +0 -1
- package/dist/utils/change-contract.d.ts.map +0 -1
- package/dist/utils/change-contract.js.map +0 -1
- package/dist/utils/cli-json.d.ts.map +0 -1
- package/dist/utils/cli-json.js.map +0 -1
- package/dist/utils/custom-policy-rules.d.ts.map +0 -1
- package/dist/utils/custom-policy-rules.js.map +0 -1
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/gitignore.d.ts.map +0 -1
- package/dist/utils/gitignore.js.map +0 -1
- package/dist/utils/governance.d.ts.map +0 -1
- package/dist/utils/governance.js.map +0 -1
- package/dist/utils/ignore.d.ts.map +0 -1
- package/dist/utils/ignore.js.map +0 -1
- package/dist/utils/manual-approvals.d.ts.map +0 -1
- package/dist/utils/manual-approvals.js.map +0 -1
- package/dist/utils/messages.d.ts.map +0 -1
- package/dist/utils/messages.js.map +0 -1
- package/dist/utils/neurcode-context.d.ts.map +0 -1
- package/dist/utils/neurcode-context.js.map +0 -1
- package/dist/utils/plan-cache.d.ts.map +0 -1
- package/dist/utils/plan-cache.js.map +0 -1
- package/dist/utils/plan-slo.d.ts.map +0 -1
- package/dist/utils/plan-slo.js.map +0 -1
- package/dist/utils/policy-audit.d.ts.map +0 -1
- package/dist/utils/policy-audit.js.map +0 -1
- package/dist/utils/policy-compiler.d.ts.map +0 -1
- package/dist/utils/policy-compiler.js.map +0 -1
- package/dist/utils/policy-exceptions.d.ts.map +0 -1
- package/dist/utils/policy-exceptions.js.map +0 -1
- package/dist/utils/policy-governance.d.ts.map +0 -1
- package/dist/utils/policy-governance.js.map +0 -1
- package/dist/utils/policy-packs.d.ts.map +0 -1
- package/dist/utils/policy-packs.js.map +0 -1
- package/dist/utils/project-detector.d.ts.map +0 -1
- package/dist/utils/project-detector.js.map +0 -1
- package/dist/utils/project-root.d.ts.map +0 -1
- package/dist/utils/project-root.js.map +0 -1
- package/dist/utils/repo-links.d.ts.map +0 -1
- package/dist/utils/repo-links.js.map +0 -1
- package/dist/utils/restore.d.ts.map +0 -1
- package/dist/utils/restore.js.map +0 -1
- package/dist/utils/runtime-guard.d.ts.map +0 -1
- package/dist/utils/runtime-guard.js.map +0 -1
- package/dist/utils/scope-telemetry.d.ts.map +0 -1
- package/dist/utils/scope-telemetry.js.map +0 -1
- package/dist/utils/secret-masking.d.ts.map +0 -1
- package/dist/utils/secret-masking.js.map +0 -1
- package/dist/utils/state.d.ts.map +0 -1
- package/dist/utils/state.js.map +0 -1
- package/dist/utils/tier.d.ts.map +0 -1
- package/dist/utils/tier.js.map +0 -1
- package/dist/utils/user-context.d.ts.map +0 -1
- package/dist/utils/user-context.js.map +0 -1
|
@@ -5,6 +5,7 @@ exports.resolveChangeContractPath = resolveChangeContractPath;
|
|
|
5
5
|
exports.writeChangeContract = writeChangeContract;
|
|
6
6
|
exports.readChangeContract = readChangeContract;
|
|
7
7
|
exports.evaluateChangeContract = evaluateChangeContract;
|
|
8
|
+
exports.groupChangeContractViolations = groupChangeContractViolations;
|
|
8
9
|
const crypto_1 = require("crypto");
|
|
9
10
|
const fs_1 = require("fs");
|
|
10
11
|
const path_1 = require("path");
|
|
@@ -27,6 +28,274 @@ function sha256Hex(input) {
|
|
|
27
28
|
function fingerprintFiles(expectedFiles) {
|
|
28
29
|
return sha256Hex(JSON.stringify(uniqueSorted(expectedFiles)));
|
|
29
30
|
}
|
|
31
|
+
function normalizePlanAction(value) {
|
|
32
|
+
if (value === 'CREATE' || value === 'MODIFY' || value === 'BLOCK') {
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function normalizeSymbolAction(value) {
|
|
38
|
+
if (value === 'CREATE' || value === 'MODIFY' || value === 'BLOCK') {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function normalizeSymbolType(value) {
|
|
44
|
+
if (!value)
|
|
45
|
+
return null;
|
|
46
|
+
const normalized = String(value).trim().toLowerCase();
|
|
47
|
+
if (normalized === 'function'
|
|
48
|
+
|| normalized === 'class'
|
|
49
|
+
|| normalized === 'interface'
|
|
50
|
+
|| normalized === 'type'
|
|
51
|
+
|| normalized === 'method'
|
|
52
|
+
|| normalized === 'const'
|
|
53
|
+
|| normalized === 'unknown') {
|
|
54
|
+
return normalized;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
function normalizeDiffSymbolAction(value) {
|
|
59
|
+
if (value === 'add' || value === 'delete' || value === 'modify') {
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
function parseNonNegativeInteger(value) {
|
|
65
|
+
if (value === undefined || value === null || value === '')
|
|
66
|
+
return undefined;
|
|
67
|
+
const parsed = Number(value);
|
|
68
|
+
if (!Number.isFinite(parsed))
|
|
69
|
+
return undefined;
|
|
70
|
+
const rounded = Math.floor(parsed);
|
|
71
|
+
if (rounded < 0)
|
|
72
|
+
return undefined;
|
|
73
|
+
return rounded;
|
|
74
|
+
}
|
|
75
|
+
function sanitizePlanFiles(planFiles) {
|
|
76
|
+
if (!Array.isArray(planFiles))
|
|
77
|
+
return [];
|
|
78
|
+
const deduped = new Map();
|
|
79
|
+
for (const item of planFiles) {
|
|
80
|
+
if (!item || typeof item !== 'object')
|
|
81
|
+
continue;
|
|
82
|
+
const path = normalizeRepoPath(item.path || '');
|
|
83
|
+
const action = normalizePlanAction(item.action);
|
|
84
|
+
if (!path || !action)
|
|
85
|
+
continue;
|
|
86
|
+
const existing = deduped.get(path);
|
|
87
|
+
if (!existing) {
|
|
88
|
+
deduped.set(path, {
|
|
89
|
+
path,
|
|
90
|
+
action,
|
|
91
|
+
...(item.reason ? { reason: String(item.reason).trim().slice(0, 240) } : {}),
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
// Keep the stricter action when duplicate file entries appear.
|
|
96
|
+
if (existing.action !== 'BLOCK' && action === 'BLOCK') {
|
|
97
|
+
deduped.set(path, {
|
|
98
|
+
path,
|
|
99
|
+
action,
|
|
100
|
+
...(item.reason ? { reason: String(item.reason).trim().slice(0, 240) } : {}),
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (existing.action === 'MODIFY' && action === 'CREATE') {
|
|
105
|
+
deduped.set(path, {
|
|
106
|
+
path,
|
|
107
|
+
action,
|
|
108
|
+
...(item.reason ? { reason: String(item.reason).trim().slice(0, 240) } : {}),
|
|
109
|
+
});
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!existing.reason && item.reason) {
|
|
113
|
+
deduped.set(path, {
|
|
114
|
+
...existing,
|
|
115
|
+
reason: String(item.reason).trim().slice(0, 240),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return [...deduped.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
120
|
+
}
|
|
121
|
+
function sanitizeExpectedSymbols(expectedSymbols) {
|
|
122
|
+
if (!Array.isArray(expectedSymbols))
|
|
123
|
+
return [];
|
|
124
|
+
const actionPrecedence = {
|
|
125
|
+
MODIFY: 1,
|
|
126
|
+
CREATE: 2,
|
|
127
|
+
BLOCK: 3,
|
|
128
|
+
};
|
|
129
|
+
const deduped = new Map();
|
|
130
|
+
for (const item of expectedSymbols) {
|
|
131
|
+
if (!item || typeof item !== 'object')
|
|
132
|
+
continue;
|
|
133
|
+
const name = String(item.name || '').trim().replace(/^['"`]+|['"`]+$/g, '').replace(/\(\)\s*$/, '');
|
|
134
|
+
const action = normalizeSymbolAction(item.action);
|
|
135
|
+
const normalizedType = normalizeSymbolType(item.type || undefined);
|
|
136
|
+
const file = typeof item.file === 'string' && item.file.trim()
|
|
137
|
+
? normalizeRepoPath(item.file)
|
|
138
|
+
: undefined;
|
|
139
|
+
if (!name || !action)
|
|
140
|
+
continue;
|
|
141
|
+
const key = `${file || '*'}::${normalizedType || 'unknown'}::${name}`;
|
|
142
|
+
const reason = item.reason ? String(item.reason).trim().slice(0, 240) : undefined;
|
|
143
|
+
const nextEntry = {
|
|
144
|
+
name,
|
|
145
|
+
action,
|
|
146
|
+
...(normalizedType ? { type: normalizedType } : {}),
|
|
147
|
+
...(file ? { file } : {}),
|
|
148
|
+
...(reason ? { reason } : {}),
|
|
149
|
+
};
|
|
150
|
+
const existing = deduped.get(key);
|
|
151
|
+
if (!existing) {
|
|
152
|
+
deduped.set(key, nextEntry);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (actionPrecedence[nextEntry.action] > actionPrecedence[existing.action]) {
|
|
156
|
+
deduped.set(key, nextEntry);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!existing.reason && nextEntry.reason) {
|
|
160
|
+
deduped.set(key, {
|
|
161
|
+
...existing,
|
|
162
|
+
reason: nextEntry.reason,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return [...deduped.values()].sort((a, b) => {
|
|
167
|
+
const fileA = a.file || '';
|
|
168
|
+
const fileB = b.file || '';
|
|
169
|
+
if (fileA !== fileB)
|
|
170
|
+
return fileA.localeCompare(fileB);
|
|
171
|
+
if (a.name !== b.name)
|
|
172
|
+
return a.name.localeCompare(b.name);
|
|
173
|
+
return (a.type || '').localeCompare(b.type || '');
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
function sanitizeChangedSymbols(changedSymbols) {
|
|
177
|
+
if (!Array.isArray(changedSymbols))
|
|
178
|
+
return [];
|
|
179
|
+
const deduped = new Map();
|
|
180
|
+
for (const item of changedSymbols) {
|
|
181
|
+
if (!item || typeof item !== 'object')
|
|
182
|
+
continue;
|
|
183
|
+
const name = String(item.name || '').trim().replace(/^['"`]+|['"`]+$/g, '').replace(/\(\)\s*$/, '');
|
|
184
|
+
const action = normalizeDiffSymbolAction(item.action);
|
|
185
|
+
const type = normalizeSymbolType(item.type || undefined) || 'unknown';
|
|
186
|
+
const file = typeof item.file === 'string' && item.file.trim()
|
|
187
|
+
? normalizeRepoPath(item.file)
|
|
188
|
+
: null;
|
|
189
|
+
if (!name || !action)
|
|
190
|
+
continue;
|
|
191
|
+
const key = `${file || '*'}::${type}::${name}`;
|
|
192
|
+
const existing = deduped.get(key);
|
|
193
|
+
if (!existing) {
|
|
194
|
+
deduped.set(key, { name, action, type, file });
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (existing.action !== 'modify' && action === 'modify') {
|
|
198
|
+
deduped.set(key, { name, action, type, file });
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (existing.action === 'delete' && action === 'add') {
|
|
202
|
+
deduped.set(key, { name, action, type, file });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return [...deduped.values()].sort((a, b) => {
|
|
206
|
+
const fileA = a.file || '';
|
|
207
|
+
const fileB = b.file || '';
|
|
208
|
+
if (fileA !== fileB)
|
|
209
|
+
return fileA.localeCompare(fileB);
|
|
210
|
+
if (a.name !== b.name)
|
|
211
|
+
return a.name.localeCompare(b.name);
|
|
212
|
+
return a.type.localeCompare(b.type);
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function sanitizeContractOptions(options) {
|
|
216
|
+
if (!options || typeof options !== 'object')
|
|
217
|
+
return undefined;
|
|
218
|
+
const next = {};
|
|
219
|
+
if (typeof options.enforceExpectedFiles === 'boolean') {
|
|
220
|
+
next.enforceExpectedFiles = options.enforceExpectedFiles;
|
|
221
|
+
}
|
|
222
|
+
if (typeof options.enforceActionMatching === 'boolean') {
|
|
223
|
+
next.enforceActionMatching = options.enforceActionMatching;
|
|
224
|
+
}
|
|
225
|
+
if (typeof options.allowRenameForModify === 'boolean') {
|
|
226
|
+
next.allowRenameForModify = options.allowRenameForModify;
|
|
227
|
+
}
|
|
228
|
+
if (typeof options.enforceExpectedSymbols === 'boolean') {
|
|
229
|
+
next.enforceExpectedSymbols = options.enforceExpectedSymbols;
|
|
230
|
+
}
|
|
231
|
+
if (typeof options.enforceSymbolActionMatching === 'boolean') {
|
|
232
|
+
next.enforceSymbolActionMatching = options.enforceSymbolActionMatching;
|
|
233
|
+
}
|
|
234
|
+
if (typeof options.symbolTypeRelaxedMatching === 'boolean') {
|
|
235
|
+
next.symbolTypeRelaxedMatching = options.symbolTypeRelaxedMatching;
|
|
236
|
+
}
|
|
237
|
+
if (typeof options.symbolFileBasenameFallback === 'boolean') {
|
|
238
|
+
next.symbolFileBasenameFallback = options.symbolFileBasenameFallback;
|
|
239
|
+
}
|
|
240
|
+
const maxUnexpectedFiles = parseNonNegativeInteger(options.maxUnexpectedFiles);
|
|
241
|
+
if (maxUnexpectedFiles !== undefined) {
|
|
242
|
+
next.maxUnexpectedFiles = maxUnexpectedFiles;
|
|
243
|
+
}
|
|
244
|
+
const maxMissingExpectedSymbols = parseNonNegativeInteger(options.maxMissingExpectedSymbols);
|
|
245
|
+
if (maxMissingExpectedSymbols !== undefined) {
|
|
246
|
+
next.maxMissingExpectedSymbols = maxMissingExpectedSymbols;
|
|
247
|
+
}
|
|
248
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
249
|
+
}
|
|
250
|
+
function isContractOptions(value) {
|
|
251
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
252
|
+
return false;
|
|
253
|
+
const record = value;
|
|
254
|
+
const booleanOrUndefined = (key) => record[key] === undefined || typeof record[key] === 'boolean';
|
|
255
|
+
const nonNegativeIntegerOrUndefined = (key) => record[key] === undefined || parseNonNegativeInteger(record[key]) !== undefined;
|
|
256
|
+
return (booleanOrUndefined('enforceExpectedFiles')
|
|
257
|
+
&& booleanOrUndefined('enforceActionMatching')
|
|
258
|
+
&& booleanOrUndefined('allowRenameForModify')
|
|
259
|
+
&& booleanOrUndefined('enforceExpectedSymbols')
|
|
260
|
+
&& booleanOrUndefined('enforceSymbolActionMatching')
|
|
261
|
+
&& booleanOrUndefined('symbolTypeRelaxedMatching')
|
|
262
|
+
&& booleanOrUndefined('symbolFileBasenameFallback')
|
|
263
|
+
&& nonNegativeIntegerOrUndefined('maxUnexpectedFiles')
|
|
264
|
+
&& nonNegativeIntegerOrUndefined('maxMissingExpectedSymbols'));
|
|
265
|
+
}
|
|
266
|
+
function isPlanFileEntry(value) {
|
|
267
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
268
|
+
return false;
|
|
269
|
+
const record = value;
|
|
270
|
+
if (typeof record.path !== 'string')
|
|
271
|
+
return false;
|
|
272
|
+
const action = typeof record.action === 'string' ? normalizePlanAction(record.action) : null;
|
|
273
|
+
if (!action)
|
|
274
|
+
return false;
|
|
275
|
+
if (record.reason !== undefined && typeof record.reason !== 'string')
|
|
276
|
+
return false;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
function isExpectedSymbolEntry(value) {
|
|
280
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
281
|
+
return false;
|
|
282
|
+
const record = value;
|
|
283
|
+
if (typeof record.name !== 'string')
|
|
284
|
+
return false;
|
|
285
|
+
const action = typeof record.action === 'string' ? normalizeSymbolAction(record.action) : null;
|
|
286
|
+
if (!action)
|
|
287
|
+
return false;
|
|
288
|
+
if (record.type !== undefined) {
|
|
289
|
+
const normalizedType = typeof record.type === 'string' ? normalizeSymbolType(record.type) : null;
|
|
290
|
+
if (!normalizedType)
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
if (record.file !== undefined && typeof record.file !== 'string')
|
|
294
|
+
return false;
|
|
295
|
+
if (record.reason !== undefined && typeof record.reason !== 'string')
|
|
296
|
+
return false;
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
30
299
|
function isChangeContract(value) {
|
|
31
300
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
32
301
|
return false;
|
|
@@ -41,26 +310,47 @@ function isChangeContract(value) {
|
|
|
41
310
|
if (!basicShape) {
|
|
42
311
|
return false;
|
|
43
312
|
}
|
|
44
|
-
if (record.signature
|
|
45
|
-
|
|
313
|
+
if (record.signature !== undefined) {
|
|
314
|
+
if (!record.signature || typeof record.signature !== 'object' || Array.isArray(record.signature)) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const signature = record.signature;
|
|
318
|
+
if (signature.algorithm !== 'hmac-sha256' ||
|
|
319
|
+
typeof signature.signedAt !== 'string' ||
|
|
320
|
+
typeof signature.payloadHash !== 'string' ||
|
|
321
|
+
typeof signature.value !== 'string') {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
if (signature.keyId !== null && signature.keyId !== undefined && typeof signature.keyId !== 'string') {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
46
327
|
}
|
|
47
|
-
if (
|
|
48
|
-
|
|
328
|
+
if (record.planFiles !== undefined) {
|
|
329
|
+
if (!Array.isArray(record.planFiles)) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
if (!record.planFiles.every((entry) => isPlanFileEntry(entry))) {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
49
335
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
336
|
+
if (record.expectedSymbols !== undefined) {
|
|
337
|
+
if (!Array.isArray(record.expectedSymbols)) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
if (!record.expectedSymbols.every((entry) => isExpectedSymbolEntry(entry))) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
56
343
|
}
|
|
57
|
-
if (
|
|
344
|
+
if (record.options !== undefined && !isContractOptions(record.options)) {
|
|
58
345
|
return false;
|
|
59
346
|
}
|
|
60
347
|
return true;
|
|
61
348
|
}
|
|
62
349
|
function createChangeContract(input) {
|
|
63
350
|
const expectedFiles = uniqueSorted(input.expectedFiles);
|
|
351
|
+
const planFiles = sanitizePlanFiles(input.planFiles);
|
|
352
|
+
const expectedSymbols = sanitizeExpectedSymbols(input.expectedSymbols);
|
|
353
|
+
const options = sanitizeContractOptions(input.options);
|
|
64
354
|
const generatedAt = input.generatedAt || new Date().toISOString();
|
|
65
355
|
const intentHash = sha256Hex(input.intent || '');
|
|
66
356
|
const expectedFilesFingerprint = fingerprintFiles(expectedFiles);
|
|
@@ -71,6 +361,9 @@ function createChangeContract(input) {
|
|
|
71
361
|
projectId: input.projectId || null,
|
|
72
362
|
intentHash,
|
|
73
363
|
expectedFilesFingerprint,
|
|
364
|
+
...(planFiles.length > 0 ? { planFiles } : {}),
|
|
365
|
+
...(expectedSymbols.length > 0 ? { expectedSymbols } : {}),
|
|
366
|
+
...(options ? { options } : {}),
|
|
74
367
|
policyLockFingerprint: input.policyLockFingerprint || null,
|
|
75
368
|
compiledPolicyFingerprint: input.compiledPolicyFingerprint || null,
|
|
76
369
|
}));
|
|
@@ -84,6 +377,9 @@ function createChangeContract(input) {
|
|
|
84
377
|
intentHash,
|
|
85
378
|
expectedFiles,
|
|
86
379
|
expectedFilesFingerprint,
|
|
380
|
+
...(planFiles.length > 0 ? { planFiles } : {}),
|
|
381
|
+
...(expectedSymbols.length > 0 ? { expectedSymbols } : {}),
|
|
382
|
+
...(options ? { options } : {}),
|
|
87
383
|
policyLockFingerprint: input.policyLockFingerprint || null,
|
|
88
384
|
compiledPolicyFingerprint: input.compiledPolicyFingerprint || null,
|
|
89
385
|
};
|
|
@@ -123,8 +419,87 @@ function readChangeContract(projectRoot, inputPath) {
|
|
|
123
419
|
};
|
|
124
420
|
}
|
|
125
421
|
}
|
|
422
|
+
function toFileBasename(pathValue) {
|
|
423
|
+
if (!pathValue)
|
|
424
|
+
return null;
|
|
425
|
+
const normalized = normalizeRepoPath(pathValue);
|
|
426
|
+
if (!normalized)
|
|
427
|
+
return null;
|
|
428
|
+
const lastSlash = normalized.lastIndexOf('/');
|
|
429
|
+
return lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
|
|
430
|
+
}
|
|
431
|
+
function areSymbolTypesCompatible(expectedType, actualType, relaxedMatching) {
|
|
432
|
+
if (!expectedType || expectedType === 'unknown' || actualType === 'unknown') {
|
|
433
|
+
return true;
|
|
434
|
+
}
|
|
435
|
+
if (expectedType === actualType) {
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
if (!relaxedMatching) {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
if (expectedType === 'function') {
|
|
442
|
+
return actualType === 'method' || actualType === 'const';
|
|
443
|
+
}
|
|
444
|
+
if (expectedType === 'method') {
|
|
445
|
+
return actualType === 'function';
|
|
446
|
+
}
|
|
447
|
+
if (expectedType === 'const') {
|
|
448
|
+
return actualType === 'function' || actualType === 'method';
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
}
|
|
452
|
+
function doesSymbolFileMatch(expectedFile, actualFile, allowSymbolFileBasenameFallback) {
|
|
453
|
+
if (!expectedFile) {
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
if (actualFile && actualFile === expectedFile) {
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
if (!allowSymbolFileBasenameFallback) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
const expectedBasename = toFileBasename(expectedFile);
|
|
463
|
+
const actualBasename = toFileBasename(actualFile);
|
|
464
|
+
if (!expectedBasename || !actualBasename || expectedBasename !== actualBasename) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
function hasLikelyRenameForExpectedSymbol(expectedSymbol, changedSymbols, options) {
|
|
470
|
+
if (expectedSymbol.action !== 'MODIFY')
|
|
471
|
+
return false;
|
|
472
|
+
const deletedMatches = changedSymbols.filter((actualSymbol) => (actualSymbol.action === 'delete'
|
|
473
|
+
&& actualSymbol.name === expectedSymbol.name
|
|
474
|
+
&& areSymbolTypesCompatible(expectedSymbol.type, actualSymbol.type, options.relaxedSymbolTypeMatching)
|
|
475
|
+
&& doesSymbolFileMatch(expectedSymbol.file, actualSymbol.file, options.allowSymbolFileBasenameFallback)));
|
|
476
|
+
if (deletedMatches.length === 0) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
for (const deletedSymbol of deletedMatches) {
|
|
480
|
+
const replacementExists = changedSymbols.some((actualSymbol) => {
|
|
481
|
+
if (actualSymbol.action !== 'add' && actualSymbol.action !== 'modify')
|
|
482
|
+
return false;
|
|
483
|
+
if (actualSymbol.name === expectedSymbol.name)
|
|
484
|
+
return false;
|
|
485
|
+
if (!areSymbolTypesCompatible(expectedSymbol.type, actualSymbol.type, options.relaxedSymbolTypeMatching)) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
// When file is known, prefer a same-file replacement for rename-like edits.
|
|
489
|
+
if (deletedSymbol.file && actualSymbol.file && deletedSymbol.file === actualSymbol.file) {
|
|
490
|
+
return true;
|
|
491
|
+
}
|
|
492
|
+
// Fallback to expected file scope matching.
|
|
493
|
+
return doesSymbolFileMatch(expectedSymbol.file, actualSymbol.file, options.allowSymbolFileBasenameFallback);
|
|
494
|
+
});
|
|
495
|
+
if (replacementExists) {
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
126
501
|
function evaluateChangeContract(contract, input) {
|
|
127
|
-
|
|
502
|
+
let violations = [];
|
|
128
503
|
if (contract.planId !== input.planId) {
|
|
129
504
|
violations.push({
|
|
130
505
|
code: 'CHANGE_CONTRACT_PLAN_MISMATCH',
|
|
@@ -155,7 +530,36 @@ function evaluateChangeContract(contract, input) {
|
|
|
155
530
|
}
|
|
156
531
|
const expectedSet = new Set(uniqueSorted(contract.expectedFiles));
|
|
157
532
|
const normalizedChanged = uniqueSorted(input.changedFiles);
|
|
533
|
+
const changedSet = new Set(normalizedChanged);
|
|
534
|
+
const normalizedChangedEntries = (input.changedFileEntries || [])
|
|
535
|
+
.map((entry) => ({
|
|
536
|
+
path: normalizeRepoPath(entry.path || ''),
|
|
537
|
+
changeType: entry.changeType,
|
|
538
|
+
}))
|
|
539
|
+
.filter((entry) => (entry.path.length > 0
|
|
540
|
+
&& (entry.changeType === 'add' || entry.changeType === 'delete' || entry.changeType === 'modify' || entry.changeType === 'rename')));
|
|
541
|
+
const changedEntryByPath = new Map();
|
|
542
|
+
for (const entry of normalizedChangedEntries) {
|
|
543
|
+
if (!changedEntryByPath.has(entry.path)) {
|
|
544
|
+
changedEntryByPath.set(entry.path, entry.changeType);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const planFileEntries = sanitizePlanFiles(contract.planFiles);
|
|
548
|
+
const planFileByPath = new Map(planFileEntries.map((item) => [item.path, item]));
|
|
549
|
+
const expectedSymbols = sanitizeExpectedSymbols(contract.expectedSymbols);
|
|
550
|
+
const changedSymbols = sanitizeChangedSymbols(input.changedSymbols);
|
|
551
|
+
const relaxedSymbolTypeMatching = contract.options?.symbolTypeRelaxedMatching !== false;
|
|
552
|
+
const allowSymbolFileBasenameFallback = contract.options?.symbolFileBasenameFallback === true;
|
|
158
553
|
for (const path of normalizedChanged) {
|
|
554
|
+
const planEntry = planFileByPath.get(path);
|
|
555
|
+
if (planEntry && planEntry.action === 'BLOCK') {
|
|
556
|
+
violations.push({
|
|
557
|
+
code: 'CHANGE_CONTRACT_BLOCKED_FILE_TOUCHED',
|
|
558
|
+
message: `File changed despite BLOCK action in change contract: ${path}`,
|
|
559
|
+
file: path,
|
|
560
|
+
});
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
159
563
|
if (!expectedSet.has(path)) {
|
|
160
564
|
violations.push({
|
|
161
565
|
code: 'CHANGE_CONTRACT_UNEXPECTED_FILE',
|
|
@@ -164,7 +568,128 @@ function evaluateChangeContract(contract, input) {
|
|
|
164
568
|
});
|
|
165
569
|
}
|
|
166
570
|
}
|
|
571
|
+
const enforceExpectedFiles = contract.options?.enforceExpectedFiles === true;
|
|
572
|
+
if (enforceExpectedFiles) {
|
|
573
|
+
for (const path of expectedSet) {
|
|
574
|
+
if (!changedSet.has(path)) {
|
|
575
|
+
violations.push({
|
|
576
|
+
code: 'CHANGE_CONTRACT_MISSING_EXPECTED_FILE',
|
|
577
|
+
message: `Expected file was not changed: ${path}`,
|
|
578
|
+
file: path,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
const enforceActionMatching = contract.options?.enforceActionMatching === true;
|
|
584
|
+
const allowRenameForModify = contract.options?.allowRenameForModify !== false;
|
|
585
|
+
if (enforceActionMatching && changedEntryByPath.size > 0 && planFileByPath.size > 0) {
|
|
586
|
+
for (const [path, actualChangeType] of changedEntryByPath.entries()) {
|
|
587
|
+
const planned = planFileByPath.get(path);
|
|
588
|
+
if (!planned || planned.action === 'BLOCK') {
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const allowedForPlan = planned.action === 'CREATE'
|
|
592
|
+
? ['add']
|
|
593
|
+
: allowRenameForModify
|
|
594
|
+
? ['modify', 'rename']
|
|
595
|
+
: ['modify'];
|
|
596
|
+
if (!allowedForPlan.includes(actualChangeType)) {
|
|
597
|
+
violations.push({
|
|
598
|
+
code: 'CHANGE_CONTRACT_ACTION_MISMATCH',
|
|
599
|
+
message: `File action mismatch for ${path}: planned ${planned.action}, got ${actualChangeType}`,
|
|
600
|
+
file: path,
|
|
601
|
+
expected: planned.action,
|
|
602
|
+
actual: actualChangeType,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const enforceExpectedSymbols = contract.options?.enforceExpectedSymbols === true;
|
|
608
|
+
const enforceSymbolActionMatching = contract.options?.enforceSymbolActionMatching === true;
|
|
609
|
+
let symbolRenameMatches = 0;
|
|
610
|
+
if (expectedSymbols.length > 0) {
|
|
611
|
+
for (const expectedSymbol of expectedSymbols) {
|
|
612
|
+
const symbolMatches = changedSymbols.filter((actualSymbol) => {
|
|
613
|
+
if (actualSymbol.name !== expectedSymbol.name)
|
|
614
|
+
return false;
|
|
615
|
+
if (!areSymbolTypesCompatible(expectedSymbol.type, actualSymbol.type, relaxedSymbolTypeMatching)) {
|
|
616
|
+
return false;
|
|
617
|
+
}
|
|
618
|
+
return doesSymbolFileMatch(expectedSymbol.file, actualSymbol.file, allowSymbolFileBasenameFallback);
|
|
619
|
+
});
|
|
620
|
+
const likelyRenameMatch = hasLikelyRenameForExpectedSymbol(expectedSymbol, changedSymbols, {
|
|
621
|
+
relaxedSymbolTypeMatching,
|
|
622
|
+
allowSymbolFileBasenameFallback,
|
|
623
|
+
});
|
|
624
|
+
if (likelyRenameMatch) {
|
|
625
|
+
symbolRenameMatches += 1;
|
|
626
|
+
}
|
|
627
|
+
if (expectedSymbol.action === 'BLOCK') {
|
|
628
|
+
if (symbolMatches.length > 0) {
|
|
629
|
+
violations.push({
|
|
630
|
+
code: 'CHANGE_CONTRACT_BLOCKED_SYMBOL_TOUCHED',
|
|
631
|
+
message: `Symbol changed despite BLOCK action in change contract: ${expectedSymbol.name}` +
|
|
632
|
+
`${expectedSymbol.file ? ` (${expectedSymbol.file})` : ''}`,
|
|
633
|
+
file: expectedSymbol.file || symbolMatches[0]?.file || undefined,
|
|
634
|
+
symbol: expectedSymbol.name,
|
|
635
|
+
symbolType: expectedSymbol.type,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const allowedActions = expectedSymbol.action === 'CREATE'
|
|
641
|
+
? ['add', 'modify']
|
|
642
|
+
: ['add', 'modify'];
|
|
643
|
+
const hasAllowedAction = symbolMatches.some((match) => allowedActions.includes(match.action));
|
|
644
|
+
if (enforceExpectedSymbols && symbolMatches.length === 0 && !likelyRenameMatch) {
|
|
645
|
+
violations.push({
|
|
646
|
+
code: 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL',
|
|
647
|
+
message: `Expected symbol was not changed: ${expectedSymbol.name}` +
|
|
648
|
+
`${expectedSymbol.file ? ` (${expectedSymbol.file})` : ''}`,
|
|
649
|
+
file: expectedSymbol.file,
|
|
650
|
+
symbol: expectedSymbol.name,
|
|
651
|
+
symbolType: expectedSymbol.type,
|
|
652
|
+
expected: expectedSymbol.action,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (enforceSymbolActionMatching && symbolMatches.length > 0 && !hasAllowedAction && !likelyRenameMatch) {
|
|
656
|
+
violations.push({
|
|
657
|
+
code: 'CHANGE_CONTRACT_SYMBOL_ACTION_MISMATCH',
|
|
658
|
+
message: `Symbol action mismatch for ${expectedSymbol.name}: planned ${expectedSymbol.action}, got ${symbolMatches
|
|
659
|
+
.map((item) => item.action)
|
|
660
|
+
.join(',')}`,
|
|
661
|
+
file: expectedSymbol.file || symbolMatches[0]?.file || undefined,
|
|
662
|
+
symbol: expectedSymbol.name,
|
|
663
|
+
symbolType: expectedSymbol.type,
|
|
664
|
+
expected: expectedSymbol.action,
|
|
665
|
+
actual: symbolMatches.map((item) => item.action).join(','),
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
const maxUnexpectedFiles = parseNonNegativeInteger(contract.options?.maxUnexpectedFiles) || 0;
|
|
671
|
+
const unexpectedFileViolations = violations.filter((item) => item.code === 'CHANGE_CONTRACT_UNEXPECTED_FILE');
|
|
672
|
+
let toleratedUnexpectedFiles = 0;
|
|
673
|
+
if (maxUnexpectedFiles > 0 && unexpectedFileViolations.length > 0 && unexpectedFileViolations.length <= maxUnexpectedFiles) {
|
|
674
|
+
toleratedUnexpectedFiles = unexpectedFileViolations.length;
|
|
675
|
+
violations = violations.filter((item) => item.code !== 'CHANGE_CONTRACT_UNEXPECTED_FILE');
|
|
676
|
+
}
|
|
677
|
+
const maxMissingExpectedSymbols = parseNonNegativeInteger(contract.options?.maxMissingExpectedSymbols) || 0;
|
|
678
|
+
const missingExpectedSymbolViolations = violations.filter((item) => item.code === 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL');
|
|
679
|
+
let toleratedMissingExpectedSymbols = 0;
|
|
680
|
+
if (maxMissingExpectedSymbols > 0
|
|
681
|
+
&& missingExpectedSymbolViolations.length > 0
|
|
682
|
+
&& missingExpectedSymbolViolations.length <= maxMissingExpectedSymbols) {
|
|
683
|
+
toleratedMissingExpectedSymbols = missingExpectedSymbolViolations.length;
|
|
684
|
+
violations = violations.filter((item) => item.code !== 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL');
|
|
685
|
+
}
|
|
167
686
|
const outOfContractFiles = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_UNEXPECTED_FILE').length;
|
|
687
|
+
const missingExpectedFiles = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_MISSING_EXPECTED_FILE').length;
|
|
688
|
+
const blockedFilesTouched = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_BLOCKED_FILE_TOUCHED').length;
|
|
689
|
+
const actionMismatches = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_ACTION_MISMATCH').length;
|
|
690
|
+
const missingExpectedSymbols = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL').length;
|
|
691
|
+
const blockedSymbolsTouched = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_BLOCKED_SYMBOL_TOUCHED').length;
|
|
692
|
+
const symbolActionMismatches = violations.filter((violation) => violation.code === 'CHANGE_CONTRACT_SYMBOL_ACTION_MISMATCH').length;
|
|
168
693
|
return {
|
|
169
694
|
valid: violations.length === 0,
|
|
170
695
|
violations,
|
|
@@ -172,7 +697,155 @@ function evaluateChangeContract(contract, input) {
|
|
|
172
697
|
expectedFiles: expectedSet.size,
|
|
173
698
|
changedFiles: normalizedChanged.length,
|
|
174
699
|
outOfContractFiles,
|
|
700
|
+
missingExpectedFiles,
|
|
701
|
+
blockedFilesTouched,
|
|
702
|
+
actionMismatches,
|
|
703
|
+
expectedSymbols: expectedSymbols.length,
|
|
704
|
+
changedSymbols: changedSymbols.length,
|
|
705
|
+
missingExpectedSymbols,
|
|
706
|
+
blockedSymbolsTouched,
|
|
707
|
+
symbolActionMismatches,
|
|
708
|
+
symbolRenameMatches,
|
|
709
|
+
toleratedUnexpectedFiles,
|
|
710
|
+
toleratedMissingExpectedSymbols,
|
|
175
711
|
},
|
|
176
712
|
};
|
|
177
713
|
}
|
|
714
|
+
function toHumanSymbolLabel(symbol, symbolType) {
|
|
715
|
+
if (!symbol)
|
|
716
|
+
return 'unknown_symbol';
|
|
717
|
+
const base = symbol.endsWith('()') ? symbol : `${symbol}()`;
|
|
718
|
+
if (!symbolType || symbolType === 'unknown') {
|
|
719
|
+
return base;
|
|
720
|
+
}
|
|
721
|
+
return `${symbolType}: ${base}`;
|
|
722
|
+
}
|
|
723
|
+
function formatViolationItem(violation) {
|
|
724
|
+
switch (violation.code) {
|
|
725
|
+
case 'CHANGE_CONTRACT_UNEXPECTED_FILE':
|
|
726
|
+
case 'CHANGE_CONTRACT_MISSING_EXPECTED_FILE':
|
|
727
|
+
case 'CHANGE_CONTRACT_BLOCKED_FILE_TOUCHED':
|
|
728
|
+
return violation.file || violation.message;
|
|
729
|
+
case 'CHANGE_CONTRACT_ACTION_MISMATCH':
|
|
730
|
+
return `${violation.file || 'unknown_file'} (expected ${violation.expected || 'unknown'}, actual ${violation.actual || 'unknown'})`;
|
|
731
|
+
case 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL':
|
|
732
|
+
case 'CHANGE_CONTRACT_BLOCKED_SYMBOL_TOUCHED':
|
|
733
|
+
return `${toHumanSymbolLabel(violation.symbol, violation.symbolType)}${violation.file ? ` (${violation.file})` : ''}`;
|
|
734
|
+
case 'CHANGE_CONTRACT_SYMBOL_ACTION_MISMATCH':
|
|
735
|
+
return `${toHumanSymbolLabel(violation.symbol, violation.symbolType)} (expected ${violation.expected || 'unknown'}, actual ${violation.actual || 'unknown'})${violation.file ? ` (${violation.file})` : ''}`;
|
|
736
|
+
case 'CHANGE_CONTRACT_PLAN_MISMATCH':
|
|
737
|
+
return `plan_id (expected ${violation.expected || 'unknown'}, actual ${violation.actual || 'unknown'})`;
|
|
738
|
+
case 'CHANGE_CONTRACT_POLICY_LOCK_MISMATCH':
|
|
739
|
+
return `policy_lock_fingerprint (expected ${violation.expected || 'unknown'}, actual ${violation.actual || 'unknown'})`;
|
|
740
|
+
case 'CHANGE_CONTRACT_COMPILED_POLICY_MISMATCH':
|
|
741
|
+
return `compiled_policy_fingerprint (expected ${violation.expected || 'unknown'}, actual ${violation.actual || 'unknown'})`;
|
|
742
|
+
default:
|
|
743
|
+
return violation.message;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function groupChangeContractViolations(violations) {
|
|
747
|
+
const groups = new Map();
|
|
748
|
+
const ensureGroup = (key, title) => {
|
|
749
|
+
const existing = groups.get(key);
|
|
750
|
+
if (existing)
|
|
751
|
+
return existing;
|
|
752
|
+
const created = {
|
|
753
|
+
key,
|
|
754
|
+
title,
|
|
755
|
+
impact: explainImpactByGroupKey(key),
|
|
756
|
+
items: [],
|
|
757
|
+
count: 0,
|
|
758
|
+
};
|
|
759
|
+
groups.set(key, created);
|
|
760
|
+
return created;
|
|
761
|
+
};
|
|
762
|
+
for (const violation of violations) {
|
|
763
|
+
let key = 'other';
|
|
764
|
+
let title = 'Other Contract Drift';
|
|
765
|
+
switch (violation.code) {
|
|
766
|
+
case 'CHANGE_CONTRACT_UNEXPECTED_FILE':
|
|
767
|
+
key = 'out_of_scope_changes';
|
|
768
|
+
title = 'Out-of-scope changes';
|
|
769
|
+
break;
|
|
770
|
+
case 'CHANGE_CONTRACT_MISSING_EXPECTED_FILE':
|
|
771
|
+
key = 'missing_expected_files';
|
|
772
|
+
title = 'Missing expected files';
|
|
773
|
+
break;
|
|
774
|
+
case 'CHANGE_CONTRACT_BLOCKED_FILE_TOUCHED':
|
|
775
|
+
key = 'blocked_files_touched';
|
|
776
|
+
title = 'Blocked files touched';
|
|
777
|
+
break;
|
|
778
|
+
case 'CHANGE_CONTRACT_ACTION_MISMATCH':
|
|
779
|
+
key = 'file_action_mismatches';
|
|
780
|
+
title = 'File action mismatches';
|
|
781
|
+
break;
|
|
782
|
+
case 'CHANGE_CONTRACT_MISSING_EXPECTED_SYMBOL':
|
|
783
|
+
key = 'missing_expected_symbols';
|
|
784
|
+
title = 'Missing expected symbols';
|
|
785
|
+
break;
|
|
786
|
+
case 'CHANGE_CONTRACT_BLOCKED_SYMBOL_TOUCHED':
|
|
787
|
+
key = 'blocked_symbols_touched';
|
|
788
|
+
title = 'Blocked symbols touched';
|
|
789
|
+
break;
|
|
790
|
+
case 'CHANGE_CONTRACT_SYMBOL_ACTION_MISMATCH':
|
|
791
|
+
key = 'symbol_action_mismatches';
|
|
792
|
+
title = 'Symbol action mismatches';
|
|
793
|
+
break;
|
|
794
|
+
case 'CHANGE_CONTRACT_PLAN_MISMATCH':
|
|
795
|
+
case 'CHANGE_CONTRACT_POLICY_LOCK_MISMATCH':
|
|
796
|
+
case 'CHANGE_CONTRACT_COMPILED_POLICY_MISMATCH':
|
|
797
|
+
key = 'contract_metadata_mismatches';
|
|
798
|
+
title = 'Contract metadata mismatches';
|
|
799
|
+
break;
|
|
800
|
+
default:
|
|
801
|
+
key = 'other';
|
|
802
|
+
title = 'Other contract drift';
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
const group = ensureGroup(key, title);
|
|
806
|
+
group.count += 1;
|
|
807
|
+
group.items.push(formatViolationItem(violation));
|
|
808
|
+
}
|
|
809
|
+
const order = [
|
|
810
|
+
'out_of_scope_changes',
|
|
811
|
+
'missing_expected_files',
|
|
812
|
+
'blocked_files_touched',
|
|
813
|
+
'file_action_mismatches',
|
|
814
|
+
'missing_expected_symbols',
|
|
815
|
+
'blocked_symbols_touched',
|
|
816
|
+
'symbol_action_mismatches',
|
|
817
|
+
'contract_metadata_mismatches',
|
|
818
|
+
'other',
|
|
819
|
+
];
|
|
820
|
+
return order
|
|
821
|
+
.map((key) => groups.get(key))
|
|
822
|
+
.filter((group) => Boolean(group))
|
|
823
|
+
.map((group) => ({
|
|
824
|
+
...group,
|
|
825
|
+
items: [...new Set(group.items)],
|
|
826
|
+
}));
|
|
827
|
+
}
|
|
828
|
+
function explainImpactByGroupKey(key) {
|
|
829
|
+
switch (key) {
|
|
830
|
+
case 'out_of_scope_changes':
|
|
831
|
+
return 'Changes escaped intended scope and may introduce architectural drift or hidden side effects.';
|
|
832
|
+
case 'missing_expected_files':
|
|
833
|
+
return 'Planned implementation work is incomplete, so intended behavior may be partially delivered.';
|
|
834
|
+
case 'blocked_files_touched':
|
|
835
|
+
return 'A protected file was edited; this can bypass governance boundaries.';
|
|
836
|
+
case 'file_action_mismatches':
|
|
837
|
+
return 'File-level operations differ from plan intent and may invalidate review assumptions.';
|
|
838
|
+
case 'missing_expected_symbols':
|
|
839
|
+
return 'Expected implementation logic is missing and critical behavior may not be enforced.';
|
|
840
|
+
case 'blocked_symbols_touched':
|
|
841
|
+
return 'A blocked symbol changed, which can re-introduce prohibited behavior.';
|
|
842
|
+
case 'symbol_action_mismatches':
|
|
843
|
+
return 'Symbol edits differ from intended action and may alter behavior unexpectedly.';
|
|
844
|
+
case 'contract_metadata_mismatches':
|
|
845
|
+
return 'Plan/policy artifacts are out of sync, reducing confidence in deterministic verification.';
|
|
846
|
+
case 'other':
|
|
847
|
+
default:
|
|
848
|
+
return 'Contract drift detected and manual review is required.';
|
|
849
|
+
}
|
|
850
|
+
}
|
|
178
851
|
//# sourceMappingURL=change-contract.js.map
|