Skip to content

Establish shared development standards for the hier-config ecosystem - #298

Open
jtdub wants to merge 36 commits into
masterfrom
shared-dev-standards
Open

Establish shared development standards for the hier-config ecosystem#298
jtdub wants to merge 36 commits into
masterfrom
shared-dev-standards

Conversation

@jtdub

@jtdub jtdub commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Why

The hier-config applications (hier-config-api, -cli, -mcp, -gpt) each carry their own copy of this repository's lint, typing, and test tooling, and the copies have drifted. hier-config-api's scripts/build.py had been rewritten to use # noqa comments because it pinned ruff <0.15, which does not understand the # ruff: ignore[...] directive used here — so neither repo could adopt the other's file. There was no mechanism to notice or fix that.

What

This repository becomes the canonical source for the shared files, and downstream projects gain a way to pull changes in.

  • .standards.yml — declares the source ref and the owned files (scripts/build.py, scripts/sync_standards.py, .yamllint.yml, .dockerignore). Downstream manifests add substitutions so one build.py serves projects with different package names.
  • scripts/sync_standards.pycheck reports drift and exits non-zero; apply writes the canonical versions. Two details worth review:
    • Substituted Python is re-run through ruff format. A longer package name (hier_confighier_config_api) can push a line past 88 chars, and without this the file never converges — apply would write content the formatter immediately rewrites, so the next check reports drift forever.
    • A file listed in a manifest but absent from the source ref reports a plain message rather than an httpx traceback. That is the normal bootstrap state (it is what this branch looks like until it merges).
  • Dockerfile / docker-compose.yml / tasks.py — the same invoke commands in every project: build, docs, pytest, lint, lint-and-test, cli, sync-standards, destroy. These are deliberately not synced: an application serves a process and a library does not. They are conventions, with this repo as the reference implementation.
  • docs/dev/shared-standards.md — the model, the manifest format, and how to change a shared file.

Adds invoke, httpx, and pyyaml to the dev group. No runtime dependency changes.

Verification

  • poetry run python scripts/build.py lint — clean
  • 762 tests pass inside the development container (invoke pytest)
  • poetry run mkdocs build --strict — clean
  • docker build --target development succeeds

Follow-up

A companion PR on hier-config-api relaxes its ruff pin to match this repo and adopts the manifest; after that its sync-standards reports zero drift against scripts/build.py. It cannot merge until this one does, since the sync fetches from master.

🤖 Generated with Claude Code

jtdub and others added 30 commits March 25, 2026 22:03
…es, and reorganize tests (#221)

Remove v2-to-v3 platform mapping functions and constants. Rename
load_hconfig_v2_options to load_driver_rules and load_hconfig_v2_tags
to load_tag_rules, preserving dict-based driver extension for Nautobot
Golden Config compatibility.

Reorganize test suite into unit/, integration/, and benchmarks/
directories mirroring the source code structure. Split the 2079-line
test_hier_config.py into focused files by module (test_root.py,
test_child.py, test_children.py). Separate driver remediation scenario
tests into integration/ and unit tests into unit/platforms/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace inline match-rule collection with existing _collect_match_rules
helper for consistency. Simplify return type from
tuple[TagRule] | tuple[TagRule, ...] to tuple[TagRule, ...].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix __hash__/__eq__ inconsistency in HConfigChild (#185)

__hash__ included new_in_config and order_weight but __eq__ intentionally
excluded them, violating the Python invariant that a == b implies hash(a) == hash(b).
__eq__ also checked tags but __hash__ did not include them.

Align __hash__ to use the same fields as __eq__: text, tags, and children.

Add five tests covering each dimension of the inconsistency and its practical
impact on set deduplication and dict key lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix pre-existing lint errors in next branch

- test_benchmarks.py: replace append loops with extend (PERF401), add
  @staticmethod to methods that don't use self (PLR6301), suppress
  intentional print calls with noqa: T201
- test_child.py: suppress pylint too-many-lines (C0302)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- tags_add()/tags_remove() → add_tags()/remove_tags()
- cisco_style_text() → indented_text()
- dump_simple() → to_lines()
- config_to_get_to() → remediation()
- depth() method → depth property
- Rename private helpers _config_to_get_to/_left/_right accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add HierConfigError as the base exception with DriverNotFoundError,
InvalidConfigError, IncompatibleDriverError, and reparent
DuplicateChildError under it. Replace generic ValueError/TypeError
raises in constructors and workflows with specific exception types.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#241)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add CODEOWNERS file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Literal type constraint for cisco_style_text() style parameter (#189) (#240)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Drop v2 migration utilities, rename to load_driver_rules/load_tag_rules, and reorganize tests (#221)

Remove v2-to-v3 platform mapping functions and constants. Rename
load_hconfig_v2_options to load_driver_rules and load_hconfig_v2_tags
to load_tag_rules, preserving dict-based driver extension for Nautobot
Golden Config compatibility.

Reorganize test suite into unit/, integration/, and benchmarks/
directories mirroring the source code structure. Split the 2079-line
test_hier_config.py into focused files by module (test_root.py,
test_child.py, test_children.py). Separate driver remediation scenario
tests into integration/ and unit tests into unit/platforms/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix load_tag_rules: use _collect_match_rules and correct return type

Replace inline match-rule collection with existing _collect_match_rules
helper for consistency. Simplify return type from
tuple[TagRule] | tuple[TagRule, ...] to tuple[TagRule, ...].

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* update gha to test against the next branch

* Fix __hash__/__eq__ inconsistency in HConfigChild (#185) (#236)

* Fix __hash__/__eq__ inconsistency in HConfigChild (#185)

__hash__ included new_in_config and order_weight but __eq__ intentionally
excluded them, violating the Python invariant that a == b implies hash(a) == hash(b).
__eq__ also checked tags but __hash__ did not include them.

Align __hash__ to use the same fields as __eq__: text, tags, and children.

Add five tests covering each dimension of the inconsistency and its practical
impact on set deduplication and dict key lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix pre-existing lint errors in next branch

- test_benchmarks.py: replace append loops with extend (PERF401), add
  @staticmethod to methods that don't use self (PLR6301), suppress
  intentional print calls with noqa: T201
- test_child.py: suppress pylint too-many-lines (C0302)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>

* Move Huawei VRP tests to integration test directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Rename inconsistent public APIs (#216)

- tags_add()/tags_remove() → add_tags()/remove_tags()
- cisco_style_text() → indented_text()
- dump_simple() → to_lines()
- config_to_get_to() → remediation()
- depth() method → depth property
- Rename private helpers _config_to_get_to/_left/_right accordingly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add custom exception hierarchy (#219) (#239)

Add HierConfigError as the base exception with DriverNotFoundError,
InvalidConfigError, IncompatibleDriverError, and reparent
DuplicateChildError under it. Replace generic ValueError/TypeError
raises in constructors and workflows with specific exception types.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add Literal type constraint for indented_text() style parameter (#189) (#241)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CHANGELOG.md
#	hier_config/__init__.py
#	hier_config/utils.py
#	tests/integration/test_cisco_ios.py
#	tests/unit/test_child.py
#	tests/unit/test_utils.py
Sync master (3.6.0–3.6.2) into next
…ics (#225) (#273)

Re-evaluation of #225 found:

- swap_negation dropping parameters is intentional FortiOS behavior:
  negation resets an attribute to default via `unset <attribute>` with no
  value, so `set description "Port 1"` correctly negates to
  `unset description`. Preserving the full text (the fix proposed in the
  issue) would emit invalid FortiOS commands.
- The IndexError paths are unreachable through the public API because
  HConfigChild.text is always stripped, so a trailing-space `set ` can
  never persist.

Both methods are nevertheless hardened with token-count guards so the
invariant no longer silently depends on the text setter's strip behavior,
idempotent_for no longer re-splits config.text on every iteration, and
the intended semantics are pinned by unit tests.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Count descendants with a generator expression instead of building a
temporary tuple of every node, avoiding a large allocation on big
configuration trees.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
)

Replace the hardcoded isinstance chain in get_hconfig_view() with a
view_class ClassVar on HConfigDriverBase. Each platform driver with a
view declares it directly, drivers without one raise DriverNotFoundError
with a clear "No view registered" message, and custom driver subclasses
can register or override a view by setting view_class - delivering the
driver-side half of #229 ahead of the #226 registration system.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The method was abstract with all five platform views raising
NotImplementedError. The logic is pure data interpretation, identical to
ConfigViewInterfaceBase.dot1q_mode (tagged_all -> TAGGED_ALL, tagged
VLANs -> TAGGED, untagged only -> ACCESS), so it now lives once as a
concrete static method on the base. The per-platform stubs and their
NotImplementedError tests are removed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
A probe HConfigChild was instantiated for every matched line to collect
potential delta children, then discarded if empty. For matched leaves -
the dominant case when remediating mostly-identical configs - both
remediation passes have no children to visit, so the subtree is provably
empty and the allocation plus recursive call can be skipped with an O(1)
guard.

Benchmarks (best of 3, ~10k-line configs):
- Remediation 10% diff: 0.0273s -> 0.0183s (~33% faster)
- Remediation 100% diff: 0.0539s -> 0.0460s (~15% faster)
- Completely different configs: unchanged (no matched leaves)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Skip probe allocation for matched leaves in _remediation_right() (#191)
* Add driver registry and HConfig classmethod constructors (#226, #229, #218)

- New hier_config.registry: register_driver()/unregister_driver()/
  get_registered_platforms(). Custom platforms register by string name
  (case-insensitive, usable anywhere a Platform is accepted); built-in
  drivers can be overridden and restored. Built-ins register at import.
  Re-evaluated vs the issue's runtime Platform-enum extension: mutating
  Enum classes at runtime is fragile; a registry keyed by Platform | str
  provides the same capability safely.
- Registered drivers carry their view via view_class, completing #229.
- HConfig.from_text()/from_lines()/from_dump() classmethods replace
  get_hconfig()/get_hconfig_fast_load()/get_hconfig_from_dump()/
  get_hconfig_fast_generic_load() (#218). get_hconfig_driver() and
  get_hconfig_view() remain standalone utilities.
- HConfigDriverBase and HConfigDriverRules are exported as public API.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Unify negation rules into a single NegationRule with strategy enum (#220)

NegationDefaultWithRule, NegationDefaultWhenRule, and NegationSubRule
collapse into one NegationRule model with a NegationStrategy enum
(REPLACE, DEFAULT, REGEX_SUB) and a single `negation` list on
HConfigDriverRules. Precedence is now explicit list order - first
matching rule wins - instead of being encoded in method ordering.

driver.negate_with() remains as the imperative extension hook (used by
HP ProCurve) and reads REPLACE-strategy rules from the unified list.
load_driver_rules() still accepts the v2 dict keys
(negation_negate_with, negation_default_when, negation_sub) and maps
them onto unified rules for Nautobot compatibility.

Also silences pylint cyclic-import, which now only fires on the lazy
(call-time) imports inside HConfig's classmethod constructors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Root duplicates, tree algorithm extraction, loader refactor, plugins, format detection (#215, #217, #186, #180, #181, #232)

- #215: a ParentAllowsDuplicateChildRule with empty match_rules now applies
  to the root, unifying root-level duplicate handling with the existing
  child mechanism (children can never match an empty rule because lineage
  matching requires equal lengths).
- #217: the diff/remediation/future/with_tags algorithms move from
  HConfigBase into hier_config/tree_algorithms.py as standalone functions
  (compute_difference, compute_remediation, compute_future,
  compute_with_tags); HConfigBase keeps thin delegating methods.
- #186: _load_from_string_lines() is now a thin wrapper over a stateful
  _ConfigTextLoader class with focused methods for banner processing,
  line normalization, and hierarchy construction; drops the C901 noqa.
- #180: HConfigDriverRules.remediation_transform_callbacks run on the
  remediation config in WorkflowRemediation.remediation_config.
- #181: RemediationPlugin ABC (hier_config.plugins); user plugins are
  passed via WorkflowRemediation(plugins=...) and applied after driver
  callbacks.
- #232: XML and JSON inputs are detected in HConfig.from_text() and
  rejected with a clear InvalidConfigError pointing at the supported
  set-style paths (JunOS/VyOS/Nokia SRL preprocessors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Docs and changelog for v4 API changes; non-mutation guarantee test (#224)

Migrate README, docs/, and CLAUDE.md to the HConfig.from_* constructors,
unified NegationRule, and the driver registry; add a Registering a Custom
Driver section; consolidate the Unreleased changelog with the full v4
change set and the documented design decisions for #222, #223, and #224.
Adds a regression test pinning that remediation() does not mutate its
input configs (#224).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Split interface views into core + capability mixins; complete EOS/NXOS/XR views (#227, #230)

ConfigViewInterfaceBase now holds only the core surface (name,
description, enabled, ipv4_interfaces, is_loopback, is_svi, number,
port_number, vrf, plus concrete helpers). Optional capabilities move to
ABC mixins - InterfaceBundleViewMixin, InterfaceVlanViewMixin,
InterfaceNACViewMixin, InterfacePhysicalViewMixin - so users check
capability with isinstance() instead of catching NotImplementedError.
HConfigViewBase.bundle_interface_views and module_numbers are
capability-aware. All mixins are exported as public API.

The EOS, NXOS, and XR views are completed to core + Bundle + Vlan
(addresses in CIDR/mask/XR forms, VRF membership, dot1q subinterfaces,
switchport access/trunk, Port-Channel/Bundle-Ether bundles, plus root
view properties: interface_names_mentioned, ipv4_default_gw, location,
stack_members, vlans). No NotImplementedError stubs remain in those
views; NAC/Physical are simply not inherited where unsupported. IOS
gains bundle_member_interfaces and a case-insensitive is_bundle fix;
ProCurve gains a real bundle_id.

Raise-only tests are replaced with behavior tests parsing real config
snippets and per-platform capability assertions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Address PR #278 self-review findings

- port_number now derives from the letter-stripped `number` property on
  all five platform views, fixing ValueError on slash-less names such as
  Port-channel10, Bundle-Ether10, and Trk1; regression tests added for
  IOS, EOS, NX-OS, and XR.
- NegationRule validates per-strategy fields at construction: REPLACE
  requires `use`, REGEX_SUB requires `search` (empty `replace` stays
  valid for deletion-style substitutions). Previously a REPLACE rule
  with empty `use` silently fell through to swap_negation.
- Align the NegationRule docstring with actual evaluation order (REPLACE
  rules are consulted first via driver.negate_with(), then remaining
  rules in list order).
- Document that the driver registry is unsynchronized (register at
  startup) and that structured-format detection only guards from_text().

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Bump version to 4.0.0b1

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* update poetry.lock

* Apply /simplify review: hoist view duplication, drop shims, unify plugins on callables

Findings from four parallel cleanup reviews (reuse, simplification,
efficiency, altitude) applied:

- Hoist ~390 duplicated lines out of the five platform view files:
  concrete defaults for name/number/port_number/description/enabled/
  is_loopback/is_svi on ConfigViewInterfaceBase; bundle_id and
  bundle_member_interfaces on InterfaceBundleViewMixin keyed by a
  _bundle_membership_prefix hook; native_vlan/tagged_vlans/tagged_all on
  InterfaceVlanViewMixin keyed by an _encapsulation_prefix hook;
  interface_names_mentioned/location/stack_members/vlans defaults on
  HConfigViewBase; shared parse_ipv4_interface() in platforms.functions.
  Platforms keep overrides only where behavior genuinely differs
  (HP ProCurve throughout; XR native_vlan).
- Remove the pass-through _future/_with_tags/_remediation/_difference
  shims from HConfigBase; HConfig calls the tree_algorithms functions
  directly.
- from_lines()/from_dump() build their empty tree via HConfig(driver)
  instead of routing "" through the full text pipeline; the structured-
  format guard now also covers the raw-str form of from_lines() and
  inspects a bounded prefix instead of copying the whole input.
- WorkflowRemediation(plugins=...) accepts plain callables;
  RemediationPlugin gained __call__ as sugar.
- Replace the side-effecting `elif predicate: pass` in _ConfigTextLoader
  with a positive branch.
- Adapt to ruff 0.15.22 (new preview rules from the renovate lock bump):
  noun-phrase property docstrings, per-file PLC0415 ignore for the lazy
  classmethod imports, unused TypeVar removal.

Deliberate skips: single-pass negation evaluation (would change driver-
facing precedence), caching the root duplicate-rule check (rules are
mutated post-init by the documented extension pattern), dropping plugin
name/description metadata (specified by #181), and the full
constructors/root cycle removal (blocked by pydantic runtime type
resolution in driver_base).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Guard bundle mixin defaults against an unset membership prefix

get_child(startswith="") matches any first child, so a platform that
inherits InterfaceBundleViewMixin without declaring
_bundle_membership_prefix would get garbage from the default bundle_id
and bundle_member_interfaces instead of failing inert. Both defaults
now short-circuit when the prefix is unset; pinned by a regression
test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Add JSON/XML config ingestion and rendering (#232)

New hier_config/formats.py maps structured configs onto the standard
HConfig tree and back:

- HConfig.from_json() ingests JSON objects or text (e.g. OpenConfig),
  with keyed lists identified via list_keys (default ("name", "id")).
- HConfig.from_xml() ingests XML documents (e.g. NETCONF payloads);
  attributes map to @name leaves, text content to value leaves, and
  repeated sibling elements are identified via list_keys.
- HConfig.to_json() / to_xml() invert the mappings, so structured
  configs can be diffed, predicted with future(), and rendered back in
  their source format. Round-trip fidelity is pinned by tests.
- The structured-format detection error now points at the new
  constructors instead of only suggesting CLI conversion.

Known limits (documented): a single-item JSON scalar list renders back
as a bare scalar, and NETCONF edit-config operation attributes carry no
remediation semantics yet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix empty-object JSON round trip; document list caveats (#279 review)

An empty JSON object collapsed to null on to_json() because a
single-word childless node was read as a valueless leaf. from_json only
produces such nodes for empty objects (scalar leaves always carry a
value word, including `key null`), so they now invert to {}. Pinned by
a round-trip test alongside tests for the DuplicateChildError behavior
on duplicate list items/identities; the module docstring now documents
the empty-list drop and duplicate-item caveats.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Apply /simplify review to structured formats (#279)

Findings from four parallel cleanup reviews applied:

- Promote driver resolution to a public registry.resolve_driver();
  constructors and formats share it instead of carrying private copies
  of the same Platform|str|driver dispatch.
- Type the JSON recursion with a recursive JsonValue alias, removing
  the annotated-assignment + pyright-ignore narrowing workarounds and
  the parsed-shadow variable.
- Single source for the ("name", "id") default: the HConfig
  classmethods take list_keys=None and formats resolves it against
  DEFAULT_LIST_KEYS.
- Merge _xml_children_into into _xml_element_into (one recursion, tag
  counting via collections.Counter) and classify children before
  splitting in _node_to_xml_element, removing the double text split
  per rendered node.
- Merge the duplicated "### Added" heading in the Unreleased changelog.
- Document that to_json()/to_xml() output is undefined for trees built
  by other constructors, and mark the @/#text line encoding as an
  implementation detail; drop issue tags from API docstrings.

Skipped per review guidance: single-scan _xml_identity_suffix (changes
list_keys priority semantics) and the _store_json_member double-lookup
micro-nit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
- WorkflowRemediation.remediation_netconf_xml() and
  formats.hconfig_to_netconf_xml() render a remediation between
  from_xml() trees as a NETCONF payload: negated nodes become
  nc:operation="delete" elements, additions use the default merge
  operation, and attribute-level changes raise InvalidConfigError.
  Keyed list-entry deletions are expressed by their key leaf, resolved
  against the running config when available; the standalone function
  falls back to value-bearing leaf deletes.
- Fix XML ingestion to key an element whenever an identifying list_keys
  child exists rather than only when its tag repeats among siblings.
  Previously the same entry got different node text depending on
  sibling count, producing spurious delete-and-re-add diffs between
  configs with different list-entry counts.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* Fix future() negation edge cases; add prune_empty_branches (#269)

compute_future now resolves negations in an explicit order:

1. Exact positive match - the negation removes the line and neither
   survives. Evaluated before the idempotency rules, which can match
   the negation line itself and previously kept it as a literal child
   while displacing the original (issue case 1).
2. Idempotency rules - interchangeable forms of one setting replace
   each other, deliberately including negated forms tracked by a rule
   (IOS `no logging console` persists in the render, which the
   remediation roundtrip depends on).
3. Shorthand prefix match - `no description` removes `description foo`
   as devices do (issue case 2).
4. Unmatched negations are kept, accounting for `no ...` lines native
   to running configs and preserving the did-not-apply-cleanly signal
   the issue asked to retain.

HConfig.future() gains prune_empty_branches (issue case 3): sections
emptied by the change are pruned as devices do on commit, cascading
upward, while sections that were already empty (or newly added empty)
are kept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Fix pylint implicit-booleaness nit in prune test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…des (#283)

The flat 16-page docs are reorganized into three audience-focused
sections with a nested mkdocs nav:

- User Guide (docs/user/): install, getting started, a new consolidated
  Loading Configurations page (all from_* constructors including
  JSON/XML), remediation workflows (now covering plugins, transform
  callbacks, and NETCONF rendering), tags, future-config (updated for
  the #269 negation semantics and prune_empty_branches), unified diff
  (previously missing from the nav), reporting (trimmed ~40%), config
  views (updated for the v4 mixin model), and set-style platforms
  (generalized from the JunOS-only page).
- Administrator Guide (docs/admin/): the per-platform reference,
  customizing driver rules (including a new post-load-callback
  override recipe), custom drivers and the registry, and loading rules
  from YAML - carved out of the 1049-line drivers.md.
- Developer Guide (docs/dev/): refreshed architecture, the driver rule
  model reference, creating a platform driver, a new contributing page,
  and an API reference extended to the full v4 public surface.

index.md is rewritten as a landing page with a runnable quick example
(output verified against the library) and audience-based entry points.
README/CONTRIBUTING links updated; mkdocs build --strict passes with
zero warnings.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
New docs/user/migrating-from-v3.md with rename tables for constructors,
methods, and utilities, the unified NegationRule mapping with
before/after examples, the typed exception hierarchy, the config-view
mixin transition (isinstance instead of NotImplementedError), behavior
changes to review (future() negation resolution, structured-input
rejection, driver registration), and pointers to the new v4 features.
Every import and API name in the guide is verified against the package.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Brings the 3.7 line's changes onto the v4 branch:

- Aruba AOS-CX platform support (#289), ported to the v4 APIs: driver
  registered in registry.py with a view_class, config view rebuilt on the
  v4 capability mixins, tests relocated to tests/integration and
  tests/unit/platforms/views and converted to HConfig.from_text/
  remediation()/to_lines().
- AI-contributor standards (#290): AGENTS.md, Claude Code skills, Copilot
  instructions, PR template, and the new testing/code-style/release/CI
  docs — all references updated to the v4 test layout, doc paths, and API
  names. Master's parallel docs reorg is dropped in favor of next's
  restructure; its old URLs are preserved via mkdocs redirect_maps.
- future() negation fixes (#282) resolve to next's own port (#281);
  the legacy tests/test_hier_config.py is superseded by the v4 test tree.
- v2→v3 platform mapper changes are dropped: the mappers were removed on
  next (#221).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add gNMI-style JSON remediation rendering (#287)

Complete the remediation-output-matches-input-format story from #232
for OpenConfig/gNMI pipelines: WorkflowRemediation.remediation_json()
(and hier_config.formats.hconfig_to_gnmi_json()) render a remediation
between HConfig.from_json() trees as a gNMI-SetRequest-style structure.

Negations become xpath-ish delete paths with [key=value] selectors for
keyed list entries, resolved against the running config via the same
key-leaf discrimination as the NETCONF renderer. Additions render into
an update object through the existing JSON mapping; modified keyed
entries re-gain their identity leaf so the update stays valid
OpenConfig, and branches emptied by deletions are pruned.

* Deduplicate keyed-entry list-key resolution (#287)

Post-review cleanup: the NETCONF and gNMI delete renderers and the gNMI
identity lookup each carried their own copy of the running-entry key
probe, letting the single definition of keyed-list-entry identity drift
across three sites. Extract _matching_list_key/_running_entry_key so
both output formats share it. Also collapse single-name deferred
imports and drop a delete-path assertion already owned by the
nested-delete test.

* Test the unresolved-identity gNMI fallback (#287)

Review flagged the fallback branch as untested: when a modified keyed
entry is rendered without a running config, its remediation subtree
lacks the identity leaf, so the key name cannot be resolved. Pin both
consequences — the delete-path selector guesses the first list_keys
name, and identity injection into the update is skipped so a guessed
key never becomes applied config.
* Make built-in post-load callbacks public (#286)

The documented recipe for disabling a built-in post-load callback
imported a private name (_remove_ipv4_acl_remarks), so a rename would
silently break downstream drivers. Rename all eight built-in callbacks
to public names in their driver modules, giving them stable identities
that users remove with rules.post_load_callbacks.remove(callback).

Two names are cleaned up beyond the underscore strip:
_rm_ipv6_acl_sequence_numbers -> remove_ipv6_acl_sequence_numbers and
the stuttering _fixup_hp_procurve_aaa_port_access_fixup ->
fixup_hp_procurve_aaa_port_access. Identity-pin tests per platform and
a recipe regression test guard the new public surface; the docs recipe
now removes by identity instead of slice-filtering.

* Polish issue-286 tests and docs after review

Rename test_rm_ipv6_acl_sequence_numbers to match the callback's new
public spelling, drop docs-recipe prose that restated itself (the
frozen-model comment duplicated the sentence above the snippet; the
ValueError caveat folds into that sentence), and trim the changelog
entry to the user-visible change.
* Add future_with_report() for explicit negation-resolution audit

A leftover literal `no ...` line was the only way a caller could
detect that a change contained a negation that did not apply cleanly
in future() output, forcing change-validation pipelines to grep the
render. HConfig.future_with_report() returns the same prediction
together with a frozen FutureReport of unresolved negations and
persisting idempotency-tracked negation replacements, so pipelines
can assert `not report.unresolved_negations` instead.

FutureReport is a frozen slotted dataclass rather than a Pydantic
model because it holds HConfigChild nodes (plain slotted classes)
and the project convention forbids arbitrary_types_allowed.

Closes #285

* Make future() delegate to future_with_report()

Both methods carried the same construct-compute-prune pipeline, so a
change to one had to be mirrored in the other. Delegating leaves one
code path; the discarded report costs one small builder allocation
per call.
jtdub and others added 6 commits August 4, 2026 20:44
* Canonicalize registry keys to uppercase platform names

The registry keyed entries by Platform | str, so listing semantics
were accidental and, because Platform is a str-Enum whose members
hash as their auto() counter values, register_driver("3", X)
silently overwrote the built-in CISCO_IOS entry and "3" resolved as
a platform lookup.

Key everything on canonical uppercase names: Platform members
convert via .name at the boundary, making a member and its name
interchangeable, and get_registered_platforms() converts enum-known
names back to members while custom names stay uppercase strings.
Error messages now echo the caller's spelling.

Closes #284

* Polish registry tests and unregister error message

Reuse the module-level _CustomDriver instead of a copy-pasted nested
IOS subclass, name the value-string constant once, and keep cleanup
in finally with the assertion outside the try. The not-overridden
error now formats the canonical name because pre-3.11 f-strings
render a str-Enum member as its meaningless value string.
* Align agent instruction files with repo reality

AGENTS.md claimed model fields must be immutable collections, but
HConfigDriverRules deliberately uses list fields so built-in rules and
callbacks can be removed by identity (#286); an agent following the rule
literally would break that API. Document the carve-out everywhere the
rule is stated (AGENTS.md, copilot-instructions, code-style.md, review
skill, new-driver skill).

The review skill diffed against master, which on any next-based branch
pulls in all of v4 and makes the report meaningless; switch to
merge-base against the actual base branch. Add the branching strategy
to AGENTS.md (previously only CLAUDE.md had it, so non-Claude agents
and Copilot would target master for v4 work) and drop the now-duplicate
section from CLAUDE.md.

Also surface facts agents had no way to learn: the CI Python matrix
(3.10-3.14), the unconditional docs --strict job and its separate
docs/requirements.txt, the formats layer (JSON/XML/NETCONF/gNMI),
future_with_report(), registry key canonicalization (#284/#295), and
per-driver unit test expectations. Fix CONTRIBUTING.md stale content
(nonexistent test file, missing yamllint/flynt, missing changelog
requirement, no branch-base guidance).

* Fix broken doc examples and stale user-facing docs

Several documented examples did not run or showed wrong output:
getting-started referenced a fixture that does not exist, tags.md
filtered on an 'ntp' tag its fixture never defines (actual output was
empty), remediation-workflows called a nonexistent delete_child()
method, config-views mixed switchport and ip address config and claimed
outputs the view never produces, and the hierarchical JunOS example was
missing six trailing set lines. All replacement outputs were verified
by running the snippets against the real fixtures.

Remove the hardcoded prerelease pin from install.md (it had already
drifted from pyproject) and add --pre to the README install so its
Quick Start, which uses the v4-only API, can actually run. Propagate
Aruba AOS-CX into the architecture driver table and config-views lists,
correct the HConfigViewBase abstract-member lists (dot1q_mode_from_vlans
is a concrete static helper), and align the interface-view example with
the in-tree base-class pattern.

Fill reference gaps: formats module, future_with_report/FutureReport,
resolve_driver, view data models, and the full built-in post-load
callback table in api-reference/rule-reference; glossary entries for
the registry, future reports, list_keys, callbacks, and
RemediationPlugin; rules-from-files loader constraints (one criterion
per lineage entry, Platform-only). Point the legacy utilities.md
redirects at admin/rules-from-files.md where that content actually
lives.

* Restructure CHANGELOG Unreleased section per Keep a Changelog

The Unreleased block had accumulated duplicate Added/Changed/Fixed
headings from successive merges, Added-type entries filed under Fixed,
a non-standard 'v4 design decisions' heading, and a stale claim that
JSON/XML ingestion was post-4.0 roadmap work when the same section
documents it as shipped. Merge to one heading per category, re-file the
misfiled entries, drop the contradictory sentence, and keep the design
decisions as an intro note. All 59 entries and every (#NNN) reference
are preserved.

Point the migration guide at the Unreleased section instead of a 4.0.0
CHANGELOG section that does not exist yet. Correct the per-file ignore
path for the benchmarks file, which moved to tests/benchmarks/ without
the lint config following; with the ignore active again, the inline
print suppressions it replaces became unused and were removed.

* Fix stale WorkflowRemediation docstring; document public members

The WorkflowRemediation class docstring, which renders into the public
API reference, still showed a v3-era example importing the removed
get_hconfig from the nonexistent hier_config.model module, and claimed
__init__ raises ValueError when it raises IncompatibleDriverError.

Add attribute docs to the FutureReport fields (the user-facing contract
of future_with_report) and docstrings to 33 previously undocumented
public members on autodoc'd classes — driver extension points
(idempotent_for, sectional_exit, prefixes, config_preprocessor), the
HConfigViewBase contract, and the HConfigBase/HConfigChild/HConfig/
HConfigChildren members that doc examples tell users to call. Also add
this PR's changelog entry (#297).
Automate the manual release checklist in docs/admin/releases.md: a
workflow_dispatch-triggered, admin-gated workflow bumps the version with
poetry, rotates CHANGELOG.md via scripts/rotate_changelog.py (skipped
for prereleases), opens the chore(release) PR against the dispatched
branch, and creates a draft GitHub release. The deploy-pypi trigger
moves from release "created" to "published" because GitHub never fires
"created" for drafts that are later published, which would have left
draft-based releases unpublished.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publishing a hier_config release now sends a repository_dispatch
(event hier-config-release, payload version + prerelease) to
netdevops/hier-config-ci, whose orchestrator releases the downstream
apps against the new version. The default GITHUB_TOKEN cannot cross
repositories, so the workflow requires an org-admin PAT in the
ECOSYSTEM_DISPATCH_TOKEN secret and fails early when it is missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hier-config applications (hier-config-api, -cli, -mcp, -gpt) each
maintained their own copy of this repository's lint, typing, and test
tooling. The copies drifted: hier-config-api's scripts/build.py had been
rewritten to use `# noqa` comments because it pinned an older ruff that
did not understand the `# ruff: ignore[...]` directive used here, so
neither repo could adopt the other's file.

Make this repository the canonical source and give downstream projects a
way to pull changes in:

- .standards.yml declares the source ref and the owned files. Downstream
  manifests add substitutions so a single scripts/build.py serves
  projects with different package names.
- scripts/sync_standards.py fetches, substitutes, and compares. `check`
  reports drift and exits non-zero; `apply` writes the canonical
  versions. Substituted Python is re-run through `ruff format`, since a
  longer package name can push a line past the limit and the file would
  otherwise never converge.
- Dockerfile, docker-compose.yml, and tasks.py give every project the
  same invoke commands. These stay per-repo: an application serves a
  process and a library does not, so they are conventions with this
  repository as the reference rather than synced files.

Documented in docs/dev/shared-standards.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant