Repository navigation
feat(domain): align Hire lifecycle with ERC-8183 escrow states (open/funded/submitted/completed/rejected/expired) - #107
Conversation
HireStatus now mirrors the ERC-8183 job 1:1 (ADR-0005 D6): open, funded, submitted, completed, rejected, expired. Events are fund, submit, complete (complete or release), reject and expire (claim_refund, or the derived expiry of an unfunded job). A submitted hire cannot expire (D4). inProgress, cancelled, failed and rated leave the lifecycle and become hire data: rejectedFrom (set by reject), RuntimeStatus queued/running/failed with failureReason (startRun, failRun; only while funded), and feedbackReference (recordFeedback; only on completed hires, once). New typed errors: InvalidRuntimeTransition, HireNotCompleted, HireAlreadyRated. Docs: hire-lifecycle.md and model.md updated to the same states and transitions. Closes #73
Deploying puls3 with
|
| Latest commit: |
6f85dcf
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f27f72cb.puls3-4lw.pages.dev |
| Branch Preview URL: | https://feat-73-hire-erc8183-lifecyc.puls3-4lw.pages.dev |
TOMOKI977
left a comment
There was a problem hiding this comment.
Approved. Really solid work: the transition table matches ADR-0005 D6 row for row, every other state × event pair is covered, the guards can't be bypassed (private constructor, no copyWith, final fields), and nothing outside puls3_domain uses the old API, so main won't break. The mutation check you did on the guards is a great habit.
Non-blocking notes:
submit()ignoresruntimeStatus(entities.dart:194-197). A funded hire whose runfailedcan still be submitted, and acompletedhire then carriesruntimeStatus: failedand afailureReason. Either require a finished run (or no failure) beforesubmit, or document in I21 what happens to the runtime fields aftersubmit.docs/blueprints/flows.md:169still says "States follow the hire lifecycle (#10): Requested → Paid → InProgress → Delivered → Rated" (also S06 and the Rated status). A short "outdated, see hire-lifecycle.md" note would avoid confusion until #22/#61 updates the UI copy.model.md:97says each invariant has a test named after it, but the I21 and I22 groups (hire_lifecycle_test.dart:272,291) don't carry the prefix.Feedbackdocs still say "a paid hire" (entities.dart:312,model.md:16) while the rule is nowcompleted. Worth a line thatHireAlreadyRatednow means "already has afeedbackReference".
Heads-up for when the tracker (#97) and relay (#96) drive this entity (not introduced here, the same was true on main):
- Replays. The tracker is at-least-once, so
onFundedcan run twice after a crash. Today a secondfundthrows the sameInvalidHireTransitionas a real conflict, soEscrowEffectscan't tell "already applied" from "wrong state". Something like an idempotent no-op whenstatus == funded && paymentTransaction == payment.tx(or a distinct error) would make that easy. - Rehydration. There is no public way to rebuild a
funded/rejected/completedhire from storage without replaying transitions (which needs thePaymentand wallet). The persistence adapter will need a restore constructor that keeps the invariants.
|
@XxHugheadxX merged into |
Use Hire.fund and HireStatus.open/funded in the funding effect, the Serverpod hire repository, the HireRepository port, tests and the hire-payment spec after the lifecycle change in #107.
#93) * feat(domain): verify escrow funding against the hire and add the HireRepository port verifyFunding checks a job read from the escrow against the hire in the order of api.md: state exactly Funded, client and evaluator equal to the consumer, provider, agent, token, budget and the prepared expired_at. Each rejection names the mismatching job field. * feat(server): persist hires and payments in the Serverpod repository hire_payment has unique indexes on hire_id, transaction_hash and job_id. A unique violation of one of them maps to HirePaymentConflict by constraint name; any other database error is rethrown. The migration is generated on top of the chain submission migrations. * feat(server): verify hire funding in the escrow effects of the chain tracker HireEscrowEffects implements EscrowEffects.onFunded: it reads the job from the escrow, checks it against the hire with verifyFunding and pays the hire through Hire.pay, or fails with JobMismatch naming the job field or JobEvidenceUnavailable. It reuses the shared JobFundedEvent parser and is idempotent for the same submission. The tracker wiring uses it instead of the no-op effects. HireService keeps createHire; the public payment confirmation is gone because the tracker owns confirmation. * docs(openspec): specify hire payment verification through the tracker effects Also documents PULS3_HIRE_JOB_DURATION_SECONDS in the server README. * test(server): cover unknown-constraint rethrow in the hire payment repository mapping * fix(server): make fund submissions without a known hire a terminal JobMismatch Document that details.field is job_id for replay, wrong-transaction and duplicate-job conflicts. * chore(openspec): drop project config unrelated to hire payment * refactor: align hire funding with the ERC-8183 hire lifecycle Use Hire.fund and HireStatus.open/funded in the funding effect, the Serverpod hire repository, the HireRepository port, tests and the hire-payment spec after the lifecycle change in #107. --------- Co-authored-by: Julio Cesar Severiche Orellana <39506930+TOMOKI977@users.noreply.github.com>
Closes #73
Summary
The Hire state machine in
puls3_domainnow mirrors the ERC-8183 escrow job 1:1 (ADR-0005 D6), so the tracker (#97) and the relay endpoints (#96) can apply each confirmed escrow transaction as one domain transition.HireStatus:open,funded,submitted,completed,rejected,expired. The last three are terminal.HireEvent:fund,submit,complete,reject,expire, oneHiremethod each:fund(payment, agentWallet:)open→fundedfund. Same guards as the oldpay: exact price, the agent wallet as provider, the consumer as payersubmit()funded→submittedsubmitcomplete()submitted→completedcomplete, or the permissionlessreleaseafter the approval windowreject()open/funded/submitted→rejectedreject. Fromopenit is the cancel before payingexpire()funded→expiredclaim_refundexpire()open→expiredA
submittedhire cannot expire (D4).Removed states:
inProgress,cancelled,failedandratedleave the lifecycle and become hire data, with the same field names asdocs/architecture/models/hire.spy.yaml:rejectedFrom: set byreject().runtimeStatus(RuntimeStatus.queued/running/failed) andfailureReason:fundsetsqueued,startRun()setsrunning, andfailRun(reason:)setsfailedwith the reason. It changes only while the hire isfunded, and the hire staysfunded.feedbackReference: set byrecordFeedback(feedback, reference:), only on acompletedhire and only once. The status does not change.New typed errors:
InvalidRuntimeTransitionHireNotCompletedandHireAlreadyRated, named as indocs/architecture/api.mdDocs:
docs/domain/hire-lifecycle.mdhas a new diagram, the transition table with the escrow call behind each transition, and the hire data rules.docs/domain/model.mdhas the updated glossary and class diagram, I18–I20 rewritten, and new I21 (runtime progress) and I22 (rejectedFrom).Acceptance criteria
HireStatushas exactlyopen,funded,submitted,completed,rejected,expiredinProgress,failed,cancelledorratedvalue remains in the domain lifecycleInvalidHireTransition)rejectedFrom)recordFeedback)RuntimeStatus(queued,running,failed) exists separately fromHireStatusgate,domain,server,scan)Verification evidence
hire_lifecycle_test.dartchecks the following:HireStatus,HireEventandRuntimeStatusvalues;InvalidHireTransition;fundguards;rejectedFromfor each of the three source states;recordFeedbackrules.I broke each guard one at a time to check that the tests catch it. All 7 changes made the suite fail:
rejectedFromrecordingexpireallowed fromsubmittedfailRunallowed twicefundwithoutqueuedcompletedhireTo verify by hand, compare
HireStatus, the table inhire_lifecycle_test.dartanddocs/domain/hire-lifecycle.mdwith the D6 table in ADR-0005. Then searchpuls3_domain/libforinProgress,cancelled,failedandratedused as hire states: there are none.failedappears only asRuntimeStatus.failed.Notes for reviewers
EscrowEffects.onFundedon main appliesHire.fund(payment, agentWallet:), the same signature as the oldpay.Hire.payandHireStatus.requested; with this PR they becomeHire.fundandHireStatus.open.puls3_serveranalyzes clean against this branch.expired_atorapproval_deadline. It applies transitions that are already final on-chain, and refuses only what the escrow itself refuses.docs/domain/model.mdI4 still says that aHireIdtravels as the payment's muxed id (ADR-0003). With ADR-0005 the escrow job id plays that role, so this needs a separate docs fix.