@ornexus/neocortex 4.60.66 → 4.60.67
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/release-source.json +1 -1
- package/dist/sbom.cdx.json +1598 -1640
- package/install.ps1 +1 -1
- package/install.sh +2 -1
- package/package.json +10 -7
- package/packages/client/dist/cli.js +15 -2
- package/packages/client/dist/commands/doctor.d.ts +135 -0
- package/packages/client/dist/commands/doctor.js +2 -0
- package/packages/client/dist/commands/invoke.d.ts +18 -0
- package/packages/client/dist/commands/invoke.js +68 -68
- package/packages/client/dist/continuity/sqlite-store.js +1 -1
- package/packages/client/dist/runner/headless/run-yoloop.d.ts +4 -1
- package/packages/client/dist/runner/headless/run-yoloop.js +2 -2
- package/packages/client/dist/runner-cli.js +0 -0
- package/packages/shared/dist/compatibility/index.js +476 -0
- package/packages/shared/dist/yoloop/release-blocked.js +653 -0
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/skill/SKILL.md +3 -2
- package/targets-stubs/claude-code/neocortex-root.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex-root.md +5 -5
- package/targets-stubs/claude-code/neocortex.agent.yaml +1 -1
- package/targets-stubs/claude-code/neocortex.md +8 -9
- package/targets-stubs/codex/AGENTS.md +3 -3
- package/targets-stubs/cursor/agent.md +4 -3
- package/targets-stubs/gemini-cli/agent.md +5 -4
- package/targets-stubs/opencode/neocortex-root.md +3 -3
- package/targets-stubs/vscode/neocortex.agent.md +5 -4
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public compatibility and release-lifecycle contracts.
|
|
3
|
+
*
|
|
4
|
+
* This module is deliberately pure. It accepts only bounded public metadata and
|
|
5
|
+
* never reads protected response fields, project state, clocks, or environment.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
export const COMPATIBILITY_OFFER_SCHEMA = 'CompatibilityOffer/1.0';
|
|
9
|
+
export const COMPATIBILITY_RESULT_SCHEMA = 'CompatibilityResult/1.0';
|
|
10
|
+
export const RELEASE_LIFECYCLE_SCHEMA = 'ReleaseLifecycle/1.0';
|
|
11
|
+
export const RELEASE_LIFECYCLE_CONTEXT_SCHEMA = 'ReleaseLifecycleContext/1.0';
|
|
12
|
+
export const INVOKE_PROTOCOL_VERSION = 'invoke/1';
|
|
13
|
+
export const COMPATIBILITY_CAPABILITIES = Object.freeze({
|
|
14
|
+
INVOKE_CORE: 'invoke-core-v1',
|
|
15
|
+
PUBLIC_SAFE_DIAGNOSTICS: 'public-safe-diagnostics-v1',
|
|
16
|
+
PROTECTED_RESPONSE_GUARD: 'protected-response-guard-v1',
|
|
17
|
+
RELEASE_LIFECYCLE: 'release-lifecycle-v1',
|
|
18
|
+
RELEASE_BLOCKED_TERMINAL: 'release-blocked-terminal-v1',
|
|
19
|
+
REVIEW_RECONCILIATION: 'review-reconciliation-v1',
|
|
20
|
+
});
|
|
21
|
+
export const COMPATIBILITY_SCHEMAS = Object.freeze({
|
|
22
|
+
INVOKE_REQUEST: 'invoke-request/1',
|
|
23
|
+
INVOKE_RESPONSE: 'invoke-response/1',
|
|
24
|
+
RELEASE_LIFECYCLE: RELEASE_LIFECYCLE_SCHEMA,
|
|
25
|
+
RELEASE_BLOCKED: 'ReleaseBlockedResult/1.0',
|
|
26
|
+
REVIEW_RECONCILIATION: 'ReviewReconciliation/1',
|
|
27
|
+
});
|
|
28
|
+
export const COMPATIBILITY_REASON_CODES = Object.freeze({
|
|
29
|
+
COMPATIBLE: 'compatibility-compatible',
|
|
30
|
+
OPTIONAL_PROJECTION_OMITTED: 'compatibility-optional-projection-omitted',
|
|
31
|
+
LEGACY_OFFER_MISSING: 'compatibility-legacy-offer-missing',
|
|
32
|
+
SERVER_NEGOTIATION_MISSING: 'compatibility-server-negotiation-missing',
|
|
33
|
+
OFFER_MALFORMED: 'compatibility-offer-malformed',
|
|
34
|
+
CLIENT_VERSION_UNKNOWN: 'compatibility-client-version-unknown',
|
|
35
|
+
SERVER_VERSION_UNKNOWN: 'compatibility-server-version-unknown',
|
|
36
|
+
PROTOCOL_MISMATCH: 'compatibility-protocol-mismatch',
|
|
37
|
+
REQUIRED_SCHEMA_MISSING: 'compatibility-required-schema-missing',
|
|
38
|
+
REQUIRED_CAPABILITY_MISSING: 'compatibility-required-capability-missing',
|
|
39
|
+
SECURITY_CAPABILITY_MISSING: 'compatibility-security-capability-missing',
|
|
40
|
+
REGISTRY_VERSION_MISMATCH: 'compatibility-registry-version-mismatch',
|
|
41
|
+
REGISTRY_CHECKSUM_MISMATCH: 'compatibility-registry-checksum-mismatch',
|
|
42
|
+
RESULT_MALFORMED: 'compatibility-result-malformed',
|
|
43
|
+
UNNEGOTIATED_PROTECTED_FIELD: 'compatibility-unnegotiated-protected-field',
|
|
44
|
+
});
|
|
45
|
+
const VERSION = /^\d{1,6}\.\d{1,6}\.\d{1,6}(?:[-+][A-Za-z0-9.-]{1,40})?$/u;
|
|
46
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,79}$/u;
|
|
47
|
+
const REGISTRY_VERSION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,31}$/u;
|
|
48
|
+
const SHA256 = /^[a-f0-9]{64}$/u;
|
|
49
|
+
const PROJECT_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{15,127}$/u;
|
|
50
|
+
const MAX_NEGOTIATED_ITEMS = 32;
|
|
51
|
+
function uniqueSorted(values) {
|
|
52
|
+
return [...new Set(values)].sort((left, right) => left.localeCompare(right));
|
|
53
|
+
}
|
|
54
|
+
function hasOnlyKeys(value, allowed) {
|
|
55
|
+
const keys = Object.keys(value);
|
|
56
|
+
return keys.length <= allowed.length && keys.every((key) => allowed.includes(key));
|
|
57
|
+
}
|
|
58
|
+
function isBoundedIdentifiers(value) {
|
|
59
|
+
return Array.isArray(value)
|
|
60
|
+
&& value.length <= MAX_NEGOTIATED_ITEMS
|
|
61
|
+
&& value.every((entry) => typeof entry === 'string' && IDENTIFIER.test(entry));
|
|
62
|
+
}
|
|
63
|
+
export function isCompatibilityOfferV1(value) {
|
|
64
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
65
|
+
return false;
|
|
66
|
+
const offer = value;
|
|
67
|
+
const registry = offer.commandRegistry;
|
|
68
|
+
return hasOnlyKeys(offer, ['schemaVersion', 'clientVersion', 'protocolVersions', 'schemaVersions', 'capabilityIds', 'commandRegistry', 'publicSafe'])
|
|
69
|
+
&& offer.schemaVersion === COMPATIBILITY_OFFER_SCHEMA
|
|
70
|
+
&& typeof offer.clientVersion === 'string' && VERSION.test(offer.clientVersion)
|
|
71
|
+
&& isBoundedIdentifiers(offer.protocolVersions) && offer.protocolVersions.length > 0
|
|
72
|
+
&& isBoundedIdentifiers(offer.schemaVersions) && offer.schemaVersions.length > 0
|
|
73
|
+
&& isBoundedIdentifiers(offer.capabilityIds) && offer.capabilityIds.length > 0
|
|
74
|
+
&& !!registry && typeof registry === 'object' && !Array.isArray(registry)
|
|
75
|
+
&& hasOnlyKeys(registry, ['version', 'checksum'])
|
|
76
|
+
&& typeof registry.version === 'string' && REGISTRY_VERSION.test(registry.version)
|
|
77
|
+
&& typeof registry.checksum === 'string' && SHA256.test(registry.checksum)
|
|
78
|
+
&& offer.publicSafe === true;
|
|
79
|
+
}
|
|
80
|
+
export function createCompatibilityOffer(input) {
|
|
81
|
+
const offer = {
|
|
82
|
+
schemaVersion: COMPATIBILITY_OFFER_SCHEMA,
|
|
83
|
+
clientVersion: input.clientVersion,
|
|
84
|
+
protocolVersions: uniqueSorted(input.protocolVersions ?? [INVOKE_PROTOCOL_VERSION]),
|
|
85
|
+
schemaVersions: uniqueSorted(input.schemaVersions ?? Object.values(COMPATIBILITY_SCHEMAS)),
|
|
86
|
+
capabilityIds: uniqueSorted(input.capabilityIds ?? Object.values(COMPATIBILITY_CAPABILITIES)),
|
|
87
|
+
commandRegistry: { version: input.registryVersion, checksum: input.registryChecksum },
|
|
88
|
+
publicSafe: true,
|
|
89
|
+
};
|
|
90
|
+
if (!isCompatibilityOfferV1(offer))
|
|
91
|
+
throw new Error('compatibility-offer-invalid');
|
|
92
|
+
return Object.freeze({
|
|
93
|
+
...offer,
|
|
94
|
+
protocolVersions: Object.freeze([...offer.protocolVersions]),
|
|
95
|
+
schemaVersions: Object.freeze([...offer.schemaVersions]),
|
|
96
|
+
capabilityIds: Object.freeze([...offer.capabilityIds]),
|
|
97
|
+
commandRegistry: Object.freeze({ ...offer.commandRegistry }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function result(input) {
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
schemaVersion: COMPATIBILITY_RESULT_SCHEMA,
|
|
103
|
+
...input,
|
|
104
|
+
schemaVersions: Object.freeze([...input.schemaVersions]),
|
|
105
|
+
capabilityIds: Object.freeze([...input.capabilityIds]),
|
|
106
|
+
commandRegistry: Object.freeze({ ...input.commandRegistry }),
|
|
107
|
+
projection: Object.freeze({ ...input.projection, omittedFields: Object.freeze([...input.projection.omittedFields]) }),
|
|
108
|
+
reasonCodes: Object.freeze([...input.reasonCodes]),
|
|
109
|
+
publicSafe: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export function negotiateCompatibility(rawOffer, policy) {
|
|
113
|
+
const serverVersionValid = VERSION.test(policy.serverVersion);
|
|
114
|
+
const safeServerVersion = serverVersionValid ? policy.serverVersion : '0.0.0';
|
|
115
|
+
if (!isCompatibilityOfferV1(rawOffer)) {
|
|
116
|
+
const unknownClientVersion = !!rawOffer && typeof rawOffer === 'object' && !Array.isArray(rawOffer)
|
|
117
|
+
&& rawOffer.schemaVersion === COMPATIBILITY_OFFER_SCHEMA
|
|
118
|
+
&& (typeof rawOffer.clientVersion !== 'string'
|
|
119
|
+
|| !VERSION.test(rawOffer.clientVersion));
|
|
120
|
+
return result({
|
|
121
|
+
status: 'incompatible', clientVersion: '0.0.0', serverVersion: safeServerVersion,
|
|
122
|
+
protocolVersion: null, schemaVersions: [], capabilityIds: [],
|
|
123
|
+
commandRegistry: { version: policy.commandRegistry.version, checksum: policy.commandRegistry.checksum, status: 'unknown' },
|
|
124
|
+
projection: { status: 'none', omittedFields: ['releaseBlocked', 'releaseLifecycle', 'reviewReconciliation'] },
|
|
125
|
+
reasonCodes: [unknownClientVersion
|
|
126
|
+
? COMPATIBILITY_REASON_CODES.CLIENT_VERSION_UNKNOWN
|
|
127
|
+
: COMPATIBILITY_REASON_CODES.OFFER_MALFORMED], remediationHint: 'upgrade-client',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const offer = rawOffer;
|
|
131
|
+
const reasons = [];
|
|
132
|
+
const has = (values, expected) => values.includes(expected);
|
|
133
|
+
if (!serverVersionValid)
|
|
134
|
+
reasons.push(COMPATIBILITY_REASON_CODES.SERVER_VERSION_UNKNOWN);
|
|
135
|
+
if (!has(offer.protocolVersions, policy.protocolVersion))
|
|
136
|
+
reasons.push(COMPATIBILITY_REASON_CODES.PROTOCOL_MISMATCH);
|
|
137
|
+
if (policy.requiredSchemaVersions.some((schema) => !has(offer.schemaVersions, schema))) {
|
|
138
|
+
reasons.push(COMPATIBILITY_REASON_CODES.REQUIRED_SCHEMA_MISSING);
|
|
139
|
+
}
|
|
140
|
+
const missingRequiredCapabilities = policy.requiredCapabilityIds.filter((capability) => !has(offer.capabilityIds, capability));
|
|
141
|
+
if (missingRequiredCapabilities.length > 0)
|
|
142
|
+
reasons.push(COMPATIBILITY_REASON_CODES.REQUIRED_CAPABILITY_MISSING);
|
|
143
|
+
if (policy.securityCapabilityIds.some((capability) => !has(offer.capabilityIds, capability))) {
|
|
144
|
+
reasons.push(COMPATIBILITY_REASON_CODES.SECURITY_CAPABILITY_MISSING);
|
|
145
|
+
}
|
|
146
|
+
if (offer.commandRegistry.version !== policy.commandRegistry.version) {
|
|
147
|
+
reasons.push(COMPATIBILITY_REASON_CODES.REGISTRY_VERSION_MISMATCH);
|
|
148
|
+
}
|
|
149
|
+
if (offer.commandRegistry.checksum !== policy.commandRegistry.checksum) {
|
|
150
|
+
reasons.push(COMPATIBILITY_REASON_CODES.REGISTRY_CHECKSUM_MISMATCH);
|
|
151
|
+
}
|
|
152
|
+
const incompatible = reasons.length > 0;
|
|
153
|
+
const supportedSchemas = uniqueSorted(policy.requiredSchemaVersions.concat(policy.optionalSchemaVersions.filter((schema) => has(offer.schemaVersions, schema))));
|
|
154
|
+
const supportedCapabilities = uniqueSorted(policy.requiredCapabilityIds.concat(policy.optionalCapabilityIds.filter((capability) => has(offer.capabilityIds, capability))));
|
|
155
|
+
const omittedFields = [];
|
|
156
|
+
if (!has(offer.capabilityIds, COMPATIBILITY_CAPABILITIES.RELEASE_LIFECYCLE)
|
|
157
|
+
|| !has(offer.schemaVersions, COMPATIBILITY_SCHEMAS.RELEASE_LIFECYCLE))
|
|
158
|
+
omittedFields.push('releaseLifecycle');
|
|
159
|
+
if (!has(offer.capabilityIds, COMPATIBILITY_CAPABILITIES.RELEASE_BLOCKED_TERMINAL)
|
|
160
|
+
|| !has(offer.schemaVersions, COMPATIBILITY_SCHEMAS.RELEASE_BLOCKED))
|
|
161
|
+
omittedFields.push('releaseBlocked');
|
|
162
|
+
if (!has(offer.capabilityIds, COMPATIBILITY_CAPABILITIES.REVIEW_RECONCILIATION)
|
|
163
|
+
|| !has(offer.schemaVersions, COMPATIBILITY_SCHEMAS.REVIEW_RECONCILIATION))
|
|
164
|
+
omittedFields.push('reviewReconciliation');
|
|
165
|
+
if (incompatible) {
|
|
166
|
+
const registryMismatch = reasons.includes(COMPATIBILITY_REASON_CODES.REGISTRY_VERSION_MISMATCH)
|
|
167
|
+
|| reasons.includes(COMPATIBILITY_REASON_CODES.REGISTRY_CHECKSUM_MISMATCH);
|
|
168
|
+
return result({
|
|
169
|
+
status: 'incompatible', clientVersion: offer.clientVersion, serverVersion: safeServerVersion,
|
|
170
|
+
protocolVersion: null, schemaVersions: supportedSchemas, capabilityIds: supportedCapabilities,
|
|
171
|
+
commandRegistry: {
|
|
172
|
+
version: policy.commandRegistry.version, checksum: policy.commandRegistry.checksum,
|
|
173
|
+
status: registryMismatch ? 'mismatched' : 'matched',
|
|
174
|
+
},
|
|
175
|
+
projection: { status: 'none', omittedFields: ['releaseBlocked', 'releaseLifecycle', 'reviewReconciliation'] },
|
|
176
|
+
reasonCodes: uniqueSorted(reasons),
|
|
177
|
+
remediationHint: registryMismatch ? 'align-command-registry' : 'upgrade-client',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const degraded = omittedFields.length > 0;
|
|
181
|
+
return result({
|
|
182
|
+
status: degraded ? 'degraded' : 'compatible',
|
|
183
|
+
clientVersion: offer.clientVersion, serverVersion: safeServerVersion,
|
|
184
|
+
protocolVersion: policy.protocolVersion, schemaVersions: supportedSchemas,
|
|
185
|
+
capabilityIds: supportedCapabilities,
|
|
186
|
+
commandRegistry: { version: policy.commandRegistry.version, checksum: policy.commandRegistry.checksum, status: 'matched' },
|
|
187
|
+
projection: { status: degraded ? 'reduced' : 'full', omittedFields: uniqueSorted(omittedFields) },
|
|
188
|
+
reasonCodes: degraded
|
|
189
|
+
? [COMPATIBILITY_REASON_CODES.OPTIONAL_PROJECTION_OMITTED]
|
|
190
|
+
: [COMPATIBILITY_REASON_CODES.COMPATIBLE],
|
|
191
|
+
remediationHint: degraded ? 'retry-supported-projection' : 'none',
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
export function createLegacyCompatibilityResult(input) {
|
|
195
|
+
return result({
|
|
196
|
+
status: 'degraded',
|
|
197
|
+
clientVersion: input.clientVersion && VERSION.test(input.clientVersion) ? input.clientVersion : '0.0.0',
|
|
198
|
+
serverVersion: VERSION.test(input.serverVersion) ? input.serverVersion : '0.0.0',
|
|
199
|
+
protocolVersion: INVOKE_PROTOCOL_VERSION,
|
|
200
|
+
schemaVersions: [COMPATIBILITY_SCHEMAS.INVOKE_REQUEST, COMPATIBILITY_SCHEMAS.INVOKE_RESPONSE],
|
|
201
|
+
capabilityIds: [COMPATIBILITY_CAPABILITIES.INVOKE_CORE, COMPATIBILITY_CAPABILITIES.PUBLIC_SAFE_DIAGNOSTICS],
|
|
202
|
+
commandRegistry: { version: input.registryVersion, checksum: input.registryChecksum, status: 'unknown' },
|
|
203
|
+
projection: { status: 'reduced', omittedFields: ['releaseBlocked', 'releaseLifecycle', 'reviewReconciliation'] },
|
|
204
|
+
reasonCodes: [input.reason ?? COMPATIBILITY_REASON_CODES.LEGACY_OFFER_MISSING],
|
|
205
|
+
remediationHint: 'retry-supported-projection',
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const COMPATIBILITY_STATUSES = new Set(['compatible', 'degraded', 'incompatible']);
|
|
209
|
+
const PROJECTION_STATUSES = new Set(['full', 'reduced', 'none']);
|
|
210
|
+
const REMEDIATIONS = new Set(['none', 'upgrade-client', 'retry-supported-projection', 'align-command-registry', 'contact-support']);
|
|
211
|
+
const REASONS = new Set(Object.values(COMPATIBILITY_REASON_CODES));
|
|
212
|
+
const OPTIONAL_FIELDS = new Set(['releaseBlocked', 'releaseLifecycle', 'reviewReconciliation']);
|
|
213
|
+
export function isCompatibilityResultV1(value) {
|
|
214
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
215
|
+
return false;
|
|
216
|
+
const candidate = value;
|
|
217
|
+
const registry = candidate.commandRegistry;
|
|
218
|
+
const projection = candidate.projection;
|
|
219
|
+
const status = candidate.status;
|
|
220
|
+
const projectionStatus = projection?.status;
|
|
221
|
+
const remediation = candidate.remediationHint;
|
|
222
|
+
return hasOnlyKeys(candidate, ['schemaVersion', 'status', 'clientVersion', 'serverVersion', 'protocolVersion', 'schemaVersions', 'capabilityIds', 'commandRegistry', 'projection', 'reasonCodes', 'remediationHint', 'publicSafe'])
|
|
223
|
+
&& candidate.schemaVersion === COMPATIBILITY_RESULT_SCHEMA
|
|
224
|
+
&& COMPATIBILITY_STATUSES.has(candidate.status)
|
|
225
|
+
&& typeof candidate.clientVersion === 'string' && VERSION.test(candidate.clientVersion)
|
|
226
|
+
&& typeof candidate.serverVersion === 'string' && VERSION.test(candidate.serverVersion)
|
|
227
|
+
&& (candidate.protocolVersion === null || typeof candidate.protocolVersion === 'string' && IDENTIFIER.test(candidate.protocolVersion))
|
|
228
|
+
&& isBoundedIdentifiers(candidate.schemaVersions) && isBoundedIdentifiers(candidate.capabilityIds)
|
|
229
|
+
&& !!registry && hasOnlyKeys(registry, ['version', 'checksum', 'status'])
|
|
230
|
+
&& typeof registry.version === 'string' && REGISTRY_VERSION.test(registry.version)
|
|
231
|
+
&& typeof registry.checksum === 'string' && SHA256.test(registry.checksum)
|
|
232
|
+
&& ['matched', 'mismatched', 'unknown'].includes(String(registry.status))
|
|
233
|
+
&& !!projection && hasOnlyKeys(projection, ['status', 'omittedFields'])
|
|
234
|
+
&& PROJECTION_STATUSES.has(projectionStatus)
|
|
235
|
+
&& Array.isArray(projection.omittedFields) && projection.omittedFields.length <= 3
|
|
236
|
+
&& projection.omittedFields.every((field) => OPTIONAL_FIELDS.has(field))
|
|
237
|
+
&& Array.isArray(candidate.reasonCodes) && candidate.reasonCodes.length > 0 && candidate.reasonCodes.length <= 12
|
|
238
|
+
&& candidate.reasonCodes.every((reason) => typeof reason === 'string' && REASONS.has(reason))
|
|
239
|
+
&& REMEDIATIONS.has(remediation)
|
|
240
|
+
&& (status !== 'compatible' || projectionStatus === 'full' && remediation === 'none')
|
|
241
|
+
&& (status !== 'degraded' || projectionStatus === 'reduced')
|
|
242
|
+
&& (status !== 'incompatible' || projectionStatus === 'none')
|
|
243
|
+
&& candidate.publicSafe === true;
|
|
244
|
+
}
|
|
245
|
+
/** Omit only explicitly optional fields. Required/security fields are never downgraded. */
|
|
246
|
+
export function projectInvokePayloadForCompatibility(payload, decision) {
|
|
247
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload) || decision.status === 'incompatible')
|
|
248
|
+
return payload;
|
|
249
|
+
const root = { ...payload };
|
|
250
|
+
if (root.metadata && typeof root.metadata === 'object' && !Array.isArray(root.metadata)) {
|
|
251
|
+
const metadata = { ...root.metadata };
|
|
252
|
+
for (const field of decision.projection.omittedFields) {
|
|
253
|
+
delete metadata[field];
|
|
254
|
+
if (field === 'releaseLifecycle')
|
|
255
|
+
delete metadata.release_lifecycle;
|
|
256
|
+
if (field === 'releaseBlocked') {
|
|
257
|
+
delete metadata.release_blocked;
|
|
258
|
+
delete metadata.yoloopReleaseBlocked;
|
|
259
|
+
delete metadata.releaseBlockedReason;
|
|
260
|
+
delete metadata.releaseBlockedResume;
|
|
261
|
+
}
|
|
262
|
+
if (field === 'reviewReconciliation') {
|
|
263
|
+
delete metadata.review_reconciliation;
|
|
264
|
+
delete metadata.reconciliationMetadata;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
root.metadata = metadata;
|
|
268
|
+
}
|
|
269
|
+
for (const field of decision.projection.omittedFields) {
|
|
270
|
+
delete root[field];
|
|
271
|
+
if (field === 'releaseLifecycle')
|
|
272
|
+
delete root.release_lifecycle;
|
|
273
|
+
if (field === 'releaseBlocked')
|
|
274
|
+
delete root.release_blocked;
|
|
275
|
+
if (field === 'reviewReconciliation') {
|
|
276
|
+
delete root.review_reconciliation;
|
|
277
|
+
delete root.reconciliationMetadata;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return root;
|
|
281
|
+
}
|
|
282
|
+
function releaseLifecycleContextDigest(input) {
|
|
283
|
+
return createHash('sha256')
|
|
284
|
+
.update('release-lifecycle-context-v1\0')
|
|
285
|
+
.update(input.projectId)
|
|
286
|
+
.update('\0')
|
|
287
|
+
.update(input.candidateId)
|
|
288
|
+
.update('\0')
|
|
289
|
+
.update(input.lineageSha256)
|
|
290
|
+
.digest('hex');
|
|
291
|
+
}
|
|
292
|
+
export function createReleaseLifecycleContext(input) {
|
|
293
|
+
const context = {
|
|
294
|
+
schemaVersion: RELEASE_LIFECYCLE_CONTEXT_SCHEMA,
|
|
295
|
+
projectId: input.projectId,
|
|
296
|
+
candidateId: input.candidateId,
|
|
297
|
+
lineageSha256: input.lineageSha256,
|
|
298
|
+
contextDigestSha256: releaseLifecycleContextDigest(input),
|
|
299
|
+
publicSafe: true,
|
|
300
|
+
};
|
|
301
|
+
if (!isReleaseLifecycleContextV1(context))
|
|
302
|
+
throw new Error('release-lifecycle-context-invalid');
|
|
303
|
+
return Object.freeze(context);
|
|
304
|
+
}
|
|
305
|
+
export function isReleaseLifecycleContextV1(value) {
|
|
306
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
307
|
+
return false;
|
|
308
|
+
const context = value;
|
|
309
|
+
if (!hasOnlyKeys(context, [
|
|
310
|
+
'schemaVersion', 'projectId', 'candidateId', 'lineageSha256', 'contextDigestSha256', 'publicSafe',
|
|
311
|
+
])
|
|
312
|
+
|| context.schemaVersion !== RELEASE_LIFECYCLE_CONTEXT_SCHEMA
|
|
313
|
+
|| typeof context.projectId !== 'string' || !PROJECT_ID.test(context.projectId)
|
|
314
|
+
|| typeof context.candidateId !== 'string' || !CANDIDATE_ID.test(context.candidateId)
|
|
315
|
+
|| typeof context.lineageSha256 !== 'string' || !SHA256.test(context.lineageSha256)
|
|
316
|
+
|| typeof context.contextDigestSha256 !== 'string' || !SHA256.test(context.contextDigestSha256)
|
|
317
|
+
|| context.publicSafe !== true)
|
|
318
|
+
return false;
|
|
319
|
+
return context.contextDigestSha256 === releaseLifecycleContextDigest({
|
|
320
|
+
projectId: context.projectId,
|
|
321
|
+
candidateId: context.candidateId,
|
|
322
|
+
lineageSha256: context.lineageSha256,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
export const RELEASE_LIFECYCLE_REASON_CODES = Object.freeze({
|
|
326
|
+
VERIFIED: 'release-evidence-verified',
|
|
327
|
+
NOT_SATISFIED: 'release-gate-not-satisfied',
|
|
328
|
+
EVIDENCE_UNAVAILABLE: 'release-evidence-unavailable',
|
|
329
|
+
MALFORMED: 'release-lifecycle-malformed',
|
|
330
|
+
CANDIDATE_MISMATCH: 'release-candidate-mismatch',
|
|
331
|
+
LINEAGE_MISMATCH: 'release-lineage-mismatch',
|
|
332
|
+
FRESHNESS_STALE: 'release-evidence-stale',
|
|
333
|
+
FRESHNESS_UNKNOWN: 'release-evidence-freshness-unknown',
|
|
334
|
+
FUTURE_OBSERVATION: 'release-evidence-future-observation',
|
|
335
|
+
EVIDENCE_REF_INVALID: 'release-evidence-ref-invalid',
|
|
336
|
+
EVIDENCE_REPLAYED: 'release-evidence-replayed',
|
|
337
|
+
IMPLEMENTATION_RECEIPT_MISSING: 'release-implementation-receipt-missing',
|
|
338
|
+
BETA_RECEIPT_MISSING: 'release-beta-receipt-missing',
|
|
339
|
+
RELEASE_GATE_RECEIPT_MISSING: 'release-gate-receipt-missing',
|
|
340
|
+
PROMOTION_RECEIPT_MISSING: 'release-promotion-receipt-missing',
|
|
341
|
+
RELEASE_READY_PREREQUISITE_MISSING: 'release-ready-prerequisite-missing',
|
|
342
|
+
PRODUCTION_PREREQUISITE_MISSING: 'release-production-prerequisite-missing',
|
|
343
|
+
});
|
|
344
|
+
const CANDIDATE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
345
|
+
const EVIDENCE_REF = /^(?:local-gate|beta-receipt|release-gate|promotion-receipt|sha256):[a-f0-9]{64}$/u;
|
|
346
|
+
const RELEASE_REASONS = new Set(Object.values(RELEASE_LIFECYCLE_REASON_CODES));
|
|
347
|
+
const DIMENSIONS = Object.freeze([
|
|
348
|
+
'implementationComplete', 'betaOperable', 'releaseReady', 'productionPromoted',
|
|
349
|
+
]);
|
|
350
|
+
function validIso(value) {
|
|
351
|
+
return typeof value === 'string' && value.length >= 20 && value.length <= 40
|
|
352
|
+
&& Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
|
|
353
|
+
}
|
|
354
|
+
function dimensionShape(value) {
|
|
355
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
356
|
+
return false;
|
|
357
|
+
const dimension = value;
|
|
358
|
+
return hasOnlyKeys(dimension, ['value', 'reasonCodes', 'evidenceRefs', 'candidateId', 'lineageSha256', 'observedAt', 'maxAgeSeconds', 'freshness'])
|
|
359
|
+
&& (typeof dimension.value === 'boolean' || dimension.value === 'unknown')
|
|
360
|
+
&& Array.isArray(dimension.reasonCodes) && dimension.reasonCodes.length > 0 && dimension.reasonCodes.length <= 8
|
|
361
|
+
&& dimension.reasonCodes.every((reason) => typeof reason === 'string' && RELEASE_REASONS.has(reason))
|
|
362
|
+
&& Array.isArray(dimension.evidenceRefs) && dimension.evidenceRefs.length <= 8
|
|
363
|
+
&& dimension.evidenceRefs.every((ref) => typeof ref === 'string' && ref.length <= 96)
|
|
364
|
+
&& typeof dimension.candidateId === 'string' && CANDIDATE_ID.test(dimension.candidateId)
|
|
365
|
+
&& typeof dimension.lineageSha256 === 'string' && SHA256.test(dimension.lineageSha256)
|
|
366
|
+
&& (dimension.observedAt === undefined || validIso(dimension.observedAt))
|
|
367
|
+
&& (dimension.maxAgeSeconds === undefined
|
|
368
|
+
|| Number.isSafeInteger(dimension.maxAgeSeconds) && dimension.maxAgeSeconds >= 1
|
|
369
|
+
&& dimension.maxAgeSeconds <= 31_536_000)
|
|
370
|
+
&& (dimension.freshness === undefined || ['fresh', 'stale', 'unknown'].includes(String(dimension.freshness)));
|
|
371
|
+
}
|
|
372
|
+
function freshness(dimension, evaluatedAtMs) {
|
|
373
|
+
if (!dimension.observedAt || !dimension.maxAgeSeconds)
|
|
374
|
+
return 'unknown';
|
|
375
|
+
const observedAtMs = Date.parse(dimension.observedAt);
|
|
376
|
+
if (observedAtMs > evaluatedAtMs)
|
|
377
|
+
return 'unknown';
|
|
378
|
+
return evaluatedAtMs - observedAtMs <= dimension.maxAgeSeconds * 1_000 ? 'fresh' : 'stale';
|
|
379
|
+
}
|
|
380
|
+
function hasEvidencePrefix(dimension, prefix) {
|
|
381
|
+
return dimension.evidenceRefs.some((ref) => ref.startsWith(`${prefix}:`));
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Validate exact-candidate monotonic release truth without inferring any higher
|
|
385
|
+
* dimension. Invalid true claims are rejected rather than normalized to false.
|
|
386
|
+
*/
|
|
387
|
+
export function validateReleaseLifecycle(input) {
|
|
388
|
+
const fail = (reasons) => Object.freeze({
|
|
389
|
+
accepted: false,
|
|
390
|
+
reasonCodes: Object.freeze(uniqueSorted(reasons)),
|
|
391
|
+
publicSafe: true,
|
|
392
|
+
});
|
|
393
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
394
|
+
return fail([RELEASE_LIFECYCLE_REASON_CODES.MALFORMED]);
|
|
395
|
+
const lifecycle = input;
|
|
396
|
+
if (!hasOnlyKeys(lifecycle, ['schemaVersion', 'candidateId', 'lineageSha256', 'evaluatedAt', ...DIMENSIONS, 'publicSafe'])
|
|
397
|
+
|| lifecycle.schemaVersion !== RELEASE_LIFECYCLE_SCHEMA
|
|
398
|
+
|| typeof lifecycle.candidateId !== 'string' || !CANDIDATE_ID.test(lifecycle.candidateId)
|
|
399
|
+
|| typeof lifecycle.lineageSha256 !== 'string' || !SHA256.test(lifecycle.lineageSha256)
|
|
400
|
+
|| !validIso(lifecycle.evaluatedAt) || lifecycle.publicSafe !== true
|
|
401
|
+
|| DIMENSIONS.some((name) => !dimensionShape(lifecycle[name]))) {
|
|
402
|
+
return fail([RELEASE_LIFECYCLE_REASON_CODES.MALFORMED]);
|
|
403
|
+
}
|
|
404
|
+
const typed = lifecycle;
|
|
405
|
+
const evaluatedAtMs = Date.parse(typed.evaluatedAt);
|
|
406
|
+
const reasons = [];
|
|
407
|
+
const seenEvidence = new Set();
|
|
408
|
+
const projected = {};
|
|
409
|
+
for (const name of DIMENSIONS) {
|
|
410
|
+
const dimension = typed[name];
|
|
411
|
+
if (dimension.candidateId !== typed.candidateId)
|
|
412
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.CANDIDATE_MISMATCH);
|
|
413
|
+
if (dimension.lineageSha256 !== typed.lineageSha256)
|
|
414
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.LINEAGE_MISMATCH);
|
|
415
|
+
if (dimension.evidenceRefs.some((ref) => !EVIDENCE_REF.test(ref)))
|
|
416
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.EVIDENCE_REF_INVALID);
|
|
417
|
+
for (const ref of dimension.evidenceRefs) {
|
|
418
|
+
if (seenEvidence.has(ref))
|
|
419
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.EVIDENCE_REPLAYED);
|
|
420
|
+
seenEvidence.add(ref);
|
|
421
|
+
}
|
|
422
|
+
const evidenceFreshness = freshness(dimension, evaluatedAtMs);
|
|
423
|
+
const suppliedFreshness = dimension.freshness;
|
|
424
|
+
if (suppliedFreshness !== undefined && suppliedFreshness !== evidenceFreshness) {
|
|
425
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.MALFORMED);
|
|
426
|
+
}
|
|
427
|
+
if (dimension.observedAt && Date.parse(dimension.observedAt) > evaluatedAtMs) {
|
|
428
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.FUTURE_OBSERVATION);
|
|
429
|
+
}
|
|
430
|
+
if (dimension.value === true && evidenceFreshness === 'stale')
|
|
431
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.FRESHNESS_STALE);
|
|
432
|
+
if (dimension.value === true && evidenceFreshness === 'unknown')
|
|
433
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.FRESHNESS_UNKNOWN);
|
|
434
|
+
projected[name] = Object.freeze({
|
|
435
|
+
...dimension,
|
|
436
|
+
reasonCodes: Object.freeze([...dimension.reasonCodes]),
|
|
437
|
+
evidenceRefs: Object.freeze([...dimension.evidenceRefs]),
|
|
438
|
+
freshness: evidenceFreshness,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (typed.implementationComplete.value === true && !hasEvidencePrefix(typed.implementationComplete, 'local-gate')) {
|
|
442
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.IMPLEMENTATION_RECEIPT_MISSING);
|
|
443
|
+
}
|
|
444
|
+
if (typed.betaOperable.value === true && !hasEvidencePrefix(typed.betaOperable, 'beta-receipt')) {
|
|
445
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.BETA_RECEIPT_MISSING);
|
|
446
|
+
}
|
|
447
|
+
if (typed.releaseReady.value === true) {
|
|
448
|
+
if (typed.implementationComplete.value !== true || typed.betaOperable.value !== true) {
|
|
449
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.RELEASE_READY_PREREQUISITE_MISSING);
|
|
450
|
+
}
|
|
451
|
+
if (!hasEvidencePrefix(typed.releaseReady, 'release-gate')) {
|
|
452
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.RELEASE_GATE_RECEIPT_MISSING);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (typed.productionPromoted.value === true) {
|
|
456
|
+
if (typed.releaseReady.value !== true)
|
|
457
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.PRODUCTION_PREREQUISITE_MISSING);
|
|
458
|
+
if (!hasEvidencePrefix(typed.productionPromoted, 'promotion-receipt')) {
|
|
459
|
+
reasons.push(RELEASE_LIFECYCLE_REASON_CODES.PROMOTION_RECEIPT_MISSING);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (reasons.length > 0)
|
|
463
|
+
return fail(reasons);
|
|
464
|
+
const projection = Object.freeze({
|
|
465
|
+
schemaVersion: RELEASE_LIFECYCLE_SCHEMA,
|
|
466
|
+
candidateId: typed.candidateId,
|
|
467
|
+
lineageSha256: typed.lineageSha256,
|
|
468
|
+
evaluatedAt: typed.evaluatedAt,
|
|
469
|
+
implementationComplete: projected.implementationComplete,
|
|
470
|
+
betaOperable: projected.betaOperable,
|
|
471
|
+
releaseReady: projected.releaseReady,
|
|
472
|
+
productionPromoted: projected.productionPromoted,
|
|
473
|
+
publicSafe: true,
|
|
474
|
+
});
|
|
475
|
+
return Object.freeze({ accepted: true, reasonCodes: Object.freeze([]), projection, publicSafe: true });
|
|
476
|
+
}
|