Skip to content

fix: read {base} as the raw template name on every module rename - #104

Merged
marcinpsk merged 8 commits into
refactor/name-template-language-seamfrom
fix/raw-base-on-reapply
Sep 23, 2026
Merged

marcinpsk merged 8 commits into
refactor/name-template-language-seamfrom
fix/raw-base-on-reapply

Conversation

@marcinpsk

@marcinpsk marcinpsk commented Sep 23, 2026

Copy link
Copy Markdown
Owner

Closes #103. Stacked on #101.

Problem

A module rename took {base} from the interface's current name. A reapply runs the planner again on rows an earlier apply already renamed. It happens on a module type change, a virtual-chassis change, Apply Rules, or a second call to apply_interface_name_rules. A rule whose templates use {base} then renamed its own output:

Rule After install Before this change
plain {base}-x 3-x 3-x-x on a forced reapply, 3-x-x-x on Apply Rules
channelized parent {base}-parent 7-parent one more -parent on every apply, forced or not
channelized channels {base}:{channel} 8:1..8:4 renamed to et-0/0/8:1.. on the first reapply

Change

family/raw_bases.py adds RawBases. It gives each live interface name the raw template name that a {base} rule reads. NetBox keeps no link from an interface to its template, so the value comes from a claim over the module's templates:

  • A name that is a template's raw name belongs to that template. It wins over another template's historical virtual-chassis form, as in the drift guard, but never over another template's renamed form.
  • Any other name belongs to a template when it matches that template's historical virtual-chassis form, or the name the rule gives the raw name. In that renamed form, {vc_position} matches any position, so a renumber still finds the row. A variable used inside arithmetic cannot take a marker, so it matches only its current value.
  • A name that no template claims, or that two templates claim, gets no base. Its plan is BLOCKED with a logged reason. Two-sided ambiguity goes through the existing resolve_template_claims, with a new raw base label kind.

Every module-context planner now takes {base} from RawBases: plain renames, installed channelized families (breakout and lockstep), creation of flat and channelized families, and the Apply Rules preview of live rows. channelized_family_targets and lockstep_family_targets take base_name separately from the parent's current name, because suffix recovery still needs the current name.

Scope decisions:

  • A rule that does not use {base} never computes the claim, so its behavior is unchanged. name_template.variable_token is the one definition the evaluator and references_variable share.
  • A module type without interface templates has no raw names. There {base} stays the current name, as before.
  • Device-interface rules keep {base} as the current name, which is their documented input.
  • Prediction receives raw names by its precondition. given_raw_names keeps each given name as its own base, so a stale name is still predicted from itself as documented. It refuses a name the claim finds ambiguous, because install leaves that name alone.

The preview selects the bay-chain relations and pins the template cache, as the batch path does, so one module type's templates load once per scan. The Apply Rules preview now plans installed flat families with plan_installed_flat_families, the plan that Apply Rules executes. Before this change, it planned their members as new families built on each member name (brk-brk-xe-1/0/3:0:0). The claim would otherwise block those members, which would hide a drifted family that Apply Rules still renames.

The claim builds its renamed-form matcher from NUL-delimited markers. PostgreSQL text cannot hold a NUL byte, so no stored template, name or variable value can spell a marker, not even through arithmetic.

Known limits, which fail closed: the interface keeps its name and a warning is logged.

  • A {base} inside arithmetic, on a template that uses the {vc_position} token, matches only the current position. On the parent branch the same rule failed to evaluate, so nothing regresses.
  • A token template whose historical form equals another template's raw name makes both claims ambiguous. The existing drift guard rejects the same overlap.
  • When the rule renames one template to another template's raw name, that name is ambiguous. For example, templates port and port.2 with {base}.{vc_position}. Such a module type renames nothing, even on install, where the parent branch renamed it. The parent branch also renamed it wrongly after two renumbers (port.2 to port.2.3).

The potentially-deprecated heuristic now runs only without force. A forced reapply reaches rows that an earlier apply renamed, so a rule that renames nothing there is working. Before this change, every virtual-chassis renumber tagged every applied module rule. test_no_op_rename_adds_deprecated_tag pinned that path. It now covers the case the heuristic is for: a rule whose output is the raw name the module already has.

TemplateNames moved from installed.py to template_names.py, so RawBases and the planners share one lazy template load. plan_installed_families lost its unused interfaces argument. The batch path calls plan_installed_families_from with the rows and bases it already has. install_channelized_family is removed: it passed the current name as {base}, and only a test called it. _historical_bases, the conversion matcher, uses the same NUL marker.

Behavior changes in existing tests

  • test_module_base_description_stays_true_of_reapplication_behavior pinned the parent growth. It now asserts that a reapply renames nothing.
  • ChannelizedSuffixRecoveryTest expected 1a-x-x and 2-x-x:2 after a forced reapply. That was the growth. The stranded child still heals, now to 2-x:2.
  • test_two_token_templates_that_do_not_overlap_both_rename: et-{base} after a renumber to position 6 now reads the raw name as it resolves at position 6 (xe-6/0/4). A flat family already uses that base.

Tests

test_raw_base.py covers each case through real installs, real Device.save() renumbers and the real Apply Rules entry points. The channelized cases run on NetBox 4.7 only. The cases:

  • plain reapply, preview and Apply Rules
  • renumber
  • an unclaimed row, and a row two templates claim
  • a flat family on an unclaimed member
  • arithmetic over {base}
  • marker text in a raw name, in a rule, and assembled by arithmetic, and arithmetic over {base} across a renumber
  • a blank parent template on a family built on a drifted interface
  • templates whose names overlap under the rule, on install and after a renumber
  • a forced reapply does not flag an applied rule as potentially deprecated
  • the preview of an installed flat family, with and without virtual-chassis drift
  • a channelized parent, channel template and lockstep rename
  • an unclaimed channelized parent, and an unclaimed row for family creation

All fail on the parent branch except the control, a rule without {base}.

The catalogue description of {base} for module contexts is now "The raw template name of the interface the rule renames." The generated references are regenerated, and "What {base} starts from" in docs/template-variables.md states the claim and its exceptions.

Summary by CodeRabbit

  • Bug Fixes

    • Module naming rules now consistently resolve {base} to the raw template name on installation and reapplication, preventing repeated name changes.
    • Preview and installed results now better agree. Interfaces without a unique matching template are left unchanged, with a warning.
    • Virtual chassis renumbering now uses the current position when resolving names.
  • Documentation

    • Clarified how {base} behaves for module and device interface naming rules.

A module rename took {base} from the interface's current name. A reapply
runs the planner again on rows an earlier apply renamed: a module type
change, a virtual-chassis change, Apply Rules, or a second apply. A rule
whose templates use {base} then renamed the result again. '{base}-x' went
from 3-x to 3-x-x, a channelized '{base}-parent' parent grew on every
apply, and '{base}:{channel}' channels changed once after install.

NetBox keeps no link from an interface to its template, so RawBases
recovers the raw name by a claim. A row belongs to a template when its name
is the template's raw name, a historical virtual-chassis form of it, or the
name the rule gives it, with {vc_position} matched at any position. An
exact raw name wins first, as in the drift guard. A row that no template
claims, or that two templates claim, is blocked and the reason is logged.

Rules without {base} skip the claim and keep their behavior. A module type
without interface templates has no raw names, so there {base} stays the
current name. Device-interface rules keep the current name. Prediction
receives raw names and takes them as they are.

Closes #103
…eviews

Three gaps in the raw-base claim, found in review:

- A raw name that spells the claim marker made the second replacement
  rewrite the inserted raw name, and re.compile raised. The pattern is now
  built from the pieces between markers.
- A rule with arithmetic over {base} matched only the current
  virtual-chassis position, so a renumber left its interfaces unclaimed.
  Evaluation now tries each marker combination and keeps a marker for every
  variable that is not in arithmetic.
- The Apply Rules preview blocked the members of an installed flat family,
  because their names are no template's renamed form. Apply Rules still
  renamed a drifted family, so the preview hid it. The preview now plans
  installed flat families with the same plan Apply Rules executes.
…not spell

A family built on a drifted interface named its parent from the recovered
raw name when the parent template was blank. A blank parent template keeps
the interface's current name, so channelized_family_names and
intended_family_names now take the current name and the base separately.

The claim matched marker text in a rule template, a raw name or a variable
value as the variable it stands for. RawBases now picks markers that none
of them contains.

A {base} inside arithmetic, on a template that uses the {vc_position}
token, still matches only the current position. The guide states it.
The claim markers were plain text, so a rule could still assemble one
during evaluation ('InrRawBaseMark{0}'), and recovery then read it as a
variable. PostgreSQL text cannot hold a NUL byte, so no stored template,
name or variable value can spell a NUL-delimited marker, and arithmetic
yields digits only. The markers now use NUL, and the free-marker search is
gone.

A forced reapply reaches rows an earlier apply already renamed, so a
rule that renames nothing there is working, not obsolete. The
potentially-deprecated heuristic now runs only without force. Before this,
every virtual-chassis renumber tagged every applied module rule. The test
that pinned that path is rewritten to cover the case the heuristic is for:
a rule whose output is the raw name the module already has.
An exact raw name claimed its interface before any other form was
checked. When one template's renamed name equals another template's raw
name, the interface went to the wrong template. Templates 'port' and
'port.2' with '{base}.{vc_position}' renamed 'port.2' to 'port.2.3' after a
renumber.

A raw name now beats only another template's historical virtual-chassis
form, as in the drift guard. A renamed form that matches it makes the
name ambiguous, so neither template claims it and the interface keeps its
name. A module type whose templates overlap under the rule therefore
renames nothing, on install too.
… marker

The Apply Rules preview now builds RawBases and installed flat plans for
each module, and both read the module's templates. find_interfaces_for_rule
did not pin the template cache, so each module cost a refetch and a
template query. It now selects the bay-chain relations and pins the cache
for its modules, as the batch path does.

_historical_bases recovered drifted flat-family bases through a plain-text
sentinel, the shape the raw-base claim left in 1884451. A rule that spells
the sentinel built the wrong matcher, and conversion stopped offering the
family. It now uses the same NUL marker.

install_channelized_family passed the row's current name as {base} and
only a test called it, so it is removed. raw_bases reads the vc_position
digit pattern from template_names instead of a copy.
Prediction took every given name as its own base, so it predicted renames
for templates whose names overlap under the rule, which installation
leaves alone. given_raw_names still keeps a given name as its own base,
so a stale name is predicted from itself as documented, but it refuses a
name the claim over the given names finds ambiguous.

A limited preview counted an installed flat family once in the modules it
visited and each interface in the modules it skipped, so its total
differed from an unlimited preview. A flat family now counts its members.
Prediction ran the raw-base claim over every given name, channels
included. A channel name that matches its parent's renamed form made the
parent ambiguous, so prediction kept names that installation renames.
Installation claims only interfaces outside a channel. Prediction now
drops the given names its templates describe as channels, and it still
claims over the given names only.
@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 71fedd50-7d64-4f06-abdc-cae85e708e3a

📥 Commits

Reviewing files that changed from the base of the PR and between de97754 and 1c1ec68.

📒 Files selected for processing (24)
  • .github/copilot-instructions.md
  • contrib/README.md
  • docs/template-variables.md
  • netbox_interface_name_rules/engine.py
  • netbox_interface_name_rules/family/__init__.py
  • netbox_interface_name_rules/family/batch.py
  • netbox_interface_name_rules/family/claims.py
  • netbox_interface_name_rules/family/conversion.py
  • netbox_interface_name_rules/family/installed.py
  • netbox_interface_name_rules/family/prospective.py
  • netbox_interface_name_rules/family/raw_bases.py
  • netbox_interface_name_rules/family/structural.py
  • netbox_interface_name_rules/family/targets.py
  • netbox_interface_name_rules/family/template_names.py
  • netbox_interface_name_rules/name_template.py
  • netbox_interface_name_rules/tests/test_bulk_families.py
  • netbox_interface_name_rules/tests/test_channelization.py
  • netbox_interface_name_rules/tests/test_channelized_mode.py
  • netbox_interface_name_rules/tests/test_documentation.py
  • netbox_interface_name_rules/tests/test_engine_advanced.py
  • netbox_interface_name_rules/tests/test_prospective_families.py
  • netbox_interface_name_rules/tests/test_raw_base.py
  • netbox_interface_name_rules/tests/test_structural_families.py
  • netbox_interface_name_rules/tests/test_vc_drift.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

Module {base} values now resolve to the raw template name of the interface being renamed. The resolved value flows through family planning and engine previews. Unclaimed or ambiguous names produce blocked plans, and documentation and tests cover the updated behavior.

Changes

Module raw-base resolution

Layer / File(s) Summary
Resolve raw template bases
netbox_interface_name_rules/family/raw_bases.py, netbox_interface_name_rules/family/claims.py, netbox_interface_name_rules/family/template_names.py, netbox_interface_name_rules/name_template.py, documentation and documentation tests
The planner identifies raw template names from live interface names and rule templates. It logs claim warnings, and blocks renames when a name has no unique claim. The documentation and tests define the module and device {base} semantics.
Apply resolved bases to family plans
netbox_interface_name_rules/family/targets.py, netbox_interface_name_rules/family/{installed,prospective,structural,batch,conversion}.py, netbox_interface_name_rules/family/__init__.py, family-planning tests
Installed, prospective, and creation plans now receive resolved base names. Target evaluation uses the resolved value for {base} and blocks plans when the value is unavailable. Planner callers, exports, and related tests are updated.
Integrate planning with apply and preview
netbox_interface_name_rules/engine.py, netbox_interface_name_rules/tests/test_raw_base.py, related engine, channelization, and virtual-chassis tests
Prediction and previews use raw-base-aware plans. Installed flat families are planned separately from prospective families. Tests cover reapplies, renumbering, unclaimed names, and preview results.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant plan_module_families
  participant module_raw_bases
  participant RawBases
  participant resolve_template_claims
  participant FamilyPlanners
  plan_module_families->>module_raw_bases: Build bases for module interfaces
  module_raw_bases->>RawBases: Create raw-base resolver
  RawBases->>resolve_template_claims: Resolve template claims
  resolve_template_claims-->>RawBases: Return claims and ambiguities
  RawBases-->>plan_module_families: Provide base lookups
  plan_module_families->>FamilyPlanners: Plan installed and prospective families
Loading

Merge Risk: ⚪ Minimal · up to 1c1ec

Module rules that use {base} now always start from the interface's raw template name, so repeated applies no longer keep growing names. Interfaces that no template claims, or that several templates claim, keep their current names and log a reason. No outstanding defects were found, and the change appears ready to merge.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.93% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 134 functions across 21 files. (3 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: resolving {base} as the raw template name for module renames.
Linked Issues check ✅ Passed Issue #103 requires raw-template {base} resolution for module renames, idempotent reapplies and Apply Rules previews, VC renumber support, blocking of unclaimed or ambiguous names with logged reason…
Out of Scope Changes check ✅ Passed The changes stay within Issue #103. The implementation refactors family planning to pass raw-base claims, updates engine preview handling, adds focused raw-base tests, and updates the related {base}
Full details: Docstring Coverage

Explanation

Docstring coverage is 64.93% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 134 functions across 21 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
✨ Simplify code
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks each template name
And finds the base beneath the frame
Through channels, previews, rules it goes
Unclaimed names keep as they arose
The bunny hops; no suffix grows

Comment @coderabbitai help to get the list of available commands.

@sonarqubecloud

Copy link
Copy Markdown

@marcinpsk

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 23, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@marcinpsk
marcinpsk merged commit 4e45a89 into refactor/name-template-language-seam Sep 23, 2026
6 checks passed
@marcinpsk
marcinpsk deleted the fix/raw-base-on-reapply branch September 23, 2026 21:37
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