Skip to content

feat(export): E2 — allow-list store, device catalog, Manage Matter Exports dialog - #122

Merged
simons-plugins merged 3 commits into
mainfrom
feat/e2-allowlist-ui
Aug 5, 2026
Merged

feat(export): E2 — allow-list store, device catalog, Manage Matter Exports dialog#122
simons-plugins merged 3 commits into
mainfrom
feat/e2-allowlist-ui

Conversation

@simons-plugins

@simons-plugins simons-plugins commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Milestone E2 (PRD §5.1/§5.2): everything between the user and the export set.

  • export_store.py — RLock'd allow-list (ExportEntry: device id, role, name override, options), schema-versioned JSON under one pluginPrefs key; corrupt blobs preserved under matterExports.corrupt, never silently discarded; a bad row drops only itself
  • export_catalog.py — §5.2 classification: eligible roles + default per device, exclusions with reasons; loop guard is pluginId-first and structural (XNG3); valve/garage/fan never offered as roles with reasons carried in EXCLUDED_ROLES
  • Manage Matter Exports dialog (MenuItems.xml + plugin.py): filter → reload → single-select picker (the documented master-detail compromise — multi-select lists have no selection callback) → role/name/polarity detail → Add/Remove acting directly (no Execute), runtime feedback via readonly textfield, excluded devices shown with reasons in labels (XAC9) and rejected server-side
  • Sensor units: Indigo has no canonical unit property, so unit detection is a pluginProps/display-value/name heuristic that picks the default role only — every numeric role stays user-selectable

Tests

+217 (suite 1504 green): XAC6 (own-plugin device excluded regardless of type — unit-tested per PRD §8), XAC9, store CRUD/locking/corruption, §5.2 zoo-style table test, all dialog callbacks incl. degrade-to-empty generators and the comma/semicolon/empty-id constraints. New modules: pylint 10.00 (repo-wide 9.24 vs 9.21 baseline).

Not in scope

bridge_client doesn't consult the store yet — E3 wires deviceUpdated → set_state and command → indigo.device.*.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JxqGhP3DcENf68AZK21U4S

Summary by CodeRabbit

  • New Features
    • Added a Matter Export Management dialog for discovering devices, selecting roles, customizing names and options, and adding, updating, or removing exports.
    • Added support for relays, dimmers, sensors, thermostats, sprinklers, speed controls, and multi-I/O devices.
    • Excluded devices remain visible with clear reasons.
    • Export selections are validated, persisted, and restored automatically, including recovery from invalid saved data.
  • Documentation
    • Updated architecture documentation for Matter exports.
  • Chores
    • Updated the plugin version to 2026.7.26.

…ports dialog

Milestone E2 of PRD-indigo-matter-export: devices become selectable for export
with a declared role, and the loop guard (XNG3) goes live.

- export_store.py — the allow-list. ExportEntry (indigo device id + §4.2 role +
  name override + options), RLock'd store persisted as ONE schema-versioned
  JSON string in pluginPrefs["matterExports"]. A blob it cannot parse is moved
  aside to "matterExports.corrupt" and the store starts empty: user config is
  logged and preserved, never silently discarded.
- export_catalog.py — PRD §5.2 mapping. Returns EligibleDevice(roles, default)
  or Excluded(reason). The loop guard is pluginId ONLY (XAC6), checked before
  any type reasoning; type dispatch walks the IOM class-name chain rather than
  isinstance, which is unusable against the MagicMock'd indigo module.
- MenuItems.xml — "Manage Matter Exports…" (UI-D). No <CallbackMethod>, so the
  dialog gets a single Close button and its buttons do the work. Master-detail
  is a filter textfield + Apply-filter button + single-select device menu with
  dynamicReload (a multi-select list has no CallbackMethod at all), role menu,
  name/polarity fields, Add/Remove buttons, a readonly status textfield
  (Indigo labels cannot change at runtime) and a readonly summary list.
- plugin.py — builds the store in startup, seeds the dialog via
  get_menu_action_config_ui_values, and owns the pickers and callbacks.
  Excluded devices are listed with their reason (XAC9) under an "x-" id and
  rejected server-side; generators degrade to an empty list, never raise.

Tests: 217 new (store CRUD/round-trip/corruption/locking, a §5.2 zoo with five
invariants per row, dialog XML shape and every callback). Full suite 1504 green;
pylint 9.24 repo-wide (9.21 on main), 10.00 for the two new modules.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxqGhP3DcENf68AZK21U4S
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@simons-plugins, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 54 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cd508ac-7cb0-4d84-949f-e3ff289c740f

📥 Commits

Reviewing files that changed from the base of the PR and between 2656dc4 and 52f2c48.

📒 Files selected for processing (2)
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py
  • tests/test_export_menu.py
📝 Walkthrough

Walkthrough

The plugin now classifies Indigo devices for Matter export, persists an export allow-list, and provides a Manage Matter Exports dialog with filtering, role selection, add/update, removal, validation, and startup restoration. Bridge-client integration remains unwired.

Changes

Matter export management

Layer / File(s) Summary
Device role classification
indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py, tests/fakes.py, tests/test_export_catalog.py
Classifies devices into Matter roles or exclusions using class ancestry, capabilities, units, and plugin-ID loop guards.
Export allow-list persistence
indigo-matter.indigoPlugin/Contents/Server Plugin/export_store.py, tests/test_export_store.py
Adds validated export entries and a thread-safe JSON store with CRUD operations, schema validation, corruption preservation, and immediate persistence.
Export management UI and wiring
indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py, indigo-matter.indigoPlugin/Contents/Server Plugin/MenuItems.xml, tests/test_export_menu.py, docs/HANDOVER.md, CLAUDE.md, docs/PRD-indigo-matter-export.md, indigo-matter.indigoPlugin/Contents/Info.plist, tests/test_xac10_no_matter_js.py
Adds the configuration dialog, dynamic pickers, export callbacks, startup restoration, architecture documentation, XAC9 requirement update, import-scan acceptance test, and plugin version update.

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

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ManageMatterExports
  participant export_catalog
  participant IndigoDevices
  participant ExportStore

  User->>ManageMatterExports: Open manageMatterExports
  ManageMatterExports->>IndigoDevices: Read candidate devices
  ManageMatterExports->>export_catalog: classify(device)
  export_catalog-->>ManageMatterExports: Eligible roles or exclusion reason
  User->>ManageMatterExports: Add or update export
  ManageMatterExports->>ExportStore: upsert(ExportEntry)
  ExportStore-->>ManageMatterExports: Persisted export entry
  User->>ManageMatterExports: Remove export
  ManageMatterExports->>ExportStore: remove(device_id)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.45% which is insufficient. The required threshold is 80.00%. 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 summarizes the E2 export allow-list, device catalog, and Manage Matter Exports dialog changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/e2-allowlist-ui

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

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (4)
tests/test_export_menu.py (2)

225-230: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The injected __iter__ does not raise the error the test names.

iter() resolves __iter__ on the type, not on the instance. boom is a plain Mock, whose type defines no __iter__, so for dev in indigo.devices raises TypeError: object is not iterable instead of the RuntimeError("boom") you configured. The assertions still pass, because getExportCandidates catches both. Use MagicMock so the failure under test is the one you intend.

♻️ Proposed fix
-def test_picker_degrades_to_empty_on_error(plug, mock_indigo_base):
-    boom = Mock()
-    boom.__iter__ = Mock(side_effect=RuntimeError("boom"))
-    mock_indigo_base.devices = boom
+def test_picker_degrades_to_empty_on_error(plug, mock_indigo_base):
+    boom = MagicMock()
+    boom.__iter__.side_effect = RuntimeError("boom")
+    mock_indigo_base.devices = boom
     assert plug.getExportCandidates(valuesDict=_values()) == []
     plug.logger.exception.assert_called()

Add MagicMock to the unittest.mock import at line 20.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_export_menu.py` around lines 225 - 230, Update
test_picker_degrades_to_empty_on_error to create boom with MagicMock instead of
Mock, and add MagicMock to the unittest.mock imports so the configured __iter__
raises RuntimeError("boom") as intended.

133-144: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Ruff RUF015: replace the single-element slice with next(...).

Both helpers build a full list to take one element.

♻️ Proposed fix
 def test_conditional_field_is_counted_in_the_height_calculation():
-    invert = [f for f in _menu_item().findall("./ConfigUI/Field")
-              if f.get("id") == "exportInvert"][0]
+    invert = next(f for f in _menu_item().findall("./ConfigUI/Field")
+                  if f.get("id") == "exportInvert")
     assert invert.get("visibleBindingId") == "exportRole"
     assert invert.get("visibleBindingValue") == "windowCovering"
     assert invert.get("alwaysUseInDialogHeightCalc") == "true"
 
 
 def test_status_field_is_readonly():
-    status = [f for f in _menu_item().findall("./ConfigUI/Field")
-              if f.get("id") == "exportStatus"][0]
+    status = next(f for f in _menu_item().findall("./ConfigUI/Field")
+                  if f.get("id") == "exportStatus")
     assert status.get("readonly") == "true"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_export_menu.py` around lines 133 - 144, Update the field lookups
in test_conditional_field_is_counted_in_the_height_calculation and
test_status_field_is_readonly to use next(...) over the matching Field elements
instead of building a list and indexing element zero. Preserve the existing
predicates and assertions.

Source: Linters/SAST tools

indigo-matter.indigoPlugin/Contents/Server Plugin/export_store.py (1)

164-169: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

upsert accepts a role the store itself would reject on reload.

ExportEntry.from_dict rejects a role outside ROLES, but the ExportEntry constructor and upsert do not. An entry created in code with an invalid role persists successfully, then disappears at the next _load with a "dropping an unusable entry" error. Today plugin.py:exportAddOrUpdate validates the role against the catalog before calling upsert, so no current caller can reach this. Enforcing the invariant in the store keeps write-time and read-time validation symmetric for E3's bridge wiring.

♻️ Validate the role at construction
     indigo_device_id: int
     role: str
     name_override: Optional[str] = None
     options: dict = field(default_factory=dict)
 
+    def __post_init__(self) -> None:
+        if self.role not in ROLES:
+            raise ValueError(f"unknown role {self.role!r} (device {self.indigo_device_id})")
+
     def to_dict(self) -> dict:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/export_store.py around
lines 164 - 169, Update ExportEntry construction and the upsert method to
validate that role belongs to ROLES before storing or persisting the entry,
matching ExportEntry.from_dict validation. Reject invalid roles consistently at
write time so unusable entries cannot reach _save.
indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py (1)

113-120: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

The " lx" hint requires a leading space, so it misses unspaced values.

Indigo plugins often format a lux value without a space, for example "340lx". That text matches no hint, so _numeric_sensor_role returns None and _sensor excludes the device with REASON_SENSOR_UNITS. The leading space exists to avoid matching words that contain "lx", but a trailing-anchored hint is unnecessary here because "lx" rarely appears inside English words used in device names. If you want to keep the guard, add the unspaced form as a separate hint.

♻️ Suggested hint addition
-    (ROLE_LIGHT_SENSOR, ("lux", " lx", "illuminance", "light level", "luminance")),
+    (ROLE_LIGHT_SENSOR, ("lux", " lx", "0lx", "1lx", "2lx", "3lx", "4lx", "5lx",
+                         "6lx", "7lx", "8lx", "9lx",
+                         "illuminance", "light level", "luminance")),

A cleaner option is a small regex such as \d\s*lx\b applied to _unit_text.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/export_catalog.py around
lines 113 - 120, Update the lux hints in _UNIT_PATTERNS so _numeric_sensor_role
recognizes unspaced values such as “340lx” while preserving protection against
incidental name matches; either add an unspaced “lx” hint as appropriate or
apply a numeric-aware regex to _unit_text in the sensor-role matching logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/test_export_menu.py`:
- Around line 86-91: Add an inline Ruff S314 suppression to the ET.parse call in
_menu_item, recording that MENU_ITEMS_XML is a trusted repository-owned XML
file. Keep the existing menu-item lookup and missing-item assertion unchanged.

---

Nitpick comments:
In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/export_catalog.py:
- Around line 113-120: Update the lux hints in _UNIT_PATTERNS so
_numeric_sensor_role recognizes unspaced values such as “340lx” while preserving
protection against incidental name matches; either add an unspaced “lx” hint as
appropriate or apply a numeric-aware regex to _unit_text in the sensor-role
matching logic.

In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/export_store.py:
- Around line 164-169: Update ExportEntry construction and the upsert method to
validate that role belongs to ROLES before storing or persisting the entry,
matching ExportEntry.from_dict validation. Reject invalid roles consistently at
write time so unusable entries cannot reach _save.

In `@tests/test_export_menu.py`:
- Around line 225-230: Update test_picker_degrades_to_empty_on_error to create
boom with MagicMock instead of Mock, and add MagicMock to the unittest.mock
imports so the configured __iter__ raises RuntimeError("boom") as intended.
- Around line 133-144: Update the field lookups in
test_conditional_field_is_counted_in_the_height_calculation and
test_status_field_is_readonly to use next(...) over the matching Field elements
instead of building a list and indexing element zero. Preserve the existing
predicates and assertions.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 66fbc537-ebc4-45e2-9bd8-acabff6894be

📥 Commits

Reviewing files that changed from the base of the PR and between a7268fa and 056ac88.

📒 Files selected for processing (11)
  • CLAUDE.md
  • docs/HANDOVER.md
  • indigo-matter.indigoPlugin/Contents/Info.plist
  • indigo-matter.indigoPlugin/Contents/Server Plugin/MenuItems.xml
  • indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py
  • indigo-matter.indigoPlugin/Contents/Server Plugin/export_store.py
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py
  • tests/fakes.py
  • tests/test_export_catalog.py
  • tests/test_export_menu.py
  • tests/test_export_store.py

Comment thread tests/test_export_menu.py
Comment on lines +86 to +91
def _menu_item():
root = ET.parse(MENU_ITEMS_XML).getroot()
for item in root.findall("MenuItem"):
if item.get("id") == "manageMatterExports":
return item
raise AssertionError("manageMatterExports menu item missing")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Ruff reports S314 as an error here.

Ruff flags ET.parse for untrusted XML. The parsed file is the repository's own MenuItems.xml, so the rule does not apply. Ruff reports it at error severity, so the lint gate fails until you silence it explicitly.

🔧 Suppress the rule with the reason recorded
 def _menu_item():
-    root = ET.parse(MENU_ITEMS_XML).getroot()
+    # S314: the parsed file is this repository's own MenuItems.xml, not untrusted input.
+    root = ET.parse(MENU_ITEMS_XML).getroot()  # noqa: S314
     for item in root.findall("MenuItem"):
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _menu_item():
root = ET.parse(MENU_ITEMS_XML).getroot()
for item in root.findall("MenuItem"):
if item.get("id") == "manageMatterExports":
return item
raise AssertionError("manageMatterExports menu item missing")
def _menu_item():
# S314: the parsed file is this repository's own MenuItems.xml, not untrusted input.
root = ET.parse(MENU_ITEMS_XML).getroot() # noqa: S314
for item in root.findall("MenuItem"):
if item.get("id") == "manageMatterExports":
return item
raise AssertionError("manageMatterExports menu item missing")
🧰 Tools
🪛 Ruff (0.16.1)

[error] 87-87: Using xml to parse untrusted data is known to be vulnerable to XML attacks; use defusedxml equivalents

(S314)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_export_menu.py` around lines 86 - 91, Add an inline Ruff S314
suppression to the ET.parse call in _menu_item, recording that MENU_ITEMS_XML is
a trusted repository-owned XML file. Keep the existing menu-item lookup and
missing-item assertion unchanged.

Source: Linters/SAST tools

simons-plugins and others added 2 commits August 5, 2026 08:05
…batch

Store integrity
- S1 persist-then-commit: _commit writes the pref, flushes through an injected
  save_prefs (indigo.server.savePluginPrefs), and only then adopts the new map
  in memory, rolling the pref back if the flush raises. Pre-fix a failed
  remove-save left memory and prefs divergent — the device vanished from the
  dialog and resurrected on the next restart.
- S2 live prefs binding: the store takes a prefs_getter callable, not the
  pluginPrefs object, so a PluginConfig save that rebinds pluginPrefs cannot
  strand writes on an orphan mapping.
- S3 load_error: a corrupt blob or dropped rows are carried on the store and
  surfaced by the dialog, which previously said "Nothing is exported yet." over
  an unreadable list — inviting a rebuild whose first save clobbers the rescue.
- S4 first rescue wins: matterExports.corrupt is never overwritten; a second
  corruption is logged as NOT preserved.
- S5 restored-entry validation: an injected entry_validator re-runs the loop
  guard over entries loaded from prefs (the one write path the dialog's guards
  never see), and from_dict enforces the options shape per role — `invert` only
  on windowCovering, and only as a bool.
- S6 the Add/Remove callbacks contain a failing store write and report
  "FAILED to save the export list", never a stale success.

Dialog
- D1 button callbacks return the values dict only. The (valuesDict, errorsDict)
  tuple is the documented shape for validation methods, not for button
  CallbackMethods (SDK ConfigUI > Button: "returns a dictionary … containing any
  field changes", and the button's own field is read-only). Every refusal now
  lands in exportStatus, including the early returns that previously returned
  without setting it at all.
- D2 the picker always leads with a real ("0", "— select a device —") row for
  the seeded value, and the truncation tail has its own id so "0" is unique.
- D3 per-device error containment in all three list callbacks: one unreadable
  device costs one row, not the whole dialog (pre-fix a single raising proxy
  made getExportCandidates return []). An outright failure returns a labelled
  error row rather than an empty list.
- D4 excluded-but-exported coherence: the "●" marker survives on excluded rows,
  the status names the incoherence, and the second excluded branch clears the
  stale name/polarity it used to leave behind.
- D5 <Description> removed from the two textfields (undocumented for that type;
  it is documented for menus and checkboxes, which keep theirs) into adjacent
  label fields.

Catalog
- C1 classify() never propagates: a device whose attribute access raises yields
  Excluded("error reading device") with one logged stack. Fail-closed — an
  unreadable pluginId cannot prove the loop guard passed.
- C2 unit heuristic matches WORDS: "Fluxcapacitor" was a lightSensor, "Attempt
  Counter" a temperatureSensor, "Epsilon Meter" a pressureSensor and "Overflow
  Alarm" a flowSensor. Prose needles are \b-anchored; symbol needles keep the
  matching that suits them. The device NAME is now the weakest tier, below
  declared units and displayStateValUi.
- C3 RGB-only dimmer zoo row (a surviving mutation until now).
- C4 reverse-coverage invariant: the union of eligible roles across the zoo IS
  bridge_protocol.ROLES, so a new role cannot ship UI-unreachable. Fixing it
  exposed that two zoo sweeps classified against OTHER_PLUGIN_ID — the id the
  zoo devices themselves carry — making them near-vacuous.

Reconcile + docs
- R1 report-only startup reconcile: warns per entry whose device is gone, no
  longer exportable, or no longer offers its saved role. Never edits the list.
- R2 PRD §5.2 said excluded devices must be "absent from the picker with a
  reason shown", contradicting XAC9 ("appear in the picker as excluded, with
  reasons"). Aligned to XAC9 — the picker has always shown them.
- R3 XAC10 test: walks every .py under Server Plugin/ and asserts no matter.js
  import, matching module names from real import statements against an
  npm-shaped denylist (AST, not grep — "matter.js" appears in prose all over
  this repo, and matter_client/matter_model/matter_handlers are ours).
- R4 ruff-style noqa in the E2 modules replaced with pylint-readable disables.

HANDOVER.md records the new store contract and, in bold, that the store is NOT
the guard: E3 must re-classify at endpoint-build time.

Tests 1504 -> 1630, all green. pylint 9.31/10 (was 9.24).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxqGhP3DcENf68AZK21U4S
XAC6 requires devices created by this plugin to be ABSENT from the
picker, verified by unit test — not merely unpickable. They previously
rendered as excluded-with-reason rows like every other exclusion, which
was both a criterion violation and pure noise (each one shadows a real
device already listed). All other exclusions keep their visible reasons
(XAC9). classify() still returns Excluded for our own devices, so the
store validator and both callbacks keep rejecting crafted picks —
absence is presentation, the guard stays structural.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JxqGhP3DcENf68AZK21U4S

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py (1)

1259-1260: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the lint suppressions with the rest of the file. The new export code suppresses only pylint. The existing callbacks in this file also carry # noqa codes, so Ruff stays quiet there. Ruff reports 7 diagnostics on the changed lines (A002 and BLE001). Add the matching # noqa codes next to the existing pylint disables.

  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1259-L1260: add # noqa: N802, A002 to the getExportCandidates signature, matching getDeviceFolders at line 976.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1114-L1114: add # noqa: BLE001 to the blind except Exception in _indigo_device.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1293-L1293: add # noqa: BLE001 to the per-row except Exception in getExportCandidates.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1309-L1310: add # noqa: N802, A002 to the getExportRoles signature.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1332-L1332: add # noqa: BLE001 to the except Exception around role_label.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1339-L1340: add # noqa: N802, A002 to the getCurrentExports signature.
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py#L1359-L1359: add # noqa: BLE001 to the per-row except Exception in getCurrentExports.
♻️ Example for the first two sites
-        except Exception:  # pylint: disable=broad-except  # KeyError/ValueError/Indigo's own
+        except Exception:  # noqa: BLE001 - KeyError/ValueError/Indigo's own
             return None
-    def getExportCandidates(self, filter="", valuesDict=None, typeId="", targetId=0):
-        # pylint: disable=redefined-builtin, unused-argument
+    def getExportCandidates(self, filter="", valuesDict=None, typeId="", targetId=0):  # noqa: N802, A002
+        # pylint: disable=redefined-builtin, unused-argument
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/plugin.py around lines
1259 - 1260, Align Ruff suppressions with the existing pylint disables in
plugin.py: add noqa N802, A002 to getExportCandidates, getExportRoles, and
getCurrentExports signatures; add noqa BLE001 to the blind exception handlers in
_indigo_device, getExportCandidates, getExportRoles’ role_label handling, and
getCurrentExports. Apply these changes at plugin.py lines 1259-1260, 1114, 1293,
1309-1310, 1332, 1339-1340, and 1359.

Source: Linters/SAST tools

tests/test_export_menu.py (1)

677-681: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Import SCHEMA_VERSION instead of hardcoding "v": 1.

tests/test_export_store.py builds every blob with SCHEMA_VERSION. This test uses the literal 1. If the schema version is bumped, the store classifies this blob as a wrong-version corruption, and the test then fails for the wrong reason instead of exercising the dropped-row path.

♻️ Proposed change
-    blob = json.dumps({"v": 1, "exports": [
+    blob = json.dumps({"v": SCHEMA_VERSION, "exports": [
         {"indigoDeviceId": 101, "role": "onOffLight"},
         {"indigoDeviceId": 102, "role": "teleporter"},
     ]})

Add the import alongside the existing PREF_KEY import:

from export_store import PREF_KEY, SCHEMA_VERSION
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_export_menu.py` around lines 677 - 681, Update
test_the_dialog_reports_dropped_rows_alongside_the_count to import
SCHEMA_VERSION alongside PREF_KEY and use it for the blob’s v field instead of
hardcoding 1, so the test remains valid when the schema version changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/PRD-indigo-matter-export.md`:
- Around line 238-239: Update §5.1 in docs/PRD-indigo-matter-export.md to remove
the requirement that indigo-matter’s own devices be filtered out of the
candidate list. Describe the loop guard as classifying those devices and
displaying them as non-selectable rows with the REASON_LOOP_GUARD reason,
consistent with _candidate_row, export_catalog.classify, and exportAddOrUpdate.

---

Nitpick comments:
In `@indigo-matter.indigoPlugin/Contents/Server` Plugin/plugin.py:
- Around line 1259-1260: Align Ruff suppressions with the existing pylint
disables in plugin.py: add noqa N802, A002 to getExportCandidates,
getExportRoles, and getCurrentExports signatures; add noqa BLE001 to the blind
exception handlers in _indigo_device, getExportCandidates, getExportRoles’
role_label handling, and getCurrentExports. Apply these changes at plugin.py
lines 1259-1260, 1114, 1293, 1309-1310, 1332, 1339-1340, and 1359.

In `@tests/test_export_menu.py`:
- Around line 677-681: Update
test_the_dialog_reports_dropped_rows_alongside_the_count to import
SCHEMA_VERSION alongside PREF_KEY and use it for the blob’s v field instead of
hardcoding 1, so the test remains valid when the schema version changes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5dcb9255-4dbc-499e-a4af-5600ee1639df

📥 Commits

Reviewing files that changed from the base of the PR and between 056ac88 and 2656dc4.

📒 Files selected for processing (11)
  • docs/HANDOVER.md
  • docs/PRD-indigo-matter-export.md
  • indigo-matter.indigoPlugin/Contents/Server Plugin/MenuItems.xml
  • indigo-matter.indigoPlugin/Contents/Server Plugin/export_catalog.py
  • indigo-matter.indigoPlugin/Contents/Server Plugin/export_store.py
  • indigo-matter.indigoPlugin/Contents/Server Plugin/plugin.py
  • tests/fakes.py
  • tests/test_export_catalog.py
  • tests/test_export_menu.py
  • tests/test_export_store.py
  • tests/test_xac10_no_matter_js.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/HANDOVER.md
  • indigo-matter.indigoPlugin/Contents/Server Plugin/MenuItems.xml

Comment on lines +238 to +239
Excluded devices must **appear in the picker as excluded, with reasons** (XAC9),
not silently missing.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

This XAC9 statement now contradicts §5.1.

Line 178 states the candidate list MUST be filtered by plugin ID to exclude indigo-matter's own devices. Lines 238-239 state excluded devices must appear in the picker with reasons. Devices caught by the loop guard fall under both rules.

The implementation follows the new rule: _candidate_row in plugin.py returns an x--prefixed row labelled with REASON_LOOP_GUARD, so own devices are listed and not selectable. The loop guard remains structural through export_catalog.classify and the server-side refusal in exportAddOrUpdate. Only the §5.1 wording is stale.

Update §5.1 so it describes the loop guard as classification plus a non-selectable row, not as removal from the list.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/PRD-indigo-matter-export.md` around lines 238 - 239, Update §5.1 in
docs/PRD-indigo-matter-export.md to remove the requirement that indigo-matter’s
own devices be filtered out of the candidate list. Describe the loop guard as
classifying those devices and displaying them as non-selectable rows with the
REASON_LOOP_GUARD reason, consistent with _candidate_row,
export_catalog.classify, and exportAddOrUpdate.

@simons-plugins
simons-plugins merged commit a3369c3 into main Aug 5, 2026
3 checks passed
@simons-plugins
simons-plugins deleted the feat/e2-allowlist-ui branch August 5, 2026 07:14
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