{"record":{"id":"fedc62f4a245c527","repo":"bmad-code-org/BMAD-METHOD","slug":"keyed-array-identifier-candidate-must-be-a-str","errorCode":null,"errorMessage":"keyed array identifier `{candidate}` must be a string, got {type(value).__name__}","messagePattern":"keyed array identifier `(.+?)` must be a string, got (.+?)","errorType":"validation","errorClass":"ConfigError","httpStatus":null,"severity":"error","filePath":"src/scripts/config_utils.py","lineNumber":45,"sourceCode":"            parsed = tomllib.load(stream)\n    except tomllib.TOMLDecodeError as error:\n        raise ConfigError(f\"failed to parse {path}: {error}\") from error\n    except OSError as error:\n        raise ConfigError(f\"failed to read {path}: {error}\") from error\n    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] = {}","sourceCodeStart":27,"sourceCodeEnd":63,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/config_utils.py#L27-L63","documentation":"During `structural_merge`, `_detect_keyed_merge_field` decides whether two arrays should be merged by identity (using a `code` or `id` key on every element) or simply concatenated. If every element has the candidate field but at least one value is not a string, this error is raised — keyed identity requires stable string keys. A non-string id (an integer, a bool, a list) would produce unreliable dedup because Python dict keys of different types can collide silently.","triggerScenarios":"A customization TOML defines `[[...]]` entries where `id` or `code` is an integer (`id = 3`) or a boolean, while the base layer has string ids. Mixed types across layers also trigger it because the check inspects `base + override`.","commonSituations":"A schema drift where ids were numeric in one layer and string in another; importing ids from a database as integers; a hand-edit that dropped quotes off an id.","solutions":["Quote every `id`/`code` value in the offending layer so all are strings.","Make the type consistent across the base and override layers (all string, or remove the id field to fall back to concatenation).","If numeric ids are intentional, drop the `code`/`id` field so the merger treats the arrays as a plain append.","Find the bad entry via the reported type and the file you last edited."],"exampleFix":"# before (customize.user.toml)\n[[review_layers]]\nid = 3\nname = \"security\"\n\n# after\n[[review_layers]]\nid = \"3\"\nname = \"security\"","handlingStrategy":"type-guard","validationCode":"def keyed_ids_are_strings(items):\n    for cand in (\"code\",\"id\"):\n        if all(isinstance(i,dict) and cand in i for i in items):\n            return all(isinstance(i[cand], str) for i in items)\n    return True","typeGuard":"def has_string_ids(items: list) -> bool:\n    return all(isinstance(i.get('id') if isinstance(i,dict) else None, str) for i in items) if items else True","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":["Quote all id/code values in TOML so they parse as strings.","Keep id types consistent across every config layer.","Drop the id field if you want plain array concatenation."],"tags":["toml","config","merge","type-mismatch","bmad"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}