Repository navigation
feat: unify operator notification sections #779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
2c81caf
feat: unify operator notification sections
Pigbibi d1ba8f0
fix: reserve required diagnostics before optional context
Pigbibi fb8e687
fix: explain preserved execution block causes
Pigbibi a7166cf
fix: preserve receipt health and fitting notification evidence
Pigbibi File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| # Unified operator notification format | ||
|
|
||
| This display contract extends [routine delivery](routine-daily-digest.md), | ||
| [notification routing](notification-routing-policy.md) and the existing | ||
| `NotificationEvent`, digest candidate and execution-receipt contracts. It does | ||
| not replace their schemas, classification, accounting or delivery policy. | ||
|
|
||
| Every strategy-risk alert, daily trading summary, run-guard alert, | ||
| execution-receipt check and candidate-confirmation warning uses one locale and | ||
| five sections, in this order. `notifications.presentation` owns the section | ||
| names and the pure renderer; QRS consumes a byte-verified snapshot so its digest | ||
| image does not need another runtime dependency. | ||
|
|
||
| | Section | Existing facts and display rule | | ||
| | --- | --- | | ||
| | Result / 结果 | Caller-proven outcome or review requirement. Keep `reason_code` with its existing localized explanation. A guard failure is not a broker outcome. | | ||
| | Scope / 范围 | Supplied `occurred_at`, `observed_at`, `due_at`, lookback or business window. A missing time is unknown. Never substitute render time. | | ||
| | Evidence / 证据 | Actual run link, risk observations, receipt categories, `order_count`, `fill_count`, known state and per-run diagnostics. Urgent diagnostics precede optional snapshots. | | ||
| | Coverage / 覆盖 | The scope actually inspected, expected/covered target counts and source status. Daily reports always name IBKR, Schwab, Firstrade, LongBridge, Binance and management console. | | ||
| | Next step / 下一步 | Review the source run, receipt and missing sources when evidence needs attention; retain execution restrictions. A verified routine window needs no action. Existing risk-specific next steps remain more specific. | | ||
|
|
||
| ## Field semantics | ||
|
|
||
| - Keep `platform_id`, `opaque_account_uid`, `target_id`, `strategy_profile` and | ||
| `source_run_id` as existing protected identity fields. Never derive missing | ||
| identities, run links or account names from each other. | ||
| - `order_count` and `fill_count` retain their independent known/unknown status. | ||
| Zero requires affirmative count evidence. Accepted receipts, inspected report | ||
| counts, skipped reports, workflow success and explicit non-execution do not | ||
| create zero orders or fills. Missing category counts remain unknown. | ||
| - Whole-window totals cover only the existing bound targets and require their | ||
| existing complete evidence gate. No data connection is shown as unknown; | ||
| an explicitly unbound source is not included, with run state unknown. | ||
| - `actually_ran=false` remains non-execution evidence. Unknown, missing, stale, | ||
| invalid and read failure states remain distinct in existing diagnostics. | ||
| - A candidate warning establishes unconfirmed persistence only. It does not | ||
| establish a failed trade, successful daily inclusion or delivery receipt. | ||
| - Preserve the producer's window. QRS consolidated display uses its existing | ||
| half-open `[D 06:00, D+1 06:00)` Singapore window. Formatters do not reinterpret | ||
| legacy date contracts or move observations across windows. | ||
| - Links must be caller-supplied HTTPS URLs. Drop query and fragment; unavailable | ||
| or invalid links are explicitly unavailable. Private storage URIs, account | ||
| identifiers and raw exception payloads are not link fallbacks. | ||
|
|
||
| ## Compatibility and rollout | ||
|
|
||
| Existing QPK formatter signatures remain compatible. Risk, guard and receipt | ||
| message copy changes to these sections when consumers adopt this revision. | ||
| Healthy heartbeat routing and its existing renderer remain unchanged. Daily | ||
| idle copy now says “No trades in verified targets” inside a complete report, | ||
| not “No trades across all accounts today”. Unknown unbound sources still appear | ||
| in coverage. This is a deliberate display change, not a new data connection. | ||
|
|
||
| The renderer reserves result, scope, coverage and next step before truncating | ||
| optional evidence. It reports omitted evidence explicitly. Normal production | ||
| uses the existing 3900 UTF-16-unit single-message budget. An artificial budget | ||
| that cannot hold the mandatory fields raises a fixed error instead of silently | ||
| omitting coverage. No extra send is created. | ||
|
|
||
| Downstream source adoption must be coordinated with platform fault fixes. | ||
| QPK consumers remain at their current pins until a separately reviewed adoption; | ||
| QRS may snapshot only the pure presentation module with recorded source bytes. | ||
| Source PRs do not change runtime flags, schedules, recipient/date reservations, | ||
| receipt acceptance, deployment or execution authority. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| """Shared display sections; no execution, routing, receipt or transport policy.""" | ||
| from __future__ import annotations | ||
|
|
||
| from collections.abc import Sequence | ||
| from urllib.parse import urlsplit, urlunsplit | ||
|
|
||
| SECTION_NAMES = { | ||
| "zh": ("结果", "范围", "证据", "覆盖", "下一步"), | ||
| "en": ("Result", "Scope", "Evidence", "Coverage", "Next step"), | ||
| } | ||
|
|
||
|
|
||
| def safe_run_url(value: object) -> str | None: | ||
| """Use only caller-supplied HTTPS links, dropping query and fragment.""" | ||
| if not isinstance(value, str) or any(c.isspace() or ord(c) < 32 for c in value): | ||
| return None | ||
| try: | ||
| parsed = urlsplit(value) | ||
| if parsed.scheme == "https" and parsed.hostname and not parsed.username and not parsed.password: | ||
| return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", "")) | ||
| except ValueError: | ||
| pass | ||
| return None | ||
|
|
||
|
|
||
| def next_step_text(locale: str, *, needs_review: bool) -> str: | ||
| if locale == "zh": | ||
| return ("核对该次运行、回执和缺失来源;保持现有执行限制。" if needs_review else | ||
| "无需操作;等待下一统计窗口。") | ||
| return ("Review the source run, receipt and missing sources; retain existing execution restrictions." | ||
| if needs_review else "No action required; await the next reporting window.") | ||
|
|
||
|
|
||
| def render_operator_message(*, title: str, locale: str, result: Sequence[str], | ||
| scope: Sequence[str], evidence: Sequence[str], coverage: Sequence[str], | ||
| next_step: Sequence[str], max_units: int | None = None, | ||
| required_evidence: Sequence[str] = ()) -> str: | ||
| """Keep result, scope, coverage and next step before optional evidence. | ||
|
|
||
| Truncation is explicit and cannot remove a section or imply full coverage. | ||
| Callers put urgent evidence before optional observations. Financial facts | ||
| must already have been validated by the producer; this helper infers none. | ||
| """ | ||
| names = SECTION_NAMES["zh" if locale == "zh" else "en"] | ||
| unknown = "未知" if locale == "zh" else "Unknown" | ||
| groups = [[str(line) for line in group if line] or [unknown] | ||
| for group in (result, scope, evidence, coverage, next_step)] | ||
|
|
||
| def assemble(): | ||
| lines = [title] | ||
| for name, group in zip(names, groups): | ||
| lines.extend([f"【{name}】" if locale == "zh" else f"[{name}]", *group]) | ||
| return "\n".join(lines) | ||
|
|
||
| if max_units is None: | ||
| return assemble() | ||
| if max_units < 256: | ||
| raise ValueError("notification_budget_too_small") | ||
| omitted = 0 | ||
| original = [line for line in groups[2] if line not in required_evidence] | ||
| required = list(dict.fromkeys(required_evidence)) | ||
| groups[2] = required + original | ||
| complete = assemble() | ||
| if len(complete.encode("utf-16-le")) // 2 <= max_units: | ||
| return complete | ||
| suffix = "部分证据已省略;请查看运行记录。" if locale == "zh" else "Some evidence omitted; inspect the source run." | ||
| groups[2] = required + [suffix] | ||
| if len(assemble().encode("utf-16-le")) // 2 > max_units: | ||
| raise ValueError("notification_required_fields_exceed_budget") | ||
| groups[2] = required.copy() | ||
| for line in original: | ||
| groups[2].append(line) | ||
| groups[2].append(suffix) | ||
| fits = len(assemble().encode("utf-16-le")) // 2 <= max_units | ||
| groups[2].pop() | ||
| if not fits: | ||
| groups[2].pop() | ||
|
Pigbibi marked this conversation as resolved.
Resolved
|
||
| omitted += 1 | ||
| if omitted: | ||
| groups[2].append(suffix) | ||
| if not groups[2]: | ||
| groups[2] = [unknown] | ||
| return assemble() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.