Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0db8bff
Drop v2 migration utilities, rename to load_driver_rules/load_tag_rul…
jtdub Mar 22, 2026
9c3c6a0
Fix load_tag_rules: use _collect_match_rules and correct return type
jtdub Mar 22, 2026
1ff7615
update gha to test against the next branch
jtdub Mar 23, 2026
7927d9b
Fix __hash__/__eq__ inconsistency in HConfigChild (#185) (#236)
jtdub Mar 23, 2026
682abf0
Move Huawei VRP tests to integration test directory
jtdub Mar 26, 2026
5307e78
Add CODEOWNERS file
jtdub Mar 26, 2026
ea09a9d
Rename inconsistent public APIs (#216)
jtdub Mar 26, 2026
1568211
Add custom exception hierarchy (#219) (#239)
jtdub Mar 26, 2026
aefb3b8
Add Literal type constraint for indented_text() style parameter (#189…
jtdub Mar 26, 2026
eb60564
Rebase next against master (#242)
jtdub Mar 26, 2026
a5fa662
Merge remote-tracking branch 'origin/master' into sync-master-into-next
jtdub Jul 18, 2026
0a8da32
Merge pull request #272 from netdevops/sync-master-into-next
jtdub Jul 18, 2026
3142e11
Harden Fortinet swap_negation and idempotent_for; pin intended semant…
jtdub Jul 18, 2026
75b1a3a
Avoid materializing all children in HConfigBase.__len__() (#188) (#274)
jtdub Jul 18, 2026
91b035b
Resolve config views from the driver's view_class attribute (#187) (#…
jtdub Jul 18, 2026
5f71f95
Implement dot1q_mode_from_vlans() in HConfigViewBase (#228) (#276)
jtdub Jul 18, 2026
30a2913
Skip probe allocation for matched leaves in _remediation_right() (#191)
jtdub Jul 18, 2026
bc43ff5
Merge remote-tracking branch 'origin/next' into issue-191-remediation…
jtdub Jul 18, 2026
7f81fa6
Merge pull request #277 from netdevops/issue-191-remediation-alloc
jtdub Jul 18, 2026
5503d6d
v4: close out all remaining v4 issues (#278)
jtdub Jul 18, 2026
7a48684
Add JSON/XML config ingestion and rendering (#232) (#279)
jtdub Jul 19, 2026
f25069c
Add NETCONF edit-config remediation rendering (#232) (#280)
jtdub Jul 19, 2026
be5b29c
Fix future() negation edge cases; add prune_empty_branches (#269) (#281)
jtdub Jul 19, 2026
3e269e7
Restructure documentation into User, Administrator, and Developer gui…
jtdub Jul 19, 2026
f66e3b6
Add v3 to v4 migration guide (#288)
jtdub Jul 19, 2026
131ae37
Merge branch 'master' into next
jtdub Aug 4, 2026
4cc6371
Merge remote-tracking branch 'origin/next' into next
jtdub Aug 4, 2026
28eaded
Add gNMI-style JSON remediation rendering (#287) (#292)
jtdub Aug 5, 2026
cb6ffe5
Make built-in post-load callbacks public (#286) (#293)
jtdub Aug 5, 2026
12dbab3
Add future_with_report() for explicit negation-resolution audit (#294)
jtdub Aug 5, 2026
503ac28
Canonicalize registry keys to uppercase platform names (#295)
jtdub Aug 5, 2026
f5e0cb7
increment prerelease (#296)
jtdub Aug 5, 2026
e18798c
Fix documentation, agent-instruction, and changelog gaps (#297)
jtdub Aug 5, 2026
aaaf542
Add admin-run prepare-release workflow
jtdub Aug 6, 2026
e9ba3fc
Notify hier-config-ci orchestrator on release publish
jtdub Aug 18, 2026
f3337d3
Establish shared development standards for the hier-config ecosystem
jtdub Aug 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions .claude/skills/hier-config-new-driver/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Use when adding hier_config support for a new network platform or o

# Build a New hier_config Platform Driver

Scaffold an in-tree platform driver the way this repo expects. The authoritative recipe is `docs/dev/extending.md`; this skill adds the concrete templates. For a driver that lives *outside* this repo (in user code), follow `docs/user/custom-drivers.md#creating-a-custom-driver` instead.
Scaffold an in-tree platform driver the way this repo expects. The authoritative recipe is `docs/dev/creating-drivers.md`; this skill adds the concrete templates. For a driver that lives *outside* this repo (in user code), follow `docs/admin/custom-drivers.md` instead.

## Step 1: Characterize the Platform

Expand All @@ -14,7 +14,7 @@ Answer these before writing code — they determine which overrides and rules th
| Question | Driver hook if non-default |
|----------|---------------------------|
| Negation prefix (`no `? `undo `? `delete `?) | `negation_prefix` property (default `"no "`) |
| Some commands reset with a different form? | `NegationDefaultWithRule` / override `swap_negation` |
| Some commands reset with a different form? | `NegationRule` (REPLACE/DEFAULT/REGEX_SUB strategy) / override `swap_negation` |
| Sections closed with an exit token (`exit`, `quit`, `end-*`)? | `SectionalExitingRule` / override `sectional_exit` |
| Last-write-wins commands (`hostname`, `description`, …)? | `IdempotentCommandsRule` |
| Comment/banner lines to strip on load? | `PerLineSubRule` / `FullTextSubRule` |
Expand All @@ -26,29 +26,29 @@ Reference implementations: `platforms/huawei_vrp/driver.py` (small, rule-based),

## Step 2: Write the Failing Test First (TDD)

Create `tests/test_driver_<platform>.py` before the driver exists — conventions in `docs/dev/testing.md`. Flat functions, full annotations, round-trip idiom:
Create `tests/integration/test_<platform>.py` before the driver exists — conventions in `docs/dev/testing.md`. Flat functions, full annotations, round-trip idiom:

```python
from hier_config import Platform, get_hconfig_fast_load
from hier_config import HConfig, Platform


def test_negation_prefix() -> None:
running_config = get_hconfig_fast_load(
running_config = HConfig.from_lines(
Platform.ACME_OS, ("interface eth0", " shutdown")
)
generated_config = get_hconfig_fast_load(
generated_config = HConfig.from_lines(
Platform.ACME_OS, ("interface eth0",)
)
remediation = running_config.config_to_get_to(generated_config)
assert remediation.dump_simple() == ("interface eth0", " no shutdown")
remediation = running_config.remediation(generated_config)
assert remediation.to_lines() == ("interface eth0", " no shutdown")

running_after = running_config.future(remediation)
rollback = running_after.config_to_get_to(running_config)
rollback = running_after.remediation(running_config)
running_after_rollback = running_after.future(rollback)
assert not tuple(running_config.unified_diff(running_after_rollback))
```

Run it and confirm it fails for the right reason (unknown platform), not an import error. Add realistic config fixtures to `tests/fixtures/` if tests need more than inline tuples.
Run it and confirm it fails for the right reason (unknown platform), not an import error. Add realistic config fixtures to `tests/integration/fixtures/` if tests need more than inline tuples.

## Step 3: Scaffold the Driver

Expand Down Expand Up @@ -77,22 +77,24 @@ class HConfigDriverAcmeOS(HConfigDriverBase):
)
```

Replace `#` in the `per_line_sub` regex with the platform's actual comment token, and keep the `^\s*` anchor so indented comments are stripped too. Rules take `match_rules: tuple[MatchRule, ...]` (immutable — never lists). A minimal driver returning bare `HConfigDriverRules()` is valid; only add rules the platform needs. Public classes require docstrings.
Replace `#` in the `per_line_sub` regex with the platform's actual comment token, and keep the `^\s*` anchor so indented comments are stripped too. Rules take `match_rules: tuple[MatchRule, ...]` (immutable — never lists), while the `HConfigDriverRules` *collection fields themselves* are intentionally `list[...]` as shown above (so built-in rules/callbacks can be removed by identity). A minimal driver returning bare `HConfigDriverRules()` is valid; only add rules the platform needs. Public classes require docstrings.

## Step 4: Register the Platform

1. Add the member to the `Platform` enum in `hier_config/models.py` (alphabetical position). Note the enum uses `auto()`, so inserting a member renumbers everything after it — fine for in-repo use, but never rely on `Platform.value` for serialization.
2. Add the mapping to the `platform_drivers` dict in `get_hconfig_driver` (`hier_config/constructors.py`) and import the driver class there.
2. Add the mapping to the `_BUILTIN_DRIVERS` dict in `hier_config/registry.py` and import the driver class there. The key must be the canonical uppercase name string — `Platform.ACME_OS.name` — not the enum member (`_normalize()` canonicalizes lookups to `.name`, so a `Platform`-member key would be silently unreachable). If the platform has a config view, set the `view_class` attribute on the driver.

## Step 5: Document and Log

- Add a driver section (behavior summary + activation snippet) and a platform-table row to `docs/user/drivers.md`. Mark the status `Experimental` for a new driver.
- Add driver-level unit tests in `tests/unit/platforms/test_<platform>.py` (every recent driver has one; see `tests/unit/platforms/test_aruba_aoscx.py`). If the driver ships a config view, add `tests/unit/platforms/views/test_<platform>.py` too.
- Add a driver section (behavior summary) and a platform-table row to `docs/admin/platforms.md`. Mark the status `Experimental` for a new driver.
- If you introduced a new *rule type* (not just rule instances), document it in `docs/dev/rule-reference.md`.
- Add a `CHANGELOG.md` entry under `## [Unreleased]` → `### Added`.

## Step 6: Run the Gates

```bash
poetry run pytest tests/test_driver_<platform>.py -v
poetry run pytest tests/integration/test_<platform>.py -v
poetry run ./scripts/build.py lint-and-test
poetry run mkdocs build --strict
```
Expand Down
25 changes: 15 additions & 10 deletions .claude/skills/hier-config-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ Review the current change set against this repository's standards and report fin

## Step 1: Establish the Diff

Pick the base branch first: v4 work branches from `next`; only v3.x maintenance work branches from `master`. Diffing a `next`-based branch against `master` would include all of v4 and make the review meaningless.

```bash
git diff master...HEAD --stat # on a branch
git diff "$(git merge-base origin/next HEAD)"...HEAD --stat # v4 branch (the usual case)
git diff "$(git merge-base origin/master HEAD)"...HEAD --stat # v3.x maintenance branch
git diff HEAD --stat # fall back: uncommitted work
git diff --staged --stat # fall back: staged only
```
Expand All @@ -26,43 +29,45 @@ poetry run ./scripts/build.py lint
poetry run ./scripts/build.py pytest --coverage
```

If docs/ or mkdocs.yml changed, also run:
Also run the docs build — CI runs it unconditionally on every push/PR, not just when docs change:

```bash
poetry run mkdocs build --strict
```

Remember CI's test matrix covers Python 3.10–3.14: flag syntax or stdlib usage newer than 3.10 even if local checks pass.

## Step 3: Review by Category

Read the referenced doc before judging that category — the docs are the standard, not your intuition.

### Models & Typing — read `docs/dev/code-style.md`

- New Pydantic models subclass the local `BaseModel` (`hier_config/models.py`), never `pydantic.BaseModel` directly.
- Model fields use `tuple`/`frozenset`, never `list`/`set`. Rule models use `match_rules: tuple[MatchRule, ...]`.
- Model fields use `tuple`/`frozenset`, never `list`/`set`. Rule models use `match_rules: tuple[MatchRule, ...]`. Exception: the rule-collection fields on `HConfigDriverRules` are intentionally `list[...]` (removal-by-identity, #286) — do not flag them.
- No `Any`, no missing annotations, no unjustified `# type: ignore` / `# noqa`.
- Lint/coverage/type-checking configuration was not loosened.

### Tests & TDD — read `docs/dev/testing.md`

- Every library code change has corresponding tests.
- Tests are flat functions with full annotations; no test classes (benchmarks excepted).
- Driver changes are tested in `tests/test_driver_<platform>.py`; view changes in `tests/config_view/`.
- Driver/rule behavior changes include the round-trip idiom: remediation asserted via `dump_simple()` tuple, rollback verified via no `unified_diff`.
- New fixtures live in `tests/fixtures/` with module-scoped accessors in `tests/conftest.py`.
- Driver changes are tested in `tests/integration/test_<platform>.py` (unit-level driver tests in `tests/unit/platforms/`); view changes in `tests/unit/platforms/views/`.
- Driver/rule behavior changes include the round-trip idiom: remediation asserted via `to_lines()` tuple, rollback verified via no `unified_diff`.
- New fixtures live in the sibling `fixtures/` directory with module-scoped accessors in the relevant `conftest.py`.

### Driver & Rule Changes — read `docs/dev/extending.md`
### Driver & Rule Changes — read `docs/dev/creating-drivers.md` and `docs/dev/rule-reference.md`

- New rule types: frozen model in `models.py` → named default factory + field on `HConfigDriverRules` → consumed in `child.py`/`root.py` → populated in drivers.
- New platforms: `Platform` enum member, `get_hconfig_driver` wiring, per-platform test file, and a driver section + table row in `docs/user/drivers.md`.
- New rule types: frozen model in `models.py` → named default factory + field on `HConfigDriverRules` → consumed in `child.py`/`root.py` → populated in drivers → documented in `docs/dev/rule-reference.md`.
- New platforms: `Platform` enum member, `_BUILTIN_DRIVERS` wiring in `hier_config/registry.py` **keyed on `Platform.X.name`** (a `Platform`-member key is silently unreachable — `_normalize()` canonicalizes to uppercase name strings), `view_class` on the driver if it has a config view, per-platform test file, and a driver section + table row in `docs/admin/platforms.md`.

### Changelog

- `CHANGELOG.md` has an entry under `## [Unreleased]`, in the right category (`Added`/`Changed`/`Fixed`/`Removed`), referencing the issue/PR (`(#NNN)`).

### Docs

- Public API or driver behavior changes are reflected in `docs/user/` (and `docs/user/api-reference.md` where relevant).
- Public API or driver behavior changes are reflected in `docs/user/` or `docs/admin/` (and `docs/dev/api-reference.md` where relevant).
- New doc pages are in the `mkdocs.yml` nav; moved pages have a `redirect_maps` entry.

### Commits — read `CONTRIBUTING.md` (Commit Message Style)
Expand Down
25 changes: 14 additions & 11 deletions .claude/skills/hier-config-troubleshoot/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,18 @@ Diagnose why hier_config produced unexpected output. Work from a minimal reprodu
Reduce the problem to the smallest config pair that shows it, using inline tuples — no fixture files needed:

```python
from hier_config import Platform, get_hconfig_fast_load
from hier_config import HConfig, Platform

running_config = get_hconfig_fast_load(Platform.CISCO_IOS, ("hostname foo",))
generated_config = get_hconfig_fast_load(Platform.CISCO_IOS, ("hostname bar",))
print("\n".join(running_config.config_to_get_to(generated_config).dump_simple()))
running_config = HConfig.from_lines(Platform.CISCO_IOS, ("hostname foo",))
generated_config = HConfig.from_lines(Platform.CISCO_IOS, ("hostname bar",))
print("\n".join(running_config.remediation(generated_config).to_lines()))
```

Bisect: delete config lines until removing one more makes the symptom disappear. That line (and its ancestry) is where to look. If the report compares platforms ("works on X, broken on Y"), reproduce **both** platforms — claimed-working references are often wrong, and the platforms that actually differ tell you which rule is responsible. If the raw config parses differently than expected, compare `get_hconfig()` (full parse with preprocessing) against `get_hconfig_fast_load()` (no preprocessing) — a difference means a `per_line_sub`/`full_text_sub`/`config_preprocessor` or indentation issue.
Bisect: delete config lines until removing one more makes the symptom disappear. That line (and its ancestry) is where to look. If the report compares platforms ("works on X, broken on Y"), reproduce **both** platforms — claimed-working references are often wrong, and the platforms that actually differ tell you which rule is responsible. If the raw config parses differently than expected, compare `HConfig.from_text()` (full parse with preprocessing) against `HConfig.from_lines()` (no preprocessing) — a difference means a `per_line_sub`/`full_text_sub`/`config_preprocessor` or indentation issue.

## Step 2: Inspect the Tree, Not the Text

- `config.dump_simple()` — the parsed tree as indented lines; wrong nesting is visible immediately.
- `config.to_lines()` — the parsed tree as indented lines; wrong nesting is visible immediately.
- `running_config.unified_diff(generated_config)` — structure-aware diff.
- `config.driver.rules` — the live rule set; check what the platform driver actually matches.

Expand All @@ -32,17 +32,20 @@ Bisect: delete config lines until removing one more makes the symptom disappear.
| Symptom | Likely cause | Where to look |
|---------|-------------|---------------|
| Command emitted as `no X` + `Y` instead of just `Y` | Missing idempotency rule — the command is last-write-wins on the device but the driver doesn't know | `idempotent_commands` in the platform driver; add `IdempotentCommandsRule` |
| Negation has the wrong form (`no shutdown` vs `default shutdown` vs truncated args) | Negation rules | `negate_with` (`NegationDefaultWithRule`), `negation_default_when`, or the driver's `swap_negation` override |
| Negation has the wrong form (`no shutdown` vs `default shutdown` vs truncated args) | Negation rules | `NegationRule` (REPLACE/DEFAULT/REGEX_SUB strategy) in the driver's `negation` list, or the driver's `swap_negation` override |
| Lines nested under the wrong parent; everything after line X collapses under it | Irregular indentation in vendor output; an `IndentAdjustRule` matching too broadly or missing | `indent_adjust` rules. Real cases: XR `template` blocks; Huawei `peer-public-key end` (see git log for #205, #268) |
| `DuplicateChildError` | Platform legitimately repeats a child text under one parent | Add `ParentAllowsDuplicateChildRule` (see #266 for a real example) |
| Section replaced wholesale (or should be, but isn't) | Sectional overwrite | `sectional_overwrite` / `sectional_overwrite_no_negate` (XR `route-policy` is the canonical case) |
| Missing/wrong exit token after a section (`exit`, `quit`, `end-policy`) | Sectional exiting | `sectional_exiting` rules; `exit_text_parent_level` for unindented exits |
| Commands in an order the device rejects | Ordering weights | `ordering` rules (lower weight applies first) |
| Junk lines in the tree (banners, comments, timestamps) | Load-time substitutions | `per_line_sub` / `full_text_sub` |
| `future()` or rollback doesn't match real device behavior | Known algorithm limitations | `docs/user/future-config.md#known-limitations` — duplicate children and order-dependent sections (ACLs need sequence numbers) are documented limits |
| Suspected unresolved negations or silent idempotent replacements in `future()` | Negation resolution ambiguity | Use `HConfig.future_with_report()` — the returned `FutureReport.unresolved_negations` / `.idempotency_replacements` name the exact nodes instead of you scanning the render |
| JSON/XML config raises on `from_text()` / parses as gibberish | Structured input fed to the text parser (rejected by design) | Use `HConfig.from_json()` / `HConfig.from_xml()`; text constructors deliberately reject structured formats |
| `InvalidConfigError: Attribute changes cannot be expressed as gNMI delete paths` (or the NETCONF equivalent) | Structured-rendering limitation on attribute-style (`@`-prefixed) changes | `hier_config/formats.py` (`hconfig_to_gnmi_json` / `hconfig_to_netconf_xml`); restructure the change as element updates |
| Wrong platform behavior entirely | Wrong driver selected | Confirm the `Platform` enum member; `GENERIC` has almost no rules |

Rule semantics reference: `docs/user/custom-drivers.md#driver-rule-types`. Layer responsibilities: `docs/dev/architecture.md`.
Rule semantics reference: `docs/dev/rule-reference.md`. Layer responsibilities: `docs/dev/architecture.md`.

## Step 4: Confirm Which Rule Fires

Expand All @@ -62,8 +65,8 @@ If a driver rule should match but doesn't, print the rule set (`config.driver.ru

## Step 5: Fix at the Source

- Driver rule gap (most common): add/adjust the rule in the platform driver's `_instantiate_rules()` — recipe in `docs/dev/extending.md`.
- Core algorithm (`base.py`, `root.py`, `child.py`): rare; read `docs/dev/architecture.md` first and check `git log` for related fixes before changing shared behavior.
- User-side workaround (can't wait for a release): customize the driver at runtime — `docs/user/custom-drivers.md#customizing-existing-drivers`.
- Driver rule gap (most common): add/adjust the rule in the platform driver's `_instantiate_rules()` — recipe in `docs/dev/creating-drivers.md`.
- Core algorithm (`base.py`, `root.py`, `child.py`, `tree_algorithms.py`): rare; read `docs/dev/architecture.md` first and check `git log` for related fixes before changing shared behavior.
- User-side workaround (can't wait for a release): customize the driver at runtime — `docs/admin/customizing-rules.md`.

Every fix ships with a regression test that reproduces the original symptom (`docs/dev/testing.md`, round-trip idiom) and a `CHANGELOG.md` entry. Fixes to one platform must not leak: run the full suite (`poetry run ./scripts/build.py lint-and-test`), not just the platform's test file.
16 changes: 16 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.git
.github
.claude
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
.coverage
htmlcov
site
dist
.dockerignore
Dockerfile
docker-compose.yml
Loading