@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.
Files changed (177) hide show
  1. package/dist/commands/contract.js +47 -0
  2. package/dist/commands/plan.js +40 -0
  3. package/dist/commands/verify.d.ts +2 -0
  4. package/dist/commands/verify.js +240 -114
  5. package/dist/index.js +41 -5
  6. package/dist/utils/advisory-signals.d.ts +20 -0
  7. package/dist/utils/advisory-signals.js +177 -0
  8. package/dist/utils/change-contract.d.ts +105 -1
  9. package/dist/utils/change-contract.js +685 -12
  10. package/dist/utils/diff-symbols.d.ts +10 -0
  11. package/dist/utils/diff-symbols.js +218 -0
  12. package/dist/utils/plan-symbols.d.ts +17 -0
  13. package/dist/utils/plan-symbols.js +209 -0
  14. package/package.json +6 -14
  15. package/LICENSE +0 -201
  16. package/dist/api-client.d.ts.map +0 -1
  17. package/dist/api-client.js.map +0 -1
  18. package/dist/commands/allow.d.ts.map +0 -1
  19. package/dist/commands/allow.js.map +0 -1
  20. package/dist/commands/apply.d.ts.map +0 -1
  21. package/dist/commands/apply.js.map +0 -1
  22. package/dist/commands/approve.d.ts.map +0 -1
  23. package/dist/commands/approve.js.map +0 -1
  24. package/dist/commands/ask.d.ts.map +0 -1
  25. package/dist/commands/ask.js.map +0 -1
  26. package/dist/commands/audit.d.ts.map +0 -1
  27. package/dist/commands/audit.js.map +0 -1
  28. package/dist/commands/bootstrap.d.ts.map +0 -1
  29. package/dist/commands/bootstrap.js.map +0 -1
  30. package/dist/commands/brain.d.ts.map +0 -1
  31. package/dist/commands/brain.js.map +0 -1
  32. package/dist/commands/check.d.ts.map +0 -1
  33. package/dist/commands/check.js.map +0 -1
  34. package/dist/commands/config.d.ts.map +0 -1
  35. package/dist/commands/config.js.map +0 -1
  36. package/dist/commands/contract.d.ts.map +0 -1
  37. package/dist/commands/contract.js.map +0 -1
  38. package/dist/commands/doctor.d.ts.map +0 -1
  39. package/dist/commands/doctor.js.map +0 -1
  40. package/dist/commands/feedback.d.ts.map +0 -1
  41. package/dist/commands/feedback.js.map +0 -1
  42. package/dist/commands/guard.d.ts.map +0 -1
  43. package/dist/commands/guard.js.map +0 -1
  44. package/dist/commands/init.d.ts.map +0 -1
  45. package/dist/commands/init.js.map +0 -1
  46. package/dist/commands/login.d.ts.map +0 -1
  47. package/dist/commands/login.js.map +0 -1
  48. package/dist/commands/logout.d.ts.map +0 -1
  49. package/dist/commands/logout.js.map +0 -1
  50. package/dist/commands/map.d.ts.map +0 -1
  51. package/dist/commands/map.js.map +0 -1
  52. package/dist/commands/plan-slo.d.ts.map +0 -1
  53. package/dist/commands/plan-slo.js.map +0 -1
  54. package/dist/commands/plan.d.ts.map +0 -1
  55. package/dist/commands/plan.js.map +0 -1
  56. package/dist/commands/policy.d.ts.map +0 -1
  57. package/dist/commands/policy.js.map +0 -1
  58. package/dist/commands/prompt.d.ts.map +0 -1
  59. package/dist/commands/prompt.js.map +0 -1
  60. package/dist/commands/refactor.d.ts.map +0 -1
  61. package/dist/commands/refactor.js.map +0 -1
  62. package/dist/commands/remediate.d.ts.map +0 -1
  63. package/dist/commands/remediate.js.map +0 -1
  64. package/dist/commands/repo.d.ts.map +0 -1
  65. package/dist/commands/repo.js.map +0 -1
  66. package/dist/commands/revert.d.ts.map +0 -1
  67. package/dist/commands/revert.js.map +0 -1
  68. package/dist/commands/security.d.ts.map +0 -1
  69. package/dist/commands/security.js.map +0 -1
  70. package/dist/commands/session.d.ts.map +0 -1
  71. package/dist/commands/session.js.map +0 -1
  72. package/dist/commands/ship.d.ts.map +0 -1
  73. package/dist/commands/ship.js.map +0 -1
  74. package/dist/commands/simulate.d.ts.map +0 -1
  75. package/dist/commands/simulate.js.map +0 -1
  76. package/dist/commands/verify.d.ts.map +0 -1
  77. package/dist/commands/verify.js.map +0 -1
  78. package/dist/commands/watch.d.ts.map +0 -1
  79. package/dist/commands/watch.js.map +0 -1
  80. package/dist/commands/whoami.d.ts.map +0 -1
  81. package/dist/commands/whoami.js.map +0 -1
  82. package/dist/config.d.ts.map +0 -1
  83. package/dist/config.js.map +0 -1
  84. package/dist/index.d.ts.map +0 -1
  85. package/dist/index.js.map +0 -1
  86. package/dist/rules.d.ts.map +0 -1
  87. package/dist/rules.js.map +0 -1
  88. package/dist/services/integrations/TicketService.d.ts.map +0 -1
  89. package/dist/services/integrations/TicketService.js.map +0 -1
  90. package/dist/services/mapper/ProjectScanner.d.ts.map +0 -1
  91. package/dist/services/mapper/ProjectScanner.js.map +0 -1
  92. package/dist/services/project-knowledge-service.d.ts.map +0 -1
  93. package/dist/services/project-knowledge-service.js.map +0 -1
  94. package/dist/services/security/SecurityGuard.d.ts.map +0 -1
  95. package/dist/services/security/SecurityGuard.js.map +0 -1
  96. package/dist/services/toolbox-service.d.ts.map +0 -1
  97. package/dist/services/toolbox-service.js.map +0 -1
  98. package/dist/services/watch/BlobStore.d.ts.map +0 -1
  99. package/dist/services/watch/BlobStore.js.map +0 -1
  100. package/dist/services/watch/CommandPoller.d.ts.map +0 -1
  101. package/dist/services/watch/CommandPoller.js.map +0 -1
  102. package/dist/services/watch/Journal.d.ts.map +0 -1
  103. package/dist/services/watch/Journal.js.map +0 -1
  104. package/dist/services/watch/Sentinel.d.ts.map +0 -1
  105. package/dist/services/watch/Sentinel.js.map +0 -1
  106. package/dist/services/watch/Syncer.d.ts.map +0 -1
  107. package/dist/services/watch/Syncer.js.map +0 -1
  108. package/dist/utils/ROILogger.d.ts.map +0 -1
  109. package/dist/utils/ROILogger.js.map +0 -1
  110. package/dist/utils/RelevanceScorer.d.ts.map +0 -1
  111. package/dist/utils/RelevanceScorer.js.map +0 -1
  112. package/dist/utils/ai-debt-budget.d.ts.map +0 -1
  113. package/dist/utils/ai-debt-budget.js.map +0 -1
  114. package/dist/utils/artifact-signature.d.ts.map +0 -1
  115. package/dist/utils/artifact-signature.js.map +0 -1
  116. package/dist/utils/ask-cache.d.ts.map +0 -1
  117. package/dist/utils/ask-cache.js.map +0 -1
  118. package/dist/utils/box.d.ts.map +0 -1
  119. package/dist/utils/box.js.map +0 -1
  120. package/dist/utils/brain-context.d.ts.map +0 -1
  121. package/dist/utils/brain-context.js.map +0 -1
  122. package/dist/utils/breakage-simulator.d.ts.map +0 -1
  123. package/dist/utils/breakage-simulator.js.map +0 -1
  124. package/dist/utils/change-contract.d.ts.map +0 -1
  125. package/dist/utils/change-contract.js.map +0 -1
  126. package/dist/utils/cli-json.d.ts.map +0 -1
  127. package/dist/utils/cli-json.js.map +0 -1
  128. package/dist/utils/custom-policy-rules.d.ts.map +0 -1
  129. package/dist/utils/custom-policy-rules.js.map +0 -1
  130. package/dist/utils/git.d.ts.map +0 -1
  131. package/dist/utils/git.js.map +0 -1
  132. package/dist/utils/gitignore.d.ts.map +0 -1
  133. package/dist/utils/gitignore.js.map +0 -1
  134. package/dist/utils/governance.d.ts.map +0 -1
  135. package/dist/utils/governance.js.map +0 -1
  136. package/dist/utils/ignore.d.ts.map +0 -1
  137. package/dist/utils/ignore.js.map +0 -1
  138. package/dist/utils/manual-approvals.d.ts.map +0 -1
  139. package/dist/utils/manual-approvals.js.map +0 -1
  140. package/dist/utils/messages.d.ts.map +0 -1
  141. package/dist/utils/messages.js.map +0 -1
  142. package/dist/utils/neurcode-context.d.ts.map +0 -1
  143. package/dist/utils/neurcode-context.js.map +0 -1
  144. package/dist/utils/plan-cache.d.ts.map +0 -1
  145. package/dist/utils/plan-cache.js.map +0 -1
  146. package/dist/utils/plan-slo.d.ts.map +0 -1
  147. package/dist/utils/plan-slo.js.map +0 -1
  148. package/dist/utils/policy-audit.d.ts.map +0 -1
  149. package/dist/utils/policy-audit.js.map +0 -1
  150. package/dist/utils/policy-compiler.d.ts.map +0 -1
  151. package/dist/utils/policy-compiler.js.map +0 -1
  152. package/dist/utils/policy-exceptions.d.ts.map +0 -1
  153. package/dist/utils/policy-exceptions.js.map +0 -1
  154. package/dist/utils/policy-governance.d.ts.map +0 -1
  155. package/dist/utils/policy-governance.js.map +0 -1
  156. package/dist/utils/policy-packs.d.ts.map +0 -1
  157. package/dist/utils/policy-packs.js.map +0 -1
  158. package/dist/utils/project-detector.d.ts.map +0 -1
  159. package/dist/utils/project-detector.js.map +0 -1
  160. package/dist/utils/project-root.d.ts.map +0 -1
  161. package/dist/utils/project-root.js.map +0 -1
  162. package/dist/utils/repo-links.d.ts.map +0 -1
  163. package/dist/utils/repo-links.js.map +0 -1
  164. package/dist/utils/restore.d.ts.map +0 -1
  165. package/dist/utils/restore.js.map +0 -1
  166. package/dist/utils/runtime-guard.d.ts.map +0 -1
  167. package/dist/utils/runtime-guard.js.map +0 -1
  168. package/dist/utils/scope-telemetry.d.ts.map +0 -1
  169. package/dist/utils/scope-telemetry.js.map +0 -1
  170. package/dist/utils/secret-masking.d.ts.map +0 -1
  171. package/dist/utils/secret-masking.js.map +0 -1
  172. package/dist/utils/state.d.ts.map +0 -1
  173. package/dist/utils/state.js.map +0 -1
  174. package/dist/utils/tier.d.ts.map +0 -1
  175. package/dist/utils/tier.js.map +0 -1
  176. package/dist/utils/user-context.d.ts.map +0 -1
  177. 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 === undefined) {
45
- return true;
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 (!record.signature || typeof record.signature !== 'object' || Array.isArray(record.signature)) {
48
- return false;
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
- const signature = record.signature;
51
- if (signature.algorithm !== 'hmac-sha256' ||
52
- typeof signature.signedAt !== 'string' ||
53
- typeof signature.payloadHash !== 'string' ||
54
- typeof signature.value !== 'string') {
55
- return false;
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 (signature.keyId !== null && signature.keyId !== undefined && typeof signature.keyId !== 'string') {
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
- const violations = [];
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