Coverage for src/rhiza_hooks/_config_schema.py: 100%

12 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-15 06:14 +0000

1#!/usr/bin/env python3 

2"""Canonical schema for ``.rhiza/template.yml`` — keys, aliases, normalization. 

3 

4This is the single source of truth for the shape of a rhiza template config. It 

5owns the canonical key sets (:data:`REQUIRED_KEYS`, :data:`OPTIONAL_KEYS`, 

6:data:`VALID_KEYS`), the :data:`KEY_ALIASES` map from accepted alias spellings to 

7their canonical names, and :func:`normalize_config`, which rewrites a raw mapping 

8so every alias becomes its canonical key. 

9 

10Both the config validator (:mod:`rhiza_hooks.check_rhiza_config`) and the 

11template-bundles reader (:mod:`rhiza_hooks._bundles_config`) import from here so 

12the alias handling never drifts between them. The module is a leaf: it imports 

13nothing from the rest of :mod:`rhiza_hooks`. 

14""" 

15 

16from __future__ import annotations 

17 

18from typing import Any 

19 

20REQUIRED_KEYS = {"template-repository", "template-branch"} 

21# `language` names the project's rhiza language layer (python, rust, go). The 

22# rhiza-claude plugin writes it for every non-Python repo it scaffolds and reads it 

23# back for language detection, gate discovery and badge rendering, so it is 

24# load-bearing on the consumer side rather than decoration. Absent means Python, 

25# which is why Python pointers omit it and why its absence from this set went 

26# unnoticed until the language layers shipped in rhiza v1.3.0 (#330). 

27# 

28# Deliberately no enum of accepted values here. rhiza pins a rhiza-hooks version, so 

29# a hard-coded {python, rust, go} would make adding a fourth language layer upstream 

30# break every repo still on an older pin — the same failure this key already caused 

31# once. The value is type-checked in check_rhiza_config instead. 

32OPTIONAL_KEYS = {"include", "exclude", "templates", "language"} 

33VALID_KEYS = REQUIRED_KEYS | OPTIONAL_KEYS 

34# Alternative (alias) key names mapped to their canonical spellings. 

35KEY_ALIASES = { 

36 "repository": "template-repository", 

37 "ref": "template-branch", 

38 "profiles": "templates", 

39} 

40 

41 

42def normalize_config(config: dict[str, Any]) -> dict[str, Any]: 

43 """Normalize configuration by replacing aliases with canonical keys. 

44 

45 Args: 

46 config: Raw configuration dictionary 

47 

48 Returns: 

49 Normalized configuration with aliases replaced 

50 """ 

51 normalized: dict[str, Any] = {} 

52 for key, value in config.items(): 

53 # Replace alias with canonical name if it exists 

54 canonical_key = KEY_ALIASES.get(key, key) 

55 normalized[canonical_key] = value 

56 return normalized