{"record":{"id":"2e573c442785199c","repo":"bmad-code-org/BMAD-METHOD","slug":"keyed-array-identifier-candidate-must-not-be-e","errorCode":null,"errorMessage":"keyed array identifier `{candidate}` must not be empty","messagePattern":"keyed array identifier `(.+?)` must not be empty","errorType":"validation","errorClass":"ConfigError","httpStatus":null,"severity":"error","filePath":"src/scripts/config_utils.py","lineNumber":50,"sourceCode":"    if not isinstance(parsed, dict):\n        raise ConfigError(f\"TOML layer did not parse to a table: {path}\")\n    return parsed\n\n\ndef _detect_keyed_merge_field(items: list[Any]) -> str | None:\n    if not items or not all(isinstance(item, dict) for item in items):\n        return None\n    for candidate in _KEYED_MERGE_FIELDS:\n        if all(candidate in item for item in items):\n            for item in items:\n                value = item[candidate]\n                if not isinstance(value, str):\n                    raise ConfigError(\n                        f\"keyed array identifier `{candidate}` must be a string, \"\n                        f\"got {type(value).__name__}\"\n                    )\n                if not value:\n                    raise ConfigError(\n                        f\"keyed array identifier `{candidate}` must not be empty\"\n                    )\n            return candidate\n    return None\n\n\ndef _merge_arrays(base: list[Any], override: list[Any]) -> list[Any]:\n    keyed_field = _detect_keyed_merge_field(base + override)\n    if keyed_field is None:\n        return list(base) + list(override)\n\n    result: list[Any] = []\n    index_by_key: dict[str, int] = {}\n    for item in base:\n        copied = dict(item)\n        index_by_key[copied[keyed_field]] = len(result)\n        result.append(copied)\n    for item in override:","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/config_utils.py#L32-L68","documentation":"Sibling to the non-string id check: `_detect_keyed_merge_field` also rejects an id that is an empty string. An empty key would collapse every entry onto the same identity slot during keyed merge, silently dropping all but the last. Raising here protects against data loss in the merged output.","triggerScenarios":"A `[[...]]` entry with `id = \"\"` or `code = \"\"` in either the base or override layer; a template that left the id field empty; a record exported with a blank identifier.","commonSituations":"Copying a TOML block and forgetting to fill in the id; an exporter that emits the id column even when the source had no value; partial edit leaving a placeholder.","solutions":["Give every entry a unique non-empty `id`/`code`.","Remove the id field entirely if you want the arrays concatenated rather than identity-merged.","Lint for blank ids: `python -c \"import tomllib;d=tomllib.load(open('f','rb'));[print(i) for a in d.values() if isinstance(a,list) for i in a if isinstance(i,dict) and i.get('id')=='']\"`.","Regenerate the layer from a source that guarantees non-empty ids."],"exampleFix":"# before\n[[review_layers]]\nid = \"\"\nname = \"security\"\n\n# after\n[[review_layers]]\nid = \"security\"\nname = \"security\"","handlingStrategy":"validation","validationCode":"def no_blank_ids(items):\n    return all(i.get('id','').strip() != '' for i in items if isinstance(i,dict) and 'id' in i)","typeGuard":"def has_nonempty_ids(items: list) -> bool:\n    return all(isinstance(i.get('id'), str) and i['id'].strip() for i in items if isinstance(i,dict) and 'id' in i)","tryCatchPattern":"from config_utils import ConfigError\ntry:\n    structural_merge(base, override)\nexcept ConfigError as e:\n    print(f\"error: {e}\", file=sys.stderr); sys.exit(2)","preventionTips":["Give every keyed entry a unique non-empty id.","Remove the id field if concatenation is intended.","Lint config for blank ids in CI."],"tags":["toml","config","merge","validation","bmad"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}