126 plikow (md, yaml, sh, py) odwolywalo sie do sciezek sprzed migracji.
15 markdown-linkow [..](..) -> policzona sciezka WZGLEDNA wobec pliku
odsylajacego (wczesniej czesc z nich byla repo-root-relative i nie
rozwiazywala sie z katalogu, w ktorym lezala)
200 odwolan tekstowych (backticki, proza, yaml, importy w kodzie)
-> nowa sciezka repo-root-relative, zgodnie z konwencja repo
5 linkow rodzenstwa (gole nazwy plikow, np. "](DEPLOY.md)") — dzialaly
tylko w starym katalogu; przeliczone recznie
Objete m.in.: CLAUDE.md (scripts/onboard/README.md -> kb/runbooks/
node-onboarding-tool.md, docs/backlog.md -> kb/phases/backlog.md),
README.md, .claude/skills/, 20 session logow, kod jobow.
Ostatnie 5 odwolan pochodzi z tresci wciagnietej rebasem z origin/master
(session log 2026-07-31, override node-agenta na SOLARII, dwie pozycje
backlogu) — wskazywaly na docs/incidents/, docs/kb/modules/ i
services/narty27/README.md sprzed migracji.
Dodany wzajemny link miedzy kb/services/control-plane.md (stub kodu)
a kb/subsystems/control-plane.md (opis, deprecated) — dwa dokumenty o tym
samym systemie, latwe do pomylenia.
Weryfikacja na 790 plikach: 0 odwolan do starych sciezek,
0 martwych linkow markdown. Lint OKF: 190/190 plikow ZGODNE.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
100 lines
3.2 KiB
Python
Executable file
100 lines
3.2 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Split HA list/dict config files into one-object-per-file layouts.
|
|
|
|
See kb/decisions/ha-configs-as-code.md ("Split + normalization"):
|
|
automations.yaml -> automations/<id>.yaml
|
|
scenes.yaml -> scenes/<id>.yaml
|
|
scripts.yaml -> scripts/<key>.yaml
|
|
"""
|
|
import os
|
|
import sys
|
|
|
|
import yaml
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
from normalize import dump_canonical_yaml # noqa: E402
|
|
|
|
|
|
def split_list_by_id(items, id_field="id"):
|
|
"""items: list of dicts, each expected to carry an `id_field` key.
|
|
|
|
Returns {id: obj}. Raises ValueError on a missing/duplicate id so a bad
|
|
split can never silently drop or overwrite an object.
|
|
"""
|
|
result = {}
|
|
for obj in items:
|
|
if not isinstance(obj, dict) or id_field not in obj:
|
|
raise ValueError(f"object missing '{id_field}' field: {obj!r}")
|
|
key = str(obj[id_field])
|
|
if key in result:
|
|
raise ValueError(f"duplicate {id_field} '{key}'")
|
|
result[key] = obj
|
|
return result
|
|
|
|
|
|
def split_dict(data):
|
|
"""scripts.yaml is already a dict keyed by script id — pass through."""
|
|
return dict(data or {})
|
|
|
|
|
|
SPLIT_SPECS = {
|
|
"automations.yaml": ("automations", lambda data: split_list_by_id(data or [], "id")),
|
|
"scenes.yaml": ("scenes", lambda data: split_list_by_id(data or [], "id")),
|
|
"scripts.yaml": ("scripts", split_dict),
|
|
}
|
|
|
|
|
|
def write_split(objects_by_key, out_dir):
|
|
"""Write out_dir/<key>.yaml (canonical form) for each key/obj pair.
|
|
|
|
Removes stale *.yaml files left over from a previous run for keys that
|
|
no longer exist, so the split output always mirrors the source exactly.
|
|
"""
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
expected = {f"{key}.yaml" for key in objects_by_key}
|
|
for existing in os.listdir(out_dir):
|
|
if existing.endswith(".yaml") and existing not in expected:
|
|
os.remove(os.path.join(out_dir, existing))
|
|
for key, obj in objects_by_key.items():
|
|
path = os.path.join(out_dir, f"{key}.yaml")
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(dump_canonical_yaml(obj))
|
|
|
|
|
|
def split_file(source_path, out_root):
|
|
"""source_path: path to e.g. .../automations.yaml. out_root: config/<instance>/.
|
|
|
|
Returns the output subdirectory written, or None if source_path's
|
|
basename isn't one of the known split targets.
|
|
"""
|
|
basename = os.path.basename(source_path)
|
|
if basename not in SPLIT_SPECS:
|
|
return None
|
|
subdir_name, splitter = SPLIT_SPECS[basename]
|
|
with open(source_path, "r", encoding="utf-8") as f:
|
|
data = yaml.safe_load(f)
|
|
objects_by_key = splitter(data)
|
|
out_dir = os.path.join(out_root, subdir_name)
|
|
write_split(objects_by_key, out_dir)
|
|
return out_dir
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 3:
|
|
print("usage: split.py <source-yaml-path> <config-out-root>", file=sys.stderr)
|
|
return 2
|
|
source_path, out_root = argv[1], argv[2]
|
|
result_dir = split_file(source_path, out_root)
|
|
if result_dir is None:
|
|
print(
|
|
f"split.py: '{os.path.basename(source_path)}' is not a known split target",
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
print(result_dir)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv))
|