@neurcode-ai/contracts 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,603 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.normalizeRepositoryRelativePath = normalizeRepositoryRelativePath;
4
+ exports.deriveTrustedHostPosture = deriveTrustedHostPosture;
5
+ exports.validateProposedChangeEnvelope = validateProposedChangeEnvelope;
6
+ exports.validateAndBindProposedChangeEnvelope = validateAndBindProposedChangeEnvelope;
7
+ const repo_intelligence_v2_1 = require("./repo-intelligence-v2");
8
+ const MAX_SERIALIZED_BYTES = 1_000_000;
9
+ const MAX_DEPTH = 14;
10
+ const MAX_CONTAINER_ENTRIES = 12_000;
11
+ const MAX_FACTS_PER_FAMILY = 4_096;
12
+ const MAX_PATH_LENGTH = 1_024;
13
+ const MAX_ID_LENGTH = 512;
14
+ const MAX_NAME_LENGTH = 256;
15
+ const MAX_METADATA_LENGTH = 1_000;
16
+ const ADAPTERS = [
17
+ 'claude-code-hooks',
18
+ 'copilot-hooks',
19
+ 'generic-mcp',
20
+ 'codex-mcp',
21
+ 'cursor-mcp',
22
+ 'vscode-extension',
23
+ 'github-action',
24
+ 'neurcode-cli',
25
+ ];
26
+ const LANGUAGES = [
27
+ 'typescript', 'javascript', 'python', 'go', 'java', 'ruby', 'rust',
28
+ 'markdown', 'yaml', 'json', 'shell', 'sql', 'other',
29
+ ];
30
+ const PARSER_DEPTHS = ['ast', 'syntax_tree', 'regex_degraded', 'metadata_only', 'unsupported'];
31
+ const FACT_FAMILIES = [
32
+ 'path', 'symbol', 'import', 'reference', 'call', 'package', 'service',
33
+ 'ownership', 'test', 'surface',
34
+ ];
35
+ const POLICY_FAMILIES = [
36
+ 'symbol_uniqueness', 'import_boundary', 'layering', 'ownership_approval',
37
+ 'service_dependency', 'required_test', 'sensitive_surface_approval',
38
+ 'review_required_surface', 'generated_file_restriction', 'scope_constraint',
39
+ ];
40
+ const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@#-]*$/;
41
+ const NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$-]*$/;
42
+ const HASH_PATTERN = /^[a-f0-9]{64}$/;
43
+ const GIT_SHA_PATTERN = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
44
+ const DANGEROUS_KEYS = new Set(['__proto__', 'prototype', 'constructor']);
45
+ const SOURCE_LIKE_METADATA = /(?:[\r\n\0]|=>|\b(?:export|import|function|class)\s+[A-Za-z_$]|\b(?:const|let|var)\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=|\bdef\s+[A-Za-z_]\w*\s*\(|\breturn\s+[^.]{0,100}[();]|[{}\x60])/;
46
+ const IMPORT_TARGET_PATTERN = /^[A-Za-z0-9@._/#:+~<>=-]+$/;
47
+ function fail(path, message) {
48
+ throw new Error(`Invalid ProposedChangeEnvelope at ${path}: ${message}`);
49
+ }
50
+ function assertPayloadBudget(value) {
51
+ const seen = new WeakSet();
52
+ let entries = 0;
53
+ let stringBytes = 0;
54
+ const stack = [
55
+ { value, path: 'envelope', depth: 0 },
56
+ ];
57
+ while (stack.length > 0) {
58
+ const current = stack.pop();
59
+ if (current.depth > MAX_DEPTH)
60
+ fail(current.path, `nesting exceeds ${MAX_DEPTH}`);
61
+ if (typeof current.value === 'string') {
62
+ stringBytes += Buffer.byteLength(current.value, 'utf8');
63
+ if (stringBytes > MAX_SERIALIZED_BYTES)
64
+ fail(current.path, 'string budget exceeded');
65
+ continue;
66
+ }
67
+ if (current.value === null || typeof current.value !== 'object')
68
+ continue;
69
+ if (seen.has(current.value))
70
+ fail(current.path, 'cyclic values are not allowed');
71
+ seen.add(current.value);
72
+ if (Array.isArray(current.value)) {
73
+ entries += current.value.length;
74
+ if (entries > MAX_CONTAINER_ENTRIES)
75
+ fail(current.path, 'container entry budget exceeded');
76
+ current.value.forEach((child, index) => stack.push({
77
+ value: child,
78
+ path: `${current.path}[${index}]`,
79
+ depth: current.depth + 1,
80
+ }));
81
+ continue;
82
+ }
83
+ const prototype = Object.getPrototypeOf(current.value);
84
+ if (prototype !== Object.prototype && prototype !== null) {
85
+ fail(current.path, 'only plain JSON objects are allowed');
86
+ }
87
+ const descriptors = Object.getOwnPropertyDescriptors(current.value);
88
+ for (const [key, descriptor] of Object.entries(descriptors)) {
89
+ if (DANGEROUS_KEYS.has(key))
90
+ fail(`${current.path}.${key}`, 'prototype-pollution key is forbidden');
91
+ if (descriptor.get || descriptor.set)
92
+ fail(`${current.path}.${key}`, 'accessor properties are forbidden');
93
+ entries += 1;
94
+ if (entries > MAX_CONTAINER_ENTRIES)
95
+ fail(current.path, 'container entry budget exceeded');
96
+ stack.push({ value: descriptor.value, path: `${current.path}.${key}`, depth: current.depth + 1 });
97
+ }
98
+ }
99
+ if (stringBytes > MAX_SERIALIZED_BYTES)
100
+ fail('envelope', 'payload exceeds byte budget');
101
+ }
102
+ function record(value, path, allowed, required = allowed) {
103
+ if (!value || typeof value !== 'object' || Array.isArray(value))
104
+ fail(path, 'must be an object');
105
+ const result = value;
106
+ const allowedKeys = new Set(allowed);
107
+ for (const key of Object.keys(result)) {
108
+ if (!allowedKeys.has(key))
109
+ fail(`${path}.${key}`, 'unknown field');
110
+ }
111
+ for (const key of required) {
112
+ if (!Object.prototype.hasOwnProperty.call(result, key))
113
+ fail(`${path}.${key}`, 'required field is missing');
114
+ }
115
+ return result;
116
+ }
117
+ function array(value, path, max = MAX_FACTS_PER_FAMILY) {
118
+ if (!Array.isArray(value))
119
+ fail(path, 'must be an array');
120
+ if (value.length > max)
121
+ fail(path, `array exceeds ${max} items`);
122
+ return value;
123
+ }
124
+ function string(value, path, max, pattern) {
125
+ if (typeof value !== 'string' || value.length === 0)
126
+ fail(path, 'must be a non-empty string');
127
+ if (Buffer.byteLength(value, 'utf8') > max)
128
+ fail(path, `string exceeds ${max} bytes`);
129
+ if (pattern && !pattern.test(value))
130
+ fail(path, 'has an invalid format');
131
+ return value;
132
+ }
133
+ function nullableString(value, path, max, pattern) {
134
+ return value === null ? null : string(value, path, max, pattern);
135
+ }
136
+ function enumeration(value, path, values) {
137
+ if (typeof value !== 'string' || !values.includes(value)) {
138
+ fail(path, `must be one of: ${values.join(', ')}`);
139
+ }
140
+ return value;
141
+ }
142
+ function boolean(value, path) {
143
+ if (typeof value !== 'boolean')
144
+ fail(path, 'must be a boolean');
145
+ return value;
146
+ }
147
+ function integer(value, path, min = 0, max = 10_000_000) {
148
+ if (!Number.isSafeInteger(value) || Number(value) < min || Number(value) > max) {
149
+ fail(path, `must be an integer between ${min} and ${max}`);
150
+ }
151
+ return Number(value);
152
+ }
153
+ function nullableInteger(value, path, min = 0, max = 10_000_000) {
154
+ return value === null ? null : integer(value, path, min, max);
155
+ }
156
+ function sourceFreeMetadata(value, path, max = MAX_METADATA_LENGTH) {
157
+ const result = string(value, path, max);
158
+ if (SOURCE_LIKE_METADATA.test(result))
159
+ fail(path, 'contains source-like text');
160
+ return result;
161
+ }
162
+ function nullableMetadata(value, path, max = MAX_METADATA_LENGTH) {
163
+ return value === null ? null : sourceFreeMetadata(value, path, max);
164
+ }
165
+ function id(value, path) {
166
+ return string(value, path, MAX_ID_LENGTH, ID_PATTERN);
167
+ }
168
+ function name(value, path) {
169
+ return string(value, path, MAX_NAME_LENGTH, NAME_PATTERN);
170
+ }
171
+ function hash(value, path) {
172
+ return string(value, path, 64, HASH_PATTERN);
173
+ }
174
+ function nullableHash(value, path) {
175
+ return value === null ? null : hash(value, path);
176
+ }
177
+ function normalizeRepositoryRelativePath(value, path) {
178
+ const candidate = string(value, path, MAX_PATH_LENGTH);
179
+ if (candidate.includes('\\') || candidate.includes('\0') || candidate.startsWith('/') || /^[A-Za-z]:/.test(candidate)) {
180
+ fail(path, 'must be a repository-relative POSIX path');
181
+ }
182
+ const segments = candidate.split('/');
183
+ if (segments.some((segment) => !segment || segment === '.' || segment === '..')) {
184
+ fail(path, 'contains an empty or traversal segment');
185
+ }
186
+ if (SOURCE_LIKE_METADATA.test(candidate))
187
+ fail(path, 'contains source-like text');
188
+ return candidate;
189
+ }
190
+ function importTarget(value, path) {
191
+ const target = string(value, path, MAX_PATH_LENGTH);
192
+ if (!IMPORT_TARGET_PATTERN.test(target) || target.includes('\0') || /[\r\n{};`]/.test(target)) {
193
+ fail(path, 'contains source-like text');
194
+ }
195
+ if (target.startsWith('/') || /^[A-Za-z]:/.test(target) || target.split('/').includes('..') && !target.startsWith('../')) {
196
+ fail(path, 'has an invalid import target');
197
+ }
198
+ return target;
199
+ }
200
+ function nullablePath(value, path) {
201
+ return value === null ? null : normalizeRepositoryRelativePath(value, path);
202
+ }
203
+ function line(value, path) {
204
+ return nullableInteger(value, path, 1, 100_000_000);
205
+ }
206
+ function validateStringArray(value, path, validator, max = 256) {
207
+ array(value, path, max).forEach((item, index) => validator(item, `${path}[${index}]`));
208
+ }
209
+ function validateSymbol(value, path, targetPath) {
210
+ const item = record(value, path, [
211
+ 'id', 'name', 'kind', 'language', 'filePath', 'line', 'exported', 'local',
212
+ 'arity', 'signatureHash', 'structuralFingerprint', 'parserDepth',
213
+ ]);
214
+ id(item.id, `${path}.id`);
215
+ name(item.name, `${path}.name`);
216
+ enumeration(item.kind, `${path}.kind`, ['function', 'class', 'interface', 'type', 'const', 'method', 'module', 'unknown']);
217
+ enumeration(item.language, `${path}.language`, LANGUAGES);
218
+ if (normalizeRepositoryRelativePath(item.filePath, `${path}.filePath`) !== targetPath) {
219
+ fail(`${path}.filePath`, 'must match target.path');
220
+ }
221
+ line(item.line, `${path}.line`);
222
+ boolean(item.exported, `${path}.exported`);
223
+ boolean(item.local, `${path}.local`);
224
+ nullableInteger(item.arity, `${path}.arity`, 0, 10_000);
225
+ nullableHash(item.signatureHash, `${path}.signatureHash`);
226
+ nullableHash(item.structuralFingerprint, `${path}.structuralFingerprint`);
227
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
228
+ }
229
+ function validateImport(value, path, targetPath) {
230
+ const item = record(value, path, [
231
+ 'id', 'fromFile', 'target', 'resolvedFile', 'resolution', 'resolutionReason',
232
+ 'sourcePackage', 'targetPackage', 'sourceService', 'targetService',
233
+ 'importedNames', 'kind', 'line', 'parserDepth',
234
+ ], ['id', 'fromFile', 'target', 'resolvedFile', 'importedNames', 'kind', 'line', 'parserDepth']);
235
+ id(item.id, `${path}.id`);
236
+ if (normalizeRepositoryRelativePath(item.fromFile, `${path}.fromFile`) !== targetPath) {
237
+ fail(`${path}.fromFile`, 'must match target.path');
238
+ }
239
+ importTarget(item.target, `${path}.target`);
240
+ const resolvedFile = nullablePath(item.resolvedFile, `${path}.resolvedFile`);
241
+ const resolution = item.resolution === undefined
242
+ ? null
243
+ : enumeration(item.resolution, `${path}.resolution`, [
244
+ 'resolved_repository', 'external_package', 'unresolved', 'ambiguous', 'dynamic',
245
+ ]);
246
+ if (resolution === 'resolved_repository' && !resolvedFile)
247
+ fail(`${path}.resolvedFile`, 'is required for resolved_repository');
248
+ if (resolution && resolution !== 'resolved_repository' && resolvedFile) {
249
+ fail(`${path}.resolvedFile`, `must be null for ${resolution}`);
250
+ }
251
+ if (item.resolutionReason !== undefined)
252
+ nullableMetadata(item.resolutionReason, `${path}.resolutionReason`);
253
+ for (const key of ['sourcePackage', 'targetPackage', 'sourceService', 'targetService']) {
254
+ if (item[key] !== undefined && item[key] !== null)
255
+ importTarget(item[key], `${path}.${key}`);
256
+ }
257
+ validateStringArray(item.importedNames, `${path}.importedNames`, name);
258
+ enumeration(item.kind, `${path}.kind`, ['static', 'dynamic', 'require', 'python_import', 'unknown']);
259
+ line(item.line, `${path}.line`);
260
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
261
+ }
262
+ function validateExport(value, path, targetPath) {
263
+ const item = record(value, path, ['id', 'filePath', 'symbolName', 'target', 'kind', 'line', 'parserDepth']);
264
+ id(item.id, `${path}.id`);
265
+ if (normalizeRepositoryRelativePath(item.filePath, `${path}.filePath`) !== targetPath) {
266
+ fail(`${path}.filePath`, 'must match target.path');
267
+ }
268
+ name(item.symbolName, `${path}.symbolName`);
269
+ if (item.target !== null)
270
+ importTarget(item.target, `${path}.target`);
271
+ enumeration(item.kind, `${path}.kind`, ['named', 'default', 're_export', 'python_public', 'unknown']);
272
+ line(item.line, `${path}.line`);
273
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
274
+ }
275
+ function validateRelationship(value, path) {
276
+ const item = record(value, path, [
277
+ 'id', 'type', 'fromId', 'toId', 'confidence', 'deterministic', 'provenance',
278
+ 'computationRepeatable', 'semanticCertainty', 'evidenceTier', 'enforcementEligible',
279
+ ], ['id', 'type', 'fromId', 'toId', 'confidence', 'deterministic', 'provenance']);
280
+ id(item.id, `${path}.id`);
281
+ enumeration(item.type, `${path}.type`, [
282
+ 'defines', 'references', 'imports', 'exports', 'calls', 'owns',
283
+ 'belongs_to_package', 'belongs_to_service', 'tests', 'depends_on',
284
+ 'structurally_resembles', 'crosses_boundary',
285
+ ]);
286
+ id(item.fromId, `${path}.fromId`);
287
+ id(item.toId, `${path}.toId`);
288
+ enumeration(item.confidence, `${path}.confidence`, ['exact', 'high', 'medium', 'low']);
289
+ boolean(item.deterministic, `${path}.deterministic`);
290
+ if (item.computationRepeatable !== undefined)
291
+ boolean(item.computationRepeatable, `${path}.computationRepeatable`);
292
+ if (item.semanticCertainty !== undefined) {
293
+ enumeration(item.semanticCertainty, `${path}.semanticCertainty`, ['exact', 'high', 'medium', 'low', 'unknown']);
294
+ }
295
+ if (item.evidenceTier !== undefined) {
296
+ enumeration(item.evidenceTier, `${path}.evidenceTier`, ['deterministic', 'advisory', 'degraded']);
297
+ }
298
+ if (item.enforcementEligible !== undefined)
299
+ boolean(item.enforcementEligible, `${path}.enforcementEligible`);
300
+ if (item.type === 'structurally_resembles' && item.enforcementEligible === true) {
301
+ fail(`${path}.enforcementEligible`, 'structural resemblance is advisory and cannot be enforcement eligible');
302
+ }
303
+ sourceFreeMetadata(item.provenance, `${path}.provenance`, 256);
304
+ }
305
+ function validateReference(value, path, targetPath) {
306
+ const item = record(value, path, [
307
+ 'id', 'filePath', 'name', 'line', 'kind', 'resolvedSymbolId', 'resolvedFile',
308
+ 'resolution', 'resolutionReason', 'parserDepth',
309
+ ]);
310
+ id(item.id, `${path}.id`);
311
+ if (normalizeRepositoryRelativePath(item.filePath, `${path}.filePath`) !== targetPath) {
312
+ fail(`${path}.filePath`, 'must match target.path');
313
+ }
314
+ name(item.name, `${path}.name`);
315
+ line(item.line, `${path}.line`);
316
+ enumeration(item.kind, `${path}.kind`, ['call_target', 'identifier', 'property', 'unknown']);
317
+ validateResolutionFields(item, path);
318
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
319
+ }
320
+ function validateCall(value, path, targetPath) {
321
+ const item = record(value, path, [
322
+ 'id', 'filePath', 'calledName', 'line', 'callerSymbolId', 'callKind',
323
+ 'resolvedSymbolId', 'resolvedFile', 'resolution', 'resolutionReason', 'parserDepth',
324
+ ]);
325
+ id(item.id, `${path}.id`);
326
+ if (normalizeRepositoryRelativePath(item.filePath, `${path}.filePath`) !== targetPath) {
327
+ fail(`${path}.filePath`, 'must match target.path');
328
+ }
329
+ name(item.calledName, `${path}.calledName`);
330
+ line(item.line, `${path}.line`);
331
+ if (item.callerSymbolId !== null)
332
+ id(item.callerSymbolId, `${path}.callerSymbolId`);
333
+ enumeration(item.callKind, `${path}.callKind`, ['direct', 'property', 'constructor', 'unknown']);
334
+ validateResolutionFields(item, path);
335
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
336
+ }
337
+ function validateResolutionFields(item, path) {
338
+ const resolution = enumeration(item.resolution, `${path}.resolution`, [
339
+ 'local_symbol', 'imported_symbol', 'repository_symbol', 'ambiguous', 'unresolved',
340
+ ]);
341
+ const resolvedSymbolId = item.resolvedSymbolId === null
342
+ ? null
343
+ : id(item.resolvedSymbolId, `${path}.resolvedSymbolId`);
344
+ const resolvedFile = nullablePath(item.resolvedFile, `${path}.resolvedFile`);
345
+ const resolved = ['local_symbol', 'imported_symbol', 'repository_symbol'].includes(resolution);
346
+ if (resolved && (!resolvedSymbolId || !resolvedFile)) {
347
+ fail(path, 'resolved facts require resolvedSymbolId and resolvedFile');
348
+ }
349
+ if (!resolved && (resolvedSymbolId || resolvedFile)) {
350
+ fail(path, 'ambiguous or unresolved facts cannot claim resolved targets');
351
+ }
352
+ nullableMetadata(item.resolutionReason, `${path}.resolutionReason`);
353
+ }
354
+ function validateBoundary(value, path, targetPath) {
355
+ const item = record(value, path, ['id', 'filePath', 'packageKey', 'serviceKey', 'parserDepth']);
356
+ id(item.id, `${path}.id`);
357
+ if (normalizeRepositoryRelativePath(item.filePath, `${path}.filePath`) !== targetPath) {
358
+ fail(`${path}.filePath`, 'must match target.path');
359
+ }
360
+ if (item.packageKey !== null)
361
+ importTarget(item.packageKey, `${path}.packageKey`);
362
+ if (item.serviceKey !== null)
363
+ importTarget(item.serviceKey, `${path}.serviceKey`);
364
+ enumeration(item.parserDepth, `${path}.parserDepth`, PARSER_DEPTHS);
365
+ }
366
+ function validateCompleteness(value, path) {
367
+ const completeness = record(value, path, ['facts', 'policies']);
368
+ const seenFacts = new Set();
369
+ const factStatuses = new Map();
370
+ array(completeness.facts, `${path}.facts`, FACT_FAMILIES.length).forEach((value, index) => {
371
+ const itemPath = `${path}.facts[${index}]`;
372
+ const item = record(value, itemPath, ['fact', 'status', 'reasons']);
373
+ const fact = enumeration(item.fact, `${itemPath}.fact`, FACT_FAMILIES);
374
+ if (seenFacts.has(fact))
375
+ fail(`${itemPath}.fact`, 'duplicate fact family');
376
+ seenFacts.add(fact);
377
+ const status = enumeration(item.status, `${itemPath}.status`, ['complete', 'partial', 'unavailable']);
378
+ factStatuses.set(fact, status);
379
+ validateStringArray(item.reasons, `${itemPath}.reasons`, sourceFreeMetadata, 64);
380
+ });
381
+ const seenPolicies = new Set();
382
+ array(completeness.policies, `${path}.policies`, POLICY_FAMILIES.length).forEach((value, index) => {
383
+ const itemPath = `${path}.policies[${index}]`;
384
+ const item = record(value, itemPath, ['family', 'status', 'requiredFacts', 'missingFacts', 'reasons']);
385
+ const family = enumeration(item.family, `${itemPath}.family`, POLICY_FAMILIES);
386
+ if (seenPolicies.has(family))
387
+ fail(`${itemPath}.family`, 'duplicate policy family');
388
+ seenPolicies.add(family);
389
+ enumeration(item.status, `${itemPath}.status`, ['complete', 'partial', 'unavailable']);
390
+ validateStringArray(item.requiredFacts, `${itemPath}.requiredFacts`, (entry, entryPath) => enumeration(entry, entryPath, FACT_FAMILIES), FACT_FAMILIES.length);
391
+ validateStringArray(item.missingFacts, `${itemPath}.missingFacts`, (entry, entryPath) => enumeration(entry, entryPath, FACT_FAMILIES), FACT_FAMILIES.length);
392
+ validateStringArray(item.reasons, `${itemPath}.reasons`, sourceFreeMetadata, 64);
393
+ });
394
+ return factStatuses;
395
+ }
396
+ function deriveTrustedHostPosture(adapterId, timing) {
397
+ const timingAllowed = adapterId === 'github-action'
398
+ ? timing === 'ci'
399
+ : adapterId === 'vscode-extension'
400
+ ? timing === 'after_write'
401
+ : adapterId === 'claude-code-hooks' || adapterId === 'copilot-hooks'
402
+ ? timing === 'before_write'
403
+ : adapterId === 'neurcode-cli'
404
+ ? timing === 'before_write' || timing === 'after_write'
405
+ : timing !== 'ci';
406
+ if (!timingAllowed) {
407
+ throw new Error(`Invalid trusted host timing: ${adapterId} cannot claim ${timing}`);
408
+ }
409
+ let capability;
410
+ if (timing === 'after_write')
411
+ capability = 'post_write';
412
+ else if (timing === 'ci' || adapterId === 'github-action')
413
+ capability = 'ci_only';
414
+ else if (adapterId === 'claude-code-hooks' || adapterId === 'copilot-hooks')
415
+ capability = 'hard_prewrite';
416
+ else if (adapterId === 'cursor-mcp')
417
+ capability = 'supervised_write';
418
+ else if (adapterId === 'codex-mcp' || adapterId === 'generic-mcp')
419
+ capability = 'cooperative_prewrite';
420
+ else if (adapterId === 'vscode-extension')
421
+ capability = 'post_write';
422
+ else if (adapterId === 'neurcode-cli')
423
+ capability = 'not_supported';
424
+ else
425
+ capability = 'not_supported';
426
+ const decisionBinding = capability === 'hard_prewrite'
427
+ ? 'host_enforced'
428
+ : capability === 'cooperative_prewrite' || capability === 'supervised_write'
429
+ ? 'cooperative'
430
+ : 'observed';
431
+ return { adapterId, capability, timing, decisionBinding };
432
+ }
433
+ function validateEnvelope(value) {
434
+ assertPayloadBudget(value);
435
+ const envelope = record(value, 'envelope', [
436
+ 'schemaVersion', 'repository', 'target', 'content', 'facts', 'host', 'session', 'privacy',
437
+ ]);
438
+ if (envelope.schemaVersion !== repo_intelligence_v2_1.PROPOSED_CHANGE_ENVELOPE_SCHEMA_VERSION) {
439
+ fail('envelope.schemaVersion', `unsupported schema ${String(envelope.schemaVersion)}`);
440
+ }
441
+ const repository = record(envelope.repository, 'envelope.repository', ['repoId', 'rootHash', 'remoteHash', 'headSha']);
442
+ id(repository.repoId, 'envelope.repository.repoId');
443
+ hash(repository.rootHash, 'envelope.repository.rootHash');
444
+ nullableHash(repository.remoteHash, 'envelope.repository.remoteHash');
445
+ if (repository.headSha !== null)
446
+ string(repository.headSha, 'envelope.repository.headSha', 64, GIT_SHA_PATTERN);
447
+ const target = record(envelope.target, 'envelope.target', ['path', 'previousPath', 'operation', 'language']);
448
+ const targetPath = normalizeRepositoryRelativePath(target.path, 'envelope.target.path');
449
+ const previousPath = nullablePath(target.previousPath, 'envelope.target.previousPath');
450
+ const operation = enumeration(target.operation, 'envelope.target.operation', ['create', 'update', 'delete', 'rename']);
451
+ enumeration(target.language, 'envelope.target.language', LANGUAGES);
452
+ if (operation === 'rename' && (!previousPath || previousPath === targetPath)) {
453
+ fail('envelope.target.previousPath', 'rename requires a distinct previousPath');
454
+ }
455
+ if (operation !== 'rename' && previousPath !== null) {
456
+ fail('envelope.target.previousPath', 'is only valid for rename operations');
457
+ }
458
+ const content = record(envelope.content, 'envelope.content', [
459
+ 'present', 'availabilityReason', 'contentHash', 'rawRetained',
460
+ ]);
461
+ const contentPresent = boolean(content.present, 'envelope.content.present');
462
+ const availabilityReason = enumeration(content.availabilityReason, 'envelope.content.availabilityReason', [
463
+ 'host_supplied', 'path_only_contract', 'post_write_disk_read', 'delete_operation', 'unsupported_host',
464
+ ]);
465
+ const contentHash = nullableHash(content.contentHash, 'envelope.content.contentHash');
466
+ if (content.rawRetained !== false)
467
+ fail('envelope.content.rawRetained', 'must be false');
468
+ if (contentPresent !== Boolean(contentHash)) {
469
+ fail('envelope.content.contentHash', 'must be present exactly when content.present is true');
470
+ }
471
+ if (contentPresent && !['host_supplied', 'post_write_disk_read'].includes(availabilityReason)) {
472
+ fail('envelope.content.availabilityReason', 'is inconsistent with present content');
473
+ }
474
+ if (!contentPresent && ['host_supplied', 'post_write_disk_read'].includes(availabilityReason)) {
475
+ fail('envelope.content.availabilityReason', 'is inconsistent with absent content');
476
+ }
477
+ if (operation === 'delete' && (contentPresent || availabilityReason !== 'delete_operation')) {
478
+ fail('envelope.content', 'delete operations cannot claim proposed content');
479
+ }
480
+ const facts = record(envelope.facts, 'envelope.facts', [
481
+ 'symbols', 'imports', 'exports', 'relationships', 'references', 'calls', 'boundaries',
482
+ 'parserDepth', 'extractionErrors', 'limitations', 'completeness',
483
+ ], ['symbols', 'imports', 'exports', 'relationships', 'parserDepth', 'extractionErrors']);
484
+ const factArrays = {
485
+ symbols: array(facts.symbols, 'envelope.facts.symbols'),
486
+ imports: array(facts.imports, 'envelope.facts.imports'),
487
+ exports: array(facts.exports, 'envelope.facts.exports'),
488
+ relationships: array(facts.relationships, 'envelope.facts.relationships'),
489
+ references: facts.references === undefined ? [] : array(facts.references, 'envelope.facts.references'),
490
+ calls: facts.calls === undefined ? [] : array(facts.calls, 'envelope.facts.calls'),
491
+ boundaries: facts.boundaries === undefined ? [] : array(facts.boundaries, 'envelope.facts.boundaries'),
492
+ };
493
+ factArrays.symbols.forEach((item, index) => validateSymbol(item, `envelope.facts.symbols[${index}]`, targetPath));
494
+ factArrays.imports.forEach((item, index) => validateImport(item, `envelope.facts.imports[${index}]`, targetPath));
495
+ factArrays.exports.forEach((item, index) => validateExport(item, `envelope.facts.exports[${index}]`, targetPath));
496
+ factArrays.relationships.forEach((item, index) => validateRelationship(item, `envelope.facts.relationships[${index}]`));
497
+ factArrays.references.forEach((item, index) => validateReference(item, `envelope.facts.references[${index}]`, targetPath));
498
+ factArrays.calls.forEach((item, index) => validateCall(item, `envelope.facts.calls[${index}]`, targetPath));
499
+ factArrays.boundaries.forEach((item, index) => validateBoundary(item, `envelope.facts.boundaries[${index}]`, targetPath));
500
+ enumeration(facts.parserDepth, 'envelope.facts.parserDepth', PARSER_DEPTHS);
501
+ validateStringArray(facts.extractionErrors, 'envelope.facts.extractionErrors', sourceFreeMetadata, 64);
502
+ if (facts.limitations !== undefined) {
503
+ validateStringArray(facts.limitations, 'envelope.facts.limitations', sourceFreeMetadata, 64);
504
+ }
505
+ const factStatuses = facts.completeness === undefined
506
+ ? null
507
+ : validateCompleteness(facts.completeness, 'envelope.facts.completeness');
508
+ if (!contentPresent && Object.values(factArrays).some((items) => items.length > 0)) {
509
+ fail('envelope.facts', 'absent proposed content cannot carry proposed source facts');
510
+ }
511
+ if (factStatuses) {
512
+ const complete = (family) => factStatuses.get(family) === 'complete';
513
+ const ast = facts.parserDepth === 'ast' || facts.parserDepth === 'syntax_tree';
514
+ const extractionErrors = facts.extractionErrors;
515
+ for (const family of ['symbol', 'import', 'reference', 'call', 'package', 'service']) {
516
+ if (complete(family) && (!contentPresent || !ast || extractionErrors.length > 0)) {
517
+ fail(`envelope.facts.completeness.${family}`, 'cannot be complete for absent, degraded, or failed extraction');
518
+ }
519
+ }
520
+ if (complete('import')) {
521
+ if (facts.imports === undefined || factArrays.imports.some((item) => {
522
+ const resolution = item.resolution;
523
+ return !['resolved_repository', 'external_package'].includes(String(resolution));
524
+ })) {
525
+ fail('envelope.facts.completeness.import', 'cannot be complete with unresolved proposed imports');
526
+ }
527
+ }
528
+ if (complete('call')) {
529
+ if (facts.calls === undefined || factArrays.calls.some((item) => !['local_symbol', 'imported_symbol', 'repository_symbol'].includes(String(item.resolution)))) {
530
+ fail('envelope.facts.completeness.call', 'cannot be complete with unresolved proposed calls');
531
+ }
532
+ }
533
+ if (complete('reference')) {
534
+ if (facts.references === undefined || factArrays.references.some((item) => !['local_symbol', 'imported_symbol', 'repository_symbol'].includes(String(item.resolution)))) {
535
+ fail('envelope.facts.completeness.reference', 'cannot be complete with unresolved proposed references');
536
+ }
537
+ }
538
+ }
539
+ const host = record(envelope.host, 'envelope.host', ['adapterId', 'capability', 'timing', 'decisionBinding']);
540
+ enumeration(host.adapterId, 'envelope.host.adapterId', ADAPTERS);
541
+ enumeration(host.capability, 'envelope.host.capability', [
542
+ 'hard_prewrite', 'cooperative_prewrite', 'supervised_write', 'post_write', 'ci_only', 'not_supported',
543
+ ]);
544
+ enumeration(host.timing, 'envelope.host.timing', ['before_write', 'during_write', 'after_write', 'ci']);
545
+ enumeration(host.decisionBinding, 'envelope.host.decisionBinding', ['host_enforced', 'cooperative', 'observed']);
546
+ const session = record(envelope.session, 'envelope.session', ['sessionId', 'planRevision']);
547
+ if (session.sessionId !== null)
548
+ id(session.sessionId, 'envelope.session.sessionId');
549
+ nullableInteger(session.planRevision, 'envelope.session.planRevision', 0, 10_000_000);
550
+ const privacy = record(envelope.privacy, 'envelope.privacy', [
551
+ 'sourceUploaded', 'sourceStored', 'diffUploaded', 'promptUploaded', 'chatUploaded', 'rawContentRetained',
552
+ ]);
553
+ for (const key of Object.keys(privacy)) {
554
+ if (privacy[key] !== false)
555
+ fail(`envelope.privacy.${key}`, 'must be false');
556
+ }
557
+ return JSON.parse(JSON.stringify(value));
558
+ }
559
+ function validateProposedChangeEnvelope(value) {
560
+ const envelope = validateEnvelope(value);
561
+ const expected = deriveTrustedHostPosture(envelope.host.adapterId, envelope.host.timing);
562
+ if (expected.capability !== envelope.host.capability
563
+ || expected.decisionBinding !== envelope.host.decisionBinding) {
564
+ fail('envelope.host', 'capability, timing, and decisionBinding are inconsistent');
565
+ }
566
+ return envelope;
567
+ }
568
+ function validateAndBindProposedChangeEnvelope(value, context) {
569
+ const envelope = validateEnvelope(value);
570
+ const trustedPath = normalizeRepositoryRelativePath(context.targetPath, 'trustedContext.targetPath');
571
+ if (envelope.target.path !== trustedPath)
572
+ fail('envelope.target.path', 'does not match trusted event path');
573
+ if (context.operation !== undefined && envelope.target.operation !== context.operation) {
574
+ fail('envelope.target.operation', 'does not match trusted operation');
575
+ }
576
+ if (Object.prototype.hasOwnProperty.call(context, 'previousPath')
577
+ && envelope.target.previousPath !== context.previousPath) {
578
+ fail('envelope.target.previousPath', 'does not match trusted previous path');
579
+ }
580
+ const effectiveHost = deriveTrustedHostPosture(context.adapterId, context.timing);
581
+ if (envelope.host.adapterId !== effectiveHost.adapterId
582
+ || envelope.host.capability !== effectiveHost.capability
583
+ || envelope.host.timing !== effectiveHost.timing
584
+ || envelope.host.decisionBinding !== effectiveHost.decisionBinding) {
585
+ fail('envelope.host', 'caller host posture conflicts with trusted ingress');
586
+ }
587
+ if (Object.prototype.hasOwnProperty.call(context, 'expectedContentHash')
588
+ && envelope.content.contentHash !== context.expectedContentHash) {
589
+ fail('envelope.content.contentHash', 'does not match trusted proposed content');
590
+ }
591
+ for (const [key, expected] of Object.entries(context.repository ?? {})) {
592
+ if (expected !== undefined && envelope.repository[key] !== expected) {
593
+ fail(`envelope.repository.${key}`, 'does not match trusted repository context');
594
+ }
595
+ }
596
+ for (const [key, expected] of Object.entries(context.session ?? {})) {
597
+ if (expected !== undefined && envelope.session[key] !== expected) {
598
+ fail(`envelope.session.${key}`, 'does not match trusted session context');
599
+ }
600
+ }
601
+ return { ...envelope, host: effectiveHost };
602
+ }
603
+ //# sourceMappingURL=proposed-change-validation.js.map