MH-4b ProposalService — Plan-Review

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


1 — Live-State Verification

CheckSollIstOK
master HEAD83dafca83dafca mh-4a: add managed_proposals + managed_assets_history tables
git statuscleanempty output
Bot in-container PID2998429984 python3 main.py --paper
Worker Host PID338185python3 -m trading.command_worker running
Worker-Daemonaliveclawbot-worker Up 18h (Healthcheck-Drift pre-existing)
cmd 13cancelled13 | apply_baseline_holdings | cancelled
BINANCE_TESTNETtrueBINANCE_TESTNET=true
runtime_config.jsonabsentnot present
baseline_holdings.jsonabsentnot present
managed_state.jsonabsentnot present
state/risk_proposals/absentnot present
managed_proposals tablelive, 0 rows0 rows
managed_assets_history tablelive, 0 rows0 rows
Tracebacks last 200 lines00 matches

2 — Konsumierte Artefakte (MH-1 → MH-4a)

KomponenteStatusVerfügbar für MH-4b
MH-1: 8 managed.* CommandTypes + Validators in CommandTypeRegistryea11637register_managed_proposal / approve_managed_proposal / reject_managed_proposal validators ready — Service nutzt $registry->get('...')
MH-2: ManagedStateReader + ProposalReader dormant50cc5c2Worker-only — Service touched NICHT
MH-3a: RiskProposalEngine dry-run26c8ab3Worker-only — Service touched NICHT
MH-3b: ProposalWriter immutable62a08f4Worker-only — Service touched NICHT
MH-4a: managed_proposals + managed_assets_history + ManagedProposalState Enum + Models83dafcadirekt konsumiert — Service READ-only auf managed_proposals für approve/reject Validation
ManagedStateService (pause/resume/release)MH-4cnicht in MH-4b Scope
Worker-Handler _handle_*MH-6nicht in MH-4b
Filament WizardMH-5nicht in MH-4b

Pattern-Source: gui/app/Services/Apply/Baseline/ApplyBaselineService.php (RECON-2.3, 600 LOC). Übernehmen:

3 — MH-4b Scope-Rekonstruktion

Service-Verantwortlichkeiten

ProposalService ist command-builder + DB-cache-validator only. Es:

Pipeline (MH-4b ↔ MH-6 Trennung)

Operator-Klick (MH-5 Wizard, future)
   ↓
ProposalService::createXxxCommand()                ← MH-4b
   ↓
DB::transaction:
   - INSERT commands (status='pending', idempotency_key='mh:...')
   - INSERT audit_events (managed.asset_*_requested)
   - return Command instance
   ↓
==================== Service boundary ====================
   ↓
Worker --once / Daemon                              ← MH-6
   ↓
_handle_request_managed_proposal:
   - call Engine.generate() → ProposalResult
   - call Writer.apply()    → ApplyResult
   - INSERT managed_proposals (proposal_id, state='risk_proposed',
       proposal_json=..., proposal_file_path=..., proposal_sha256=...,
       proposal_cached_at=now(), generated_at=..., expires_at=...)
   - INSERT managed_assets_history
   - emit audit_event managed.asset_proposal_generated

3 Service-Methoden im Detail

a) createRequestCommand(asset, userId, intent='operator_request')

b) createApproveCommand(proposalId, userId, variant, overrides=[], hardConfirm, confidenceOverride=false)

c) createRejectCommand(proposalId, userId, reason)

Operator-Frage-Klärungen

FrageAntwort
Wie wird proposal_json aus Datei in DB-Cache gespiegelt?NICHT in MH-4b. Worker (MH-6) liest on-disk risk_proposals/<id>.json und UPDATE managed_proposals SET proposal_json=... NACH Engine.generate() läuft. MH-4b Service touched proposal_json niemals (auch nicht READ — außer für approve/reject validation, dann reads from DB-cache nicht Datei).
Wie werden proposal_file_path / proposal_sha256 / proposal_cached_at gesetzt?NICHT in MH-4b. Worker (MH-6) füllt diese Spalten nach ProposalWriter.apply() aus ApplyResult.target_path / .sha256 / .written_at.
Wie wird proposal_id Regex validiert?Service hat private static helper validateProposalId(string $pid) mit regex /^[a-zA-Z0-9_-]{1,128}$/ (1:1 mirror MH-3b PROPOSAL_ID_REGEX). Aufgerufen in createApproveCommand und createRejectCommand. Nicht in createRequestCommand (proposal_id existiert dort noch nicht).
Welche State-Transitions sind erlaubt?Service erzeugt nur commands, keine State-Transitions. Worker (MH-6) führt Transitions aus mit ManagedStateGuard. Service prüft READ-only: Approve/Reject erfordern state==='risk_proposed' (Caller-Side Validation — Worker macht zusätzlich Guard-Check).
Wie werden approve/reject vorbereitet ohne Worker auszuführen?Service inserts commands.status='pending'. Worker-Daemon pollt commands und führt Handler aus, wenn Worker --once läuft oder Daemon pollt. Service kehrt nach DB::transaction zurück — Operator sieht Command instance + idempotency_key + UUID.
Wo endet MH-4b?Service writes commands+audit_events only. Service liest managed_proposals READ-only. Service ruft NIEMALS Worker.
Wo beginnt MH-4c?ManagedStateService (pause/resume/release). Selbe Pattern, andere CommandTypes.
Wo beginnt MH-5?Filament-Page + Wizard + Filament-Actions die ProposalService-Methoden aufrufen.
Wo beginnt MH-6?Worker-Handler _handle_request_managed_proposal etc. in command_worker.py. Liest Engine, ruft Writer, UPDATEs managed_proposals, INSERTs managed_assets_history.

4 — Konfliktcheck Sonder-Anforderungen

AnforderungMH-4b-TouchVerdict
JSON Bot-SoT bleibt führendService touched keine JSON-Datei. Liest nur DB-Cache (managed_proposals row).Hybrid C eingehalten
DB bleibt nur GUI-CacheService liest managed_proposals für Validation. Writes ausschließlich commands + audit_events.konform
Proposal-Dateien bleiben immutableService touched NICHT risk_proposals/<id>.json. ProposalWriter wird NICHT aus PHP aufgerufen.MH-3b Immutability intact
Kein ProposalWriter-Aufruf aus PHPService-Code importiert NICHT ProposalWriter. AST-pin: kein use App\Services\.*Writer|Engine. (PHP hat das nicht — Bot ist Python.)Architekturell unmöglich + Test-Pin
Kein managed_state.json writeService touched keine state files.konform
Kein baseline_holdings.json writeService touched keine state files.konform
Kein command_worker Touchcommand_worker.py bleibt unverändert.konform
Kein Bot-Wiringmain.py / live_trade.py / paper_trade.py / risk_manager.py bleiben unverändert.konform
Kein Filament WizardMH-5. Service ist Builder, nicht UI.konform
Kein MainnetService hard-restricts environment='testnet' in Payload-Core. CommandTypeRegistry-Validator prüft das zusätzlich.Defense-in-Depth

5 — Identifizierte Risiken

#RisikoSeverityMitigation
R1Approve auf Proposal mit proposal_json IS NULL — Worker hat Initial-INSERT noch nicht durchgeführtMEDIUMService::createApproveCommand prüft proposal_json IS NOT NULL vor INSERT. Throws ProposalNotCachedException sonst. Test simuliert die Race.
R2proposal_id-Regex-Drift PHP↔Bot — MH-3b Bot-Regex und PHP-Service-Regex könnten divergierenLOWPrivate const PROPOSAL_ID_REGEX = '/^[a-zA-Z0-9_-]{1,128}$/' im Service + Test-Pin (test_proposal_id_regex_parity_with_bot) der gegen hardcoded mirror der MH-3b regex prüft.
R3Confidence-Override-Bypass — Operator setzt confidenceOverride=true ohne reale NotwendigkeitLOWService prüft score < 0.50 UND benötigt explicit confidenceOverride=true. Audit-Metadata kennzeichnet confidence_override_used=true für Audit-Trail. Test: 3 Pfade (high-score happy, low-score-no-override exception, low-score-with-override happy + audit-flag).
R4Hard-Confirm-Bypass — User klickt approve mit falschem Confirm-StringLOWService exact-match-validates hardConfirm === "{$asset}:{$variant}" (case-sensitive). HardConfirmMismatchException. Test: 5 Pfade (mismatch case, mismatch asset, mismatch variant, lowercase mismatch, happy).
R5Approve+Reject Race (gleiche proposal_id)LOWShared idempotency-key mh:decide:<pid>. DB-Unique-Constraint auf commands.idempotency_key (existing). Erste schreibt → ok; zweite findet existing → returnt existing Command (kein 2nd INSERT). Test: simuliert Race.
R6Service insertet managed_proposals row prematurely (proposal_id noch nicht bekannt im createRequestCommand)MEDIUMArchitektur-Entscheidung MH-4b: Service INSERTs managed_proposals NICHT. Worker (MH-6) macht das nach Engine-Run wenn proposal_id existiert. Hierdurch entfällt die "tentative proposal_id"-Problematik. Plan-Review-Doc pinnt das.
R7Worker hat Initial-INSERT noch nicht — GUI zeigt nichtsLOWUX-Issue, kein Service-Bug. Filament (MH-5) zeigt "Proposal angefragt, Worker pending" status basierend auf commands.status='pending' ohne managed_proposals row. Erst nach Worker-Run erscheint Row.
R8AuditMetadataScrubber-BypassLOWService nutzt existing scrubber, Pattern aus ApplyBaselineService. Test pinnt.
R9DB::transaction-Rollback bei Audit-Insert-FailureLOWDB::transaction wirft automatic rollback bei Exception. Test: simuliert AuditEvent::create() failure → kein Command row übrig.
R10Idempotency-Key-Collision verschiedener OperatorsLOWcreateRequestCommand idempotency mh:propose:{asset}:{YmdHis} ist Operator-agnostic. Wenn zwei Operators denselben Asset in derselben Sekunde anfragen → second returns first's command (audit-trail erhalten). Akzeptable Semantik.

6 — Test-Plan

Test-KlasseAnzahlInhalt
ProposalServiceCreateRequestTest7happy path (commands + audit inserted) · asset regex validation · userId positive int · intent length cap · idempotency same-second collision · CommandTypeRegistry validator integration · environment=testnet hardcoded
ProposalServiceCreateApproveTest10happy path · proposalId regex · proposal-not-found · state-not-risk_proposed · already-decided · proposal_json-not-cached · hardConfirm mismatch (5 cases) · confidence-override-required · confidence-override-accepted (audit flag) · overrides whitelist (rejects strategy_group)
ProposalServiceCreateRejectTest6happy path · proposalId regex · proposal-not-found · state-not-risk_proposed · already-decided · reason non-empty
IdempotencyTests4request: same-second double-click returns existing · approve+reject share key · reject after approve returns approve's command · DB-Unique-Constraint blocks 2nd INSERT
MainnetGuardTest3Service payload always has environment='testnet' · Service rejects payload with environment='mainnet' explicit · CommandTypeRegistry validator blocks mainnet
AuditPrefixTest3All audit_events have managed. prefix · no baseline.* / runtime_config.* leakage · scrubber called for metadata
DbTransactionAtomicityTest3failed audit insert rolls back command · failed command insert rolls back nothing (atomicity preserved) · no orphan rows
ProposalIdRegexParityTest2PHP regex matches MH-3b Bot regex char-for-char · accept/reject cases match Bot tests
CustomExceptionTests4ProposalAlreadyDecidedException · HardConfirmMismatchException · ConfidenceOverrideRequiredException · ProposalNotCachedException (new for R1)
Total~42 Tests(etwas über Roadmap-Schätzung ~30)

7 — Betroffene Dateien (Final-Inventory)

Neue Files

DateiStatusKategorie
gui/app/Services/Managed/ProposalService.phpNEUBuilder Service
gui/app/Exceptions/ProposalAlreadyDecidedException.phpNEUCustom Exception
gui/app/Exceptions/HardConfirmMismatchException.phpNEUCustom Exception
gui/app/Exceptions/ConfidenceOverrideRequiredException.phpNEUCustom Exception
gui/app/Exceptions/ProposalNotCachedException.phpNEUCustom Exception (R1)
gui/tests/Feature/ProposalServiceCreateRequestTest.phpNEUTest
gui/tests/Feature/ProposalServiceCreateApproveTest.phpNEUTest
gui/tests/Feature/ProposalServiceCreateRejectTest.phpNEUTest
gui/tests/Feature/ProposalServiceIdempotencyTest.phpNEUTest
gui/tests/Feature/ProposalServiceMainnetGuardTest.phpNEUTest
gui/tests/Feature/ProposalServiceAuditPrefixTest.phpNEUTest
gui/tests/Feature/ProposalServiceAtomicityTest.phpNEUTest
gui/tests/Feature/ProposalIdRegexParityTest.phpNEUTest

Unverändert

Erwartete LOC: ~450 Service + ~120 Exceptions (5 × ~25) + ~900 Tests = ~1500 LOC total.

8 — Erlaubte vs. Verbotene Service-Methoden

ERLAUBT

VERBOTEN

9 — Stop-Regeln MH-4b

IDStop wenn…
MH-4b-SR-1Service schreibt nach risk_proposals/*.json / managed_state.json / baseline_holdings.json
MH-4b-SR-2Service INSERT auf managed_proposals oder managed_assets_history
MH-4b-SR-3Service UPDATE auf managed_proposals
MH-4b-SR-4Service akzeptiert environment != 'testnet'
MH-4b-SR-5Service ruft Command::update() oder triggert Worker (shell_exec, process etc.)
MH-4b-SR-6Service-Methode NICHT in DB::transaction
MH-4b-SR-7proposal_id Regex weicht von MH-3b ab (Parity-Test bricht)
MH-4b-SR-8hardConfirm Validierung ist case-insensitive
MH-4b-SR-9confidenceOverride=true ohne audit-metadata-flag
MH-4b-SR-10Audit-Event-Prefix ist nicht managed.*
MH-4b-SR-11proposal_json IS NULL Pfad in createApproveCommand fehlt
MH-4b-SR-12Service ruft Engine.generate() / Writer.apply() (Python-side — architektonisch unmöglich, aber Test-Pin)

10 — Backup / Migration / Restart-Bedarf

AktionPflicht?Begründung
pg_dump GUI-DBNEINkeine Migration; reine Service-Code-Phase
live_portfolio.json snapshotNEINkein State-Touch
.env snapshotNEINkeine Mutation
state/ snapshotNEINkein State-Touch
Memory-SicherungNEINClosure-Pin reicht
DB MigrationNEINMH-4a Schema bleibt unverändert
Bot-RestartNEINkein Bot-Code-Touch
Worker-RestartNEINcommand_worker.py unverändert
docker cpNEINPHP via Laravel Live-Volume mount
GUI-Container Cache-ClearJA (sicherheitshalber)php artisan config:clear && php artisan route:clear && php artisan view:clear nach Service-Files-Add

MH-4b = Reine Code+Test+Commit Phase. Bot komplett unbehelligt.

11 — Kleinste sichere Code-Phase + GO/NO-GO Empfehlung

Subschnitt-Optionen

VarianteInhaltLOCTestsRisk
MH-4b-monolithicProposalService (3 Methoden) + 5 Exceptions + 9 Test-Klassen~1500~42LOW-MEDIUM
MH-4b-1 ← empfohlenNUR createRequestCommand + 0 Exceptions + 4 Test-Klassen~250~15LOW
MH-4b-2 (folgt)createApproveCommand + 3 Exceptions + 3 Test-Klassen~600~17LOW-MEDIUM (HardConfirm + Confidence + R1-Race)
MH-4b-3 (folgt)createRejectCommand + 1 Exception + 2 Test-Klassen~250~8LOW
MH-4b-4 (folgt)Cross-cutting tests (atomicity / parity / audit-prefix)~150~9LOW

Empfehlung: MH-4b-1 — createRequestCommand zuerst

Begründung:

  1. Maximaler Scope-Lock: Eine Methode + 0 Exceptions = klar auditierbar
  2. Einfachster Pfad: Keine managed_proposals READ (das kommt erst bei approve/reject)
  3. Pattern-Klarheit: Service-Skelett wird in MH-4b-1 etabliert; MH-4b-2/3 folgen demselben Schema
  4. Test-First: Idempotency + Mainnet-Guard + Audit-Prefix + Atomicity-Tests bauen direkt am Anfang
  5. Rollback-Cost minimal: git revert <commit> + die managed_proposals Migration bleibt intakt (MH-4a unverändert)

Pre-Implementation-Q an Operator

  1. MH-4b-1 (NUR createRequestCommand) oder MH-4b-monolithic?Empfehlung MH-4b-1 (max scope-lock + Pattern-Etablierung)
  2. R6 architektonisch bestätigen: Service INSERTs NICHT in managed_proposals (Worker MH-6 macht das). ← Empfehlung JA bestätigen — vermeidet "tentative proposal_id" Problematik
  3. captured_via-Parameter in createRequestCommand: pflicht oder optional? Roadmap-Pattern ist optional. ← Empfehlung optional (Default 'gui_operator_request')
  4. intent-Parameter: free-form string oder enum? ← Empfehlung free-form mit Length-Cap 64 chars (Operator soll kurz beschreiben können)
  5. Service-Namespace: App\Services\Managed\ProposalService oder App\Services\ProposalService (flach)? Existing baselineservice ist App\Services\Apply\Baseline\. ← Empfehlung App\Services\Managed\ für Konsistenz mit Apply/Baseline-Nesting

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

AspektBewertung
LOC~250 (Service-Skelett + 1 Methode) + ~250 (Tests)
Tests~15
KomplexitätPattern-Mirror ApplyBaselineService::createApplyCommand
Blast-RadiusNULL — INSERT-only auf commands + audit_events (existing tables)
Roll-Back-Costminimal: git revert + DB hat 0 neue Rows (alles ist pending bis Worker läuft)
Dependencies-KlärungR6 (managed_proposals INSERT-Strategie) — Operator approval needed
Backup-PflichtNEIN
Migration-PflichtNEIN
Restart-PflichtNEIN
Mainnet-TouchNEIN
docker cpNEIN

STOP vor Implementierung. Erwarte Operator-GO mit Subschnitt-Wahl (MH-4b-1 empfohlen) + Q1-Q5-Approval, insbesondere R6 architektonisches Verdict (Service INSERTs NICHT in managed_proposals).

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