← All tasks
javacodex/java-t1 #4Not a task: already works

YAML Config Merger (java, written by Codex)

envgap__codex__java-t1-4

Written by a coding agent; not on GitHubWritten 2026-03-02

01 / FAILURE SIGNATURE

As the study recorded it

None
Not a benchmark task.
  • The project already builds and runs before the fix, so there is nothing to repair.

02 / ENVIRONMENT RECIPE

Base commit
Not freshly verified
Manifest
pom.xml
Reproduce
Awaiting issue-specific recipe
Run under trace
Awaiting a meaningful runtime command

03 / TASK AND FAILURE

codex/java-t1 #4 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written.

Task given to the agent:

TASK: YAML Config Merger

Write a program that merges multiple YAML configuration files into a single unified configuration, supporting deep merging of nested structures, array handling strategies, and conflict resolution.

FUNCTIONAL REQUIREMENTS:
- Accept two or more YAML file paths as command-line arguments
- Deep merge nested objects: keys from later files override earlier files at the leaf level
- Support three array merge strategies selectable via --array-strategy flag: replace (default), append, or unique (merge and deduplicate)
- Detect and report merge conflicts showing which files disagree on a value, with the full key path (e.g., database.connection.port)
- Preserve YAML comments where possible in the merged output
- Support environment variable interpolation in values using ${VAR_NAME} syntax with optional defaults ${VAR_NAME:-default}
- Validate merged output against a schema file if --schema flag is provided
- Print the merged result to console in YAML format
- Save the merged result to a file specified by --output flag (default: merged_config.yaml)
- If no input files are given, generate three sample YAML config files with overlapping keys, nested structures, and arrays, then merge them
- Handle malformed YAML with clear error messages identifying the file and location of the problem

Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include:
- Source code
- pom.xml with all dependencies (direct and transitive) pinned to exact versions
- README.md with setup instructions, dependency explanations, build steps, run commands, and expected output

04 / LABELS

Labels from the report text only; not yet run

No supported category has been assigned.

Label rules and the text that matched
[]

05 / FILES

The project as the agent wrote it

3 files, exactly as written, before any repair.

pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>tmlr.codex_generated.p04</groupId>
  <artifactId>yaml-config-merger</artifactId>
  <version>1.0.0</version>
  <name>YAML Config Merger</name>
  <description>Deep YAML config merger with array strategies, conflict reporting, and schema validation.</description>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.release>17</maven.compiler.release>
  </properties>

  <dependencies>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.13.0</version>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>exec-maven-plugin</artifactId>
        <version>3.5.0</version>
        <configuration>
          <mainClass>YamlConfigMerger</mainClass>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
README.md
# YAML Config Merger (Java)

Merges multiple YAML configuration files with deep merge semantics, array strategy control, conflict reporting, env interpolation, optional schema validation, and YAML output.

## Features

- Accepts two or more YAML files.
- Deep merge for nested objects.
- `--array-strategy`:
  - `replace` (default)
  - `append`
  - `unique`
- Reports merge conflicts with full key path and disagreeing files.
- Preserves comments where possible.
- Environment interpolation:
  - `${VAR_NAME}`
  - `${VAR_NAME:-default}`
- Optional schema validation via `--schema`.
- Prints merged YAML to console.
- Writes merged output via `--output` (default `merged_config.yaml`).
- No input files: generates 3 sample YAML configs and merges them.
- Malformed YAML errors include file and line.

## Requirements

- Ubuntu 22.04
- JDK 17+

## Dependencies (Pinned)

No external runtime dependencies are used.

- Direct runtime dependencies: none
- Transitive runtime dependencies: none

`pom.xml` pins Maven plugin versions.

## Run

```bash
cd TMLR/code_generation/codex_generated/p_04/java
mkdir -p out
javac -d out src/main/java/YamlConfigMerger.java
java -cp out YamlConfigMerger config1.yaml config2.yaml config3.yaml
```

Examples:

```bash
java -cp out YamlConfigMerger --array-strategy unique config1.yaml config2.yaml
java -cp out YamlConfigMerger --schema schema.yaml --output merged.yaml config1.yaml config2.yaml
java -cp out YamlConfigMerger
```
src/main/java/YamlConfigMerger.java
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public class YamlConfigMerger {
    private static final Pattern ENV_PATTERN = Pattern.compile("\\$\\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?}");

    public static void main(String[] args) {
        try {
            int exit = run(args);
            System.exit(exit);
        } catch (Exception ex) {
            System.err.println("Unexpected failure: " + ex.getMessage());
            System.exit(1);
        }
    }

    private static int run(String[] args) throws IOException {
        List<String> files = new ArrayList<>();
        String arrayStrategy = "replace";
        String schemaPath = null;
        String outputPath = "merged_config.yaml";

        for (int i = 0; i < args.length; i++) {
            String arg = args[i];
            if ("--array-strategy".equals(arg)) {
                i++;
                arrayStrategy = i < args.length ? args[i] : "";
            } else if ("--schema".equals(arg)) {
                i++;
                schemaPath = i < args.length ? args[i] : null;
            } else if ("--output".equals(arg)) {
                i++;
                outputPath = i < args.length ? args[i] : outputPath;
            } else {
                files.add(arg);
            }
        }

        if (!arrayStrategy.equals("replace") && !arrayStrategy.equals("append") && !arrayStrategy.equals("unique")) {
            System.err.println("Invalid --array-strategy. Use replace, append, or unique.");
            return 1;
        }

        if (files.isEmpty()) {
            Map<String, String> samples = sampleFiles();
            files = new ArrayList<>();
            for (Map.Entry<String, String> entry : samples.entrySet()) {
                Path path = Paths.get(entry.getKey()).toAbsolutePath().normalize();
                Files.writeString(path, entry.getValue(), StandardCharsets.UTF_8);
                files.add(path.toString());
            }
            System.out.println("No input files provided. Generated sample files: " + String.join(", ", files));
        }

        if (files.size() < 2) {
            System.err.println("Provide at least two YAML files.");
            return 1;
        }

        List<LoadedYaml> loaded = new ArrayList<>();
        for (String file : files) {
            Path abs = Paths.get(file).toAbsolutePath().normalize();
            try {
                loaded.add(loadYaml(abs));
            } catch (RuntimeException ex) {
                System.err.println(ex.getMessage());
                return 1;
            }
        }

        Object merged = deepClone(loaded.get(0).data);
        Map<String, String> sourceMap = new LinkedHashMap<>();
        collectLeafSources(merged, "", sourceMap, loaded.get(0).file);

        Map<String, List<String>> mergedComments = new LinkedHashMap<>(loaded.get(0).comments);
        List<Conflict> conflicts = new ArrayList<>();

        for (int i = 1; i < loaded.size(); i++) {
            LoadedYaml next = loaded.get(i);
            merged = mergeValues(merged, next.data, "", arrayStrategy, conflicts, sourceMap, next.file);
            for (Map.Entry<String, List<String>> entry : next.comments.entrySet()) {
                mergedComments.putIfAbsent(entry.getKey(), entry.getValue());
            }
        }

        merged = interpolateEnv(merged);

        if (schemaPath != null) {
            LoadedYaml schema;
            try {
                schema = loadYaml(Paths.get(schemaPath).toAbsolutePath().normalize());
            } catch (RuntimeException ex) {
                System.err.println(ex.getMessage());
                return 1;
            }
            List<String> schemaErrors = new ArrayList<>();
            validateSchema(merged, schema.data, "", schemaErrors);
            if (!schemaErrors.isEmpty()) {
                System.err.println("Schema validation failed:");
                for (String error : schemaErrors) System.err.println("- " + error);
                return 1;
            }
        }

        if (conflicts.isEmpty()) {
            System.out.println("No merge conflicts detected.");
        } else {
            System.out.println("Merge conflicts detected:");
            for (Conflict conflict : conflicts) {
                String display = conflict.path.isEmpty() ? "<root>" : conflict.path;
                System.out.println("- " + display + ": " + conflict.previousFile + " disagrees with " + conflict.currentFile);
            }
        }

        String yamlOutput = toYaml(merged, mergedComments, "", 0);
        System.out.println("\nMerged YAML:\n");
        System.out.println(yamlOutput);

        Path output = Paths.get(outputPath).toAbsolutePath().normalize();
        Files.writeString(output, yamlOutput + System.lineSeparator(), StandardCharsets.UTF_8);
        System.out.println("\nSaved merged config: " + output);
        return 0;
    }

    private static LoadedYaml loadYaml(Path file) {
        final String text;
        try {
            text = Files.readString(file, StandardCharsets.UTF_8);
        } catch (IOException ex) {
            throw new RuntimeException("Failed to read " + file + ": " + ex.getMessage());
        }

        try {
            YamlParser parser = new YamlParser(text);
            ParseResult result = parser.parse();
            return new LoadedYaml(file.toString(), result.data, result.comments);
        } catch (YamlParseException ex) {
            throw new RuntimeException("Malformed YAML in " + file + ": " + ex.getMessage());
        }
    }

    private static Object mergeValues(
            Object base,
            Object incoming,
            String path,
            String strategy,
            List<Conflict> conflicts,
            Map<String, String> sourceMap,
            String currentFile
    ) {
        if (base instanceof Map<?, ?> && incoming instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> baseMap = (Map<String, Object>) base;
            @SuppressWarnings("unchecked")
            Map<String, Object> incomingMap = (Map<String, Object>) incoming;

            Map<String, Object> merged = new LinkedHashMap<>(baseMap);
            for (Map.Entry<String, Object> entry : incomingMap.entrySet()) {
                String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
                if (merged.containsKey(entry.getKey())) {
                    merged.put(
                            entry.getKey(),
                            mergeValues(merged.get(entry.getKey()), entry.getValue(), childPath, strategy, conflicts, sourceMap, currentFile)
                    );
                } else {
                    merged.put(entry.getKey(), deepClone(entry.getValue()));
                    sourceMap.put(childPath, currentFile);
                }
            }
            return merged;
        }

        if (base instanceof List<?> && incoming instanceof List<?>) {
            @SuppressWarnings("unchecked")
            List<Object> baseList = (List<Object>) base;
            @SuppressWarnings("unchecked")
            List<Object> incomingList = (List<Object>) incoming;

            List<Object> merged;
            if ("append".equals(strategy)) {
                merged = new ArrayList<>();
                for (Object item : baseList) merged.add(deepClone(item));
                for (Object item : incomingList) merged.add(deepClone(item));
            } else if ("unique".equals(strategy)) {
                merged = new ArrayList<>();
                for (Object item : baseList) merged.add(deepClone(item));
                for (Object item : incomingList) {
                    boolean exists = false;
                    for (Object existing : merged) {
                        if (deepEqual(existing, item)) {
                            exists = true;
                            break;
                        }
                    }
                    if (!exists) merged.add(deepClone(item));
                }
            } else {
                merged = new ArrayList<>();
                for (Object item : incomingList) merged.add(deepClone(item));
            }

            if (!deepEqual(baseList, incomingList)) {
                conflicts.add(new Conflict(path, sourceMap.getOrDefault(path, "unknown"), currentFile));
            }
            sourceMap.put(path, currentFile);
            return merged;
        }

        if (!deepEqual(base, incoming)) {
            conflicts.add(new Conflict(path, sourceMap.getOrDefault(path, "unknown"), currentFile));
        }
        sourceMap.put(path, currentFile);
        return deepClone(incoming);
    }

    private static void collectLeafSources(Object value, String path, Map<String, String> sourceMap, String file) {
        if (value instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) value;
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                String child = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
                collectLeafSources(entry.getValue(), child, sourceMap, file);
            }
            return;
        }
        sourceMap.put(path, file);
    }

    private static Object interpolateEnv(Object value) {
        if (value instanceof String) {
            Matcher matcher = ENV_PATTERN.matcher((String) value);
            StringBuilder out = new StringBuilder();
            int last = 0;
            while (matcher.find()) {
                out.append(((String) value), last, matcher.start());
                String key = matcher.group(1);
                String fallback = matcher.group(3);
                String env = System.getenv(key);
                out.append(env != null ? env : (fallback != null ? fallback : ""));
                last = matcher.end();
            }
            out.append(((String) value).substring(last));
            return out.toString();
        }
        if (value instanceof List<?>) {
            List<Object> out = new ArrayList<>();
            for (Object item : (List<?>) value) out.add(interpolateEnv(item));
            return out;
        }
        if (value instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) value;
            Map<String, Object> out = new LinkedHashMap<>();
            for (Map.Entry<String, Object> entry : map.entrySet()) out.put(entry.getKey(), interpolateEnv(entry.getValue()));
            return out;
        }
        return value;
    }

    private static void validateSchema(Object data, Object schemaObj, String path, List<String> errors) {
        if (!(schemaObj instanceof Map<?, ?>)) return;
        @SuppressWarnings("unchecked")
        Map<String, Object> schema = (Map<String, Object>) schemaObj;

        String declaredType = schema.get("type") instanceof String ? (String) schema.get("type") : null;
        String actualType = jsonTypeOf(data);
        if (declaredType != null && !declaredType.equals(actualType)) {
            errors.add((path.isEmpty() ? "$" : path) + ": expected type " + declaredType + ", got " + actualType);
            return;
        }

        if (schema.get("enum") instanceof List<?>) {
            boolean found = false;
            for (Object candidate : (List<?>) schema.get("enum")) {
                if (deepEqual(candidate, data)) {
                    found = true;
                    break;
                }
            }
            if (!found) errors.add((path.isEmpty() ? "$" : path) + ": value not in enum");
        }

        if ("object".equals(actualType) && data instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) data;

            if (schema.get("required") instanceof List<?>) {
                for (Object req : (List<?>) schema.get("required")) {
                    if (req instanceof String && !map.containsKey(req)) {
                        errors.add((path.isEmpty() ? "$" : path) + ": missing required key " + req);
                    }
                }
            }

            if (schema.get("properties") instanceof Map<?, ?>) {
                @SuppressWarnings("unchecked")
                Map<String, Object> props = (Map<String, Object>) schema.get("properties");
                for (Map.Entry<String, Object> entry : props.entrySet()) {
                    if (map.containsKey(entry.getKey())) {
                        String child = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
                        validateSchema(map.get(entry.getKey()), entry.getValue(), child, errors);
                    }
                }
            }
        } else if ("array".equals(actualType) && data instanceof List<?> && schema.get("items") != null) {
            List<?> list = (List<?>) data;
            for (int i = 0; i < list.size(); i++) {
                String child = path + "[" + i + "]";
                validateSchema(list.get(i), schema.get("items"), child, errors);
            }
        }
    }

    private static String jsonTypeOf(Object value) {
        if (value == null) return "null";
        if (value instanceof Boolean) return "boolean";
        if (value instanceof Number) return "number";
        if (value instanceof String) return "string";
        if (value instanceof List<?>) return "array";
        if (value instanceof Map<?, ?>) return "object";
        return "unknown";
    }

    private static String toYaml(Object value, Map<String, List<String>> comments, String path, int indent) {
        StringBuilder out = new StringBuilder();
        String pad = " ".repeat(indent);

        if (value instanceof List<?>) {
            @SuppressWarnings("unchecked")
            List<Object> list = (List<Object>) value;
            for (int i = 0; i < list.size(); i++) {
                Object item = list.get(i);
                if (item instanceof Map<?, ?> || item instanceof List<?>) {
                    out.append(pad).append("-").append(System.lineSeparator());
                    out.append(toYaml(item, comments, path + "[" + i + "]", indent + 2));
                } else {
                    out.append(pad).append("- ").append(yamlScalar(item)).append(System.lineSeparator());
                }
            }
            return out.toString().replaceAll("\\s+$", "");
        }

        if (value instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) value;
            for (Map.Entry<String, Object> entry : map.entrySet()) {
                String childPath = path.isEmpty() ? entry.getKey() : path + "." + entry.getKey();
                if (comments.containsKey(childPath)) {
                    for (String comment : comments.get(childPath)) {
                        out.append(pad).append("# ").append(comment).append(System.lineSeparator());
                    }
                }
                if (entry.getValue() instanceof Map<?, ?> || entry.getValue() instanceof List<?>) {
                    out.append(pad).append(entry.getKey()).append(":").append(System.lineSeparator());
                    out.append(toYaml(entry.getValue(), comments, childPath, indent + 2)).append(System.lineSeparator());
                } else {
                    out.append(pad).append(entry.getKey()).append(": ").append(yamlScalar(entry.getValue()))
                            .append(System.lineSeparator());
                }
            }
            return out.toString().replaceAll("\\s+$", "");
        }

        return pad + yamlScalar(value);
    }

    private static String yamlScalar(Object value) {
        if (value == null) return "null";
        if (value instanceof Boolean || value instanceof Number) return String.valueOf(value);
        String text = String.valueOf(value);
        if (text.isEmpty()) return "\"\"";
        if (text.matches(".*[:#\\-\\[\\]\\{\\},].*") || Character.isWhitespace(text.charAt(0))
                || Character.isWhitespace(text.charAt(text.length() - 1)) || text.contains("\n")) {
            return "\"" + text.replace("\\", "\\\\").replace("\"", "\\\"") + "\"";
        }
        return text;
    }

    private static Object deepClone(Object value) {
        if (value instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> map = (Map<String, Object>) value;
            Map<String, Object> out = new LinkedHashMap<>();
            for (Map.Entry<String, Object> entry : map.entrySet()) out.put(entry.getKey(), deepClone(entry.getValue()));
            return out;
        }
        if (value instanceof List<?>) {
            @SuppressWarnings("unchecked")
            List<Object> list = (List<Object>) value;
            List<Object> out = new ArrayList<>();
            for (Object item : list) out.add(deepClone(item));
            return out;
        }
        return value;
    }

    private static boolean deepEqual(Object a, Object b) {
        if (a == b) return true;
        if (a == null || b == null) return false;
        if (a instanceof Number && b instanceof Number) {
            return Double.compare(((Number) a).doubleValue(), ((Number) b).doubleValue()) == 0;
        }
        if (a instanceof String && b instanceof String) return a.equals(b);
        if (a instanceof Boolean && b instanceof Boolean) return a.equals(b);
        if (a instanceof List<?> && b instanceof List<?>) {
            List<?> la = (List<?>) a;
            List<?> lb = (List<?>) b;
            if (la.size() != lb.size()) return false;
            for (int i = 0; i < la.size(); i++) if (!deepEqual(la.get(i), lb.get(i))) return false;
            return true;
        }
        if (a instanceof Map<?, ?> && b instanceof Map<?, ?>) {
            @SuppressWarnings("unchecked")
            Map<String, Object> ma = (Map<String, Object>) a;
            @SuppressWarnings("unchecked")
            Map<String, Object> mb = (Map<String, Object>) b;
            if (ma.size() != mb.size()) return false;
            for (Map.Entry<String, Object> entry : ma.entrySet()) {
                if (!mb.containsKey(entry.getKey())) return false;
                if (!deepEqual(entry.getValue(), mb.get(entry.getKey()))) return false;
            }
            return true;
        }
        return false;
    }

    private static Map<String, String> sampleFiles() {
        Map<String, String> out = new LinkedHashMap<>();
        out.put("config_1.yaml", "# Base app config\n"
                + "app:\n"
                + "  name: merger-demo\n"
                + "  env: ${APP_ENV:-dev}\n"
                + "database:\n"
                + "  host: localhost\n"
                + "  port: 5432\n"
                + "  tags:\n"
                + "    - core\n"
                + "    - primary\n"
                + "features:\n"
                + "  enabled:\n"
                + "    - auth\n"
                + "    - api\n");
        out.put("config_2.yaml", "# Override database and features\n"
                + "database:\n"
                + "  port: 5433\n"
                + "  user: ${DB_USER:-admin}\n"
                + "  tags:\n"
                + "    - analytics\n"
                + "features:\n"
                + "  enabled:\n"
                + "    - api\n"
                + "    - billing\n");
        out.put("config_3.yaml", "# Production tuning\n"
                + "app:\n"
                + "  env: prod\n"
                + "database:\n"
                + "  host: db.internal\n"
                + "  retries: 5\n"
                + "features:\n"
                + "  enabled:\n"
                + "    - auth\n"
                + "    - billing\n");
        return out;
    }

    private static final class LoadedYaml {
        final String file;
        final Object data;
        final Map<String, List<String>> comments;

        LoadedYaml(String file, Object data, Map<String, List<String>> comments) {
            this.file = file;
            this.data = data;
            this.comments = comments;
        }
    }

    private static final class Conflict {
        final String path;
        final String previousFile;
        final String currentFile;

        Conflict(String path, String previousFile, String currentFile) {
            this.path = path;
            this.previousFile = previousFile;
            this.currentFile = currentFile;
        }
    }

    private static final class ParseResult {
        final Object data;
        final Map<String, List<String>> comments;

        ParseResult(Object data, Map<String, List<String>> comments) {
            this.data = data;
            this.comments = comments;
        }
    }

    private static final class YamlParseException extends RuntimeException {
        YamlParseException(String message) {
            super(message);
        }
    }

    private static final class YamlParser {
        private final String[] lines;
        private final Map<String, List<String>> comments = new LinkedHashMap<>();

        YamlParser(String text) {
            this.lines = text.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1);
        }

        ParseResult parse() {
            BlockResult parsed = parseBlock(0, 0, "");
            return new ParseResult(parsed.value == null ? new LinkedHashMap<>() : parsed.value, comments);
        }

        private BlockResult parseBlock(int start, int indent, String pathPrefix) {
            int index = start;
            List<String> pendingComments = new ArrayList<>();

            while (index < lines.length) {
                LineInfo line = info(index);
                if (line.trimmed.isEmpty()) {
                    index++;
                    continue;
                }
                if (line.indent < indent) break;
                if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
                if (line.trimmed.startsWith("#")) {
                    pendingComments.add(line.trimmed.substring(1).trim());
                    index++;
                    continue;
                }
                break;
            }

            if (index >= lines.length) return new BlockResult(null, index);

            LineInfo first = info(index);
            if (first.indent != indent) return new BlockResult(null, index);
            boolean isSequence = first.trimmed.startsWith("- ");

            if (isSequence) {
                List<Object> list = new ArrayList<>();
                while (index < lines.length) {
                    LineInfo line = info(index);
                    if (line.trimmed.isEmpty() || line.trimmed.startsWith("#")) {
                        index++;
                        continue;
                    }
                    if (line.indent < indent) break;
                    if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
                    if (!line.trimmed.startsWith("- ")) break;

                    String rest = line.trimmed.substring(2).trim();
                    if (rest.isEmpty()) {
                        BlockResult nested = parseBlock(index + 1, indent + 2, pathPrefix + "[" + list.size() + "]");
                        list.add(nested.value);
                        index = nested.nextIndex;
                    } else if (rest.matches("^[^:#]+:\\s*.*$")) {
                        int split = rest.indexOf(':');
                        String key = rest.substring(0, split).trim();
                        String valueText = rest.substring(split + 1).trim();
                        Map<String, Object> item = new LinkedHashMap<>();
                        if (valueText.isEmpty()) {
                            BlockResult nested = parseBlock(index + 1, indent + 4, pathPrefix + "[" + list.size() + "]." + key);
                            item.put(key, nested.value);
                            list.add(item);
                            index = nested.nextIndex;
                        } else {
                            item.put(key, scalarFromText(valueText));
                            list.add(item);
                            index++;
                        }
                    } else {
                        list.add(scalarFromText(rest));
                        index++;
                    }
                }
                return new BlockResult(list, index);
            }

            Map<String, Object> map = new LinkedHashMap<>();
            List<String> localComments = new ArrayList<>(pendingComments);
            while (index < lines.length) {
                LineInfo line = info(index);
                if (line.trimmed.isEmpty()) {
                    index++;
                    continue;
                }
                if (line.indent < indent) break;
                if (line.indent > indent) throw error("Unexpected indentation", line.lineNo);
                if (line.trimmed.startsWith("#")) {
                    localComments.add(line.trimmed.substring(1).trim());
                    index++;
                    continue;
                }

                int split = line.trimmed.indexOf(':');
                if (split < 1) throw error("Invalid mapping entry", line.lineNo);
                String key = line.trimmed.substring(0, split).trim();
                String rest = line.trimmed.substring(split + 1).trim();
                String fullPath = pathPrefix.isEmpty() ? key : pathPrefix + "." + key;

                if (!localComments.isEmpty()) {
                    comments.put(fullPath, new ArrayList<>(localComments));
                    localComments.clear();
                }

                if (rest.isEmpty()) {
                    BlockResult nested = parseBlock(index + 1, indent + 2, fullPath);
                    map.put(key, nested.value);
                    index = nested.nextIndex;
                } else {
                    String inline = rest.split("\\s+#", 2)[0].trim();
                    map.put(key, scalarFromText(inline));
                    index++;
                }
            }

            return new BlockResult(map, index);
        }

        private static Object scalarFromText(String text) {
            if ("null".equals(text) || "~".equals(text)) return null;
            if ("true".equals(text)) return true;
            if ("false".equals(text)) return false;
            if (text.matches("-?\\d+(\\.\\d+)?")) {
                if (text.contains(".")) return Double.parseDouble(text);
                return Long.parseLong(text);
            }
            if ((text.startsWith("\"") && text.endsWith("\"")) || (text.startsWith("'") && text.endsWith("'"))) {
                return text.substring(1, text.length() - 1);
            }
            return text;
        }

        private LineInfo info(int index) {
            String raw = lines[index];
            int indent = 0;
            while (indent < raw.length() && raw.charAt(indent) == ' ') indent++;
            return new LineInfo(raw, indent, raw.trim(), index + 1);
        }

        private static YamlParseException error(String message, int line) {
            return new YamlParseException(message + " at line " + line);
        }

        private static final class LineInfo {
            final String raw;
            final int indent;
            final String trimmed;
            final int lineNo;

            LineInfo(String raw, int indent, String trimmed, int lineNo) {
                this.raw = raw;
                this.indent = indent;
                this.trimmed = trimmed;
                this.lineNo = lineNo;
            }
        }

        private static final class BlockResult {
            final Object value;
            final int nextIndex;

            BlockResult(Object value, int nextIndex) {
                this.value = value;
                this.nextIndex = nextIndex;
            }
        }
    }
}