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
« 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.
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.
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"""
16from __future__ import annotations
18from typing import Any
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}
42def normalize_config(config: dict[str, Any]) -> dict[str, Any]:
43 """Normalize configuration by replacing aliases with canonical keys.
45 Args:
46 config: Raw configuration dictionary
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