Files
mxaccessgw/code-reviews/regen-readme.py
Joseph Doherty d2d2e5f68f code-review 2026-05-24: re-review at d692232 across all 11 modules
Restores the `code-reviews/` tree (was unwritten on this working copy)
and re-reviews every module per `REVIEW-PROCESS.md` against HEAD
`d692232`. The diff in scope is the five commits since the last sweep:
`dc9c0c9` (ZB.MOM.WW gateway-side rename + slnx migrate),
`397d3c5` (client SDK rename + the missing alarm-RPC proto types and
the .NET DiscoverHierarchyOptions POCO), `27ed651` (role-based LDAP
auth + HubToken bearer, drop PathBase), `6594359` (sidebar layout +
three SignalR push hubs), and `d692232` (EventsHub publisher + doc
refresh).

Module status

| Module | Open | Total | Delta this pass |
|---|---|---|---|
| Server           | 8 | 43 | +6 |
| Contracts        | 2 | 17 | +2 |
| Tests            | 2 | 26 | +2 |
| IntegrationTests | 3 | 24 | +3 |
| Client.Java      | 5 | 31 | +5 |
| Client.Rust      | 1 | 21 | +1 |
| Worker           | 0 | 25 |  0 (rename-only diff, clean) |
| Worker.Tests     | 0 | 30 |  0 (rename-only diff, clean) |
| Client.Dotnet    | 0 | 17 |  0 (rename + alarm-fix diff, clean) |
| Client.Python    | 0 | 21 |  0 (rename + alarm-fix diff, clean) |
| Client.Go        | 0 | 21 |  0 (rename + alarm-fix diff, clean) |

Total new findings: 19. Severity breakdown: 1 Medium-security
(Server-038), 4 Medium-documentation/coverage, 14 Low.

New findings

  * Server-038 (Medium / Security) — EventsHub.SubscribeSession accepts
    any session id from any Viewer; no per-session ACL guards the
    EventsHub group fan-out.
  * Server-039 (Low / Error handling) — HubTokenService.Validate
    accepts a payload with null Name/NameIdentifier.
  * Server-040 (Low / Conventions) — MapGroupsToRoles undocumented
    full-vs-RDN lookup precedence.
  * Server-041 (Low / Design adherence) — EventStreamService calls
    IDashboardEventBroadcaster.Publish without a try/catch — fragile
    seam relying on the never-throw contract.
  * Server-042 (Low / Performance) — DashboardSnapshotPublisher tight
    retry loop with no backoff (vs AlarmsHubPublisher 5s delay).
  * Server-043 (Low / Documentation) — HubTokenService singleton
    sharing across login + hub-token validation undocumented.

  * Contracts-016 (Low / Conventions) — QueryActiveAlarmsRequest.session_id
    reserved-for-future-use ambiguity.
  * Contracts-017 (Low / Documentation) — rpc QueryActiveAlarms doc
    omits the alarm_filter_prefix filter description.

  * Tests-025 (Low / Conventions) — duplicate NullDashboardEventBroadcaster
    fakes in EventStreamServiceTests and GatewayEndToEndFakeWorkerSmokeTests.
  * Tests-026 (Medium / Testing coverage) — no test proves
    EventStreamService actually calls IDashboardEventBroadcaster.Publish.

  * IntegrationTests-022 (Low / Conventions) — ResolveRepositoryRoot
    silent fallback to Directory.GetCurrentDirectory().
  * IntegrationTests-023 (Low / Testing coverage) — DashboardLdapLiveTests
    success-path asserts ldap_group but not the Role claim.
  * IntegrationTests-024 (Low / Conventions) — inline
    NullDashboardEventBroadcaster fake duplicates Tests-side copies.

  * Client.Java-027 (Medium / Documentation) — README + JavaClientDesign
    Gradle task names still use the old short project names.
  * Client.Java-028 (Medium / Design adherence) — JavaClientDesign
    build-layout shows the old `com/dohertylan/mxgateway/` package paths.
  * Client.Java-029 (Low / Documentation) — README installDist path
    cites the wrong directory.
  * Client.Java-030 (Low / Testing coverage) — no Java test exercises
    the regenerated QueryActiveAlarmsRequest RPC.
  * Client.Java-031 (Low / Conventions) — README prose uses old short
    project names instead of canonical prefixed ones.

  * Client.Rust-021 (Low / Design adherence) — RustClientDesign.md
    "Crate layout" shows an aspirational nested `crates/zb-mom-ww-mxgateway-client/`
    that does not exist; actual layout is the flat top-level crate.

Two pre-existing pending findings (Server-031 lock-contention,
Server-032 bounded event channel) remain unchanged — neither was
touched by this wave of commits.

Process notes

- The `code-reviews/` tree was not in this working copy's git
  history (the local extract pre-dates the divergent branch that
  carried the reviews). Restored from `dd7ca16` via
  `git checkout dd7ca16 -- code-reviews/` before the re-review.
- Some "Resolved" entries in the restored findings.md reference
  fixes that landed on the divergent branch (the same one that
  carried the reviews) and are not present on the current main
  lineage. The re-review treats those statuses as historical;
  the new pass only files findings against HEAD's actual state.
- `python code-reviews/regen-readme.py --check` is green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 02:34:30 -04:00

237 lines
8.1 KiB
Python

#!/usr/bin/env python3
"""Regenerate code-reviews/README.md from the per-module findings.md files.
The per-module findings.md files are the source of truth. This script aggregates
them into the single cross-module README.md (module status + pending/closed
finding tables).
Usage:
python code-reviews/regen-readme.py # rewrite README.md
python code-reviews/regen-readme.py --check # exit 1 if stale or inconsistent
`--check` fails when README.md is out of date OR when a module's header
`Open findings` count disagrees with its finding statuses, or a finding
carries an unrecognised Status value.
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
README = ROOT / "README.md"
PENDING_STATUSES = {"Open", "In Progress"}
KNOWN_STATUSES = {"Open", "In Progress", "Resolved", "Won't Fix", "Deferred"}
SEVERITY_ORDER = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
GENERATED_NOTE = (
"<!-- GENERATED FILE - do not edit by hand. "
"Regenerate with: python code-reviews/regen-readme.py -->"
)
def cell(value: str) -> str:
"""Escape a value for safe inclusion in a markdown table cell."""
return value.replace("|", "\\|").strip()
def summarize(value: str, limit: int = 240) -> str:
"""Trim a long description to a single-cell-friendly summary."""
value = value.strip()
if len(value) <= limit:
return value
return value[: limit - 1].rstrip() + ""
def first_table(text: str) -> dict[str, str]:
"""Parse the first contiguous block of '| key | value |' rows into a dict."""
rows: dict[str, str] = {}
started = False
for line in text.splitlines():
stripped = line.strip()
if stripped.startswith("|"):
started = True
cells = [c.strip() for c in stripped.strip("|").split("|")]
if len(cells) >= 2:
key, value = cells[0], cells[1]
if key and not set(key) <= {"-", ":"} and key != "Field":
rows[key] = value
elif started:
break
return rows
def parse_module(findings_path: Path) -> dict:
"""Parse one module's findings.md into its header and finding list."""
text = findings_path.read_text(encoding="utf-8")
module = findings_path.parent.name
parts = re.split(r"^##\s+Findings\s*$", text, maxsplit=1, flags=re.M)
header = first_table(parts[0])
findings: list[dict] = []
if len(parts) > 1:
for chunk in re.split(r"^###\s+", parts[1], flags=re.M)[1:]:
fid = chunk.splitlines()[0].strip()
tbl = first_table(chunk)
desc_m = re.search(
r"\*\*Description:\*\*\s*(.*?)(?=\n\*\*|\Z)", chunk, re.S
)
desc = re.sub(r"\s+", " ", desc_m.group(1)).strip() if desc_m else ""
findings.append(
{
"id": fid,
"severity": tbl.get("Severity", ""),
"category": tbl.get("Category", ""),
"location": tbl.get("Location", ""),
"status": tbl.get("Status", ""),
"description": desc,
}
)
return {"module": module, "header": header, "findings": findings}
def build_readme(modules: list[dict]) -> str:
modules = sorted(modules, key=lambda m: m["module"])
all_findings = [
dict(f, module=m["module"]) for m in modules for f in m["findings"]
]
pending = [f for f in all_findings if f["status"] in PENDING_STATUSES]
closed = [
f
for f in all_findings
if f["status"] and f["status"] not in PENDING_STATUSES
]
def sev_key(f: dict) -> tuple:
return (SEVERITY_ORDER.get(f["severity"], 9), f["id"])
pending.sort(key=sev_key)
closed.sort(key=sev_key)
out: list[str] = [
"# Code Reviews",
"",
GENERATED_NOTE,
"",
"Cross-module code review index for the `mxaccessgw` codebase. The review "
"process is defined in [../REVIEW-PROCESS.md](../REVIEW-PROCESS.md).",
"",
"Each module's `findings.md` is the source of truth; this file is generated "
"from them by `regen-readme.py` and must not be edited by hand.",
"",
"## Module status",
"",
"| Module | Reviewer | Date | Commit | Status | Open | Total |",
"|---|---|---|---|---|---|---|",
]
for m in modules:
h = m["header"]
open_n = sum(
1 for f in m["findings"] if f["status"] in PENDING_STATUSES
)
out.append(
f"| [{m['module']}]({m['module']}/findings.md) "
f"| {cell(h.get('Reviewer', ''))} "
f"| {cell(h.get('Review date', ''))} "
f"| {cell(h.get('Commit reviewed', ''))} "
f"| {cell(h.get('Status', ''))} "
f"| {open_n} | {len(m['findings'])} |"
)
out += ["", "## Pending findings", ""]
out.append(
"Findings with status `Open` or `In Progress`, ordered by severity."
)
out.append("")
if pending:
out.append("| ID | Severity | Category | Location | Description |")
out.append("|---|---|---|---|---|")
for f in pending:
out.append(
f"| {cell(f['id'])} | {cell(f['severity'])} "
f"| {cell(f['category'])} | {cell(f['location'])} "
f"| {cell(summarize(f['description']))} |"
)
else:
out.append("_No pending findings._")
out += ["", "## Closed findings", ""]
out.append("Findings with status `Resolved`, `Won't Fix`, or `Deferred`.")
out.append("")
if closed:
out.append("| ID | Severity | Status | Category | Location |")
out.append("|---|---|---|---|---|")
for f in closed:
out.append(
f"| {cell(f['id'])} | {cell(f['severity'])} "
f"| {cell(f['status'])} | {cell(f['category'])} "
f"| {cell(f['location'])} |"
)
else:
out.append("_No closed findings._")
return "\n".join(out) + "\n"
def find_inconsistencies(modules: list[dict]) -> list[str]:
"""Return human-readable problems in the per-module findings.md files.
Checks that each module header's `Open findings` count agrees with its
finding statuses, and that every finding carries a known Status value.
"""
issues: list[str] = []
for m in modules:
open_n = sum(
1 for f in m["findings"] if f["status"] in PENDING_STATUSES
)
declared = m["header"].get("Open findings", "").strip()
if declared != str(open_n):
issues.append(
f"{m['module']}: header 'Open findings' = '{declared}' but "
f"{open_n} finding(s) are Open/In Progress"
)
for f in m["findings"]:
if f["status"] not in KNOWN_STATUSES:
issues.append(
f"{m['module']}: finding {f['id']} has unrecognised "
f"Status '{f['status']}'"
)
return issues
def main(argv: list[str]) -> int:
check = "--check" in argv[1:]
module_dirs = sorted(
d
for d in ROOT.iterdir()
if d.is_dir() and d.name != "_template" and (d / "findings.md").is_file()
)
modules = [parse_module(d / "findings.md") for d in module_dirs]
content = build_readme(modules)
issues = find_inconsistencies(modules)
if check:
stale = (
README.read_text(encoding="utf-8") if README.exists() else ""
) != content
for issue in issues:
print(f"inconsistent: {issue}", file=sys.stderr)
if stale:
print(
"code-reviews/README.md is stale - run regen-readme.py",
file=sys.stderr,
)
if stale or issues:
return 1
print("code-reviews/README.md is up to date and consistent.")
return 0
for issue in issues:
print(f"warning: {issue}", file=sys.stderr)
README.write_text(content, encoding="utf-8", newline="\n")
print(f"Wrote {README} ({len(modules)} modules).")
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))