Skip to content
Merged
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,41 @@
All notable changes to this project are documented here. Versions follow
[semantic versioning](https://semver.org/).

## [Unreleased]

## [1.3.0] — 2026-07-31

Read the diff in whichever colour scheme suits the screen, with A2L coloured
like the rest and noise pushed out of the way instead of out of the file.

### Added

- **Light and dark colour schemes.** Both the viewer and the report can be read
either way. `--theme dark|light` sets which one they open with (dark stays the
default), the viewer has a switch in its toolbar, and the report carries both
schemes inside the file — so its own switch works with no internet, and your
choice is remembered for the next report you open.
- **A2L files are syntax-coloured** in the viewer, keywords and block types
apart from the calibration object names — so a name still stands out in a
page of ASAM keywords.

### Changed

- **Hiding comment or unimportant differences now greys those lines out instead
of removing them.** They keep their place and their line numbers, so the code
around a real change is still there to read it in, and they no longer count
as changes on the minimap or when stepping through changes.
- **The report shows unimportant differences when you click their badge**, in
grey rather than red or green, so a revealed category still reads as one that
does not count. Comment differences stay out of the report entirely — only
the count of hidden lines is shown — while the viewer keeps showing them.

### Fixed

- **A model's `_data` companion file is filed under that model** in the report
instead of landing in Shared / other, so everything generated for one
component is read in one place.

## [1.2.0] — 2026-07-29

Compare a folder against its own git history, sign off a whole file at once,
Expand Down
19 changes: 16 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,25 @@ place, `diff_engine._status_of`.
Only noise verdicts are foldable (`scanner.FOLDABLE`). `real-change`, `added`,
`deleted` and `error` can **never** be folded away by a UI toggle.

Folding a category in the viewer changes the file's verdict and **greys** its
rows (`view_model.mute_rows`) — it does not remove them. The lines stay
readable, and only the "where should I look next" surfaces (minimap, F7/F8)
stop counting them. Collapsing them to a `⋯ N lines hidden` placeholder was
tried and reverted: a regenerated file is mostly banner churn, so it took the
context the surviving hunks have to be read in.

## 3. One seam per shared decision

Any fact two renderers need lives in **one** module they both import.
`compare_tool/view_model.py` holds `mode_of`, `char_span` and `aligned_rows`;
the HTML report and the Qt viewer both consume them, so they cannot disagree
about what changed or how it is coloured.
`compare_tool/view_model.py` holds `mode_of`, `char_span`, `aligned_rows` and
`mute_rows`; `compare_tool/theme.py` holds every colour as a named role, one
value per theme. The HTML report and the Qt viewer both consume them, so they
cannot disagree about what changed or how it is coloured.

A colour literal outside `theme.py` is a bug: it paints one theme correctly and
the other by accident. Add a role to **both** palettes (an import-time assert
enforces it), then use `var(--role)` in the report's CSS or `theme.c(role)` in
Qt.

Re-implementing a mapping inline "because it is only four lines" is the bug:
the copies drift the moment a new kind is added. If you find a duplicated
Expand Down
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ scan did find is still printed.
| `--exit-zero` | Always exit 0 even when real changes exist (report-only mode for pipelines). Compare errors still exit 2 |
| `--arxml-only` | Scan only `.arxml`/`.xml`/`.a2l` and write a compact per-type report (default `arxml_update.html`) — always written, even when nothing changed |
| `--review FILE` | Render notes and sign-offs from a review file (`codegen-review.json`, written by the viewer) next to the changes they belong to, plus a `Reviewed` badge that hides the changes already signed off. Must be named explicitly — a report must not pick up someone else's sign-off by accident; no effect with `--arxml-only` |
| `--theme dark\|light` | Colour scheme the report and the viewer open with (default `dark`). The report carries **both** and has its own switch, so this only sets what the reader sees first |
| `--qt`, `--viewer` | Open the side-by-side viewer on folders named on the command line, instead of comparing them in the terminal. Needs the `viewer` extra (see below) |

Omitting `old_dir`/`new_dir` opens the viewer. `--gui` (the tkinter panel) was removed in 1.1.0.
Expand All @@ -113,6 +114,8 @@ Reading a scan:
- `F8` / `F7` step through the changes in the open file and then **carry on into the next (previous) file** with something to review, wrapping at the end. `Ctrl+Home` / `Ctrl+End` stay inside the file.
- `Ctrl+F` **finds text in the open file** (either side, `F3` / `Shift+F3` to step, `Esc` to close). The query survives moving to another file, so an identifier can be chased across the compare.
- `Hide identical` leaves only the files with a difference in the tree. It is a view: verdicts, counts and the exported report are untouched.
- Unticking `Comment` / `Unimportant` **greys those lines out** rather than removing them: they stay where they are, keep their line numbers, lose their red/green, and drop off the minimap and out of `F7`/`F8`. The code around a change is what makes it readable, and a regenerated file is mostly banner churn — folding it away took most of the file with it.
- `☀ Light` / `☾ Dark` in the toolbar switches the colour scheme; `--theme` picks the one it starts in. C, ARXML and A2L are syntax-coloured in both.

`Review mode` adds the note box and a `Review` column in the tree — green when every change in a row is signed off, amber part way, grey when none is. Sign off one change (`Ctrl+R`) or a whole file (`Ctrl+Shift+R`); the notes travel into the exported report.

Expand All @@ -138,7 +141,7 @@ A shorter name can stop an argument wrapping at 80 columns, so the two sides hol

Everything else keeps its suffix as meaning. `SIG_TORQUE_MIN` → `SIG_TORQUE_MAX` and `CFG_TIMEOUT_MS` → `CFG_TIMEOUT_US` are real changes, and so are `rtb_AND_…` → `rtb_OR_…` (a different block drives that buffer) and `Sub_…_step` → `Sub_…_Init` (a different entry point). Digits glued to a block name (`rtb_Switch1` vs `rtb_Switch2`) are part of the name, not a mangle tail.

**Comment changes are their own category.** A file whose differences are *only* comments is reported as **Comment**, separate from **Unimportant** (UUIDs, timestamps, SW-VERSION, renames, whitespace) — a rewritten comment banner triages differently from a renamed identifier. Separate counts in the CLI summary and its own tree marker in the viewer. A file mixing comments *with* other noise stays Unimportant. The HTML report keeps the verdict but does not display comment content — see [HTML report](#html-report).
**Comment changes are their own category.** A file whose differences are *only* comments is reported as **Comment**, separate from **Unimportant** (UUIDs, timestamps, SW-VERSION, renames, whitespace) — a rewritten comment banner triages differently from a renamed identifier. Separate counts in the CLI summary and its own tree marker in the viewer. A file mixing comments *with* other noise stays Unimportant. The viewer has a rule toggle for each; the HTML report gives `Unimportant` a badge and leaves comment lines out altogether — see [HTML report](#html-report).

## Moved block detection

Expand Down Expand Up @@ -168,11 +171,13 @@ A file whose XML fails to parse is skipped from this summary (its text diff stil

## Grouping by model / SWC

Files are grouped by **Simulink model** using the Embedded Coder AUTOSAR naming convention (`X.c`, `X.h`, `X.arxml`, `Rte_X.h`, the modular ARXML set, …). Files that match no model land in a final **Shared / other** group.
Files are grouped by **Simulink model** using the Embedded Coder AUTOSAR naming convention (`X.c`, `X.h`, `X.arxml`, `Rte_X.h`, `X_data.c`, the modular ARXML set, …). Files that match no model land in a final **Shared / other** group.

## HTML report

Self-contained file, one per compare: badge toggles, folder tree, filter box, collapsible diffs per file. Opens `Unimportant` hidden and `Modified` expanded, so it opens on what matters.
Self-contained file, one per compare: badge toggles, folder tree, filter box, collapsible diffs per file. Opens `Unimportant` hidden, `Modified` expanded, so it opens on what matters. Clicking `Unimportant` reveals the actual noise lines — painted flat grey rather than red/green, so a revealed category still reads as "does not count" instead of looking like another change. Comment changes never render in the report at all — only a placeholder states how many comment lines were hidden — the report is a record meant to be sent around, and comment churn is left out of it entirely; the side-by-side viewer still shows them, greyed, for a reviewer working file by file. A `☀ Light` / `☾ Dark` button sits in the top right — both palettes are embedded in the file, so switching fetches nothing and works on a machine with no internet.

A whole file with nothing but comment differences still gets no detail section of its own (there is nothing beyond the comment lines to show); it keeps its own `≉` mark and `Comment` count in the folder tree either way.

![Report viewer](resources/pic/report_page.png)

Expand Down Expand Up @@ -230,13 +235,14 @@ compare_tool/
├── arxml_rules.py # ARXML rules: UUID, ADMIN-DATA, DATE, comments + extract port interfaces, SWCs (ports/runnables/events)
├── a2l_rules.py # A2L rules: strip C-style comments + extract CHARACTERISTIC/MEASUREMENT
├── view_model.py # renderer-agnostic view model (paint mode, intra-line span, row alignment) shared by the report and the viewer
├── syntax.py # line-at-a-time C / XML token spans, Qt-free so it ships in the .pyz
├── theme.py # the dark and light palettes as named roles, shared by the report's CSS and every Qt surface
├── syntax.py # line-at-a-time C / XML / A2L token spans, Qt-free so it ships in the .pyz
├── review.py # reviewer notes and sign-offs, keyed by change content so they survive a rescan
├── gitsource.py # read-only `git archive` of a commit into a temp folder, so a commit can be the OLD side
└── report.py # self-contained HTML report (badge toggles, model overview, grouping, filter, collapsible diffs)
```

[docs/architecture.md](docs/architecture.md) covers how these fit together and why: the two diff passes, where a verdict is decided, the shared seams and the result-dict contract. Anything both renderers need lives in `view_model.py` — reimplementing a mapping inline lets the HTML report and the viewer drift apart about what changed.
[docs/architecture.md](docs/architecture.md) covers how these fit together and why: the two diff passes, where a verdict is decided, the shared seams and the result-dict contract. Anything both renderers need lives in `view_model.py` (what changed) or `theme.py` (what colour it gets) — reimplementing a mapping inline lets the HTML report and the viewer drift apart.

To add a rule: write the strip function in `c_rules.py` / `arxml_rules.py` / `a2l_rules.py`, join it into that ruleset's shadow, register one labelled variant in `_build_variants` in `diff_engine.py`, and add both tests — the pattern alone is noise, and the same pattern *beside* a real change still reports the real change.

Expand Down
2 changes: 1 addition & 1 deletion compare_tool/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""CodeGen Compare Tool - AUTOSAR MATLAB codegen diff with noise filtering."""

__version__ = "1.2.0"
__version__ = "1.3.0"
22 changes: 15 additions & 7 deletions compare_tool/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@

Usage:
python -m compare_tool <old_dir> <new_dir> [--report out.html] [--arxml-only]
python -m compare_tool # side-by-side viewer
python -m compare_tool [--theme light] # side-by-side viewer
"""

import argparse
import sys
from pathlib import Path

from . import review
from . import review, theme
from .diff_engine import RULES
from .report import build_arxml_report, build_report
from .scanner import (scan, summarize, summarize_a2l, summarize_ifaces,
Expand Down Expand Up @@ -47,7 +47,7 @@ def default_report_name(arxml_only):


def run_compare(old_root, new_root, out, arxml_only=False, exclude=(),
progress=None, reviews=None):
progress=None, reviews=None, theme_name=theme.DEFAULT):
"""Scan two trees and write the HTML report.
Returns (results, counts). Raises :class:`ReportWriteError` when the report
could not be written -- a run whose record does not exist is not a run that
Expand All @@ -68,9 +68,11 @@ def run_compare(old_root, new_root, out, arxml_only=False, exclude=(),
if arxml_only:
# ALWAYS written: "no changes" must be an explicit statement, never
# a silently absent file (indistinguishable from a run that died)
page = build_arxml_report(results, old_root, new_root)
page = build_arxml_report(results, old_root, new_root,
theme_name=theme_name)
else:
page = build_report(results, old_root, new_root, reviews)
page = build_report(results, old_root, new_root, reviews,
theme_name=theme_name)
try:
out.write_text(page, encoding='utf-8')
except OSError as e:
Expand Down Expand Up @@ -184,6 +186,12 @@ def _parser():
'diff report; the report is ALWAYS written -- when '
'nothing real changed it states "no changes" '
'explicitly per file type')
ap.add_argument('--theme', choices=theme.THEMES, default=theme.DEFAULT,
help='colour scheme the viewer opens with, and the one the '
'HTML report opens with (default: dark). The report '
'always carries both, so its own button switches with '
'nothing to download; the viewer has the same button '
'in its toolbar')
ap.add_argument('--exclude', metavar='PATTERN', action='append', default=[],
help='skip files matching this glob (relative path or bare '
'file name); repeatable. Example: --exclude compare_report.html')
Expand Down Expand Up @@ -230,7 +238,7 @@ def main(argv=None):
from .qtviewer import run_viewer # deferred: PySide6 may be absent
try:
return run_viewer(args.old_dir, args.new_dir, exclude=args.exclude,
arxml_only=args.arxml_only)
arxml_only=args.arxml_only, theme_name=args.theme)
except ImportError as e:
# a stdlib-only install (the .pyz, a locked-down box) has no Qt.
# Say so plainly instead of dumping a traceback.
Expand Down Expand Up @@ -275,7 +283,7 @@ def progress(done, total, rel):
try:
results, counts = run_compare(old_root, new_root, out, args.arxml_only,
exclude=args.exclude, progress=progress,
reviews=reviews)
reviews=reviews, theme_name=args.theme)
except ReportWriteError as e:
# what WAS scanned still goes to the terminal -- the compare itself may
# have been fine, it is only the record that is missing
Expand Down
Loading
Loading