Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
af29e46
chore(codex): bootstrap issue #239
github-actions[bot] Feb 25, 2026
1d15322
chore(codex-keepalive): apply updates (PR #249)
github-actions[bot] Feb 25, 2026
3b46e81
test: cover registry-source clearing house resolution
Feb 25, 2026
09259b2
Document source metadata in counterparty normalization API
Codex-Agent Feb 25, 2026
6fa9f0a
Use source-aware counterparty normalization in reconciliation
Feb 25, 2026
3fcf275
Add Soc Gen Inc alias to default name registry
Codex-Agent Feb 25, 2026
4618420
Add output-format forwarding coverage for mapping diff CLI
Codex-Agent Feb 25, 2026
de0ef73
chore(codex-keepalive): apply updates (PR #249)
github-actions[bot] Feb 25, 2026
40cf31b
Fix clearing-house registry-first normalization and source diff test
Codex-Agent Feb 25, 2026
d118bf2
test(fixtures): expand after-registry mappings for integration parity
Codex-Agent Feb 25, 2026
2a5e6f3
Add explicit acceptance tests for registry source and CLI params
Codex-Agent Feb 25, 2026
8919f1f
chore(codex-keepalive): apply updates (PR #249)
github-actions[bot] Feb 25, 2026
b5af361
chore(codex-keepalive): apply updates (PR #249)
github-actions[bot] Feb 25, 2026
04dbf66
chore(scope): remove out-of-scope pipeline delta from PR
Codex-Agent Feb 25, 2026
7113dde
chore(codex-keepalive): apply updates (PR #249)
github-actions[bot] Feb 25, 2026
7d220f7
Merge remote-tracking branch 'origin/main' into codex/issue-239
claude Feb 25, 2026
5ef6482
Merge pull request #254 from stranske/claude/merge-main-into-249-I1gRT
stranske Feb 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/name_registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ entries:
display_name: Soc Gen
aliases:
- Soc Gen
- Soc Gen Inc
- Societe Generale

- canonical_key: barclays
Expand Down
12 changes: 11 additions & 1 deletion src/counter_risk/cli/mapping_diff_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ def build_parser() -> argparse.ArgumentParser:
default=[],
help="Raw input name observed during reconciliation. Can be provided multiple times.",
)
parser.add_argument(
"--output-format",
choices=("text",),
default="text",
help="Report output format.",
)
return parser


Expand All @@ -48,7 +54,11 @@ def main(argv: list[str] | None = None) -> int:
"reconciliation": list(args.reconciliation_name),
}
try:
report = generate_mapping_diff_report(args.registry, input_sources)
report = generate_mapping_diff_report(
args.registry,
input_sources,
output_format=args.output_format,
)
except ValueError as exc:
error_line = " ".join(str(exc).splitlines())
print(error_line, file=sys.stderr)
Expand Down
45 changes: 43 additions & 2 deletions src/counter_risk/normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,49 @@ def normalize_counterparty(name: str) -> str:
return resolve_counterparty(name).canonical_name


def normalize_counterparty_with_source(
name: str,
*,
registry_path: str | Path = Path("config/name_registry.yml"),
) -> NameResolution:
"""Normalize a counterparty name and return full mapping metadata.

The returned :class:`NameResolution` includes ``source`` indicating where
the mapping came from:
- ``"registry"`` when matched via configured name registry entries/aliases.
- ``"fallback"`` when matched via built-in fallback mappings.
- ``"unmapped"`` when no mapping is found and canonicalized input is used.
"""

return resolve_counterparty(name, registry_path=registry_path)


def resolve_clearing_house(
name: str,
*,
registry_path: str | Path = Path("config/name_registry.yml"),
) -> NameResolution:
"""Resolve clearing house name with registry-first semantics.

Clearing-house source attribution is binary for reconciliation reporting:
registry hits are labeled ``"registry"``, and all non-registry paths are
labeled ``"fallback"`` (including identity/no-op normalization).
"""

normalized = canonicalize_name(name)
alias_lookup = _load_alias_lookup(str(Path(registry_path).resolve()))
registry_match = alias_lookup.get(normalized.casefold())
if registry_match is not None:
return NameResolution(raw_name=name, canonical_name=registry_match, source="registry")

fallback_match = _CLEARING_HOUSE_FALLBACK_MAPPINGS.get(normalized)
if fallback_match is not None:
return NameResolution(raw_name=name, canonical_name=fallback_match, source="fallback")

return NameResolution(raw_name=name, canonical_name=normalized, source="fallback")


def normalize_clearing_house(name: str) -> str:
"""Normalize a clearing house name to the canonical historical workbook label."""

normalized = _normalize_whitespace(name)
return _CLEARING_HOUSE_FALLBACK_MAPPINGS.get(normalized, normalized)
return resolve_clearing_house(name).canonical_name
5 changes: 5 additions & 0 deletions src/counter_risk/reports/mapping_diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,14 @@ def _iter_input_names(input_sources: Mapping[str, Any]) -> Iterable[str]:
def generate_mapping_diff_report(
registry_path: str | Path,
input_sources: Mapping[str, Any],
*,
output_format: str = "text",
) -> str:
"""Generate a deterministic mapping diff report."""

if output_format != "text":
raise ValueError(f"Unsupported output format: {output_format}")

# Load once so missing/unreadable/invalid registry is treated as fatal for report generation.
load_name_registry(registry_path)

Expand Down
4 changes: 4 additions & 0 deletions tests/fixtures/fallback_mapped_names.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
raw_name
Societe Generale
Citigroup
Bank of America, NA
5 changes: 5 additions & 0 deletions tests/fixtures/name_registry_after.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,8 @@ entries:
aliases:
- Soc Gen Inc
- Societe Generale
- canonical_key: jp_morgan_chase
display_name: JPMorgan Chase
aliases:
- J.P. Morgan
- JP Morgan Chase
3 changes: 3 additions & 0 deletions tests/fixtures/unmapped_names.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
raw_name
Unknown House
Zeta Broker
120 changes: 120 additions & 0 deletions tests/test_mapping_diff_report_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

from __future__ import annotations

import csv
import os
import stat
import subprocess
import sys
from pathlib import Path

from counter_risk.cli import mapping_diff_report


def _cli_cmd() -> list[str]:
return [sys.executable, "-m", "counter_risk.cli.mapping_diff_report"]
Expand All @@ -22,6 +25,13 @@ def _cli_env() -> dict[str, str]:
return env


def _load_fixture_names(filename: str) -> list[str]:
fixture_path = Path("tests/fixtures") / filename
with fixture_path.open(newline="", encoding="utf-8") as fixture_file:
reader = csv.DictReader(fixture_file)
return [row["raw_name"] for row in reader]


def test_mapping_diff_report_help_exits_zero() -> None:
result = subprocess.run(
[*_cli_cmd(), "--help"],
Expand All @@ -34,6 +44,20 @@ def test_mapping_diff_report_help_exits_zero() -> None:
assert "mapping_diff_report" in result.stdout


def test_mapping_diff_report_with_repo_registry_exits_zero() -> None:
result = subprocess.run(
[*_cli_cmd(), "--registry", "config/name_registry.yml"],
check=False,
capture_output=True,
text=True,
env=_cli_env(),
)
assert result.returncode == 0
assert "UNMAPPED" in result.stdout
assert "FALLBACK_MAPPED" in result.stdout
assert "SUGGESTIONS" in result.stdout


def test_mapping_diff_report_missing_registry_exits_nonzero(tmp_path: Path) -> None:
missing_registry = tmp_path / "missing_registry.yml"
result = subprocess.run(
Expand Down Expand Up @@ -119,3 +143,99 @@ def test_mapping_diff_report_deterministic_sections(tmp_path: Path) -> None:
assert "UNMAPPED\nUnknown House\n" in first.stdout
assert "FALLBACK_MAPPED\nSociete Generale -> Soc Gen\n" in first.stdout
assert "SUGGESTIONS\nUnknown House -> Unknown House\n" in first.stdout


def test_mapping_diff_report_with_fixture_inputs_contains_required_sections() -> None:
fallback_names = _load_fixture_names("fallback_mapped_names.csv")
unmapped_names = _load_fixture_names("unmapped_names.csv")

args: list[str] = [*_cli_cmd(), "--registry", "config/name_registry.yml"]
for name in fallback_names + unmapped_names:
args.extend(["--normalization-name", name])
for name in unmapped_names:
args.extend(["--reconciliation-name", name])

result = subprocess.run(args, check=False, capture_output=True, text=True, env=_cli_env())

assert result.returncode == 0
assert "UNMAPPED" in result.stdout
assert "FALLBACK_MAPPED" in result.stdout
assert "SUGGESTIONS" in result.stdout


def test_mapping_diff_report_forwards_registry_path_parameter(
tmp_path: Path,
monkeypatch,
) -> None:
captured_call: dict[str, object] = {}

def _fake_generate_mapping_diff_report(
registry_path: Path,
input_sources: dict[str, list[str]],
*,
output_format: str = "text",
) -> str:
captured_call["registry_path"] = registry_path
captured_call["input_sources"] = input_sources
captured_call["output_format"] = output_format
return "UNMAPPED\n\nFALLBACK_MAPPED\n\nSUGGESTIONS\n"

monkeypatch.setattr(
mapping_diff_report,
"generate_mapping_diff_report",
_fake_generate_mapping_diff_report,
)
registry_path = tmp_path / "name_registry.yml"
registry_path.write_text("schema_version: 1\nentries: []\n", encoding="utf-8")

exit_code = mapping_diff_report.main(["--registry", str(registry_path)])

assert exit_code == 0
assert captured_call["registry_path"] == registry_path
assert captured_call["input_sources"] == {"normalization": [], "reconciliation": []}
assert captured_call["output_format"] == "text"


def test_mapping_diff_report_forwards_output_format_parameter(
tmp_path: Path,
monkeypatch,
) -> None:
captured_call: dict[str, object] = {}

def _fake_generate_mapping_diff_report(
registry_path: Path,
input_sources: dict[str, list[str]],
*,
output_format: str = "text",
) -> str:
captured_call["registry_path"] = registry_path
captured_call["input_sources"] = input_sources
captured_call["output_format"] = output_format
return "UNMAPPED\n\nFALLBACK_MAPPED\n\nSUGGESTIONS\n"

monkeypatch.setattr(
mapping_diff_report,
"generate_mapping_diff_report",
_fake_generate_mapping_diff_report,
)
registry_path = tmp_path / "name_registry.yml"
registry_path.write_text("schema_version: 1\nentries: []\n", encoding="utf-8")

exit_code = mapping_diff_report.main(
[
"--registry",
str(registry_path),
"--output-format",
"text",
"--normalization-name",
"Societe Generale",
]
)

assert exit_code == 0
assert captured_call["registry_path"] == registry_path
assert captured_call["input_sources"] == {
"normalization": ["Societe Generale"],
"reconciliation": [],
}
assert captured_call["output_format"] == "text"
Loading
Loading