fix: tolerate AI manifest YAML colons
Some checks failed
Deploy to OCIWP / deploy (push) Failing after 37s

This commit is contained in:
jade
2026-09-03 14:16:57 +09:00
parent 82c087e2da
commit 69204cb6dd
4 changed files with 112 additions and 22 deletions

View File

@@ -16,6 +16,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Stream;
@@ -475,7 +476,7 @@ public class PodScaffolder {
private static String normalizeToolServiceManifest(String source, Path rootDir, String moduleName, String categoryKey,
List<String> targetModules)
throws IOException {
Map<String, Object> root = YAML_MAPPER.readValue(source, Map.class);
Map<String, Object> root = parseManifestYaml(source);
if (!(root.get("mcp") instanceof Map<?, ?> mcp)
|| !(mcp.get("manifest") instanceof Map<?, ?> manifest)) {
throw new IOException("tool-service-manifest.yml의 mcp.manifest.routing-functions 형식이 올바르지 않습니다.");
@@ -526,6 +527,56 @@ public class PodScaffolder {
}
}
@SuppressWarnings("unchecked")
private static Map<String, Object> parseManifestYaml(String source) throws IOException {
try {
return YAML_MAPPER.readValue(source, Map.class);
} catch (IOException original) {
String normalized = quoteUnquotedManifestScalars(source);
if (normalized.equals(source)) {
throw original;
}
return YAML_MAPPER.readValue(normalized, Map.class);
}
}
private static String quoteUnquotedManifestScalars(String source) {
Set<String> stringKeys = Set.of(
"name", "server-id", "category-key", "product-boundary", "business-domain",
"business-outcome", "select-if", "reject-if", "decision-policy", "description-serialization");
Pattern property = Pattern.compile("^(\\s*)([a-z-]+):(\\s*)(.*)$");
Pattern listItem = Pattern.compile("^(\\s*-\\s+)(.*)$");
StringBuilder normalized = new StringBuilder();
for (String line : source.split("\\r?\\n", -1)) {
Matcher propertyMatcher = property.matcher(line);
if (propertyMatcher.matches() && stringKeys.contains(propertyMatcher.group(2))) {
String value = propertyMatcher.group(4).trim();
if (!value.isEmpty() && !isQuoted(value)) {
line = propertyMatcher.group(1) + propertyMatcher.group(2) + ": " + yamlQuoted(value);
}
} else {
Matcher listItemMatcher = listItem.matcher(line);
if (listItemMatcher.matches()) {
String value = listItemMatcher.group(2).trim();
if (value.contains(":") && !isQuoted(value) && !value.matches("^[a-z-]+:.*$")) {
line = listItemMatcher.group(1) + yamlQuoted(value);
}
}
}
normalized.append(line).append(System.lineSeparator());
}
return normalized.toString();
}
private static boolean isQuoted(String value) {
return (value.startsWith("\"") && value.endsWith("\""))
|| (value.startsWith("'") && value.endsWith("'"));
}
private static String yamlQuoted(String value) {
return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
}
private static List<String> allowedTargetModules(Path rootDir, String moduleName, List<String> targetModules)
throws IOException {
List<String> candidates = targetModules == null || targetModules.isEmpty()

View File

@@ -114,6 +114,29 @@ class PodScaffolderTest {
assertTrue(compose.contains("SPRING_PROFILES_ACTIVE=${ACTIVE_PROFILE:-local}"));
}
@Test
void normalizesAiManifestWhenAnUnquotedKoreanDescriptionContainsColon() throws Exception {
String aiManifest = """
mcp:
manifest:
routing-functions:
- name: route_to_dat-was-ehr
server-id: dat-was-ehr
category-key: ehr
business-outcome: 인사 정보와 휴가 정보를 제공합니다.
reject-if: 인사 운영의 핵심 기능(예: 급여, 승진, 보상)과 관련된 경우 다른 서버로 전달합니다.
confusable-servers: [dat-was-hrd, dat-was-pay]
""";
String normalized = PodScaffolder.normalizeToolServiceManifest(aiManifest, "dat-was-ehr",
List.of("dat-was-cus", "dat-was-ehr", "dat-was-sal"));
Map<String, Object> routingFunction = routingFunction(normalized);
assertEquals("인사 운영의 핵심 기능(예: 급여, 승진, 보상)과 관련된 경우 다른 서버로 전달합니다.",
routingFunction.get("reject-if"));
assertEquals(List.of("dat-was-cus", "dat-was-sal"), routingFunction.get("confusable-servers"));
}
@Test
void generatesRedisOptionalPodComposeConfiguration() throws Exception {
Files.writeString(root.resolve("settings.gradle"), "rootProject.name = 'test'\n");