Skip to content

Rust rewrite - #302

Draft
aedwardstx wants to merge 32 commits into
netdevops:nextfrom
aedwardstx:rust-rewrite
Draft

Rust rewrite#302
aedwardstx wants to merge 32 commits into
netdevops:nextfrom
aedwardstx:rust-rewrite

Conversation

@aedwardstx

Copy link
Copy Markdown
Collaborator

Summary

This PR lands the complete Rust rewrite of hier_config for the v4.0.0 major release (4.0.0-beta.4), rebased on top of next.

Parsing, tree data structures, post-load fixups, remediation algorithms, structured formats (JSON, XML, NETCONF, gNMI), and configuration views are now implemented natively in Rust (crates/hier_config_core) and exposed to Python via PyO3 (_hier_config_rust). hier_config_core is also structured for direct, standalone Rust library consumption without Python or PyO3 dependencies.

⚠️ Major Version Bump & Breaking Changes

  1. Compiled Extension Distribution (No Pure-Python Fallback):
    • hier_config now ships as compiled platform wheels built with maturin for CPython 3.10–3.14 across Linux, macOS, and Windows. Non-wheel platforms require a Rust 1.98+ toolchain to build from source.
  2. Dead Driver Hooks Removed from HConfigDriverBase:
    • idempotent_for(), negate_with(), sectional_exit(), and swap_negation() (and their private helper machinery) have been removed. These behaviors are resolved natively by the Rust core from driver rule definitions (rules.negation, rules.idempotent_commands, etc.). Subclasses defining any of these four hooks now raise TypeError.
  3. Config View Native Port & Exception Behavior Changes:
    • The view layer (hier_config/platforms/view_base.py and platform views) is now a native PyO3 facade backed by Rust.
    • nac_max_dot1x_clients and nac_max_mab_clients return None instead of raising NotImplementedError (Cisco IOS, Aruba AOS-CX).
    • module_number returns None instead of raising AttributeError (Arista EOS, Cisco NX-OS, Cisco XR).
    • bundle_member_interfaces returns () instead of raising on a non-trunk interface (HP ProCurve).
  4. Structured Formats (formats.py):
    • Serializers and deserializers (from_json, to_json, from_xml, to_xml, hconfig_to_netconf_xml, hconfig_to_gnmi_json) are implemented in Rust. Duplicates in XML/JSON lists raise DuplicateChildError instead of being silently deduplicated or flattened to generic errors.

Key Additions & Improvements

  • Comprehensive Parity Corpora:
    • 83 round-trip remediation test cases across 13 platforms (testdata/cases/) executed against both Python and Rust test harnesses.
    • 385-case structured formats parity corpus (testdata/formats/expected.json).
  • Property-Based Invariant Suite:
    • Proptest suite in crates/hier_config_core/tests/properties.rs verifying structural invariants, parse totality, self-remediation emptiness, future consistency, rollback fidelity, and panic robustness across arbitrary inputs.
  • CI-Enforced Type Stub Parity & Integrity:
    • scripts/gen_stubs.py --check prevents undocumented PyO3 symbols and stub drift.
    • mypy.stubtest compares .pyi signatures against compiled extension runtime symbols with audited exemptions in stubs/stubtest-allowlist.txt.
    • scripts/check_stub_types.py inspects live returned values across test fixtures against declared stub return types, backed by stubs/unobserved-allowlist.txt to eliminate silent type degradation under strict mypy and pyright.

Self-Review Checklist

  • python scripts/build.py lint-and-test passes locally (lint + 88% test coverage).
  • Tests were written first (TDD) and cover the change, following the testing conventions.
  • CHANGELOG.md has an entry under ## [Unreleased] referencing this issue/PR ((#NNN)).
  • Documentation is updated if public API or driver behavior changed (and mkdocs build --strict passes if docs were touched).
  • Commit messages follow the contributing guide: imperative mood, subject ≤72 characters, body explains why.

AI-Assisted Contributions

This PR was developed and reviewed with AI coding assistance following the repository standards outlined in AGENTS.md and the developer documentation. All linting, strict typing, cargo tests, proptest suites, coverage floors, and documentation builds have been validated locally.

Andrew Edwards and others added 30 commits September 7, 2026 09:14
Bring across every file the rewrite added that upstream/next never
touched: the hier_config_core and hier_config_py crates, the generated
config corpus under testdata/, the Rust-native and parity test suites,
the benchmark regression gate, and the cargo tooling config.

These 446 files are disjoint from next's own changes, so they land
wholesale. The 70 files both branches touched are re-derived from next's
versions in the following commits rather than overwritten.

Python packaging is deliberately left on next's pure-Python layout here;
this commit only establishes that the Rust workspace builds, lints and
tests clean against the new base.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 3a of docs/plans/rust-rewrite-onto-next.md.

Convert next's tree modules into re-export shims over the Rust core and
rebuild the exception hierarchy so the native classes stay canonical.
HierConfigError and DuplicateChildError are raised from Rust, so
redefining them in Python would stop `except DuplicateChildError:` from
matching errors the core raises; they are now imported from the
extension and next's three Python-only errors subclass the native base.

Repoint the benchmark helper imports at next's relocated
tests/benchmarks/test_benchmarks.py.

Record the Phase 3b sizing finding in the plan: diffing next against the
merge base rather than against the rewrite shows the per-driver deltas
are 7-32 mechanical lines, that the Rust rules.json needs no change
under next's unified negation model, and that the overridable
negate_with() hook is the one real API incompatibility left.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Phase 3b of docs/plans/rust-rewrite-onto-next.md.

next (netdevops#220) collapsed NegationDefaultWhenRule, NegationDefaultWithRule
and NegationSubRule into a single ordered `negation` list carrying a
NegationStrategy. The Python driver rules are handed to the core by
serializing them and deserializing into DriverRules, and DriverRules did
not declare that field. serde is not configured to deny unknown fields,
so every v4-spelled negation rule was being dropped on the floor: a
driver using the new spelling would silently negate with a bare prefix
swap instead of its declared strategy, producing wrong remediation with
no error to point at it.

Teach DriverRules about the unified list and evaluate it the way the
Python contract describes: REPLACE is consulted first regardless of
position, then the remaining strategies are tried in declaration order
rather than grouped by kind. The three v3 lists are still accepted and
are folded in after the unified list, matching all_negation_rules(), so
the committed platform rules.json files keep validating unchanged and
drivers may mix both spellings.

Keep a fast path for the common case: when no unified rules are present
the original grouped walk runs and nothing is allocated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Reconcile the Rust rewrite with `next` across four seams that the two
branches had diverged on.

Views: `next` restructured config views into a shared Python base plus four
prefix-constant-driven mixins, which the Rust view layer never mirrored --
and the Rust Arista and generic views were never more than stubs. The view
layer is not on the remediation hot path, so adopt `next`'s Python views
wholesale; all 277 of its view tests pass unmodified against the native
tree. `parse_ipv4_interface` comes along because the views need it.

Ingestion/serialization: `future()` and `remediation()` build native trees,
so any method defined only on a Python `HConfig` subclass silently vanished
from the objects they returned. Move the seven v4 classmethods into the Rust
class -- delegating back into Python for the actual work -- so every tree the
core hands back carries the full API, and `root.py` becomes a plain shim.

Negation prefixes: `sync_rules()` reads `negation_prefix` from the Python
driver, so the thin set-style drivers that had dropped the override were
silently resetting `delete `/`undo `/`unset ` back to `no `. Restore the
overrides and sync `declaration_prefix` alongside it.

Workflows: add the `plugins` argument (netdevops#181), run the driver's
`remediation_transform_callbacks` (netdevops#180), add `remediation_json` (netdevops#287) and
`remediation_netconf_xml` (netdevops#232), and raise `IncompatibleDriverError`
rather than `ValueError` on a driver mismatch.

Suite: 360 -> 33 failures.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add FutureReport plumbing through the Rust remediation engine and expose
future_with_report() natively, with future() delegating to it.

Restore the swap_negation / idempotent_for / negate_with / sectional_exit
overrides that next's drivers rely on. The v4 engine never calls these
hooks during remediation, but they remain part of the public driver API,
so mark the core-mirrored ones @core_owned to satisfy the removed-hook
guard while keeping direct calls correct.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The report's nodes must be identical objects to those the caller reaches
through get_child(), so build them with the interning path rather than
fresh handles. Also restore the config_preprocessor staticmethods that
next's set-style drivers expose as public API.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
PyO3 exposes __init__ as an ordinary method rather than the tp_init slot, so
the native constructor could not record the object it had just built. The
first accessor to ask for the tree's root therefore won the race and every
later caller got a different Python object for the same tree.

Claim the handle at the library's construction sites, hand recursive
traversals back through a real generator using interned child handles, keep
transform callbacks out of the rules JSON bridge, and raise the Python-side
InvalidConfigError for unterminated banners.
Four behaviors from next were still being overridden by the Rust core.

Post-load callbacks are removable by identity in next (netdevops#286), but the core
dispatched its own implementations unconditionally, so dropping the Python
callable changed nothing. DriverRules now carries an optional
enabled_post_load allow-list that sync_rules populates from the driver's
surviving callbacks; None keeps every callback on for pure-Rust callers.

is_duplicate_child_allowed returned early for the root node, which pre-empted
rule evaluation entirely. A rule with empty match_rules describes root's empty
lineage and must match it (netdevops#215).

get_hconfig_from_dump ran callbacks against the still-empty tree. Callbacks
normalize parsed configuration, so they now run once the dumped lines are in
place. This is a deliberate behavior change from v3.

The extension-surface test also now enumerates the unified negation rule field
and the remediation transform callbacks.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
next's Python view layer is the one that ships: it is complete, fully tested,
and referenced everywhere. The Rust port never got past stubs (66 unimplemented
methods for Arista, 38 for generic) and nothing -- Python or Rust -- imported
it, so keeping it only invited drift between two view implementations.

Removes crates/hier_config_core/src/view/, the seven per-platform view.rs
files, the PyO3 wrapper, and the NativeHConfigView / NativeConfigViewInterface
exports and their stub declarations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The pyproject graft that brought over next's packaging tail dropped the
tooling configuration the Rust rewrite depends on, so every Python linter
failed for reasons unrelated to the code: ruff lost its per-file-ignores,
mypy and pyright lost the `stubs/` search path that resolves the compiled
`_hier_config_rust` extension, and pylint lost the extension allow-list.
Restore all of them, translating the paths through next's test renames.

Regenerate the four tree stubs. The generator now knows about v4: `depth`
is a property rather than a method, `__eq__` and `__hash__` are emitted
(object defines them too, so the "not inherited" filter was dropping them
and every `config == other` assertion read as a non-overlapping equality
check), and the private native loaders `hier_config.constructors` drives
are declared. Its `from_json`/`from_xml`/`from_text` signatures and the
non-existent `HConfig.from_file` were fabricated; correct them against the
live extension.

Reconcile `_instantiate_rules` back to next's staticmethod contract. The
rewrite had made it a classmethod so built-in drivers could inherit a
platform-driven implementation, but that silently broke the v4 extension
point next published: custom drivers written against next declare a
staticmethod. Give the nine drivers that were inheriting an explicit
one-line override instead.

Make `core_owned` generic. It marks both post-load callbacks and the
core-owned hook overrides, which have unrelated signatures, so a fixed
`Callable[[HConfig], None]` annotation erased the type of every hook it
decorated.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The workflows on next still installed poetry and ran `poetry install`, which
cannot build this project any more: the extension module is compiled by
maturin and `[project.version]` is dynamic. Restore the maturin pipeline —
Rust fmt/clippy/doc/MSRV/coverage, cargo-deny, a wheel-in-clean-venv
packaging check, and the calibrated performance gate — and add `next` to the
branch triggers so it runs on the v4 integration branch too.

`poetry version` was the release workflow's version bumper. The number now
lives on `[workspace.package]` in Cargo.toml, so replace it with
scripts/bump_version.py, which emits both the SemVer form Cargo requires and
the PEP 440 form maturin gives the wheel. The tag follows the wheel.

Relocate the Rust-specific test modules under tests/native/ rather than
leaving them loose at the root of next's restructured tests/ tree, and move
the two remaining flat driver tests into tests/integration/ alongside their
eleven siblings.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
next restructured docs/ and wrote its own user/migrating-from-v3.md covering
the v4 API renames. The Rust branch had brought a separate page with a nearly
identical title covering a different subject — the behavior differences that
fall out of the engine swap. Two pages both called "migrating to v4" is worse
than either, so rename this one to user/rust-core-changes.md, scope it
explicitly to the engine, and point it at next's page as the prerequisite.

Three of its sections had gone stale against the reconciled implementation
and were actively misleading:

- The hook matrix said idempotent_for/negate_with/sectional_exit were removed
  and could not be overridden. They can, when the override carries the
  @core_owned marker, which is how the in-tree drivers declare them.
- It claimed HConfigDriverBase drops ABC and that _instantiate_rules became a
  classmethod. Neither is true: next's staticmethod contract was kept, and
  only the @AbstractMethod decorator was dropped.
- It claimed every traversal returns a tuple. all_children and
  all_children_sorted are real generators; only the two that cannot produce a
  result before walking the whole tree return tuples.

Repoint the links the restructure broke, add nav entries for the two new
developer pages, and add a redirect for the renamed migration page.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
griffe reads the .pyi stubs, not the compiled extension, so a member whose
signature comes from RAW_OVERRIDES rendered as a bare signature on the API
reference page even when the Rust source documented it. That hit the five v4
ingestion constructors -- from_text, from_lines, from_dump, from_json,
from_xml -- which are the first block on that page.

Splice the live __doc__ into a raw override block when the block does not
already spell one out, so a docstring written once in Rust reaches both
help() and the rendered docs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the compiled-wheel packaging change, the core_owned marker, the
poetry-to-maturin move, the from_dump callback-timing change, and the two
removals the rewrite makes. Qualify the netdevops#222 design-decision bullet: the
three engine-resolved hooks are no longer freely overridable.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The performance gate caught iteration at roughly twice its budget. Two causes,
both introduced while reconciling with next:

lazy_children() materialized every node through get_or_create_child(), the
interning lookup, which costs a lock, a hash, a weakref allocation and an
upgrade per node. That path exists to keep handle identity stable for
single-node lookups; a whole-tree walk needs none of it. Use the documented
bulk materializer instead, as all_children_sorted_by_tags() already did.

all_children_sorted() also wrapped its result in a Python generator, adding a
frame resume per node. Sorting has to see the whole tree before it can yield
anything, so the generator only misrepresented the cost. Return the sequence.
Only all_children() is contractually a generator.

A full all_children_sorted() walk over a 7,400-node config drops from 0.70ms
to 0.11ms; the gate goes from 0.1158 to 0.0378 calibration units against a
0.06 ceiling.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
migrating-from-v3.md covered the rename surface and stopped there; the
engine-level differences lived in a page nothing pointed at. Name it as the
required next read, and summarize the six core-driven behavior changes -- most
importantly the driver-hook audit -- in the section where a reader is already
looking for things that will break.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Mark all six phases complete and correct the two Phase 3 notes that execution
overturned: the unified negation list did need a core change (DriverRules has
no deny_unknown_fields, so next's rules would have been silently discarded),
and the negate_with divergence was resolved with the core_owned marker rather
than left open.

Add an execution record covering the rename trap, the seven reconciliations
that needed judgement, the lost-tooling-config lesson, and the two defects the
Phase 6 audit surfaced.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The built-in drivers were already Rust-native -- their rules live as JSON
inside the core and Python reads them, so there is a single source of truth.
The constructors were already there too. What blocked a Rust-only consumer
was the view layer: 1,368 lines of Python with no native equivalent.

Port it. Views are traits with default method bodies rather than the mixin
union that sank the first attempt (abf3d6f), so a platform implements only
the hooks its Python sibling overrides; optional behaviour is gated behind
supports_vlan/supports_nac/supports_physical/bundle_prefix. Six platforms
have a Python view.py and so get a port; the other seven are listed
explicitly in the dispatch match, making a new Platform variant a compile
error until someone decides.

The Python view layer is deliberately untouched. Instead both sides are
pinned to a shared corpus under testdata/views/, generated from Python and
asserted from Rust, with a Python-side --check that fails when the snapshots
go stale. That caught three real bugs in the port on its first run.

Add constructors.rs for one-call Rust entry points and a standalone_usage
integration test that walks driver -> load -> remediate -> view using only
hier_config_core.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
scripts/gen_stubs.py has supported --check since it was written, but nothing
ever invoked it. The .pyi stubs for the four Rust-backed tree types are the
only type information mypy, pyright, and griffe have for the compiled
extension, so a PyO3 signature change that was not mirrored into the stubs
would ship wrong types with a fully green gate.

Add a check-stubs build command and wire it into both lint and lint-and-test,
alongside the existing check_displacement_markers step. Verified it exits 1 on
injected drift and 0 on a clean tree.

Also document the generator in docs/dev/architecture.md, which the stub file
headers already pointed at but which had never mentioned .pyi files.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Rust core resolves negation, idempotency, and sectional exiting from
each driver's rule data. The Python implementations of idempotent_for(),
negate_with(), sectional_exit(), and swap_negation() were therefore never
called at runtime -- a monkeypatch probe across every platform confirmed
none of the four hooks executes during remediation.

Leaving ~560 lines of unreachable code in driver_base.py and the platform
drivers made the Python surface look authoritative when it was not, and
invited contributors to fix bugs in code that cannot run. The @core_owned
escape hatch existed only to let those dead overrides past the guard, so
it goes too; __init_subclass__ now rejects all four hooks unconditionally
and core_owned() reverts to marking post-load callbacks.

Rule data, negation_prefix, and declaration_prefix are unchanged and stay
the supported way to shape these behaviors. Remediation and rollback
output is byte-identical before and after across nine platforms.

Tests that drove the deleted methods directly are rewritten to assert the
live behavior through child.negate() and end-to-end remediation, so the
semantics they covered -- including the FortiOS unset handling from netdevops#225
-- remain pinned.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
`crates/hier_config_py/src/workflow.rs` called `py.import("hier_config.formats")`
to render NETCONF and gNMI payloads, so the Rust core depended on the Python
package it is supposed to back. A pure-Rust consumer got no structured output
at all, and the 552-line Python implementation was untyped surface area that
no longer earned its keep in a Rust-first v4.

Move all six conversions into `hier_config_core::formats` and reduce
`hier_config/formats.py` to a 157-line documented wrapper. Both interpreter
call-backs in `workflow.rs` are gone.

Behavior is pinned by a 385-case parity corpus captured from the previous
Python implementation before any of it was touched; the corpus is generated
from Python and made to pass in Rust, never the reverse.

The port surfaced two real defects that the corpus did not cover but
`tests/unit/test_formats.py` did: duplicate list entries were silently
de-duplicated instead of raising `DuplicateChildError`, and `FormatError`
flattened `TreeError` to a string, degrading the exception type at the Python
boundary. Both are fixed.

Wire two anti-drift guards into the lint gate, since the hand-written
extension stub is what gives mypy and pyright their view of the native
surface: `gen_stubs.py --check` now also diffs the live `_hier_config_rust`
surface against `stubs/_hier_config_rust.pyi`, and `gen_formats_corpus.py
--check` joins the gate.

Adds `quick-xml` as a Rust build dependency; it compiles into the wheel and
does not change the package's Python dependencies.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The view layer existed twice: ~1,321 lines of pure Python under
hier_config/platforms/*/view.py, and a complete Rust implementation in
hier_config_core/src/view/ that no Python caller could reach. Keeping the
two honest required a generated corpus (scripts/gen_view_corpus.py,
testdata/views/, plus a Rust and a Python test harness) that only existed
because the duplication existed.

Expose the Rust view through PyO3 (crates/hier_config_py/src/view.rs) and
reduce the Python modules to a facade that re-exports the native classes.
The duplicate implementation and its entire anti-drift apparatus are gone.

Two isinstance protocols had to survive the collapse: the netdevops#227 capability
mixins, which tests assert *negatively*, and the per-platform interface
classes used for type narrowing. A @runtime_checkable Protocol cannot serve
either, because the native class carries every attribute and so would
satisfy every negative assertion. ViewMarkerMeta.__instancecheck__ answers
both from the native `capabilities` and `platform` data instead.

Three properties that raised in Python now return None or () — a v4
breaking change, documented in docs/user/rust-core-changes.md.

scripts/gen_stubs.py --check now also diffs each stub class's declared
members against the runtime pyclass, so a #[getter] added without a stub
entry fails the lint gate rather than silently degrading callers to
Unknown under pyright strict.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The existing guards only compared *names*: gen_stubs.py --check diffs the
module and class member sets, so a stub could promise a parameter the
compiled extension rejects and nothing noticed. That is not hypothetical.
add_tags/remove_tags/tags_add/tags_remove documented `tag` but took
`tag_or_tags`, and get_child_deep/get_children_deep documented
`match_rules` but took `rules`. Calling them by keyword as the stubs
describe type checked cleanly under both mypy and pyright, then raised
TypeError at runtime.

Rename the native parameters to the published names and lock the contract
with a test. Run mypy.stubtest over both _hier_config_rust and the
hier_config package in the lint gate so signature drift fails CI. The
extension needs no exemptions; the package carries an audited allowlist
covering pydantic, PyO3 enum, tp_new and sentinel-default idioms that
stubtest cannot model. Unused entries are an error, so it cannot rot.

This also lets the stale HConfigChildrenIter/GnmiRemediation exemption in
verify_extension_surface go away, so the native stub's __all__ is now
exactly the runtime surface.

Return and parameter type annotations stay outside any gate: they are not
introspectable from a compiled extension, so they remain the type
checkers' responsibility alone.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Return annotations cannot be read back from a compiled extension, so the
stub is the type checkers' *premise* rather than something they verify.
Rewriting `vlan_ids -> frozenset[int]` as `dict[str, bytes]` changed the
pyright strict error count by zero and stubtest passed; the one case that
did move pyright (`vlans -> list[str]`) reported 38 errors in the test
files that consume the value, never in the stub that lied. That coverage
is incidental, and it blames the wrong file.

Generating the stub from Rust does not close this. 39 of 131 exported
self-methods (30%) return an opaque `PyObject`, so `vlans`,
`stack_members` and `ipv4_default_gw` share one Rust signature and three
Python types; derivation would emit `Any` for exactly the members most
worth checking. `pyo3-introspection` erases the same set --
`type_object.rs:64` defaults `TYPE_HINT` to `_typeshed.Incomplete` -- and
`pyo3-stub-gen`'s `override_type` only relocates the same hand-written
strings into Rust: `type_repr` is stored as an unvalidated `String`, and
their own test fixture ships a malformed `Callable[[str]]` unnoticed. It
would also require moving off the pinned pyo3 0.24. That remains
complementary for *parameter* types and is worth revisiting whenever
PyO3 is upgraded for other reasons; the reasoning is recorded in the
script's docstring.

So check the stub against reality instead: exercise each declared member
against a corpus of real configs and compare the observed value to its
annotation, descending into container element types. Empty containers
count as unobserved rather than failing, which keeps the check one-sided
and free of false positives.

Its first run found two stubs that contradicted their objects.
`all_children_sorted()` was declared `Iterator[HConfigChild]` but returns
an eager sequence, so the documented `next(...)` raised `TypeError`.
`ConfigViewInterface.poe` was declared `bool` but is `Option<bool>` and
returns `None` on EOS, NX-OS and XR.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Listing the members the type checker could not observe showed my own
framing of them was wrong. They were not one category of fixture gap:
six of the nineteen were holes in the harness.

`HConfigChildren` lives in `hier_config.children`, a module the
annotation resolver never imported, so `HConfigBase.children` failed to
resolve. No `WorkflowRemediation` was ever constructed, leaving
`remediation_config` and `rollback_config` — the members most worth
verifying — entirely unchecked. And `RemediationTransform` is declared
only in the stub, so `plugins` could not resolve either.

Pair same-platform fixtures into workflows, harvest stub-only
`TypeAlias` declarations into the resolver namespace, and teach the
value check about `Callable`. One workflow is built with a no-op plugin
so that arm is exercised rather than merely written. Coverage goes from
63 members to 69.

The thirteen that remain are now honestly one category: no fixture
produces a non-empty value, and an empty container cannot contradict an
element type. Gating on a coverage count would fail whenever a fixture
changed, teaching people to edit the threshold. Pin the identities
instead, following `stubs/stubtest-allowlist.txt`: an unlisted gap fails
so a new stub member cannot arrive unverified, and a stale entry fails
so the list cannot outlive the gaps it records.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Remove obsolete ephemeral task list root-handle-identity-fix.md (specified self-deletion on completion).
- Archive fully implemented rust-native-workflow-remediation.md to docs/plans/archived/ with status updated to Shipped.
- Archive fully implemented rust-native-drivers-constructors-views.md to docs/plans/archived/ with status updated to Shipped.
- Archive rust-rewrite-onto-next.md to docs/plans/archived/ following completed rebase.
- Archive python-test-suite-trim.md to docs/plans/archived/ with status updated to Closed/Superseded by upstream/next migration and Python facade consolidation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Delete docs/plans directory containing completed and archived planning docs
that are no longer needed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Upgrade quick-xml to 0.41 to resolve cargo-deny advisory RUSTSEC-2026-0195.
- Update XML parser for quick-xml 0.41 Event::GeneralRef entity handling and
  attribute value normalization.
- Fix rustdoc intra-doc link to private method in models.rs and remove
  redundant link target in view/platforms/mod.rs under -D warnings.
- Create and activate virtual environments in GitHub Actions workflows before
  invoking maturin develop and test/lint tooling.
- Ignore .venv directory in yamllint configuration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
- Replace Option<Tree> and Option<Py<PyHConfig>> lazy caches in WorkflowRemediation with OnceLock, converting query methods and properties from &mut self to &self
- Eliminate PyO3 BorrowMutError during concurrent or re-entrant property access from Python
- Encapsulate loose parser accumulators into ParserState, ParserCursor, and IndentTracker in hier_config_core::parser
- Introduce pure transactional tree constructors (parse_tree, parse_fast, etc.) returning complete trees without caller-provided mutable instances
- Update Tree::from_str and Tree::from_str_with_callbacks to delegate to pure constructors
- Update tests, benchmarks, stubs, and changelog

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…zing

Document the distinction between direct children (`len(config.children)`)
and recursive descendants (`len(config)`), highlighting O(1) sizing on
root nodes and zero-allocation descendant counting in the Rust core.
Detail lazy stack-based descendant iterators (`Tree::descendants`),
encapsulation of remediation context structs, and `WorkflowRemediation`
thread safety with `OnceLock` lazy caching.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Andrew Edwards and others added 2 commits September 7, 2026 19:45
…e stubs

- Localize vendor-specific platform logic (sectional exits, negation
  swapping, preprocessors, post-load callbacks) behind a unified
  PlatformOps trait in platforms/<platform>/mod.rs.
- Implement native Tree domain logic (merge, with_tags, add_ancestor_copy_of,
  from_json, from_xml) and WorkflowRemediation serialization in
  hier_config_core without Python runtime dependencies.
- Fallback to source driver idempotency rules when target/change config
  is generic in Tree::idempotent_for, Tree::base_idempotent_for, and
  Tree::is_idempotent_command.
- Provide root _hier_config_rust.pyi for maturin wheel stub packaging
  and assert sync with stubs/_hier_config_rust.pyi in lint scripts and
  unit tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
_hier_config_rust is recognized as first-party now that root stubs
are packaged, so remove the blank line separating it from hier_config.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.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