{"record":{"id":"4097c0c28ff42826","repo":"bmad-code-org/BMAD-METHOD","slug":"missing-label-dotted-path","errorCode":null,"errorMessage":"missing {label} `{dotted_path}`","messagePattern":"missing (.+?) `(.+?)`","errorType":"validation","errorClass":"RenderError","httpStatus":null,"severity":"error","filePath":"src/scripts/render_skill.py","lineNumber":50,"sourceCode":"_CUSTOM_TOKEN = re.compile(r\"\\{workflow\\.([A-Za-z0-9_.-]+)\\}\")\n_SNAPSHOT_TOKEN = re.compile(r\"\\[\\[bmad-snapshot:([A-Za-z0-9_./-]+\\.md)\\]\\]\")\n\n\ndef _hash_bytes(content: bytes) -> str:\n    return hashlib.sha256(content).hexdigest()\n\n\ndef _canonical_json(value: Any) -> bytes:\n    return json.dumps(\n        value, ensure_ascii=False, sort_keys=True, separators=(\",\", \":\")\n    ).encode(\"utf-8\")\n\n\ndef _lookup(data: dict[str, Any], dotted_path: str, label: str) -> Any:\n    current: Any = data\n    for part in dotted_path.split(\".\"):\n        if not isinstance(current, dict) or part not in current:\n            raise RenderError(f\"missing {label} `{dotted_path}`\")\n        current = current[part]\n    return current\n\n\ndef _require_string(value: Any, label: str, *, allow_empty: bool = False) -> str:\n    if not isinstance(value, str):\n        raise RenderError(f\"{label} must be a string, got {type(value).__name__}\")\n    if not allow_empty and not value.strip():\n        raise RenderError(f\"{label} must not be empty\")\n    return value\n\n\ndef _require_string_list(value: Any, label: str) -> list[str]:\n    if not isinstance(value, list):\n        raise RenderError(f\"{label} must be a list, got {type(value).__name__}\")\n    result = []\n    for index, item in enumerate(value):\n        result.append(_require_string(item, f\"{label}[{index}]\"))","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/bmad-code-org/BMAD-METHOD/blob/b70486b9bdcb0a404d329e2a763b57964e7f1360/src/scripts/render_skill.py#L32-L68","documentation":"render_skill.py's `_lookup` walks a dotted path (e.g. `workflow.open_spec`, `project.name`) through a config or customization dict; if any segment is missing or a non-leaf resolves to a non-dict, it raises `RenderError` naming the label and full path. It is invoked when a `{{config.foo.bar}}` or `{workflow.foo.bar}` token in a skill's markdown sources references a value that the merged config/customization layers do not provide. The render refuses to publish a snapshot with an unresolved token rather than emit a blank or `None`.","triggerScenarios":"A markdown source contains `{{config.project.codename}}` but `_bmad/config.toml` has no `codename` under `[project]`; a `{workflow.open_spec}` token but `customize.toml` lacks that key and no default ships; a typo in the token (`proejct`); the central config file was deleted so the whole subtree is absent.","commonSituations":"Authoring a new token before adding the config value; renaming a config key without updating tokens; disabling a config layer (e.g. removing `config.user.toml`) that previously supplied the value.","solutions":["Add the missing value at the reported path in the relevant TOML layer (`_bmad/config.toml` for config tokens, `customize.toml` for workflow tokens).","Check for typos by comparing the token path against the TOML key spelling.","If the value should be optional, remove the token from the markdown source instead of leaving it dangling.","Run render with verbose output to list all tokens and confirm each has a source."],"exampleFix":"# before: token {{config.project.codename}} in workflow.md, config.toml has\n[project]\nname = \"demo\"\n\n# after: add the key\n[project]\nname = \"demo\"\ncodename = \"atlas\"","handlingStrategy":"validation","validationCode":"def path_present(data, dotted):\n    cur = data\n    for part in dotted.split('.'):\n        if not isinstance(cur, dict) or part not in cur: return False\n        cur = cur[part]\n    return True\n\nfor tok in tokens:\n    assert path_present(config, tok), f'missing config value `{tok}`'","typeGuard":"def has_path(data: dict, dotted: str) -> bool:\n    cur = data\n    for part in dotted.split('.'):\n        if not isinstance(cur, dict) or part not in cur: return False\n        cur = cur[part]\n    return True","tryCatchPattern":"from render_skill import RenderError\ntry:\n    _lookup(central, path, 'config value')\nexcept RenderError as e:\n    print(f\"error: {e}\", file=sys.stderr); sys.exit(2)","preventionTips":["Add the config value before authoring a token that references it.","When renaming a config key, grep the markdown sources for tokens and update them together.","Run render in CI to catch unresolved tokens early."],"tags":["render","config","templating","missing-value","bmad"],"backgroundTag":null,"analyzedSha":"b70486b9bdcb0a404d329e2a763b57964e7f1360","analyzedAt":"2026-08-13T01:21:12.247Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}