migrateOrParseBlueprint function
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,
);
}