feat(s52): export Notifier public contract + sync extending.md B11

This commit is contained in:
Kjell Tore Guttormsen 2026-07-16 19:53:57 +02:00
commit b84f4d46bb
3 changed files with 48 additions and 2 deletions

View file

@ -117,8 +117,15 @@ territory for a deployer, with the seam named:
layers (`write_verdict`, `promote_verdict`) are last-write-wins per file. The full taxonomy
(rejection categories + a rule for conflicting expert verdicts) is deferred until real experts
produce conflicting verdicts.
- **B11 — expert notification.** `run_project(notify=...)` is a stub seam (`run.py`): pass any
callable; no delivery mechanism (e-mail/Teams/webhook) ships with the core.
- **B11 — expert notification.** `run_project(notify=...)` remains a plain-callable seam
(`run.py` auto-wires no default notifier), but the core now ships the declared `Notifier`
contract (`notify.py`, exported from the package top) with three implementations:
`ConsoleNotifier`, `FileNotifier` (byte-deterministic JSONL), and `WebhookNotifier` — plus a
fail-fast `build_notifier(config, *, allow_egress=...)` factory. The webhook is the ONLY egress
point and is fail-closed behind an explicit per-run `allow_egress=True` opt-in (a code kwarg,
never a config field — mirroring the ingest layer's `allow_network`). SSRF guards, HMAC
signing, and auth headers remain deployer-owned extension points on the injectable
`WebhookPost` transport seam.
- **U12 — checkpointing / crash-survival of a run.** A run either completes or is re-run; the
async verdict inbox (step 7) is the resumable boundary, not intra-run state.
- **U14 — OpenTelemetry / observability.** Provenance stamping is the audit trail the core

View file

@ -8,6 +8,16 @@ from portfolio_optimiser.ledger import (
SavingsLedger,
realize,
)
from portfolio_optimiser.notify import (
ConsoleNotifier,
FileNotifier,
Notifier,
NotifierConfig,
NotifyError,
NotifyRefused,
WebhookNotifier,
build_notifier,
)
from portfolio_optimiser.run import (
GoalReached,
PortfolioResult,
@ -35,5 +45,14 @@ __all__ = [
"GoalConfig",
"load_goal_config",
"GoalReached",
# S5.2 varsling (B11): the declared Notifier contract + deliverable implementations
"Notifier",
"ConsoleNotifier",
"FileNotifier",
"WebhookNotifier",
"build_notifier",
"NotifierConfig",
"NotifyError",
"NotifyRefused",
"__version__",
]

View file

@ -129,3 +129,23 @@ def test_config_fail_fast() -> None:
notifier = build_notifier(webhook_cfg, allow_egress=True)
assert isinstance(notifier, WebhookNotifier)
def test_public_contract_exported() -> None:
"""The declared B11 contract is public authoring API: importable from the package top and
listed in ``__all__`` (the brief-permitted export path run.py's seam stays byte-intact)."""
import portfolio_optimiser
exported = (
"Notifier",
"ConsoleNotifier",
"FileNotifier",
"WebhookNotifier",
"build_notifier",
"NotifierConfig",
"NotifyError",
"NotifyRefused",
)
for name in exported:
assert name in portfolio_optimiser.__all__, f"{name} missing from __all__"
assert hasattr(portfolio_optimiser, name), f"{name} not importable from package top"