**Project Flow**

The active app flow is:

1. main.py starts the PyQt app.
2. The app calls the package analyzer in runner.py (line 51).
3. runner.py loads Excel, normalizes configured columns, filters in-scope rows, runs all default rules, sums failures into Check, and joins rule messages into Comment.
4. Rule configuration lives in config.py (line 119).
5. Rule execution lives in validator.py (line 160).

So for the desktop app, add new quality rules mainly in som_analyzer/src/som_analyzer/config.py and som_analyzer/src/som_analyzer/analysis/validator.py.

**How To Add A New Quality Rule**

Example: add rule “NOTE must not be empty”.

1. Add the column to WANTED_COLUMNS if it is not already there.

In this case NOTE already exists in config.py (line 15).

2. Add a validator function in validator.py (line 117):

`def is_not_empty(value) -> bool: if pd.isna(value): return False return str(value).strip() != ""`

3. Register that function in predicate_registry inside build_default_rules (line 161):

`"not_empty": is_not_empty,`

4. Add a rule definition in DEFAULT_RULE_DEFINITIONS (line 119):

`PredicateRuleDefinition( rule_name="note_required", columns=("NOTE",), predicate_name="not_empty", message_template="Missing required value: {columns}", ),`

That is enough for package behavior. runner.py already handles the rest automatically: it evaluates every default rule at runner.py (line 65), adds failures into Check at runner.py (line 67), and adds messages into Comment at runner.py (line 70).

**Rule Types**

Use PredicateRuleDefinition when the rule checks one or more columns with a boolean function. Existing examples: email, COFOR pattern, 12-character length, location, Excel reference errors.

Use AllowedValueRuleDefinition when a column must be one of fixed values. Existing example: Contacted.

Use a custom ValidationRule class only for row-level consistency checks across multiple columns. Existing example: StatusInfoConsistencyRule.

**After Adding Rule**

Run:

`uv run python main.py`

Or package smoke flow:

`Push-Location som_analyzer uv run som-analyze-smoke Pop-Location`

Important: the root som_analyzer.ipynb still contains notebook-first validation logic. If the notebook must stay canonical, mirror the same rule there too: constant/list, validator function, fail mask, add to Check, and append reason to Comment.