| 2421 | * Accepts injected maps for testing; defaults to the real registries. |
| 2422 | */ |
| 2423 | export function assertEnvRegistryConsistent( |
| 2424 | services: Record< |
| 2425 | string, |
| 2426 | { environments?: Record<string, unknown> } |
| 2427 | > = SERVICES, |
| 2428 | envIdByName: Record<string, string> = ENV_ID_BY_NAME, |
| 2429 | envIds: Record<string, string> = ENV_IDS, |
| 2430 | ): void { |
| 2431 | const problems: string[] = []; |
| 2432 | for (const [key, entry] of Object.entries(services)) { |
| 2433 | for (const env of Object.keys(entry.environments ?? {})) { |
| 2434 | if (!Object.hasOwn(envIdByName, env)) { |
| 2435 | problems.push( |
| 2436 | ` - service "${key}" declares env "${env}", which is not a canonical env name in ENV_ID_BY_NAME — no accessor or redeploy path could ever resolve it`, |
| 2437 | ); |
| 2438 | } |
| 2439 | } |
| 2440 | } |
| 2441 | const byId = new Map<string, string>(); // env-id -> first canonical name |
| 2442 | for (const [name, id] of Object.entries(envIdByName)) { |
| 2443 | const prior = byId.get(id); |
| 2444 | if (prior !== undefined) { |
| 2445 | problems.push( |
| 2446 | ` - duplicate env-id "${id}" in ENV_ID_BY_NAME (canonical names: ${prior}, ${name}) — resolveEnv resolves the FIRST name, silently shadowing the second`, |
| 2447 | ); |
| 2448 | } else { |
| 2449 | byId.set(id, name); |
| 2450 | } |
| 2451 | } |
| 2452 | const spelledIds = new Set(Object.values(envIds)); |
| 2453 | for (const [name, id] of Object.entries(envIdByName)) { |
| 2454 | if (!spelledIds.has(id)) { |
| 2455 | problems.push( |
| 2456 | ` - canonical env "${name}" (env-id "${id}") has no ENV_IDS spelling — resolveEnv can never produce it; register at least its own name in ENV_IDS`, |
| 2457 | ); |
| 2458 | } |
| 2459 | } |
| 2460 | // (iv) Cross-wire: a key in BOTH registries must carry the SAME env-id. |
| 2461 | for (const [key, id] of Object.entries(envIds)) { |
| 2462 | if (Object.hasOwn(envIdByName, key) && envIdByName[key] !== id) { |
| 2463 | problems.push( |
| 2464 | ` - cross-wired env "${key}": ENV_IDS maps it to "${id}" but ENV_ID_BY_NAME maps it to "${envIdByName[key]}" — resolveEnv("${key}") would silently resolve to the OTHER env`, |
| 2465 | ); |
| 2466 | } |
| 2467 | } |
| 2468 | // (v) Orphan spelling: every ENV_IDS env-id must have a canonical name. |
| 2469 | const canonicalIds = new Set(Object.values(envIdByName)); |
| 2470 | for (const [spelling, id] of Object.entries(envIds)) { |
| 2471 | if (!canonicalIds.has(id)) { |
| 2472 | problems.push( |
| 2473 | ` - orphan ENV_IDS spelling "${spelling}": its env-id "${id}" is carried by no ENV_ID_BY_NAME canonical name — resolveEnv would reject it only lazily at call time`, |
| 2474 | ); |
| 2475 | } |
| 2476 | } |
| 2477 | // (vi)+(vii) Key hygiene on both registries: lowercase-normalized and |
| 2478 | // never an Object.prototype property name. |
| 2479 | const protoNames = new Set(Object.getOwnPropertyNames(Object.prototype)); |
| 2480 | const registries: Array<[string, Record<string, string>]> = [ |