refactor(quick-dev): drop render.py YAML fallback and smart defaults

Single happy path: central _bmad/config.toml with four-layer merge,
Python 3.11+ required (no ImportError guard), HALT if config missing.
Deletes load_flat_yaml, the YAML fallback branch, the setdefault block
for planning_artifacts/implementation_artifacts/communication_language,
and the tomllib ImportError fallback.

Part of plan-quick-dev-python-config-hardening.md (F0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-04-21 23:46:47 -07:00
parent 7701cbea62
commit 7428054805
1 changed files with 12 additions and 69 deletions

View File

@ -5,22 +5,21 @@ Resolves compile-time {{.variable}} placeholders from BMad's central config,
bakes absolute paths for {project-root} into derived values, and writes bakes absolute paths for {project-root} into derived values, and writes
rendered .md files to {project-root}/_bmad/render/bmad-quick-dev/. rendered .md files to {project-root}/_bmad/render/bmad-quick-dev/.
Config sources, tried in order: Config: four-layer merge of _bmad/config.toml + config.user.toml +
1. Central _bmad/config.toml + config.user.toml + custom/config.toml + custom/config.toml + custom/config.user.toml (post-#2285 installs).
custom/config.user.toml (four-layer merge; post-#2285 installs). Keys surface from [core] and [modules.bmm]. Missing config.toml HALT.
Keys surface from [core] and [modules.bmm].
2. _bmad/bmm/config.yaml (flat-YAML fallback for pre-#2285 installs).
Runtime {variable} placeholders (single curly) pass through untouched for Runtime {variable} placeholders (single curly) pass through untouched for
the LLM to resolve during workflow execution. the LLM to resolve during workflow execution.
Every invocation rebuilds from scratch no hash, no cache. Every invocation rebuilds from scratch no hash, no cache.
Python 3 stdlib only. UTF-8 I/O. Python 3.11+ stdlib only. UTF-8 I/O.
""" """
import os import os
import re import re
import sys import sys
import tomllib
def find_project_root(): def find_project_root():
@ -53,21 +52,16 @@ def _deep_merge(base, override):
def load_central_config(root): def load_central_config(root):
"""Four-layer merge of _bmad/config.toml and its peers. Returns the merged """Four-layer merge of _bmad/config.toml and its peers. HALTs if the base
dict, or None if the base _bmad/config.toml is absent (pre-#2285 install) _bmad/config.toml is absent."""
or if tomllib is unavailable."""
bmad_dir = os.path.join(root, "_bmad") bmad_dir = os.path.join(root, "_bmad")
base = os.path.join(bmad_dir, "config.toml") base = os.path.join(bmad_dir, "config.toml")
if not os.path.isfile(base): if not os.path.isfile(base):
return None
try:
import tomllib
except ImportError:
print( print(
"render.py: Python 3.11+ required for central TOML config; falling back", f"HALT and report to the user: central config not found at {base}"
file=sys.stderr, "ensure this is a post-#2285 BMAD install"
) )
return None sys.exit(1)
layers = [ layers = [
base, base,
@ -106,41 +100,6 @@ def flatten_central_config(merged):
return flat return flat
def load_flat_yaml(path):
"""Parse a flat key: value YAML file. Quotes stripped; indented values ignored.
Returns {} if the file is missing (with a stderr warning)."""
result = {}
try:
with open(path, "r", encoding="utf-8") as fh:
lines = fh.readlines()
except FileNotFoundError:
print(
f"render.py: config not found at {path}; using smart defaults",
file=sys.stderr,
)
return result
for line in lines:
stripped = line.strip()
if not stripped or stripped.startswith("#") or stripped.startswith("---"):
continue
if line.startswith(" ") or line.startswith("\t"):
continue
colon = stripped.find(":")
if colon < 0:
continue
key = stripped[:colon].strip()
value = stripped[colon + 1 :].strip().strip("'\"")
if not key or not value:
continue
# Skip YAML inline dict/list literals (balanced braces/brackets)
if (value.startswith("{") and value.endswith("}")) or (
value.startswith("[") and value.endswith("]")
):
continue
result[key] = value
return result
def render_template(content, vars_): def render_template(content, vars_):
"""Resolve {{.var}} substitutions. Unresolved references emit an empty string """Resolve {{.var}} substitutions. Unresolved references emit an empty string
(Go's missingkey=zero semantics).""" (Go's missingkey=zero semantics)."""
@ -153,29 +112,13 @@ def main():
root = find_project_root() root = find_project_root()
bmad_dir = os.path.join(root, "_bmad") bmad_dir = os.path.join(root, "_bmad")
central = load_central_config(root) vars_ = flatten_central_config(load_central_config(root))
if central is not None:
vars_ = flatten_central_config(central)
main_config_path = os.path.join(bmad_dir, "config.toml")
else:
legacy_path = os.path.join(bmad_dir, "bmm", "config.yaml")
vars_ = load_flat_yaml(legacy_path)
main_config_path = legacy_path
vars_.setdefault(
"planning_artifacts", "{project-root}/_bmad-output/planning-artifacts"
)
vars_.setdefault(
"implementation_artifacts",
"{project-root}/_bmad-output/implementation-artifacts",
)
vars_.setdefault("communication_language", "English")
for key in list(vars_.keys()): for key in list(vars_.keys()):
vars_[key] = vars_[key].replace("{project-root}", root) vars_[key] = vars_[key].replace("{project-root}", root)
vars_["project_root"] = root vars_["project_root"] = root
vars_["main_config"] = main_config_path vars_["main_config"] = os.path.join(bmad_dir, "config.toml")
vars_["sprint_status"] = os.path.join( vars_["sprint_status"] = os.path.join(
vars_["implementation_artifacts"], "sprint-status.yaml" vars_["implementation_artifacts"], "sprint-status.yaml"
) )