MH-4b-3 ProposalService::createRejectCommand — Plan-Review

Projekt: Steve-TradingBot · Phase: RECON-MH-4b-3 · Author: claude-opus-4-7[1m]
Generated: 2026-05-12 20:51 UTC · master HEAD: be9cab7 (MH-4b-2 closed)
Status: NO CODE Plan-Review only — Operator-GO erforderlich vor MH-4b-3 Code-Phase


1 — Live-State Verification

CheckSollIstOK
master HEADbe9cab7be9cab7 mh-4b-2: add proposal approve command service
git statuscleanempty output
Bot in-container PID2998429984 python3 main.py --paper (unverändert seit MH-3a)
Worker Host PID(egal)2486135 (vorher 338185 — Watchdog-Respawn 22:04 UTC, nicht durch MH-4b-2 verursacht)
cmd 13cancelled13 | apply_baseline_holdings | cancelled
managed_proposals rows00
managed_assets_history rows00
BINANCE_TESTNETtrueBINANCE_TESTNET=true
runtime_config.jsonabsentnot present
baseline_holdings.jsonabsentnot present
managed_state.jsonabsentnot present
Tracebacks last 200 lines00 matches

Worker-PID-Hinweis: Container clawbot-worker ist restart: unless-stopped in docker-compose. Wechsel auf 2486135 deutet auf Watchdog-Respawn (Healthcheck-Probe-Fail). Funktional kein Problem — Worker pollt weiter. Nicht MH-4b-3-blocking; durabel BACKLOG für separates Worker-Healthcheck-Hardening.

2 — Konsumierte Artefakte

KomponenteStatusKonsumiert in MH-4b-3
MH-4a: managed_proposals table + ManagedProposalState enum + Model83dafcaREAD-only auf managed_proposals für state-validation
MH-4b-1: createRequestCommand3fd7846Service-Skelett unverändert; Validation-Helpers wiederverwendet
MH-4b-2: createApproveCommand + 5 Exceptions + Hard-Confirm-Builder + Parity-Pinsbe9cab7massive Wiederverwendung: 4 Exceptions reusable, validate-Helpers reusable, idempotency-Pattern symmetrisch
MH-1: reject_managed_proposal CommandType-Stubea11637$registry->get('reject_managed_proposal') Existence-Check
Worker-Handler _handle_reject_managed_proposalMH-6nicht in MH-4b-3
Filament WizardMH-5nicht in MH-4b-3
ManagedStateService (pause/resume/release)MH-4cnicht in MH-4b-3

Pattern-Source: ProposalService::createApproveCommand (MH-4b-2). MH-4b-3 ist nahezu spiegelsymmetrisch — gleicher Idempotency-Key, gleiche state-allowlist, kürzerer Validation-Stack.

3 — MH-4b-3 Scope-Definition

Welche States dürfen rejected werden?

Erlaubt:

Block-Verhalten (jeweils mit Exception):

StateReasonException
frozenKein Proposal existiert zum AblehnenProposalAlreadyDecidedException
proposal_pendingWorker hat Engine noch nicht laufen lassenProposalNotCachedException (gleiches Verhalten wie approve)
proposal_rejectedbereits abgelehntProposalAlreadyDecidedException
proposal_abortedEngine selbst hat abortedProposalAlreadyDecidedException
managed_active / managed_paused / managed_drift_alert / managed_released / exit_executedbereits promotedProposalAlreadyDecidedException

Wichtig: Bei reject ist proposal_json/sha256/file_path = NULL theoretisch kein harter Reject-Blocker. ABER: Hard-Confirm braucht proposal_sha256 für die sha8-Komponente — ohne sha8 ist kein Hard-Confirm möglich. → Reject ohne Worker-Cache ist konzeptionell auch geblockt mit ProposalNotCachedException. Symmetrie zu approve für Operator-Mental-Model.

Reject-Payload-Schema

{
  "environment":     "testnet",
  "proposal_id":     "<pid>",
  "asset":           "<asset>",
  "reason":          "<operator-text>",
  "hard_confirm":    "<asset>:reject:<sha8>",
  "proposal_sha256": "sha256:abc...",
  "decided_by":      <userId>,
  "expires_at":      "<iso8601-z>"
}

Felder die approve hat aber reject NICHT braucht:

Variante A — Minimal-Reject (7 Felder oben, ohne confidence_score / file_path)
Variante B — Symmetrisch-Reject (plus confidence_score_at_decide + proposal_file_path für identische Audit-Trail-Shape zu approve)

Empfehlung Variante B — Reject und Approve sollten identische Forensik-Trail-Struktur haben. Hilft Filament-Wizard + Worker-Handler MH-6 mit gleichem Payload-Schema arbeiten.

Hard-Confirm-Format

Operator-Frage: Braucht Reject einen Hard-Confirm?

Empfehlung JA, mit Format: <asset>:reject:<sha8>

Begründung:

  1. Symmetrie zu approve <asset>:<variant>:<sha8> — derselbe Wizard-Pattern
  2. Verhindert versehentliches Reject des falschen Proposals (z.B. nach Browser-Tab-Wechsel oder stale URL)
  3. sha8 bindet an konkrete Proposal-Version — wenn Worker einen neuen Proposal generiert hat, ist alter Reject-Versuch ungültig
  4. "reject" als String-Literal in der Mitte ist intentional menschenlesbar — Operator tippt bewusst "reject"

Build-Helper: ProposalService::buildRejectHardConfirmString($asset, $proposalSha256): string

Reason-Regeln

Pflicht: ja. Operator muss begründen, warum er ablehnt — Audit-Trail-Anforderung (G-DR-5 audit-heavy).

Validierung:

Default: KEIN default. Reason muss explizit übergeben werden.

Idempotency-Verhalten

Existing CommandVerhalten
reject_managed_proposal (any status)return existing, kein 2nd audit
approve_managed_proposal mit gleichem KeyProposalAlreadyDecidedException(detail='already_approved')
Anderer command_type mit gleichem KeyProposalAlreadyDecidedException(detail='idempotency_key_conflict') defensive STOP
Keine existing CommandINSERT new + audit

Symmetrisch zu MH-4b-2 approve-Verhalten, nur mit gespiegelten Rollen.

Service-API

ProposalService::createRejectCommand(
    string $proposalId,    // ^[a-zA-Z0-9_-]{1,128}$
    int    $userId,        // > 0
    string $reason,        // non-empty, trim, max 256 chars
    string $hardConfirm,   // exact "<asset>:reject:<sha8>"
): Command

ProposalService::buildRejectHardConfirmString(
    string $asset,
    string $proposalSha256,
): string  // returns "<asset>:reject:<sha8>"

4 — Konfliktcheck Sonder-Anforderungen

AnforderungMH-4b-3-TouchVerdict
Idempotency mh:decide:<pid> sharedPattern symmetrisch zu approvekonform
Existing reject → return existingkein 2nd auditkonform
Existing approve → ProposalAlreadyDecidedExceptiondetail='already_approved'konform
Other type → defensive STOPsymmetrisch zu approvekonform
audit_event prefix managed.*managed.asset_reject_requestedkonform
Transaction rollbackDB::transaction-Pattern wiederverwendetkonform
Hard-Confirm <asset>:reject:<sha8>neuer Helperkonform
Mainnet/testnet enforcementenvironment='testnet' hardcodedkonform

5 — Exception-Wiederverwendung

ExceptionReuse aus MH-4b-2?Neue Variante?
ProposalNotFoundExceptionReusenein
ProposalAlreadyDecidedExceptionReusenein
ProposalNotCachedExceptionReusenein
HardConfirmMismatchExceptionReusenein
ConfidenceOverrideRequiredExceptionnicht relevant für rejectnein
InvalidCommandPayloadException (existing)Reuse für reason-Validierungnein

Ergebnis: 0 neue Exception-Klassen. Alle bestehenden 5 (4 davon relevant) sind reusable.

6 — Service-Implementation (Skizze)

Validation-Stack (in Reihenfolge)

1. validateProposalId(...)      → InvalidCommandPayloadException
2. validateUserId(...)          → InvalidCommandPayloadException
3. validateReason(...)          → InvalidCommandPayloadException  (NEU: trim + length cap)
4. loadProposal(...)            → ProposalNotFoundException
5. assertRejectable(...)        → ProposalAlreadyDecidedException OR ProposalNotCachedException
   (== assertApprovable Logik — gleiche state-allowlist + cache-fields)
6. expectedHardConfirm =
   buildRejectHardConfirmString($proposal->asset, $proposal->proposal_sha256)
7. hardConfirm exact match      → HardConfirmMismatchException
8. commandRegistry->get('reject_managed_proposal')
9. DB::transaction:
   a. Idempotency-Pre-Check + command_type discrimination
      → ProposalAlreadyDecidedException OR return existing
   b. Command::create(...)
   c. writeAudit(...) — event_type='managed.asset_reject_requested'

Hauptunterschied zu approve: keine Confidence-Validation, keine variant-Validierung, keine overrides-Validierung. Stattdessen reason-Validierung.

assertRejectable vs assertApprovable

Empfehlung: Refactor assertApprovableassertDecidable(ManagedProposal $p, string $cmdToken) mit $cmdToken als Error-Message-Prefix. createApproveCommand und createRejectCommand rufen beide auf mit ihrem jeweiligen Token.

7 — Boundary-Contract

TabuPin
managed_proposals INSERT/UPDATEbereits MH-4b-1/2-pinned via Source-Grep
managed_assets_history Writebereits pinned
File-IObereits pinned
Subprocessbereits pinned
ProposalWriter/ProposalReader (Python)architektonisch unmöglich
command_worker.py Touchgit-diff
Bot-Code Touchgit-diff
Filament-UIMH-5
Mainnetenvironment='testnet' hardcoded

8 — Test-Plan

Test-KlasseAnzahlInhalt
ProposalServiceCreateRejectTest (happy)5INSERT commands + audit · payload env=testnet · alle required keys · audit prefix managed.asset_reject_requested · hardConfirm exact match
ProposalServiceCreateRejectStateTests9reject when state=frozen / proposal_pending (→ NotCached) / proposal_rejected / proposal_aborted / managed_active / managed_paused / managed_drift_alert / managed_released / exit_executed
ProposalServiceCreateRejectNotFoundTest2proposal_id not in DB · empty managed_proposals table
ProposalServiceCreateRejectCacheTests4proposal_json/sha256/file_path NULL (alle 3 + kombiniert)
ProposalServiceCreateRejectDecidedTest1decided_at IS NOT NULL → ProposalAlreadyDecidedException
ProposalServiceCreateRejectHardConfirmTests6exact match · asset mismatch · sha8 mismatch · case mismatch · empty string · missing 'reject' segment · missing sha8 segment
ProposalServiceCreateRejectReasonTests6non-empty required · whitespace-only rejected · max 256 chars · exactly 256 chars accepted · 257 chars rejected · free-form chars/unicode accepted
ProposalServiceCreateRejectIdempotencyTests4reject-twice returns existing (no 2nd audit) · reject after existing approve → ProposalAlreadyDecidedException · idempotency-key format pinned · other-command-type conflict
ProposalServiceCreateRejectAtomicityTest1failed audit insert rolls back command
RejectHardConfirmBuilderTest3builds <asset>:reject:<sha8> · case preserved · works with raw hex
ProposalServiceCreateRejectBoundaryTests3no managed_proposals UPDATE during reject · no managed_assets_history INSERT · source-grep symmetric to approve
ProposalServiceCreateRejectUserIdTests2zero rejected · negative rejected
Total~46 Tests(kleiner als approve weil keine variant/overrides/confidence)

9 — Stop-Regeln MH-4b-3

IDStop wenn…
MH-4b-3-SR-1Service UPDATE auf managed_proposals
MH-4b-3-SR-2Service INSERT in managed_assets_history
MH-4b-3-SR-3Service file IO oder subprocess
MH-4b-3-SR-4Service akzeptiert environment != 'testnet'
MH-4b-3-SR-5Service ruft Worker / Engine / Writer / Reader
MH-4b-3-SR-6Service-Methode NICHT in DB::transaction
MH-4b-3-SR-7reason NULL / empty / whitespace-only akzeptiert
MH-4b-3-SR-8reason > 256 chars akzeptiert
MH-4b-3-SR-9Hard-Confirm-Validierung ist case-insensitive
MH-4b-3-SR-10Audit-Event-Prefix ist nicht managed.*
MH-4b-3-SR-11Shared-Key approve-Command wird stumm zurückgegeben statt ProposalAlreadyDecidedException
MH-4b-3-SR-12Reject akzeptiert variant / overrides / confidence_override Parameter (nicht zur API gehören)

10 — Backup / Migration / Restart-Bedarf

AktionPflicht?
pg_dump GUI-DBNEIN — keine Migration
State-Files snapshotNEIN
Memory-SicherungNEIN
DB MigrationNEIN
Bot-RestartNEIN
Worker-RestartNEIN
docker cpNEIN
GUI Cache-ClearNEIN (per Operator-Korrektur seit MH-4b-1)

MH-4b-3 = Reine Code+Test+Commit Phase.

11 — Risiken-Übersicht

#RisikoSeverityMitigation
R1reject-without-Worker-cache desired aber blockiertLOWCache-Pflicht ist symmetric mit approve; Operator-Workflow: wait+retry oder Worker --once manuell
R2Hard-Confirm-Format-Verwechslung mit approveLOWWizard MH-5 baut den expected-string explizit; Operator copy-pastet aus UI
R3reason free-form erlaubt XSS-Vektoren in späterer Filament-AnzeigeLOWFilament 3 escapes by default; AuditMetadataScrubber filtered sensitive tokens
R4reason zu lang verschwendet DB-StorageLOW256 chars cap; pragmatischer Wert
R5Reuse of assertApprovable ist semantisch reject-fremdLOWRefactor zu assertDecidable($p, $cmdToken) löst
R6approve-after-reject Race im WizardMEDIUMIdempotency-Pre-Check mit command_type-Discrimination; Test pinnt
R7DB::transaction rollback bei audit failureLOWPattern bewährt
R8reason mit Newlines / Unicode bricht Audit-Metadata-ScrubberLOWAuditMetadataScrubber existing; Reuse + Test
R9Reject-Hard-Confirm-Builder-Drift mit approve-BuilderLOWbeide getrennte static methods; Parity-Test pinnt buildRejectHardConfirmString

12 — Kleinste sichere Code-Phase + GO/NO-GO

Subschnitt-Optionen

VarianteInhaltLOCTestsRisk
MH-4b-3-monolithic ← empfohlencreateRejectCommand + buildRejectHardConfirmString + assertDecidable-Refactor + Tests~600~46LOW
MH-4b-3-aNUR createRejectCommand happy-path + 1 Test-Klasse~200~10LOW
MH-4b-3-b (folgt)State + Cache + Decided + Hard-Confirm Tests~250~22LOW
MH-4b-3-c (folgt)Reason + Idempotency + Atomicity + Boundary~150~14LOW

Empfehlung: MH-4b-3-monolithic

Begründung:

  1. Massive Code-Wiederverwendung aus MH-4b-2 — kein neuer Pattern, kein neuer Discovery-Aufwand
  2. 0 neue Exception-Klassen — reduziert Surface gegenüber MH-4b-2
  3. Symmetrie zu approve — Test-Skelett ist mirror; reject ohne approve würde Asymmetrie hinterlassen
  4. assertDecidable Refactor ist klein + risk-frei + reuse-fördernd
  5. Validation-Stack ist kürzer als approve (kein variant/overrides/confidence) → kleinere Komplexität
  6. Rollback-Cost minimalgit revert + 0 DB-Rows

Pre-Implementation-Q an Operator

  1. MH-4b-3-monolithic oder Sub-Cut? ← Empfehlung monolithisch
  2. Hard-Confirm-Format <asset>:reject:<sha8> bestätigen? ← Empfehlung JA (symmetrisch zu approve)
  3. Reason max-Length 256 chars ok? ← Empfehlung 256 (pragmatisch)
  4. Payload-Variante A (minimal) oder B (symmetric mit confidence_score + file_path)?Empfehlung B (Forensik-Symmetrie)
  5. assertApprovable umbenennen zu assertDecidable mit $cmdToken-Parameter? ← Empfehlung JA (saubere Reuse, kein Code-Duplikat)
  6. Reuse aller 4 relevanten Exceptions ohne neue? ← Empfehlung JA bestätigen (0 neue Exception-Klassen in MH-4b-3)
  7. audit_event_type = managed.asset_reject_requested? ← Empfehlung JA

Scope-Größe + Risiko-Bewertung MH-4b-3-monolithic

AspektBewertung
LOC~600 (Service-Erweiterung + ~500 Tests)
Tests~46
KomplexitätLOW — massive Reuse aus MH-4b-2
Blast-RadiusNULL — INSERT-only auf commands + audit_events
Roll-Back-Costminimal: git revert + 0 neue DB-Rows
Dependencies-KlärungQ-2/Q-3/Q-4/Q-5/Q-7 Operator-Approval
Backup-PflichtNEIN
Migration-PflichtNEIN
Restart-PflichtNEIN
Mainnet-TouchNEIN
docker cpNEIN
Worker-PID-Wechsel-AuswirkungKEINE (Service-Code-only)

STOP vor Implementierung. Erwarte Operator-GO mit Subschnitt-Wahl + Q1-Q7-Approval, insbesondere Hard-Confirm-Format-Pin + Payload-Variante A/B + assertDecidable-Refactor.

© Steve-TradingBot · RECON-MH-4b-3 · Plan-Review (no-code phase)