chore: initial commit with Phase 0 setup

This commit is contained in:
OG T
2026-06-06 22:55:45 +08:00
commit 9e79e58f87
56 changed files with 16088 additions and 0 deletions

View File

@@ -0,0 +1,230 @@
/**
* @agent-bounty/contracts — Enums
*
* 所有狀態機 enum、分類常數的唯一真實來源。
* MCP server、後端 API、前端 UI 都必須 import 這裡,禁止各自重新定義。
*/
// ─────────────────────────────────────────────
// Task 任務生命週期狀態機
// ─────────────────────────────────────────────
/**
* TaskStatus任務在系統中的完整生命週期。
*
* 狀態轉換規則前後端、MCP、Judge Agent 必遵守):
*
* OPEN
* → EXECUTING (claim_taskDB SELECT FOR UPDATE + Stripe Auth-Hold + Redis setex TTL 3600s)
* → CANCELLED (client_cancel)
*
* EXECUTING
* → VERIFYING (submit_solution)
* → OPEN (Redis TTL expired → rollback + 釋放 Auth-Hold)
* → CANCELLED (agent_cancel / client_cancel)
* → FAILED (agent_abandon)
*
* VERIFYING
* → COMPLETED (judge exit_code=0 → Stripe Capture → Ledger)
* → FAILED_RETRYABLE (judge fail retryable, or timeout ≤ 5min)
* → FAILED (judge fail non-retryable)
*
* FAILED_RETRYABLE
* → OPEN (retry_count ≤ 3, auto)
* → FAILED (retry_count > 3)
*
* FAILED
* → OPEN (policy=auto_open)
* → ARCHIVED (policy=archive)
* → DISPUTED (client_dispute)
*
* COMPLETED
* → DISPUTED (client_dispute, within 72h)
* → PAYOUT_READY (dispute window closed, no dispute)
* → PAYOUT_SETTLED (payout processed)
*
* DISPUTED
* → OPEN (dispute_resolved: reopen)
* → COMPLETED (dispute_resolved: keep)
* → REFUND_PENDING (dispute_resolved: refund)
* → ARCHIVED (dispute_resolved: archive)
*
* REFUND_PENDING → ARCHIVED
* CANCELLED → ARCHIVED
* PAYOUT_SETTLED → ARCHIVED
* ARCHIVED → (終態)
*/
export const TaskStatus = {
OPEN: "OPEN",
EXECUTING: "EXECUTING",
VERIFYING: "VERIFYING",
COMPLETED: "COMPLETED",
FAILED: "FAILED",
FAILED_RETRYABLE: "FAILED_RETRYABLE",
CANCELLED: "CANCELLED",
DISPUTED: "DISPUTED",
REFUND_PENDING: "REFUND_PENDING",
PAYOUT_READY: "PAYOUT_READY",
PAYOUT_SETTLED: "PAYOUT_SETTLED",
ARCHIVED: "ARCHIVED",
} as const;
export type TaskStatus = (typeof TaskStatus)[keyof typeof TaskStatus];
/** 終態集合:進入後不可再轉換(除人工介入) */
export const TERMINAL_TASK_STATUSES: ReadonlySet<TaskStatus> = new Set([
TaskStatus.ARCHIVED,
]);
/** 可從 FAILED/FAILED_RETRYABLE 自動重開的最大重試次數 */
export const MAX_TASK_RETRY_COUNT = 3;
// ─────────────────────────────────────────────
// Task 難度分級
// ─────────────────────────────────────────────
/**
* TaskDifficulty對應種子任務的定價與驗收複雜度。
* HELLO_WORLD = $1 / COMPONENT = $5 / VIEW = $10 / EPIC = $20+
*/
export const TaskDifficulty = {
HELLO_WORLD: "HELLO_WORLD",
COMPONENT: "COMPONENT",
VIEW: "VIEW",
EPIC: "EPIC",
} as const;
export type TaskDifficulty = (typeof TaskDifficulty)[keyof typeof TaskDifficulty];
/** 難度對應的預設獎金USD cents */
export const TASK_DIFFICULTY_DEFAULT_REWARD_CENTS: Record<TaskDifficulty, number> = {
HELLO_WORLD: 100,
COMPONENT: 500,
VIEW: 1000,
EPIC: 2000,
};
// ─────────────────────────────────────────────
// Task 驗收模式
// ─────────────────────────────────────────────
export const ValidationMode = {
VITEST_UNIT: "VITEST_UNIT",
PLAYWRIGHT_E2E: "PLAYWRIGHT_E2E",
AST_PARSING: "AST_PARSING", // 僅用於靜態 HTML/CSS不得用於 React 元件
VISUAL_REGRESSION: "VISUAL_REGRESSION",
} as const;
export type ValidationMode = (typeof ValidationMode)[keyof typeof ValidationMode];
// ─────────────────────────────────────────────
// Judge 執行結果
// ─────────────────────────────────────────────
export const JudgeOverallResult = {
PASS: "pass",
FAIL: "fail",
TIMEOUT: "timeout",
} as const;
export type JudgeOverallResult = (typeof JudgeOverallResult)[keyof typeof JudgeOverallResult];
export const JudgeTestStatus = {
PASSED: "passed",
FAILED: "failed",
SKIPPED: "skipped",
} as const;
export type JudgeTestStatus = (typeof JudgeTestStatus)[keyof typeof JudgeTestStatus];
/**
* JudgeErrorClassification標準化失敗分類。
* 影響後端決策:是否重試 / 扣押 Agent 分數 / 進入人工仲裁。
*/
export const JudgeErrorClassification = {
TEST_FAIL: "TEST_FAIL", // 邏輯/行為不符(可 retryable
LINT_FAIL: "LINT_FAIL", // lint 錯誤(可 retryable
TIMEOUT: "TIMEOUT", // 超過 5 分鐘(可 retryable
RESOURCE_EXHAUSTED: "RESOURCE_EXHAUSTED", // CPU/Memory 超限(可 retryable
ENVIRONMENT_ERROR: "ENVIRONMENT_ERROR", // 沙盒或工具層失敗(不計 Agent 分)
NETWORK_DENIED: "NETWORK_DENIED", // 試圖違規網路存取non-retryable
SANDBOX_CRASH: "SANDBOX_CRASH", // E2B 內部崩潰(不計 Agent 分)
TEST_SETUP_FAIL: "TEST_SETUP_FAIL", // 映像檔缺少依賴(不計 Agent 分)
ENV_MISCONFIG: "ENV_MISCONFIG", // 設定錯誤(不計 Agent 分)
} as const;
export type JudgeErrorClassification = (typeof JudgeErrorClassification)[keyof typeof JudgeErrorClassification];
/** 不計入 Agent 失敗分數的環境層錯誤 */
export const JUDGE_ENVIRONMENT_ERROR_TYPES: ReadonlySet<JudgeErrorClassification> = new Set([
JudgeErrorClassification.ENVIRONMENT_ERROR,
JudgeErrorClassification.SANDBOX_CRASH,
JudgeErrorClassification.TEST_SETUP_FAIL,
JudgeErrorClassification.ENV_MISCONFIG,
]);
// ─────────────────────────────────────────────
// 金流 / Settlement
// ─────────────────────────────────────────────
export const SettlementPhase = {
AUTH_HOLD: "auth_hold",
CAPTURE: "capture",
RELEASE: "release",
REFUND: "refund",
PAYOUT: "payout",
DISPUTE: "dispute",
CORRECTION: "correction",
} as const;
export type SettlementPhase = (typeof SettlementPhase)[keyof typeof SettlementPhase];
export const SettlementCaptureMode = {
STRIPE_AUTH_CAPTURE: "STRIPE_AUTH_CAPTURE",
BASE_SMART_CONTRACT: "BASE_SMART_CONTRACT",
} as const;
export type SettlementCaptureMode = (typeof SettlementCaptureMode)[keyof typeof SettlementCaptureMode];
export const SupportedCurrency = {
USD: "USD",
TWD: "TWD",
USDC: "USDC",
} as const;
export type SupportedCurrency = (typeof SupportedCurrency)[keyof typeof SupportedCurrency];
// ─────────────────────────────────────────────
// Lead / Scout
// ─────────────────────────────────────────────
export const LeadStatus = {
DRAFT: "DRAFT",
CONFIRMED: "CONFIRMED",
PAYMENT_AUTHORIZED: "PAYMENT_AUTHORIZED",
TASK_CREATED: "TASK_CREATED",
EXPIRED: "EXPIRED",
CANCELLED: "CANCELLED",
} as const;
export type LeadStatus = (typeof LeadStatus)[keyof typeof LeadStatus];
/** Scout 引流歸因模式。Phase 1 強制使用 LAST_CLICK。 */
export const AttributionModel = {
LAST_CLICK: "LAST_CLICK",
FIRST_CLICK: "FIRST_CLICK",
LINEAR: "LINEAR",
} as const;
export type AttributionModel = (typeof AttributionModel)[keyof typeof AttributionModel];
// ─────────────────────────────────────────────
// Error 分類
// ─────────────────────────────────────────────
export const TaskErrorClassification = {
RETRYABLE: "retryable",
NON_RETRYABLE: "non_retryable",
} as const;
export type TaskErrorClassification = (typeof TaskErrorClassification)[keyof typeof TaskErrorClassification];
// ─────────────────────────────────────────────
// MCP Tool 名稱(唯一常數來源)
// ─────────────────────────────────────────────
export const MCPToolName = {
LIST_OPEN_TASKS: "list_open_tasks",
CLAIM_TASK: "claim_task",
SUBMIT_SOLUTION: "submit_solution",
CHECK_PAYOUT_STATUS: "check_payout_status",
} as const;
export type MCPToolName = (typeof MCPToolName)[keyof typeof MCPToolName];

View File

@@ -0,0 +1,101 @@
/**
* @agent-bounty/contracts — Error Codes
*
* 所有 API / MCP 層的標準錯誤碼。
* 格式VW_<DOMAIN>_<CODE>
*/
// ─────────────────────────────────────────────
// Task 操作錯誤
// ─────────────────────────────────────────────
export const TaskErrorCode = {
// claim_task
TASK_NOT_FOUND: "VW_TASK_NOT_FOUND",
TASK_NOT_OPEN: "VW_TASK_NOT_OPEN", // 狀態不是 OPEN無法 claim
TASK_ALREADY_CLAIMED: "VW_TASK_ALREADY_CLAIMED", // 409 Race ConditionSELECT FOR UPDATE 失敗)
TASK_CLAIM_LIMIT_REACHED: "VW_TASK_CLAIM_LIMIT_REACHED", // Agent 已達同時接案上限
TASK_AGENT_SUSPENDED: "VW_TASK_AGENT_SUSPENDED", // Agent 被降權或封鎖
// submit_solution
TASK_NOT_EXECUTING: "VW_TASK_NOT_EXECUTING", // 無法提交,任務不在 EXECUTING
TASK_SUBMIT_UNAUTHORIZED: "VW_TASK_SUBMIT_UNAUTHORIZED", // 403非任務持有者提交
TASK_PAYLOAD_TOO_LARGE: "VW_TASK_PAYLOAD_TOO_LARGE", // 413超過 payload 上限
TASK_FILE_COUNT_EXCEEDED: "VW_TASK_FILE_COUNT_EXCEEDED", // 超過最大檔案數20
// 通用
TASK_INVALID_INPUT: "VW_TASK_INVALID_INPUT",
TASK_FORBIDDEN: "VW_TASK_FORBIDDEN",
} as const;
export type TaskErrorCode = (typeof TaskErrorCode)[keyof typeof TaskErrorCode];
// ─────────────────────────────────────────────
// Judge 執行錯誤
// ─────────────────────────────────────────────
export const JudgeErrorCode = {
JUDGE_TIMEOUT: "VW_JUDGE_TIMEOUT",
JUDGE_SANDBOX_UNAVAILABLE: "VW_JUDGE_SANDBOX_UNAVAILABLE",
JUDGE_IMAGE_NOT_FOUND: "VW_JUDGE_IMAGE_NOT_FOUND",
JUDGE_INVALID_RESULT: "VW_JUDGE_INVALID_RESULT",
JUDGE_ALREADY_RUNNING: "VW_JUDGE_ALREADY_RUNNING", // 冪等保護:同一任務重複觸發
} as const;
export type JudgeErrorCode = (typeof JudgeErrorCode)[keyof typeof JudgeErrorCode];
// ─────────────────────────────────────────────
// 金流錯誤
// ─────────────────────────────────────────────
export const SettlementErrorCode = {
PAYMENT_INTENT_NOT_FOUND: "VW_PAYMENT_INTENT_NOT_FOUND",
AUTH_HOLD_FAILED: "VW_AUTH_HOLD_FAILED",
CAPTURE_FAILED: "VW_CAPTURE_FAILED",
RELEASE_FAILED: "VW_RELEASE_FAILED",
REFUND_FAILED: "VW_REFUND_FAILED",
IDEMPOTENCY_CONFLICT: "VW_IDEMPOTENCY_CONFLICT", // 同 key 但不同金額
LEDGER_DRIFT_DETECTED: "VW_LEDGER_DRIFT_DETECTED", // 對帳漂移,啟動 incident
DUPLICATE_WEBHOOK: "VW_DUPLICATE_WEBHOOK", // webhook 重複接收
} as const;
export type SettlementErrorCode = (typeof SettlementErrorCode)[keyof typeof SettlementErrorCode];
// ─────────────────────────────────────────────
// Lead / Scout 錯誤
// ─────────────────────────────────────────────
export const LeadErrorCode = {
LEAD_NOT_FOUND: "VW_LEAD_NOT_FOUND",
LEAD_EXPIRED: "VW_LEAD_EXPIRED",
LEAD_INVALID_TEMPLATE: "VW_LEAD_INVALID_TEMPLATE", // Scout 使用未授權模板
SCOUT_RATE_LIMIT: "VW_SCOUT_RATE_LIMIT",
SCOUT_SUSPENDED: "VW_SCOUT_SUSPENDED",
ATTRIBUTION_TOKEN_INVALID: "VW_ATTRIBUTION_TOKEN_INVALID",
} as const;
export type LeadErrorCode = (typeof LeadErrorCode)[keyof typeof LeadErrorCode];
// ─────────────────────────────────────────────
// 通用 API 錯誤
// ─────────────────────────────────────────────
export const CommonErrorCode = {
UNAUTHORIZED: "VW_UNAUTHORIZED",
FORBIDDEN: "VW_FORBIDDEN",
VALIDATION_ERROR: "VW_VALIDATION_ERROR",
RATE_LIMITED: "VW_RATE_LIMITED",
INTERNAL_ERROR: "VW_INTERNAL_ERROR",
SERVICE_UNAVAILABLE: "VW_SERVICE_UNAVAILABLE",
} as const;
export type CommonErrorCode = (typeof CommonErrorCode)[keyof typeof CommonErrorCode];
// ─────────────────────────────────────────────
// 統一 error response 結構MCP + REST 共用)
// ─────────────────────────────────────────────
export interface VWError {
code: string;
message: string;
details?: Record<string, unknown>;
/** 若為 true呼叫方可在稍後重試 */
retryable?: boolean;
/** 建議的重試等待秒數 */
retryAfterSeconds?: number;
}

View File

@@ -0,0 +1,22 @@
/**
* @agent-bounty/contracts
*
* VibeWork 共用資料契約套件
* 版本0.1.0
*
* 這是整個 VibeWork 系統的「單一資料契約真實來源」。
* MCP server、後端 API、前端表單都必須從這裡 import
* 禁止在各自的程式庫中重新定義任何 VibeWork 資料結構。
*/
// Enums — 狀態機、分類常數
export * from "./enums/index.js";
// Schemas — Zod 驗證 schema
export * from "./schemas/index.js";
// Types — TypeScript 型別(從 Zod 推導)
export * from "./types/index.js";
// Errors — 標準錯誤碼與 VWError 介面
export * from "./errors/index.js";

View File

@@ -0,0 +1,377 @@
/**
* @agent-bounty/contracts — Zod Validation Schemas
*
* 唯一的驗證真實來源。
* MCP server、後端 API、前端表單都必須 import 這裡的 schema
* 禁止在各自的程式庫中重新定義任何 VibeWork 資料結構。
*/
import { z } from "zod";
import {
TaskStatus,
TaskDifficulty,
ValidationMode,
JudgeOverallResult,
JudgeTestStatus,
JudgeErrorClassification,
SettlementPhase,
SettlementCaptureMode,
SupportedCurrency,
LeadStatus,
AttributionModel,
TaskErrorClassification,
MAX_TASK_RETRY_COUNT,
} from "../enums/index.js";
// ─────────────────────────────────────────────
// 通用基礎型別
// ─────────────────────────────────────────────
const UUIDSchema = z.string().uuid("無效的 UUID 格式");
const CUIDSchema = z.string().cuid("無效的 CUID 格式");
const PositiveIntSchema = z.number().int().positive();
const NonNegativeIntSchema = z.number().int().nonnegative();
/** 金額(以最小單位計,例如美分 / 台幣分) */
const MoneyAmountSchema = z
.number()
.int()
.nonnegative("金額不得為負數")
.max(1_000_000_00, "單筆金額超過上限($1,000,000");
/** scope_clarity_scoreAI 評估任務清晰度Phase 1 要求 ≥ 0.90 */
const ScopeScoreSchema = z
.number()
.min(0)
.max(1)
.refine((v) => v >= 0.9, {
message: "scope_clarity_score 必須 ≥ 0.90 才能進入主任務池",
});
// ─────────────────────────────────────────────
// Task 驗收條件 Schema
// ─────────────────────────────────────────────
export const AcceptanceCriteriaSchema = z.object({
validation_mode: z.enum([
ValidationMode.VITEST_UNIT,
ValidationMode.PLAYWRIGHT_E2E,
ValidationMode.AST_PARSING,
ValidationMode.VISUAL_REGRESSION,
]),
/** 由平台提供的最小測試檔內容stringAgent 不得修改此欄位 */
test_file_content: z.string().min(20, "測試檔內容不得為空"),
/** 關鍵斷言規則(可選,補充說明) */
rules: z
.array(
z.object({
assertion: z.string(),
expected: z.unknown(),
description: z.string().optional(),
})
)
.optional(),
});
// ─────────────────────────────────────────────
// Task Bounty 核心 SchemaOpen Bounty Board
// ─────────────────────────────────────────────
export const TaskBountySchema = z.object({
task_id: UUIDSchema,
title: z.string().min(5).max(120),
description: z.string().min(20).max(2000),
status: z.enum([
TaskStatus.OPEN,
TaskStatus.EXECUTING,
TaskStatus.VERIFYING,
TaskStatus.COMPLETED,
TaskStatus.FAILED,
TaskStatus.FAILED_RETRYABLE,
TaskStatus.CANCELLED,
TaskStatus.DISPUTED,
TaskStatus.REFUND_PENDING,
TaskStatus.PAYOUT_READY,
TaskStatus.PAYOUT_SETTLED,
TaskStatus.ARCHIVED,
]),
difficulty: z.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
]),
/** 最小可行要求 ≥ 0.90 才能進入公開任務池 */
scope_clarity_score: ScopeScoreSchema,
error_classification: z.enum([
TaskErrorClassification.RETRYABLE,
TaskErrorClassification.NON_RETRYABLE,
]),
reward: z.object({
/** 金額整數以最小貨幣單位計USD cents / TWD 元) */
amount: MoneyAmountSchema,
currency: z.enum([
SupportedCurrency.USD,
SupportedCurrency.TWD,
SupportedCurrency.USDC,
]),
/** 顯示用格式化金額(例如 "NT$30" / "$1.00"),由後端產生 */
display_amount: z.string().optional(),
}),
acceptance_criteria: AcceptanceCriteriaSchema,
/** 要求使用的技術棧 */
required_stack: z.array(z.string()).min(1).default(["React", "Tailwind CSS"]),
/** 已重試次數 */
retry_count: NonNegativeIntSchema.max(MAX_TASK_RETRY_COUNT).default(0),
/** Auth-Hold 的 Stripe payment_intent_id */
stripe_payment_intent_id: z.string().optional(),
/** 任務到期時間(對應 Redis TTL */
expires_at: z.string().datetime().optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// claim_task Request/Response
// ─────────────────────────────────────────────
export const ClaimTaskRequestSchema = z.object({
task_id: UUIDSchema,
/** Agent 收款錢包Stripe Connect account 或 EVM 地址) */
developer_wallet: z
.string()
.regex(
/^(0x[a-fA-F0-9]{40}|acct_[a-zA-Z0-9]+)$/,
"developer_wallet 必須是有效的 EVM 地址或 Stripe Connect ID"
),
});
export const ClaimTaskResponseSchema = z.object({
task_id: UUIDSchema,
status: z.literal(TaskStatus.EXECUTING),
/** Stripe Auth-Hold 的金額(供 Agent 確認) */
held_amount: MoneyAmountSchema,
held_currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
/** Redis TTL 過期時間 */
expires_at: z.string().datetime(),
/** 接案 Agent 的冪等憑證 */
claim_token: z.string().uuid(),
});
// ─────────────────────────────────────────────
// submit_solution Request/Response
// ─────────────────────────────────────────────
const DeliverableFileSchema = z.object({
path: z.string().min(1).max(260, "路徑長度超過限制"),
content: z
.string()
.min(10, "檔案內容不得為空")
.max(500_000, "單檔案內容超過 500KB 限制"),
language: z.enum(["typescript", "javascript", "tsx", "jsx", "css", "html"]),
});
export const SubmitSolutionRequestSchema = z.object({
task_id: UUIDSchema,
/** 接案時取得的冪等憑證,防止重複提交 */
claim_token: z.string().uuid(),
deliverables: z.object({
files: z
.array(DeliverableFileSchema)
.min(1, "至少需要提交一個檔案")
.max(20, "單次提交不得超過 20 個檔案"),
notes: z.string().max(1000).optional(),
}),
});
export const SubmitSolutionResponseSchema = z.object({
task_id: UUIDSchema,
submission_id: UUIDSchema,
status: z.literal(TaskStatus.VERIFYING),
/** Judge 預計完成時間ISO 8601 */
estimated_judge_complete_at: z.string().datetime().optional(),
});
// ─────────────────────────────────────────────
// Judge Result SchemaE2B 沙盒回傳)
// ─────────────────────────────────────────────
export const JudgeTestResultSchema = z.object({
name: z.string(),
status: z.enum([
JudgeTestStatus.PASSED,
JudgeTestStatus.FAILED,
JudgeTestStatus.SKIPPED,
]),
duration_ms: NonNegativeIntSchema,
logs: z.string().optional(),
assertion_diff: z.string().optional(),
});
export const JudgeResultSchema = z.object({
attempt_id: UUIDSchema,
task_id: UUIDSchema,
submission_id: UUIDSchema,
overall_result: z.enum([
JudgeOverallResult.PASS,
JudgeOverallResult.FAIL,
JudgeOverallResult.TIMEOUT,
]),
tests: z.array(JudgeTestResultSchema),
artifacts: z
.object({
screenshot_url: z.string().url().optional(),
logs_url: z.string().url().optional(),
coverage_summary: z.string().optional(),
diff_url: z.string().url().optional(),
})
.optional(),
/** 失敗類型overall_result=fail/timeout 時必填) */
error_classification: z
.enum([
JudgeErrorClassification.TEST_FAIL,
JudgeErrorClassification.LINT_FAIL,
JudgeErrorClassification.TIMEOUT,
JudgeErrorClassification.RESOURCE_EXHAUSTED,
JudgeErrorClassification.ENVIRONMENT_ERROR,
JudgeErrorClassification.NETWORK_DENIED,
JudgeErrorClassification.SANDBOX_CRASH,
JudgeErrorClassification.TEST_SETUP_FAIL,
JudgeErrorClassification.ENV_MISCONFIG,
])
.optional(),
/** 供 Builder/Scout 質量分數模型使用的錯誤指紋 */
error_signature: z.string().optional(),
/** 若為 true後端可以安排重試不計入 Agent 失敗分數) */
retryable: z.boolean(),
resource_usage: z.object({
cpu_ms: NonNegativeIntSchema,
mem_peak_mb: NonNegativeIntSchema,
io_bytes: NonNegativeIntSchema,
}),
judge_completed_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Settlement Ledger Schema對帳台帳
// ─────────────────────────────────────────────
export const SettlementLedgerEntrySchema = z.object({
id: CUIDSchema,
task_id: UUIDSchema,
agent_id: z.string(),
human_client_id: z.string(),
/** Stripe 冪等 key格式 "{task_id}_{phase}_{attempt}" */
idempotency_key: z.string().min(10).max(255),
phase: z.enum([
SettlementPhase.AUTH_HOLD,
SettlementPhase.CAPTURE,
SettlementPhase.RELEASE,
SettlementPhase.REFUND,
SettlementPhase.PAYOUT,
SettlementPhase.DISPUTE,
SettlementPhase.CORRECTION,
]),
capture_mode: z.enum([
SettlementCaptureMode.STRIPE_AUTH_CAPTURE,
SettlementCaptureMode.BASE_SMART_CONTRACT,
]),
/** Stripe object IDpayment_intent_id / charge_id / refund_id */
stripe_object_id: z.string().optional(),
amount: MoneyAmountSchema,
currency: z.enum([SupportedCurrency.USD, SupportedCurrency.TWD]),
request_payload_hash: z.string(),
response_status: z.string(),
http_status: z.number().int().min(100).max(599),
attempt: PositiveIntSchema,
source: z.enum(["api", "webhook", "manual_replay"]),
/** 分潤快照capture 時寫入,不得事後修改) */
split: z
.object({
platform_amount: MoneyAmountSchema,
builder_amount: MoneyAmountSchema,
scout_amount: MoneyAmountSchema.optional(),
platform_rate: z.number().min(0).max(1),
builder_rate: z.number().min(0).max(1),
scout_rate: z.number().min(0).max(1).optional(),
})
.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// Lead SchemaScout 導流任務草案)
// ─────────────────────────────────────────────
export const LeadSchema = z.object({
lead_id: UUIDSchema,
scout_agent_id: z.string().optional(),
/** Scout 的 affiliate token用於歸因 */
affiliate_token: z.string().uuid().optional(),
attribution_model: z.enum([
AttributionModel.LAST_CLICK,
AttributionModel.FIRST_CLICK,
AttributionModel.LINEAR,
]),
status: z.enum([
LeadStatus.DRAFT,
LeadStatus.CONFIRMED,
LeadStatus.PAYMENT_AUTHORIZED,
LeadStatus.TASK_CREATED,
LeadStatus.EXPIRED,
LeadStatus.CANCELLED,
]),
/** 需求者原始需求(純文字,不超過 500 字) */
raw_requirement: z.string().min(10).max(500),
/** AI 生成的 PRD 草稿JSON */
prd_draft: z.record(z.string(), z.unknown()).optional(),
/** 付款連結Stripe Checkout URL */
payment_link: z.string().url().optional(),
/** 付款連結 TTL 過期時間 */
payment_link_expires_at: z.string().datetime().optional(),
/** 成功後建立的正式 task_id */
task_id: UUIDSchema.optional(),
created_at: z.string().datetime(),
updated_at: z.string().datetime(),
});
// ─────────────────────────────────────────────
// list_open_tasks Response Schema
// ─────────────────────────────────────────────
export const ListOpenTasksRequestSchema = z.object({
skills: z
.array(z.string().min(1).max(50))
.min(1, "至少需要指定一個技能"),
limit: z.number().int().min(1).max(20).default(5),
difficulty: z
.enum([
TaskDifficulty.HELLO_WORLD,
TaskDifficulty.COMPONENT,
TaskDifficulty.VIEW,
TaskDifficulty.EPIC,
])
.optional(),
});
/** 精簡版 Tasklist 用,避免暴露內部欄位) */
export const TaskSummarySchema = TaskBountySchema.pick({
task_id: true,
title: true,
status: true,
difficulty: true,
reward: true,
required_stack: true,
scope_clarity_score: true,
created_at: true,
}).extend({
description_preview: z.string().max(200),
});
export const ListOpenTasksResponseSchema = z.object({
tasks: z.array(TaskSummarySchema),
total_open: z.number().int().nonnegative(),
/** 任務池是否告警(供 MCP client 顯示警示) */
stockout_warning: z.boolean(),
});

View File

@@ -0,0 +1,146 @@
/**
* @agent-bounty/contracts — TypeScript Types
*
* 從 Zod schema 推導出的 TypeScript 型別。
* 永遠不要手寫這些型別——只從 z.infer 推導,確保型別與 schema 永遠同步。
*/
import type { z } from "zod";
import type {
TaskBountySchema,
TaskSummarySchema,
AcceptanceCriteriaSchema,
ClaimTaskRequestSchema,
ClaimTaskResponseSchema,
SubmitSolutionRequestSchema,
SubmitSolutionResponseSchema,
JudgeResultSchema,
JudgeTestResultSchema,
SettlementLedgerEntrySchema,
LeadSchema,
ListOpenTasksRequestSchema,
ListOpenTasksResponseSchema,
} from "../schemas/index.js";
import type { VWError } from "../errors/index.js";
// ─────────────────────────────────────────────
// Task
// ─────────────────────────────────────────────
export type TaskBounty = z.infer<typeof TaskBountySchema>;
export type TaskSummary = z.infer<typeof TaskSummarySchema>;
export type AcceptanceCriteria = z.infer<typeof AcceptanceCriteriaSchema>;
// ─────────────────────────────────────────────
// MCP Tool I/O
// ─────────────────────────────────────────────
export type ClaimTaskRequest = z.infer<typeof ClaimTaskRequestSchema>;
export type ClaimTaskResponse = z.infer<typeof ClaimTaskResponseSchema>;
export type SubmitSolutionRequest = z.infer<typeof SubmitSolutionRequestSchema>;
export type SubmitSolutionResponse = z.infer<typeof SubmitSolutionResponseSchema>;
export type ListOpenTasksRequest = z.infer<typeof ListOpenTasksRequestSchema>;
export type ListOpenTasksResponse = z.infer<typeof ListOpenTasksResponseSchema>;
// ─────────────────────────────────────────────
// Judge
// ─────────────────────────────────────────────
export type JudgeResult = z.infer<typeof JudgeResultSchema>;
export type JudgeTestResult = z.infer<typeof JudgeTestResultSchema>;
// ─────────────────────────────────────────────
// Settlement
// ─────────────────────────────────────────────
export type SettlementLedgerEntry = z.infer<typeof SettlementLedgerEntrySchema>;
// ─────────────────────────────────────────────
// Lead / Scout
// ─────────────────────────────────────────────
export type Lead = z.infer<typeof LeadSchema>;
// ─────────────────────────────────────────────
// 通用 API Response Envelope
// ─────────────────────────────────────────────
/** 統一 API 成功回應格式 */
export interface ApiSuccess<T> {
ok: true;
data: T;
/** 請求追蹤 ID供 SRE 查 log */
request_id: string;
}
/** 統一 API 錯誤回應格式 */
export interface ApiFailure {
ok: false;
error: VWError;
request_id: string;
}
export type ApiResponse<T> = ApiSuccess<T> | ApiFailure;
// ─────────────────────────────────────────────
// State Machine 合法轉換圖(型別層面防護)
// ─────────────────────────────────────────────
import type { TaskStatus } from "../enums/index.js";
/**
* 合法的狀態轉換對。
* 後端 API 層應驗證每次轉換是否在此集合內。
*/
export type TaskStateTransition =
| { from: "OPEN"; to: "EXECUTING" }
| { from: "OPEN"; to: "CANCELLED" }
| { from: "EXECUTING"; to: "VERIFYING" }
| { from: "EXECUTING"; to: "OPEN" } // Redis TTL timeout rollback
| { from: "EXECUTING"; to: "CANCELLED" }
| { from: "EXECUTING"; to: "FAILED" } // agent_abandon
| { from: "VERIFYING"; to: "COMPLETED" }
| { from: "VERIFYING"; to: "FAILED_RETRYABLE" }
| { from: "VERIFYING"; to: "FAILED" }
| { from: "FAILED_RETRYABLE"; to: "OPEN" }
| { from: "FAILED_RETRYABLE"; to: "FAILED" }
| { from: "FAILED"; to: "OPEN" }
| { from: "FAILED"; to: "ARCHIVED" }
| { from: "FAILED"; to: "DISPUTED" }
| { from: "COMPLETED"; to: "DISPUTED" }
| { from: "COMPLETED"; to: "PAYOUT_READY" }
| { from: "PAYOUT_READY"; to: "PAYOUT_SETTLED" }
| { from: "PAYOUT_SETTLED"; to: "ARCHIVED" }
| { from: "DISPUTED"; to: "OPEN" }
| { from: "DISPUTED"; to: "COMPLETED" }
| { from: "DISPUTED"; to: "REFUND_PENDING" }
| { from: "DISPUTED"; to: "ARCHIVED" }
| { from: "REFUND_PENDING"; to: "ARCHIVED" }
| { from: "CANCELLED"; to: "ARCHIVED" };
/** 取得從某狀態出發的合法下一狀態集合runtime 用) */
export function getAllowedNextStatuses(current: TaskStatus): TaskStatus[] {
const map: Partial<Record<TaskStatus, TaskStatus[]>> = {
OPEN: ["EXECUTING", "CANCELLED"],
EXECUTING: ["VERIFYING", "OPEN", "CANCELLED", "FAILED"],
VERIFYING: ["COMPLETED", "FAILED_RETRYABLE", "FAILED"],
FAILED_RETRYABLE: ["OPEN", "FAILED"],
FAILED: ["OPEN", "ARCHIVED", "DISPUTED"],
COMPLETED: ["DISPUTED", "PAYOUT_READY"],
PAYOUT_READY: ["PAYOUT_SETTLED"],
PAYOUT_SETTLED: ["ARCHIVED"],
DISPUTED: ["OPEN", "COMPLETED", "REFUND_PENDING", "ARCHIVED"],
REFUND_PENDING: ["ARCHIVED"],
CANCELLED: ["ARCHIVED"],
ARCHIVED: [],
};
return map[current] ?? [];
}
/** 驗證某狀態轉換是否合法 */
export function isValidStateTransition(
from: TaskStatus,
to: TaskStatus
): boolean {
return getAllowedNextStatuses(from).includes(to);
}