OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / scripts / verify-blind-ecosystem-migration.mjs

609 lines · javascript ref:
#!/usr/bin/env node

import fs from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import {
  LEGACY_RETIREMENT_EVIDENCE_BLOCKERS,
  evaluateLegacyRetirement,
  loadLegacyRetirementPolicy,
  productionRetirementBlockers
} from './verify-blind-legacy-retirement-policy.mjs'

const here = path.dirname(fileURLToPath(import.meta.url))

export const ECOSYSTEM_MIGRATION_MATRIX_PATH = path.resolve(
  here,
  '../deploy/blind/ecosystem-migration-matrix.json'
)

const GATE_IDS = Object.freeze(['G0', 'G1', 'G2', 'G3', 'G4', 'G5', 'G6', 'G7', 'G8'])
const FAMILY_IDS = Object.freeze(['CELL', 'INBOX', 'CORE', 'FORWARD'])
const BOUNDARY_IDS = Object.freeze([
  'blind-relay-base',
  'application-client',
  'nonblind-provider',
  'nonblind-index',
  'operator-control'
])
const EXTERNAL_DEPENDENCY_IDS = Object.freeze([
  'client-derived-index',
  'signed-app-catalog',
  'native-push-bridge',
  'semantic-index-provider',
  'ai-compute-provider',
  'payment-settlement-provider',
  'mobility-provider',
  'realtime-media-provider',
  'device-automation-provider',
  'sports-data-provider',
  'git-ci-provider',
  'identity-arbitration-provider',
  'operator-admin-plane',
  'operator-accounting-plane'
])
const EXTERNAL_DEPENDENCY_BOUNDARIES = Object.freeze({
  'client-derived-index': 'application-client',
  'signed-app-catalog': 'application-client',
  'native-push-bridge': 'nonblind-provider',
  'semantic-index-provider': 'nonblind-index',
  'ai-compute-provider': 'nonblind-provider',
  'payment-settlement-provider': 'nonblind-provider',
  'mobility-provider': 'nonblind-provider',
  'realtime-media-provider': 'nonblind-provider',
  'device-automation-provider': 'nonblind-provider',
  'sports-data-provider': 'nonblind-provider',
  'git-ci-provider': 'nonblind-provider',
  'identity-arbitration-provider': 'nonblind-provider',
  'operator-admin-plane': 'operator-control',
  'operator-accounting-plane': 'operator-control'
})
const ROLLOUT_TRACK_IDS = Object.freeze([
  'data-log-consumer',
  'content-publication',
  'marketplace-transaction',
  'private-coordination',
  'realtime-application',
  'provider-mediated',
  'registry-distribution',
  'operator-infrastructure'
])
const COMPONENT_IDS = Object.freeze([
  'legacy-service-plane',
  'outboxlog',
  'witness-repair',
  'shard-custody',
  'seed-storage-gateway',
  'catalog-federation-index',
  'notify',
  'poker',
  'compute-trust-services',
  'legacy-circuit-forward',
  'edge-transports',
  'legacy-client-verifier',
  'operator-control-economics',
  'peerit-legacy-consumer-plane'
])
const APPLICATION_IDS = Object.freeze([
  'peerit',
  'p2pbuilders',
  'pearfeed',
  'bazaar',
  'exchange',
  'rides',
  'stays',
  'comms',
  'dealroom',
  'home',
  'openclaw',
  'pos',
  'sahifa',
  'sanduq',
  'tickets',
  'matchday-mesh',
  'pearpaste',
  'peartube',
  'ultimate-sports',
  'pearcup',
  'hiveworm',
  'pear-registry',
  'opengit',
  'anongpt',
  'platforms'
])
const EVIDENCE_IDS = Object.freeze([
  'final-authorities',
  'all-family-runtime',
  'blindness-sentinel',
  'semantic-parity',
  'durability-scale',
  'independent-operators',
  'cross-runtime-consumers',
  'migration-restore',
  'zero-legacy-traffic'
])
const EVIDENCE_BY_GATE = Object.freeze(Object.fromEntries(
  GATE_IDS.map((gate, index) => [gate, EVIDENCE_IDS[index]])
))

const FINAL_AUTHORITY_PRODUCTION_SOURCES = new Set([
  'blind-protocol.ABI_RELEASE_BLOCKERS',
  'blind-ipc.PRIVATE_IPC_STATUS.releaseBlockers',
  'blind-client.BLIND_CLIENT_BROWSER_ARTIFACT_STATUS.releaseBlockers',
  'blind-daemon.BLIND_CELL_STORAGE_PRODUCTION_BLOCKERS',
  'blind-daemon.BLIND_INBOX_STORAGE_BLOCKERS',
  'blind-daemon.BLIND_CORE_STORAGE_BLOCKERS'
])
const FINAL_AUTHORITY_RUNTIME_CODES = new Set([
  'FINAL_BUILD_PROFILE_LOCAL_BINDING_UNASSEMBLED',
  'TWO_SLOT_MANIFEST_RUNTIME_INTEGRATION_UNASSEMBLED',
  'DESCRIPTOR_REFRESH_PERSISTED_FLOOR_UNASSEMBLED'
])
const CROSS_RUNTIME_PRODUCTION_SOURCES = new Set([
  'blind-protocol.ABI_RELEASE_BLOCKERS',
  'blind-client.BLIND_CLIENT_BROWSER_ARTIFACT_STATUS.releaseBlockers'
])
const BROWSER_ARTIFACT_PRODUCTION_SOURCE =
  'blind-client.BLIND_CLIENT_BROWSER_ARTIFACT_STATUS.releaseBlockers'

// A row cannot become eligible merely because the shared gates turn green.
// Each row remains blocked until its own executable parity authority replaces
// this code-owned placeholder. Nothing in deployment JSON can clear it.
export const APPLICATION_MIGRATION_EVIDENCE_BLOCKERS = Object.freeze(Object.fromEntries(
  APPLICATION_IDS.map(id => [id, Object.freeze([
    `APPLICATION_${blockerToken(id)}_SEMANTIC_PARITY_AUTHORITY_UNASSEMBLED`
  ])])
))

export const COMPONENT_MIGRATION_EVIDENCE_BLOCKERS = Object.freeze(Object.fromEntries(
  COMPONENT_IDS.map(id => [id, Object.freeze([
    `COMPONENT_${blockerToken(id)}_MIGRATION_PARITY_AUTHORITY_UNASSEMBLED`
  ])])
))

export async function loadEcosystemMigrationMatrix (
  matrixPath = ECOSYSTEM_MIGRATION_MATRIX_PATH,
  retirementPolicy
) {
  const bytes = await fs.readFile(matrixPath)
  if (bytes.byteLength === 0 || bytes.byteLength > 2 * 1024 * 1024) {
    matrixFailure('ecosystem migration matrix must be 1..2097152 bytes')
  }
  let matrix
  try {
    matrix = JSON.parse(bytes.toString('utf8'))
  } catch (error) {
    matrixFailure(`ecosystem migration matrix is not valid JSON: ${error.message}`)
  }
  const policy = retirementPolicy || await loadLegacyRetirementPolicy()
  return validateEcosystemMigrationMatrix(matrix, policy)
}

export function validateEcosystemMigrationMatrix (matrix, retirementPolicy) {
  assertPlainObject(matrix, 'matrix')
  assertAllowedKeys(matrix, [
    'matrixVersion',
    'matrixId',
    'decisionAuthority',
    'authorizationRule',
    'boundaries',
    'externalDependencies',
    'rolloutTracks',
    'legacyComponents',
    'applications',
    'evidenceProfiles',
    'evidenceCatalog'
  ], 'matrix')
  rejectAuthorizationBooleans(matrix)
  if (matrix.matrixVersion !== 1) matrixFailure('matrixVersion must equal 1')
  if (matrix.matrixId !== 'hiverelay.blind.ecosystem-migration.v1') {
    matrixFailure('matrixId does not match the registered ecosystem migration matrix')
  }
  if (matrix.decisionAuthority !== 'computed-code-owned-blockers-only') {
    matrixFailure('decisionAuthority must remain computed-code-owned-blockers-only')
  }

  assertPlainObject(matrix.authorizationRule, 'authorizationRule')
  assertAllowedKeys(matrix.authorizationRule, [
    'computedBy', 'editableDecisionFields', 'legacyRemovalPolicy', 'rule'
  ], 'authorizationRule')
  if (matrix.authorizationRule.computedBy !== 'scripts/verify-blind-ecosystem-migration.mjs') {
    matrixFailure('authorizationRule.computedBy does not name the registered verifier')
  }
  if (matrix.authorizationRule.editableDecisionFields !== 'forbidden') {
    matrixFailure('authorizationRule.editableDecisionFields must equal forbidden')
  }
  if (matrix.authorizationRule.legacyRemovalPolicy !== 'deploy/blind/legacy-retirement-policy.json') {
    matrixFailure('authorizationRule.legacyRemovalPolicy does not name the registered policy')
  }
  assertNonEmptyString(matrix.authorizationRule.rule, 'authorizationRule.rule')

  assertExactIdRows(matrix.boundaries, BOUNDARY_IDS, 'boundaries', row => {
    const keys = ['id', 'allowedSurface', 'owner', 'visibility']
    if (row.id === 'blind-relay-base') keys.push('forbiddenResponsibilities')
    assertAllowedKeys(row, keys, `boundary ${row.id}`)
    assertNonEmptyStringArray(row.allowedSurface, `boundary ${row.id}.allowedSurface`)
    assertNonEmptyString(row.owner, `boundary ${row.id}.owner`)
    assertNonEmptyString(row.visibility, `boundary ${row.id}.visibility`)
    if (row.id === 'blind-relay-base') {
      assertExactStringArray(
        row.allowedSurface,
        ['DESCRIBE', ...FAMILY_IDS],
        'blind-relay-base.allowedSurface'
      )
      assertNonEmptyStringArray(row.forbiddenResponsibilities, 'blind-relay-base.forbiddenResponsibilities')
    }
  })

  assertExactIdRows(matrix.externalDependencies, EXTERNAL_DEPENDENCY_IDS, 'externalDependencies', row => {
    assertAllowedKeys(row, ['id', 'boundary', 'purpose'], `external dependency ${row.id}`)
    if (row.boundary !== EXTERNAL_DEPENDENCY_BOUNDARIES[row.id]) {
      matrixFailure(`external dependency ${row.id} is assigned to the wrong boundary`)
    }
    if (row.boundary === 'blind-relay-base') {
      matrixFailure(`external dependency ${row.id} cannot be assigned to blind-relay-base`)
    }
    assertNonEmptyString(row.purpose, `external dependency ${row.id}.purpose`)
  })

  assertExactIdRows(matrix.rolloutTracks, ROLLOUT_TRACK_IDS, 'rolloutTracks', row => {
    assertAllowedKeys(row, ['id', 'sequence'], `rollout track ${row.id}`)
    assertNonEmptyStringArray(row.sequence, `rollout track ${row.id}.sequence`)
    if (row.sequence[0] !== 'inventory' || row.sequence.at(-1) !== 'legacy-drain') {
      matrixFailure(`rollout track ${row.id} must begin at inventory and end at legacy-drain`)
    }
  })

  assertRetirementPolicy(retirementPolicy)
  const retirementComponents = new Map(retirementPolicy.components.map(row => [row.id, row]))
  const dependencyIds = new Set(EXTERNAL_DEPENDENCY_IDS)
  const rolloutIds = new Set(ROLLOUT_TRACK_IDS)
  assertExactIdRows(matrix.legacyComponents, COMPONENT_IDS, 'legacyComponents', row => {
    assertAllowedKeys(row, [
      'id',
      'semanticOwner',
      'families',
      'outsideBaseDependencies',
      'retainedSemantics',
      'rolloutTrack',
      'cutoverGates',
      'requiredEvidenceIds'
    ], `legacy component ${row.id}`)
    assertNonEmptyString(row.semanticOwner, `legacy component ${row.id}.semanticOwner`)
    assertFamilyArray(row.families, `legacy component ${row.id}.families`, row.id === 'operator-control-economics')
    assertReferences(row.outsideBaseDependencies, dependencyIds, `legacy component ${row.id}.outsideBaseDependencies`, true)
    assertNonEmptyStringArray(row.retainedSemantics, `legacy component ${row.id}.retainedSemantics`)
    if (!rolloutIds.has(row.rolloutTrack)) matrixFailure(`legacy component ${row.id} has an unknown rolloutTrack`)
    const retirementRow = retirementComponents.get(row.id)
    const expectedCutoverGates = retirementRow.deletionGates.filter(gate => gate !== 'G8')
    assertExactStringArray(row.cutoverGates, expectedCutoverGates, `legacy component ${row.id}.cutoverGates`)
    assertExactStringArray(
      row.requiredEvidenceIds,
      expectedCutoverGates.map(gate => EVIDENCE_BY_GATE[gate]),
      `legacy component ${row.id}.requiredEvidenceIds`
    )
  })

  assertExactIdRows(matrix.evidenceCatalog, EVIDENCE_IDS, 'evidenceCatalog', (row, index) => {
    assertAllowedKeys(row, ['id', 'gate'], `evidence ${row.id}`)
    if (row.gate !== GATE_IDS[index]) matrixFailure(`evidence ${row.id} is bound to the wrong gate`)
  })
  assertExactIdRows(matrix.evidenceProfiles, ['full-application-cutover'], 'evidenceProfiles', row => {
    assertAllowedKeys(row, ['id', 'requiredGates', 'requiredEvidenceIds'], `evidence profile ${row.id}`)
    assertExactStringArray(row.requiredGates, GATE_IDS.slice(0, 8), `evidence profile ${row.id}.requiredGates`)
    assertExactStringArray(row.requiredEvidenceIds, EVIDENCE_IDS.slice(0, 8),
      `evidence profile ${row.id}.requiredEvidenceIds`)
  })

  const componentIds = new Set(COMPONENT_IDS)
  assertExactIdRows(matrix.applications, APPLICATION_IDS, 'applications', row => {
    assertAllowedKeys(row, [
      'id',
      'name',
      'families',
      'retainedSemantics',
      'outsideBaseDependencies',
      'legacyDependencies',
      'rolloutTrack',
      'evidenceProfile'
    ], `application ${row.id}`)
    assertNonEmptyString(row.name, `application ${row.id}.name`)
    assertFamilyArray(row.families, `application ${row.id}.families`)
    assertNonEmptyStringArray(row.retainedSemantics, `application ${row.id}.retainedSemantics`)
    assertReferences(row.outsideBaseDependencies, dependencyIds, `application ${row.id}.outsideBaseDependencies`, true)
    assertReferences(row.legacyDependencies, componentIds, `application ${row.id}.legacyDependencies`, true)
    if (!rolloutIds.has(row.rolloutTrack)) matrixFailure(`application ${row.id} has an unknown rolloutTrack`)
    if (row.evidenceProfile !== 'full-application-cutover') {
      matrixFailure(`application ${row.id} must use full-application-cutover evidence`)
    }
  })
  return matrix
}

export function ecosystemMigrationGateReports () {
  const productionBlockers = productionRetirementBlockers()
  const retirementEvidenceByGate = new Map(GATE_IDS.map(gate => [gate, []]))
  for (const code of LEGACY_RETIREMENT_EVIDENCE_BLOCKERS) {
    const match = /^(G[2-8])_/.exec(code)
    if (!match) matrixFailure(`retirement evidence blocker has no registered gate: ${code}`)
    retirementEvidenceByGate.get(match[1]).push(blocker(
      'blind-legacy-retirement.LEGACY_RETIREMENT_EVIDENCE_BLOCKERS',
      code
    ))
  }

  const gateBlockers = {
    G0: productionBlockers.filter(row =>
      FINAL_AUTHORITY_PRODUCTION_SOURCES.has(row.source) || FINAL_AUTHORITY_RUNTIME_CODES.has(row.code)
    ),
    G1: productionBlockers.filter(row => row.source !== BROWSER_ARTIFACT_PRODUCTION_SOURCE),
    G2: retirementEvidenceByGate.get('G2'),
    G3: retirementEvidenceByGate.get('G3'),
    G4: retirementEvidenceByGate.get('G4'),
    G5: retirementEvidenceByGate.get('G5'),
    G6: [
      ...productionBlockers.filter(row => CROSS_RUNTIME_PRODUCTION_SOURCES.has(row.source)),
      ...retirementEvidenceByGate.get('G6')
    ],
    G7: retirementEvidenceByGate.get('G7'),
    G8: retirementEvidenceByGate.get('G8')
  }

  return Object.freeze(GATE_IDS.map(gate => {
    const blockers = uniqueBlockers(gateBlockers[gate])
    return Object.freeze({
      gate,
      evidenceId: EVIDENCE_BY_GATE[gate],
      status: blockers.length === 0 ? 'satisfied' : 'blocked',
      satisfied: blockers.length === 0,
      blockerCount: blockers.length,
      blockers
    })
  }))
}

export function evaluateEcosystemMigration (matrix, retirementPolicy) {
  validateEcosystemMigrationMatrix(matrix, retirementPolicy)
  const gates = ecosystemMigrationGateReports()
  const gatesById = new Map(gates.map(row => [row.gate, row]))
  const profile = matrix.evidenceProfiles[0]

  const applications = Object.freeze(matrix.applications.map(row => {
    const rowBlockers = APPLICATION_MIGRATION_EVIDENCE_BLOCKERS[row.id].map(code => blocker(
      `blind-ecosystem-migration.APPLICATION_MIGRATION_EVIDENCE_BLOCKERS.${row.id}`,
      code
    ))
    const blockers = uniqueBlockers([
      ...profile.requiredGates.flatMap(gate => gatesById.get(gate).blockers),
      ...rowBlockers
    ])
    const blockedGates = profile.requiredGates.filter(gate => !gatesById.get(gate).satisfied)
    return Object.freeze({
      id: row.id,
      status: blockers.length === 0 ? 'cutover-authorized' : 'cutover-blocked',
      cutoverAuthorized: blockers.length === 0,
      rolloutTrack: row.rolloutTrack,
      families: Object.freeze([...row.families]),
      blockedGates: Object.freeze(blockedGates),
      blockerCount: blockers.length,
      blockers
    })
  }))

  const retirementReport = evaluateLegacyRetirement(retirementPolicy)
  const components = Object.freeze(matrix.legacyComponents.map(row => {
    const rowBlockers = COMPONENT_MIGRATION_EVIDENCE_BLOCKERS[row.id].map(code => blocker(
      `blind-ecosystem-migration.COMPONENT_MIGRATION_EVIDENCE_BLOCKERS.${row.id}`,
      code
    ))
    const cutoverBlockers = uniqueBlockers([
      ...row.cutoverGates.flatMap(gate => gatesById.get(gate).blockers),
      ...rowBlockers
    ])
    const removalBlockers = uniqueBlockers([...retirementReport.blockers, ...rowBlockers])
    return Object.freeze({
      id: row.id,
      cutoverStatus: cutoverBlockers.length === 0 ? 'cutover-authorized' : 'cutover-blocked',
      cutoverAuthorized: cutoverBlockers.length === 0,
      removalStatus: removalBlockers.length === 0 ? 'removal-authorized' : 'removal-blocked',
      removalAuthorized: removalBlockers.length === 0,
      blockedGates: Object.freeze(row.cutoverGates.filter(gate => !gatesById.get(gate).satisfied)),
      cutoverBlockerCount: cutoverBlockers.length,
      removalBlockerCount: removalBlockers.length,
      cutoverBlockers,
      removalBlockers
    })
  }))

  const allApplicationCutoversAuthorized = applications.every(row => row.cutoverAuthorized)
  const allLegacyComponentCutoversAuthorized = components.every(row => row.cutoverAuthorized)
  const allLegacyRemovalsAuthorized = components.every(row => row.removalAuthorized)
  const authorized = allApplicationCutoversAuthorized && allLegacyComponentCutoversAuthorized &&
    allLegacyRemovalsAuthorized
  return Object.freeze({
    matrixId: matrix.matrixId,
    matrixVersion: matrix.matrixVersion,
    decisionAuthority: matrix.decisionAuthority,
    status: authorized ? 'authorized' : 'blocked',
    authorized,
    allApplicationCutoversAuthorized,
    allLegacyComponentCutoversAuthorized,
    allLegacyRemovalsAuthorized,
    applicationCount: applications.length,
    legacyComponentCount: components.length,
    externalDependencyCount: matrix.externalDependencies.length,
    boundaryCount: matrix.boundaries.length,
    productionBlockerCount: productionRetirementBlockers().length,
    retirementBlockerCount: retirementReport.blockerCount,
    gates,
    applications,
    legacyComponents: components
  })
}

export function assertAllApplicationCutoversAuthorized (matrix, retirementPolicy) {
  const report = evaluateEcosystemMigration(matrix, retirementPolicy)
  if (report.allApplicationCutoversAuthorized) return report
  const error = new Error('one or more ecosystem application cutovers are blocked by code-owned evidence')
  error.code = 'BLIND_ECOSYSTEM_APPLICATION_CUTOVER_BLOCKED'
  error.report = report
  throw error
}

export function assertAllLegacyRemovalsAuthorized (matrix, retirementPolicy) {
  const report = evaluateEcosystemMigration(matrix, retirementPolicy)
  if (report.allLegacyRemovalsAuthorized) return report
  const error = new Error('one or more legacy component removals are blocked by code-owned evidence')
  error.code = 'BLIND_ECOSYSTEM_LEGACY_REMOVAL_BLOCKED'
  error.report = report
  throw error
}

function assertRetirementPolicy (policy) {
  assertPlainObject(policy, 'retirementPolicy')
  if (!Array.isArray(policy.components)) matrixFailure('retirementPolicy.components must be an array')
  assertExactStringArray(
    policy.components.map(row => row.id),
    COMPONENT_IDS,
    'retirementPolicy component ids'
  )
}

function assertFamilyArray (families, field, allowEmpty = false) {
  if (!Array.isArray(families) || (!allowEmpty && families.length === 0)) {
    matrixFailure(`${field} must be ${allowEmpty ? 'an array' : 'a non-empty array'}`)
  }
  const expected = FAMILY_IDS.filter(family => families.includes(family))
  assertExactStringArray(families, expected, field)
}

function assertReferences (values, authority, field, allowEmpty = false) {
  if (!Array.isArray(values) || (!allowEmpty && values.length === 0)) {
    matrixFailure(`${field} must be ${allowEmpty ? 'an array' : 'a non-empty array'}`)
  }
  const seen = new Set()
  for (const value of values) {
    assertNonEmptyString(value, field)
    if (!authority.has(value)) matrixFailure(`${field} references unknown value ${value}`)
    if (seen.has(value)) matrixFailure(`${field} contains a duplicate value`)
    seen.add(value)
  }
}

function rejectAuthorizationBooleans (value, at = 'matrix') {
  if (Array.isArray(value)) {
    value.forEach((entry, index) => rejectAuthorizationBooleans(entry, `${at}[${index}]`))
    return
  }
  if (!value || typeof value !== 'object') return
  for (const [key, entry] of Object.entries(value)) {
    const normalized = key.replace(/[^a-z0-9]/gi, '').toLowerCase()
    if (typeof entry === 'boolean' &&
        /(authorized|authorization|approved|ready|candelete|canremove|canmigrate|cancutover)/.test(normalized)) {
      matrixFailure(`${at}.${key} is a forbidden hand-editable decision boolean`)
    }
    rejectAuthorizationBooleans(entry, `${at}.${key}`)
  }
}

function assertAllowedKeys (value, allowed, field) {
  assertPlainObject(value, field)
  const allowedSet = new Set(allowed)
  for (const key of Object.keys(value)) {
    if (!allowedSet.has(key)) matrixFailure(`${field} contains unknown field ${key}`)
  }
  for (const key of allowed) {
    if (!Object.hasOwn(value, key)) matrixFailure(`${field} is missing field ${key}`)
  }
}

function assertExactIdRows (rows, expectedIds, field, inspect) {
  if (!Array.isArray(rows)) matrixFailure(`${field} must be an array`)
  assertExactStringArray(rows.map(row => {
    assertPlainObject(row, `${field} row`)
    assertNonEmptyString(row.id, `${field} row id`)
    return row.id
  }), expectedIds, `${field} ids`)
  rows.forEach(inspect)
}

function assertExactStringArray (actual, expected, field) {
  if (!Array.isArray(actual) || actual.length !== expected.length ||
      actual.some((value, index) => value !== expected[index])) {
    matrixFailure(`${field} must equal the registered ordered values`)
  }
}

function assertNonEmptyStringArray (value, field) {
  if (!Array.isArray(value) || value.length === 0) matrixFailure(`${field} must be a non-empty array`)
  const seen = new Set()
  for (const entry of value) {
    assertNonEmptyString(entry, field)
    if (seen.has(entry)) matrixFailure(`${field} contains a duplicate value`)
    seen.add(entry)
  }
}

function assertNonEmptyString (value, field) {
  if (typeof value !== 'string' || value.length === 0 || value !== value.trim()) {
    matrixFailure(`${field} must be a non-empty trimmed string`)
  }
}

function assertPlainObject (value, field) {
  if (!value || typeof value !== 'object' || Array.isArray(value) ||
      (Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null)) {
    matrixFailure(`${field} must be a plain object`)
  }
}

function blockerToken (id) {
  return id.replace(/[^a-z0-9]/gi, '_').toUpperCase()
}

function blocker (source, code) {
  return Object.freeze({ source, code })
}

function uniqueBlockers (rows) {
  const byKey = new Map()
  for (const row of rows) {
    const normalized = blocker(row.source, row.code)
    byKey.set(`${normalized.source}\0${normalized.code}`, normalized)
  }
  return Object.freeze([...byKey.values()].sort((left, right) =>
    left.source.localeCompare(right.source) || left.code.localeCompare(right.code)
  ))
}

function matrixFailure (message) {
  const error = new Error(message)
  error.code = 'BLIND_ECOSYSTEM_MIGRATION_MATRIX_INVALID'
  throw error
}

async function main () {
  const args = new Set(process.argv.slice(2))
  for (const arg of args) {
    if (arg !== '--json' && arg !== '--require-cutovers' && arg !== '--require-removals') {
      throw new Error(`unknown argument: ${arg}`)
    }
  }
  const retirementPolicy = await loadLegacyRetirementPolicy()
  const matrix = await loadEcosystemMigrationMatrix(ECOSYSTEM_MIGRATION_MATRIX_PATH, retirementPolicy)
  const report = evaluateEcosystemMigration(matrix, retirementPolicy)
  process.stdout.write(`${JSON.stringify(report, null, args.has('--json') ? 2 : 0)}\n`)
  if (args.has('--require-cutovers') &&
      (!report.allApplicationCutoversAuthorized || !report.allLegacyComponentCutoversAuthorized)) {
    process.exitCode = 1
  }
  if (args.has('--require-removals') && !report.allLegacyRemovalsAuthorized) process.exitCode = 1
}

if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  main().catch(error => {
    process.stderr.write(`${error.stack || error.message}\n`)
    process.exitCode = 1
  })
}