# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

```bash
# Run all tests
python -m pytest tests/ -v

# Run a single test file
python -m pytest tests/test_qgisred_field_utils.py -v

# Run a single test
python -m pytest tests/test_qgisred_field_utils.py::TestClassName::test_method -v

# Setup git pre-commit hook (runs pytest automatically on commit)
scripts\setup-hooks.bat

# Compile Qt resources (images) → resources3x.py
scripts\CompileImages.bat

# Update translation strings from source → .ts files
scripts\UpdateTranslations.bat

# Compile translations .ts → .qm
scripts\CompileTranslations.bat

# Create release ZIP (patch version bump)
scripts\release_beta.bat
```

Tests use a fully mocked QGIS environment (`tests/conftest.py`) so no QGIS installation is needed to run them.

## Architecture

### Section mixin pattern

`qgisred.py` is a thin orchestrator (~45 lines) that inherits from 11 Section classes via multiple inheritance:

```python
class QGISRed(LifecycleSection, MenuSection, ProjectManagementSection,
              LayerManagementSection, UtilsSection, NetworkEditingSection,
              AnalysisSection, DebugValidationSection, ToolsSection,
              DigitalTwinSection, QueriesSection):
```

All plugin logic lives in `sections/`. Each section is a mixin with no `__init__`. Shared state (`self.NetworkName`, `self.ProjectDirectory`, `self.iface`, etc.) is set up by `LifecycleSection` and accessed by all mixins via `self`.

To add a new section: create the file, add it to `sections/__init__.py`, add it to the inheritance list in `qgisred.py`, register its class in `lifecycle_section._SECTION_CONTEXTS` for translations, and add its source files to `qgisred.pro`.

### DLL integration

`tools/qgisred_dependencies.py` wraps a Windows .NET DLL via `ctypes.WinDLL`. All DLL calls are static methods that return a string. The canonical handler is `processCsharpResult()` in `LayerManagementSection`:

| Return value | Meaning |
|---|---|
| `"True"` | Success; show optional message, no layer changes |
| `"False"` / `"Cancelled"` | No-op |
| `"commit"` | Reload input layers |
| `"shps"` | Open query/result shapefiles |
| `"commit/shps"` | Both of the above |
| anything else | Error message |

The `layerType` parameter (`"issues"`, `"sectors"`, `"connectivity"`) and optional `onOpenLayers` callback control which layer-opening path runs. See `INTERNALS.md §7` for the full flow.

### Layer lifecycle — critical rule

**Never delete a shapefile that QGIS has open.** On Windows, deleting and recreating a file creates a new inode; open QGIS handles point to the zombie. Always overwrite in-place:

```python
shutil.copy2(src, dst)   # preserves inode at dst
os.remove(src)
layer.dataProvider().reloadData()
```

`_tryReloadExistingLayer(layerPath)` in `QGISRedLayerUtils` is the central helper — it finds the open layer and reloads it in-place. `openLayer()` calls it automatically before opening a fresh layer. See `INTERNALS.md` for the full table of safe vs. unsafe file operations.

### File locations by layer group

| Group | Subdirectory |
|---|---|
| Inputs | `ProjectDirectory/` root |
| Issues | `ProjectDirectory/Issues/` |
| Queries / Sectors / Tree / IsolatedSegments | `ProjectDirectory/Queries/` |
| Results | `ProjectDirectory/Results/` |

Each group uses a dedicated open method: `openElementLayers`, `openIssuesLayers`, `openSectorLayers`, `openTreeLayer`, `openIsolatedSegmentsLayers`. These call `openLayer()` with the appropriate flags (`issues=`, `sectors=`, `results=`), which selects the right styling call. Do not use the legacy `openGroupLayers()` for new code.

### Group visibility

`getOrCreateNestedGroup(path_list)` in `QGISRedLayerUtils` creates or finds a nested group and automatically hides sibling groups at each level (except the network root). Every `open*Layers()` method calls it, so visibility management is transparent. `getOrCreateGroup(name)` is a simpler variant that does NOT apply visibility — use it only when you need a group reference without loading layers.

### Styling

`tools/utils/qgisred_styling_utils.py` provides:
- `setStyle(layer, name)` — loads a QML from the plugin's styles directory
- `setResultStyle`, `setSectorsStyle`, `setHydraulicSectorStyle`, `setIsolatedSegmentsStyle` — data-driven styles rebuilt from layer values

Sector/hydraulic layers use categorized renderers that must be **re-applied after every reload** because each DLL run can produce a different set of categories. `openLayer()` handles this: when `_tryReloadExistingLayer()` returns `True` and `sectors=True`, it immediately calls the appropriate style method.

### Qt5/Qt6 compatibility

`compat.py` at the root is the single shim for all Qt5 vs Qt6 API differences (QAction location, QVariant types, enum scoping, QgsMapLayer enums, etc.). All plugin files import from `qgis.PyQt.*` (not `PyQt5.*`) and use `compat.py` for anything version-dependent.

### Metadata persistence

When a project has no `.qgs/.qgz` file, `updateMetadata()` serializes the current layer tree to `{NetworkName}_Metadata.txt` (XML). The DLL handles the XML write via `GISRed.UpdateMetadata(project, net, layersNames)` where `layersNames` is a string like `"[Inputs]Pipes;Junctions;...[Issues]Pipes_Issues;..."`. On open, `openProjectInQgis()` in `qgisred_project_io.py` reads this XML and recreates the layer tree. When a `.qgs/.qgz` exists, `QgsProject.instance().read()` is used instead and `openProjectInQgis()` returns `True`; a manual load returns `False`.

### `layerOperationInProgress` flag

`runLegendChanged` (connected to QGIS legend signals) calls `updateMetadata()` on any layer tree change. This guard prevents it from firing in the middle of a multi-step open operation. `processCsharpResult` sets it `True` before opening layers; `runOpenTemporaryFiles` (or `onOpenLayers` callback) resets it `False` when done.

### Results dock

`ui/analysis/qgisred_results_dock.py` manages the simulation workflow. Result layers follow the new naming format `NetworkName_Scenario_Node.shp` / `NetworkName_Scenario_Link.shp` in `Results/`. The old format `NetworkName_Scenario_Node_Variable.shp` (with a Variable suffix) is detected and deleted on project open via `removeOldResultLayers()` in `ProjectManagementSection`.
