migrateOrParseBlueprint function

Blueprint migrateOrParseBlueprint(
  1. Map<String, dynamic> json, {
  2. required RecipeLock lock,
})

Resolves external Blueprint/config input by schema version — the read-time migration rule (R-202, FR-048-FR-050), not a workflow:

  • 3 → parsed as-is after rejecting retired relocated keys.
  • 2 → parsed and relocated into v3 without injecting foundation defaults.
  • 1 → an explicit token→capability-ID mapping onto the Recommended preset (delegating to resolveRecommended so migration and Recommended converge on identical selections by construction, satisfying INV-MIG1/SC-007 without maintaining two token tables); every v1 identity field is preserved verbatim from the input. A malformed input or one naming an unrecognized recipe/token set is refused, never defaulted to Recommended.
  • anything else → refused with a structured error (FR-049).

Implementation

Blueprint migrateOrParseBlueprint(
  Map<String, dynamic> json, {
  required RecipeLock lock,
}) {
  final schemaVersion = json['schemaVersion'];
  if (schemaVersion == 3) {
    final blueprint = Blueprint.fromJson(Map<String, dynamic>.from(json));
    if (containsLegacyRelocatedSelections(blueprint.singleSelections)) {
      throw const BlueprintMigrationException(
        'LEGACY_RELOCATED_KEY',
        'schema_version 3 uses a retired org.project_organization or '
            'org.localization key.',
      );
    }
    return blueprint;
  }
  if (schemaVersion == 2) {
    final v2 = Blueprint.fromJson(Map<String, dynamic>.from(json));
    return _asV3(
      v2,
      singleSelections: relocateV2SingleSelections(v2.singleSelections),
      multiSelections: v2.multiSelections,
    );
  }
  if (schemaVersion != 1) {
    throw BlueprintMigrationException(
      'UNKNOWN_SCHEMA_VERSION',
      'Unknown or future schema_version "$schemaVersion" — refusing rather '
          'than partially interpreting it.',
    );
  }

  final recipeId = json['recipeId'] is String
      ? json['recipeId'] as String
      : null;
  final integrations = json['integrations'] is Iterable
      ? Set<String>.from((json['integrations'] as Iterable).whereType<String>())
      : const <String>{};
  final recognized =
      recipeId == lock.recipeId &&
      integrations.containsAll(_recognizedV1Integrations);
  if (!recognized) {
    throw BlueprintMigrationException(
      'UNRECOGNIZED_V1_RECIPE',
      'schema_version 1 input names an unrecognized recipe or integration '
          'set — refusing rather than defaulting to Recommended.',
    );
  }

  // Type-safe reads: a wrong-typed identity field is treated as absent and
  // reported as MALFORMED_V1_INPUT below, never crashed on a bare cast.
  final projectName = json['projectName'] is String
      ? json['projectName'] as String
      : null;
  final organization = json['organization'] is String
      ? json['organization'] as String
      : null;
  final destination = json['destination'] is String
      ? json['destination'] as String
      : null;
  final targetPlatforms = json['targetPlatforms'] is Iterable
      ? Set<String>.from(
          (json['targetPlatforms'] as Iterable).whereType<String>(),
        )
      : null;
  final gitPreference = json['gitPreference'] is bool
      ? json['gitPreference'] as bool
      : null;
  if (projectName == null ||
      organization == null ||
      destination == null ||
      targetPlatforms == null ||
      targetPlatforms.isEmpty ||
      gitPreference == null) {
    throw const BlueprintMigrationException(
      'MALFORMED_V1_INPUT',
      'schema_version 1 input is missing a required identity field '
          '(projectName, organization, destination, targetPlatforms, or '
          'gitPreference).',
    );
  }

  return Blueprint(
    schemaVersion: 3,
    projectName: projectName,
    description: json['description'] is String
        ? json['description'] as String
        : '',
    organization: organization,
    destination: destination,
    flutterSdkRequirement: lock.flutterRange,
    targetPlatforms: targetPlatforms,
    recipeId: lock.recipeId,
    recipeVersion: lock.recipeVersion,
    engineVersion: lock.engineVersion,
    gitPreference: gitPreference,
    validationProfile: ValidationProfile.values.firstWhere(
      (e) => e.name == json['validationProfile'],
      orElse: () => ValidationProfile.standard,
    ),
    setupMode: SetupMode.recommended,
    singleSelections: migratedRecommendedSingleSelections,
    multiSelections: migratedRecommendedMultiSelections,
  );
}