Establish shared development standards for the hier-config ecosystem - #298
Open
jtdub wants to merge 36 commits into
Open
Establish shared development standards for the hier-config ecosystem#298jtdub wants to merge 36 commits into
jtdub wants to merge 36 commits into
Conversation
…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>
) 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>
…-alloc # Conflicts: # CHANGELOG.md
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.
* 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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'sscripts/build.pyhad been rewritten to use# noqacomments 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 addsubstitutionsso onebuild.pyserves projects with different package names.scripts/sync_standards.py—checkreports drift and exits non-zero;applywrites the canonical versions. Two details worth review:ruff format. A longer package name (hier_config→hier_config_api) can push a line past 88 chars, and without this the file never converges —applywould write content the formatter immediately rewrites, so the nextcheckreports drift forever.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, andpyyamlto the dev group. No runtime dependency changes.Verification
poetry run python scripts/build.py lint— cleaninvoke pytest)poetry run mkdocs build --strict— cleandocker build --target developmentsucceedsFollow-up
A companion PR on
hier-config-apirelaxes its ruff pin to match this repo and adopts the manifest; after that itssync-standardsreports zero drift againstscripts/build.py. It cannot merge until this one does, since the sync fetches frommaster.🤖 Generated with Claude Code