@seasonkoh/webaz 0.1.18 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/layer0-foundation/L0-2-state-machine/engine.js +171 -56
  2. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +23 -0
  3. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +65 -2
  4. package/dist/layer1-agent/L1-1-mcp-server/server.js +46 -22
  5. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +64 -5
  6. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +4 -0
  7. package/dist/pwa/contract-fingerprint.js +46 -0
  8. package/dist/pwa/economic-participation.js +122 -0
  9. package/dist/pwa/endpoint-actions.js +112 -0
  10. package/dist/pwa/entity-dictionary.js +125 -0
  11. package/dist/pwa/goal-index.js +60 -0
  12. package/dist/pwa/integration-contract.js +64 -0
  13. package/dist/pwa/limits.js +30 -0
  14. package/dist/pwa/negative-space.js +64 -0
  15. package/dist/pwa/public/app.js +5 -0
  16. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +287 -0
  17. package/dist/pwa/public/docs/INTEGRATOR.md +67 -0
  18. package/dist/pwa/public/docs/META-RULES-FULL.md +543 -0
  19. package/dist/pwa/public/i18n.js +4 -0
  20. package/dist/pwa/routes/build-feedback.js +3 -2
  21. package/dist/pwa/routes/disputes-write.js +68 -0
  22. package/dist/pwa/routes/orders-action.js +93 -1
  23. package/dist/pwa/routes/orders-create.js +7 -2
  24. package/dist/pwa/routes/orders-read.js +18 -0
  25. package/dist/pwa/routes/public-utils.js +131 -1
  26. package/dist/pwa/routes/webauthn.js +9 -1
  27. package/dist/pwa/server.js +69 -123
  28. package/dist/pwa/verifiability-index.js +63 -0
  29. package/dist/version.js +32 -0
  30. package/package.json +2 -1
@@ -133,6 +133,28 @@ export function checkTimeouts(db) {
133
133
  }
134
134
  }
135
135
  }
136
+ // RFC-007 stage 3:终结【临时判责】的客观拒单 —— 举证窗口逾期仍无人仲裁 → 落定为违约结算。
137
+ // (被仲裁接手的订单 stage 5 会清 pending / 改 disputed,不会命中此扫描;settled_fault_at 防重入。)
138
+ // 注:deadline 存为 SQLite datetime(空格)、now 为 JS ISO(带 T/Z),字符串直比会错(空格<T) → 两侧都用 datetime() 归一化。
139
+ const staleProvisional = db.prepare(`
140
+ SELECT id FROM orders
141
+ WHERE status = 'fault_seller' AND COALESCE(decline_objective_pending, 0) = 1
142
+ AND COALESCE(decline_contested, 0) = 0
143
+ AND settled_fault_at IS NULL
144
+ AND decline_contest_deadline IS NOT NULL AND datetime(decline_contest_deadline) < datetime(?)
145
+ `).all(now);
146
+ for (const o of staleProvisional) {
147
+ try {
148
+ const sys = getSystemUser(db);
149
+ settleFault(db, o.id, 'fault_seller');
150
+ transition(db, o.id, 'completed', sys.id, [], 'RFC-007:客观拒单举证窗口逾期未仲裁 → 终结为违约');
151
+ db.prepare('UPDATE orders SET decline_objective_pending = 0 WHERE id = ?').run(o.id);
152
+ details.push({ orderId: o.id, action: '临时判责 → fault_seller(举证逾期终结)' });
153
+ }
154
+ catch (e) {
155
+ console.error(`[decline finalize] order=${o.id}`, e);
156
+ }
157
+ }
136
158
  return { processed: details.length, details };
137
159
  }
138
160
  /**
@@ -186,7 +208,9 @@ export function getOrderStatus(db, orderId) {
186
208
  // - fault_logistics→ buyer 全额退款 + seller stake 全额返还(卖家无责,物流坏账协议吸收)
187
209
  // - fault_buyer → 仅库存回退(escrow 未锁,无资金动作)
188
210
  // 不发放 commission / PV / 基金池入金 — 无真实成交
189
- function settleFault(db, orderId, faultState) {
211
+ // export: tests/test-fault-forfeit-conservation.ts 直接验证守恒(真实代码,非复刻)
212
+ // 生产仅由本模块 checkTimeouts 内部调用;导出不改变其调用语义。
213
+ export function settleFault(db, orderId, faultState) {
190
214
  const sysUserId = 'sys_protocol';
191
215
  // 幂等检查 + 资金处置全部包在 transaction 内(防未来迁 PG 时并发 race)
192
216
  db.transaction(() => {
@@ -199,13 +223,98 @@ function settleFault(db, orderId, faultState) {
199
223
  const buyerId = order.buyer_id;
200
224
  const sellerId = order.seller_id;
201
225
  const isSecondhand = order.source === 'secondhand';
202
- // QA 7 P0 修复 改 per-order stake
203
- // 旧逻辑读 product.stake_amount + stake_locked_at(per-product 模型残留,已废弃)
204
- // 新逻辑:stake = order.total_amount × 0.15,下单时已锁,fault 时必然 lock(除非该订单未到 paid)
205
- const sellerStakeRate = 0.15;
206
- const stakeAmount = isSecondhand ? 0 : Math.round(total * sellerStakeRate * 100) / 100;
207
- // 订单到 paid 之后 stake 必然 lock(place_order 已扣,余额不足会 reject)
208
- const stakeLocked = !isSecondhand && Number(order.total_amount) > 0 && order.status !== 'created';
226
+ // RFC-008 stage 1(印钱 bug 修复)+ stage 2(罚没解耦):
227
+ // stage 1:违约没收按订单 stake_backing 快照,绝不假设已锁、绝不超背书 根治"staked 转负+印钱"bug。
228
+ // stage 2:罚没率【与质押率解耦】—— penalty = fault_penalty_rate(默认 30%) × total,独立于 stake_rate。
229
+ // · 背书订单(stake_backing>0):先扣 staked(封顶背书额),不足再扣卖家【自由 balance】(责任自负,
230
+ // 罚没真可执行,不被薄质押架构性封顶)
231
+ // · 起步免赔付(require_seller_stake=0 backing=0):仍 0 没收,【绝不碰新商家自由余额】(否则重新引入
232
+ // 我们刚移除的门槛),买家已全额退款,卖家仅掉信誉。
233
+ // 守恒不变:实扣 F(staked+balance) === 分出去的 F(协议/买家/推广/公池),永不印钱、永不转负。
234
+ const faultPenaltyRate = () => {
235
+ const row = db.prepare("SELECT value FROM protocol_params WHERE key = 'fault_penalty_rate'").get();
236
+ const v = Number(row?.value);
237
+ return Number.isFinite(v) && v >= 0 ? v : 0.30;
238
+ };
239
+ const penaltyAmount = isSecondhand ? 0 : Math.round(total * faultPenaltyRate() * 100) / 100;
240
+ const orderStakeBacking = Math.max(0, Math.round(Number(order.stake_backing || 0) * 100) / 100);
241
+ // RFC-007 stage 4:没收后的【守恒 + 不牟利】再分配(取代旧的 buyer 50% / protocol 50%)。
242
+ // 旧分配漏掉推广人(违反 §谁责任谁承担:推广人承担了真实推广成本却零补偿)。
243
+ // 新规则(全部用订单快照,可复算,绝不印钱):
244
+ // 1. 协议只回收【原本该收的平台费】protocolTake = min(F, total × protocol_fee_rate)
245
+ // —— 协议不从违约牟利;fund_base(1%) 排除(无成交=无 GMV,社区基金不应从罚没获利)。
246
+ // 2. R = F − protocolTake;买家补偿 = R × 50%(受损对手方固定份额)。
247
+ // 3. 推广人 = R 的另一半,按 l1/l2/l3 原始佣金比例分,【封顶各自原始佣金】—— 永不超过其真实损失。
248
+ // 4. 推广半残值(超封顶 / 无推广人)→ commission_reserve 三级公池(与正常单未归属佣金同去向,不给买家=不过补)。
249
+ // 守恒:protocolTake + buyerComp + promotersPaid + reserveResidual ≡ F(按构造,残值兜底)。
250
+ const FORFEIT_LEVEL_RATES = { 1: 0.70, 2: 0.20, 3: 0.10 };
251
+ const round2 = (n) => Math.round(n * 100) / 100;
252
+ const reserveResidual = (amount, note) => {
253
+ const a = round2(amount);
254
+ if (a <= 0)
255
+ return;
256
+ // 复制 redirectToCommissionReserve 的两笔写入(L0 不依赖 server.ts;kind 用既有 orphan_sponsor 桶)
257
+ db.prepare(`UPDATE commission_reserve SET balance = balance + ?, total_orphan_sponsor = total_orphan_sponsor + ?, updated_at = datetime('now') WHERE id = 'main'`).run(a, a);
258
+ db.prepare(`INSERT INTO commission_reserve_txns (id, kind, from_user_id, amount, related_order_id, note) VALUES (?,?,?,?,?,?)`)
259
+ .run(generateId('crt'), 'redirect_orphan_sponsor', sellerId, a, orderId, note);
260
+ };
261
+ const protocolFeeRate = () => {
262
+ const key = isSecondhand ? 'protocol_fee_rate_secondhand' : 'protocol_fee_rate_shop';
263
+ const row = db.prepare('SELECT value FROM protocol_params WHERE key = ?').get(key);
264
+ const v = Number(row?.value);
265
+ return Number.isFinite(v) && v >= 0 ? v : (isSecondhand ? 0.01 : 0.02);
266
+ };
267
+ // 罚没【收取 + RFC-007 守恒分配】,返回实扣额(= 0 表示起步免赔付,无没收无印钱)
268
+ const forfeitAndDistribute = (penalty) => {
269
+ // 起步免赔付:无背书订单(stake_backing=0)绝不没收、绝不碰新商家自由余额
270
+ if (orderStakeBacking <= 0)
271
+ return 0;
272
+ // stage 2 收取:先扣 staked(封顶背书额),不足部分再扣卖家自由 balance(责任自负;封顶其真实余额→不转负)
273
+ const fromStaked = Math.min(penalty, orderStakeBacking);
274
+ const remainder = round2(penalty - fromStaked);
275
+ const sellerBal = Math.max(0, Number(db.prepare('SELECT COALESCE(balance,0) AS b FROM wallets WHERE user_id = ?').get(sellerId)?.b ?? 0));
276
+ const fromBalance = round2(Math.min(remainder, sellerBal));
277
+ const F = round2(fromStaked + fromBalance);
278
+ if (F <= 0)
279
+ return 0;
280
+ if (fromStaked > 0)
281
+ db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(fromStaked, sellerId);
282
+ if (fromBalance > 0)
283
+ db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(fromBalance, sellerId);
284
+ // 1. 协议回收原始平台费(封顶 F,不牟利)
285
+ const protocolTake = round2(Math.min(F, total * protocolFeeRate()));
286
+ if (protocolTake > 0)
287
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolTake, sysUserId);
288
+ // 2. R 与买家补偿
289
+ const R = round2(F - protocolTake);
290
+ const buyerComp = round2(R * 0.5);
291
+ if (buyerComp > 0)
292
+ db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(buyerComp, buyerId);
293
+ // 3. 推广半 → l1/l2/l3 按原始佣金比例,封顶各自原始佣金
294
+ const promoterHalf = round2(R - buyerComp);
295
+ const commissionRate = Number(order.snapshot_commission_rate ?? 0);
296
+ const pool = round2(total * (Number.isFinite(commissionRate) && commissionRate > 0 ? commissionRate : 0));
297
+ const levels = [
298
+ { uid: order.l1_uid, orig: round2(pool * FORFEIT_LEVEL_RATES[1]) },
299
+ { uid: order.l2_uid, orig: round2(pool * FORFEIT_LEVEL_RATES[2]) },
300
+ { uid: order.l3_uid, orig: round2(pool * FORFEIT_LEVEL_RATES[3]) },
301
+ ].filter(l => l.uid); // 仅【真实存在的推广人】参与
302
+ const originalCommissionTotal = round2(levels.reduce((s, l) => s + l.orig, 0));
303
+ let promotersPaid = 0;
304
+ if (promoterHalf > 0 && originalCommissionTotal > 0) {
305
+ const payable = Math.min(promoterHalf, originalCommissionTotal); // 封顶原始佣金总额
306
+ for (const l of levels) {
307
+ const share = round2(payable * (l.orig / originalCommissionTotal));
308
+ if (share <= 0)
309
+ continue;
310
+ db.prepare('UPDATE wallets SET balance = balance + ?, earned = earned + ? WHERE user_id = ?').run(share, share, l.uid);
311
+ promotersPaid = round2(promotersPaid + share);
312
+ }
313
+ }
314
+ // 4. 推广半残值(超封顶 / 无推广人 / 取整余数)→ 三级公池;绝不给买家、绝不印钱
315
+ reserveResidual(promoterHalf - promotersPaid, `RFC-007 fault forfeit residual order=${orderId}`);
316
+ return F;
317
+ };
209
318
  // P0.1:RFQ 路径的 bid_stake_held — fault 时由各分支按规则处理
210
319
  const bidStakeHeld = Number(order.bid_stake_held || 0);
211
320
  if (faultState === 'fault_seller') {
@@ -222,31 +331,9 @@ function settleFault(db, orderId, faultState) {
222
331
  if (compToSys > 0)
223
332
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compToSys, sysUserId);
224
333
  }
225
- // 2. stake 50/50 扣罚(严格资金守恒:实扣多少就分多少,不允许协议印钱)
226
- if (stakeAmount > 0) {
227
- // 先确定实际可扣金额
228
- let actualDeduct = 0;
229
- if (stakeLocked) {
230
- // 已锁 → stake 全额扣(必然可扣,因 stake 已 lock 不可挪用)
231
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(stakeAmount, sellerId);
232
- actualDeduct = stakeAmount;
233
- }
234
- else {
235
- // 未锁 → 从 seller.balance 扣(不足则只扣到 0,差额不发放,不印钱)
236
- const w = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(sellerId);
237
- actualDeduct = Math.min(stakeAmount, Number(w?.balance ?? 0));
238
- if (actualDeduct > 0) {
239
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(actualDeduct, sellerId);
240
- }
241
- }
242
- // 按 actualDeduct 比例分配(buyer 50% + sys_protocol 50%),保证 入 = 出
243
- const compensation = Math.round(actualDeduct * 0.5 * 100) / 100;
244
- const protocolShare = Math.round((actualDeduct - compensation) * 100) / 100;
245
- if (compensation > 0)
246
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compensation, buyerId);
247
- if (protocolShare > 0)
248
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolShare, sysUserId);
249
- }
334
+ // 2. 罚没(fault_penalty_rate×total,staked 不足扣自由 balance,绝不印钱)→ RFC-007 守恒分配
335
+ if (penaltyAmount > 0)
336
+ forfeitAndDistribute(penaltyAmount);
250
337
  // 3. 库存回退(非二手)
251
338
  if (!isSecondhand)
252
339
  db.prepare('UPDATE products SET stock = stock + 1 WHERE id = ?').run(order.product_id);
@@ -278,27 +365,9 @@ function settleFault(db, orderId, faultState) {
278
365
  if (compToSys > 0)
279
366
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compToSys, sysUserId);
280
367
  }
281
- // 3. stake 50/50 扣罚(seller 违约:自选 self-fulfill 但没送达)
282
- if (stakeAmount > 0) {
283
- let actualDeduct = 0;
284
- if (stakeLocked) {
285
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(stakeAmount, sellerId);
286
- actualDeduct = stakeAmount;
287
- }
288
- else {
289
- const w = db.prepare('SELECT balance FROM wallets WHERE user_id = ?').get(sellerId);
290
- actualDeduct = Math.min(stakeAmount, Number(w?.balance ?? 0));
291
- if (actualDeduct > 0) {
292
- db.prepare('UPDATE wallets SET balance = balance - ? WHERE user_id = ?').run(actualDeduct, sellerId);
293
- }
294
- }
295
- const compensation = Math.round(actualDeduct * 0.5 * 100) / 100;
296
- const protocolShare = Math.round((actualDeduct - compensation) * 100) / 100;
297
- if (compensation > 0)
298
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compensation, buyerId);
299
- if (protocolShare > 0)
300
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolShare, sysUserId);
301
- }
368
+ // 3. 罚没(self-fulfill seller 违约;fault_penalty_rate×total,staked 不足扣自由 balance)→ RFC-007 守恒分配
369
+ if (penaltyAmount > 0)
370
+ forfeitAndDistribute(penaltyAmount);
302
371
  // 4. 库存回退
303
372
  if (!isSecondhand)
304
373
  db.prepare('UPDATE products SET stock = stock + 1 WHERE id = ?').run(order.product_id);
@@ -318,9 +387,10 @@ function settleFault(db, orderId, faultState) {
318
387
  if (bidStakeHeld > 0) {
319
388
  db.prepare('UPDATE wallets SET balance = balance + ?, staked = staked - ? WHERE user_id = ?').run(bidStakeHeld, bidStakeHeld, sellerId);
320
389
  }
321
- if (stakeAmount > 0 && stakeLocked) {
390
+ // seller 无责 退还其【该单实际背书的 stake】(= stake_backing;起步阶段=0,无可退)
391
+ if (orderStakeBacking > 0) {
322
392
  db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?')
323
- .run(stakeAmount, stakeAmount, sellerId);
393
+ .run(orderStakeBacking, orderStakeBacking, sellerId);
324
394
  }
325
395
  // 3. 库存回退
326
396
  if (!isSecondhand)
@@ -353,6 +423,51 @@ function settleFault(db, orderId, faultState) {
353
423
  db.prepare("UPDATE orders SET settled_fault_at = datetime('now') WHERE id = ?").run(orderId);
354
424
  })();
355
425
  }
426
+ // ─── RFC-007 stage 5:无责拒单结算(仲裁认定客观无责后调用)──────────────────
427
+ // §无责零成本:买家全额退款 + 卖家质押全退,零罚没、零佣金、零基金入金(无真实成交)。
428
+ // + 中性 no_fault_decline 信誉事件(points=0,不降分,仅作 rate-observable 信号防滥用)。
429
+ // 守恒:仅做"escrow→买家 balance"和"staked→卖家 balance"两笔内部移动,系统总额不变,绝不印钱。
430
+ export function settleDeclinedNoFault(db, orderId) {
431
+ db.transaction(() => {
432
+ const order = db.prepare('SELECT * FROM orders WHERE id = ?').get(orderId);
433
+ if (!order)
434
+ return;
435
+ if (order.settled_fault_at)
436
+ return; // 幂等(复用 settled_fault_at 标记)
437
+ const total = Number(order.total_amount);
438
+ const buyerId = order.buyer_id;
439
+ const sellerId = order.seller_id;
440
+ const isSecondhand = order.source === 'secondhand';
441
+ const orderStakeBacking = Math.max(0, Math.round(Number(order.stake_backing || 0) * 100) / 100);
442
+ const bidStakeHeld = Number(order.bid_stake_held || 0);
443
+ // 1. 买家 escrow 全额退回
444
+ db.prepare('UPDATE wallets SET escrowed = escrowed - ?, balance = balance + ? WHERE user_id = ?').run(total, total, buyerId);
445
+ // 2. 卖家质押全退(封顶其实际 staked,绝不转负)—— 无责零成本
446
+ const sellerStaked = Math.max(0, Number(db.prepare('SELECT COALESCE(staked,0) AS s FROM wallets WHERE user_id = ?').get(sellerId)?.s ?? 0));
447
+ const returnStake = Math.min(orderStakeBacking + bidStakeHeld, sellerStaked);
448
+ if (returnStake > 0)
449
+ db.prepare('UPDATE wallets SET staked = staked - ?, balance = balance + ? WHERE user_id = ?').run(returnStake, returnStake, sellerId);
450
+ // 3. 库存 / 二手状态恢复
451
+ if (!isSecondhand)
452
+ db.prepare('UPDATE products SET stock = stock + 1 WHERE id = ?').run(order.product_id);
453
+ else {
454
+ try {
455
+ db.prepare("UPDATE secondhand_items SET status = 'available', updated_at = datetime('now') WHERE id = ?").run(order.product_id);
456
+ }
457
+ catch { }
458
+ }
459
+ // 4. 中性 no_fault_decline 信誉事件(points=0 → 不降分;rate-observable 防"假客观"滥用)
460
+ try {
461
+ db.prepare('INSERT INTO reputation_events (id, user_id, order_id, event_type, points, reason) VALUES (?,?,?,?,?,?)')
462
+ .run(generateId('rep'), sellerId, orderId, 'no_fault_decline', 0, `客观无责拒单(仲裁认定) reason=${order.decline_reason_code || ''}`);
463
+ }
464
+ catch (e) {
465
+ console.warn('[no_fault_decline rep event]', e.message);
466
+ }
467
+ // 5. 结算标记 + 清临时判责 flag
468
+ db.prepare("UPDATE orders SET settled_fault_at = datetime('now'), decline_objective_pending = 0 WHERE id = ?").run(orderId);
469
+ })();
470
+ }
356
471
  // ─── 内部工具函数 ─────────────────────────────────────────────
357
472
  /** 找出当前订单超时的转移(如果有) */
358
473
  function findActiveDeadlineTransition(order, now) {
@@ -42,6 +42,29 @@ export function initOrderChainSchema(db) {
42
42
  }
43
43
  catch { }
44
44
  }
45
+ export function listOrderEventsSince(db, userId, since, limit) {
46
+ const lim = Math.min(200, Math.max(1, Math.floor(limit) || 50));
47
+ const sinceRid = since && /^\d+$/.test(since) ? Number(since) : 0;
48
+ const rows = db.prepare(`
49
+ SELECT e.rowid AS rid, e.order_id, e.seq, e.event_type, e.from_status, e.to_status, e.actor_role,
50
+ e.event_hash, e.prev_event_hash, e.signed_at, e.created_at
51
+ FROM order_events e
52
+ JOIN orders o ON o.id = e.order_id
53
+ WHERE (o.buyer_id = ? OR o.seller_id = ? OR o.logistics_id = ?)
54
+ AND e.rowid > ?
55
+ ORDER BY e.rowid ASC
56
+ LIMIT ?
57
+ `).all(userId, userId, userId, sinceRid, lim);
58
+ const events = rows.map(r => ({
59
+ cursor: String(r.rid),
60
+ order_id: r.order_id, seq: r.seq, event_type: r.event_type,
61
+ from_status: r.from_status, to_status: r.to_status, actor_role: r.actor_role,
62
+ event_hash: r.event_hash, prev_event_hash: r.prev_event_hash, signed_at: r.signed_at, created_at: r.created_at,
63
+ }));
64
+ const has_more = events.length === lim;
65
+ const next_cursor = events.length ? events[events.length - 1].cursor : (since ?? null);
66
+ return { events, next_cursor, has_more };
67
+ }
45
68
  // canonical_payload — 递归 stringify,每层 key 字母序
46
69
  // 修复 ultrareview bug_012:浅排序让嵌套对象用插入序,破坏"独立验证"承诺
47
70
  // (已知问题:arbitration_ruling 时 liability_parties 是嵌套数组)
@@ -177,9 +177,12 @@ export const VALID_TRANSITIONS = {
177
177
  description: '买家超时未付款,自动取消并标记违约'
178
178
  },
179
179
  'paid→fault_seller': {
180
- allowedRoles: ['system'],
180
+ // RFC-007 stage 2:除 system 超时判责外,允许 seller 【主动拒单】(decline) 显式触发此转移。
181
+ // 主动拒单 = 卖家明确不接此单(vs 沉默超时),记 decline_reason_code + declined_at。
182
+ // stage 2 一律走违约路径(与超时同结算);stage 3 auto-verify 上线后,客观无责拒单将改判 declined_nofault。
183
+ allowedRoles: ['system', 'seller'],
181
184
  requiresEvidence: false,
182
- description: '卖家超时未接单,自动退款并标记违约'
185
+ description: '卖家未接单:system 超时判责 或 seller 主动拒单(decline),退款买家并按违约结算'
183
186
  },
184
187
  'accepted→fault_seller': {
185
188
  allowedRoles: ['system'],
@@ -201,6 +204,18 @@ export const VALID_TRANSITIONS = {
201
204
  requiresEvidence: false,
202
205
  description: '物流超时未投递,标记物流违约'
203
206
  },
207
+ // ── RFC-007 stage 5:客观拒单仲裁翻案 ─────────────────────────────
208
+ // 临时判责(fault_seller + decline_objective_pending)经【人工仲裁】认定客观无责 → declined_nofault。
209
+ 'fault_seller→declined_nofault': {
210
+ allowedRoles: ['arbitrator', 'system'],
211
+ requiresEvidence: false,
212
+ description: 'RFC-007:客观拒单经人工仲裁认定无责 → 翻案为无责拒单(全退买家+退卖家质押,零罚没)'
213
+ },
214
+ 'declined_nofault→completed': {
215
+ allowedRoles: ['system'],
216
+ requiresEvidence: false,
217
+ description: 'RFC-007:无责拒单结算完成(买家全额退款,卖家质押全退,无罚没无佣金)'
218
+ },
204
219
  // ── 判责后的处置结算 ─────────────────────────────────────────
205
220
  'fault_seller→completed': {
206
221
  allowedRoles: ['system'],
@@ -236,3 +251,51 @@ export const CURRENT_RESPONSIBLE_SELF_FULFILL = {
236
251
  picked_up: 'seller',
237
252
  in_transit: 'seller',
238
253
  };
254
+ // ─── RFC-011 §① 实体语义:订单状态机契约(doc=code,从 VALID_TRANSITIONS 生成)───────────
255
+ // 状态【含义】是 authored(枚举注释无法运行时取);转移由 VALID_TRANSITIONS 生成(零漂移)。
256
+ // 覆盖锁:tests/test-order-lifecycle-contract.ts 断言每个 OrderStatus 有含义 + 每条转移被序列化。
257
+ export const ORDER_STATE_MEANINGS = {
258
+ created: { zh: '已下单,等待买家付款', en: 'placed, awaiting buyer payment' },
259
+ paid: { zh: '资金已托管(escrow),等待卖家接单', en: 'funds in escrow, awaiting seller acceptance' },
260
+ accepted: { zh: '卖家已接单,承诺履约', en: 'seller accepted, committed to fulfil' },
261
+ shipped: { zh: '卖家已交物流/自履行发出', en: 'handed to logistics / self-fulfil dispatched' },
262
+ picked_up: { zh: '物流已揽收', en: 'picked up by logistics' },
263
+ in_transit: { zh: '运输中', en: 'in transit' },
264
+ delivered: { zh: '已投递,等待买家确认', en: 'delivered, awaiting buyer confirmation' },
265
+ confirmed: { zh: '买家确认收货 → 触发结算', en: 'buyer confirmed → triggers settlement' },
266
+ disputed: { zh: '争议中,等待人工仲裁', en: 'in dispute, awaiting human arbitration' },
267
+ completed: { zh: '交易完成,资金已分配(终态)', en: 'completed, funds settled (terminal)' },
268
+ cancelled: { zh: '已取消(终态)', en: 'cancelled (terminal)' },
269
+ fault_buyer: { zh: '买家违约(超时未付,终态)', en: 'buyer fault (payment timeout, terminal)' },
270
+ fault_seller: { zh: '卖家违约(超时未接/发 或 主动拒单)', en: 'seller fault (accept/ship timeout or active decline)' },
271
+ fault_logistics: { zh: '物流违约', en: 'logistics fault' },
272
+ declined_nofault: { zh: '卖家无责拒单(仲裁认定客观)→ 全退买家+退卖家质押,零罚没(终态)', en: 'seller no-fault decline (arbitration-cleared) → full refund + stake returned, no forfeit (terminal)' },
273
+ resolved_for_seller: { zh: '仲裁裁卖家胜诉,资金释放(终态)', en: 'arbitration ruled for seller, funds released (terminal)' },
274
+ refunded_partial: { zh: '仲裁裁部分退款(终态)', en: 'arbitration partial refund (terminal)' },
275
+ refunded_full: { zh: '仲裁裁全额退款,订单作废(终态)', en: 'arbitration full refund, order voided (terminal)' },
276
+ dispute_dismissed: { zh: '争议被驳回(无效,终态)', en: 'dispute dismissed (terminal)' },
277
+ expired: { zh: '订单超时自动失败(通用兜底,终态)', en: 'order expired (generic timeout, terminal)' },
278
+ };
279
+ /** 订单/争议生命周期契约 —— 集成方 agent 读它即懂"订单怎么流转 + 每步谁驱动 + 何时 + 含义"。 */
280
+ export function orderLifecycleContract() {
281
+ const keys = Object.keys(VALID_TRANSITIONS);
282
+ const states = Object.keys(ORDER_STATE_MEANINGS).map(s => ({
283
+ state: s, zh: ORDER_STATE_MEANINGS[s].zh, en: ORDER_STATE_MEANINGS[s].en,
284
+ responsible: CURRENT_RESPONSIBLE[s] ?? null,
285
+ terminal: !keys.some(k => k.startsWith(s + '→')), // 无出边 = 终态
286
+ }));
287
+ const transitions = Object.entries(VALID_TRANSITIONS).map(([key, t]) => {
288
+ const arrow = key.indexOf('→');
289
+ return {
290
+ from: key.slice(0, arrow), to: key.slice(arrow + 1),
291
+ allowed_roles: t.allowedRoles, deadline_field: t.deadlineField ?? null,
292
+ requires_evidence: !!t.requiresEvidence, auto_fault_state: t.autoFaultState ?? null,
293
+ description: t.description,
294
+ };
295
+ });
296
+ return {
297
+ entity: 'order',
298
+ note: 'Order/dispute lifecycle, generated from the protocol state machine (VALID_TRANSITIONS) — doc=code. State changes are observable via the event stream (§⑥ GET /api/agent/events) and integrity-verifiable via the signed chain (§⑤ GET /api/orders/:id/chain).',
299
+ states, transitions,
300
+ };
301
+ }
@@ -30,7 +30,9 @@ import { initReputationSchema, recordOrderReputation, getReputation, getSearchBo
30
30
  import { generateManifest, getManifestSummary, MANIFEST_URI, } from '../../layer0-foundation/L0-5-manifest/manifest.js';
31
31
  import { requireAuth } from './auth.js';
32
32
  import { createHash, randomBytes } from 'node:crypto';
33
- const SERVER_VERSION = '0.1.8';
33
+ import { SOFTWARE_VERSION } from '../../version.js';
34
+ // RFC-011 §④:版本单一来源 = package.json(经 src/version.ts)。不再硬编码(旧 '0.1.8' 早漂移到 0.1.19)。
35
+ const SERVER_VERSION = SOFTWARE_VERSION;
34
36
  const TELEMETRY_URL = process.env.WEBAZ_TELEMETRY_URL ?? 'https://webaz.xyz/api/mcp-telemetry';
35
37
  // 2026-06-01: phase A pre-launch 默认 OFF(opt-in)— W8 public launch 时翻回 default ON + 加 README 披露段
36
38
  // Phase A pre-launch: telemetry default OFF (opt-in). Flip to default ON at W8 launch + add README disclosure section.
@@ -239,7 +241,7 @@ const TOOLS = [
239
241
  {
240
242
  name: 'webaz_info',
241
243
  description: `Get WebAZ documentation and usage guide. Call this FIRST when onboarding a new agent.
242
- Returns: protocol overview, available tools, role responsibilities, operation flows, **network_state (pre-launch disclaimer)**, **commission_model.compliance_notice (MLM-form disclosure)**.
244
+ Returns: protocol overview, available tools, role responsibilities, operation flows, **network_state (pre-launch disclaimer)**, **commission_model (3-tier share, jurisdiction-graded, explicit attribution, opt-in)**.
243
245
  No auth required, no parameters needed.
244
246
 
245
247
  ⚠️ Important: WebAZ is currently **pre-launch** with ~0 real users on the canonical endpoint. All stats / counts returned by this and other tools come from the **local MCP SQLite DB**, not protocol-wide prod state. Read network_state field BEFORE you treat any number as real-economy data.`,
@@ -253,7 +255,7 @@ No auth required, no parameters needed.
253
255
  // was ~1732 chars, now ~780 chars
254
256
  description: `Register a new WebAZ account. Returns: api_key (36-char 128-bit credential, store securely) + permanent_code (6-char recovery code, pair with handle in webaz_mykey to recover lost api_key) + handle (URL-safe ID; if taken, system appends numeric suffix — check handle_modified flag) + created_at.
255
257
 
256
- ⚠️ **Consent required (MLM disclosure)**: WebAZ is pre-launch with 3-tier commission + binary PV pairing structure (may overlap with MLM legal definitions; see webaz_info.commission_model.compliance_notice). Agent acting on behalf of human user **MUST get explicit informed consent BEFORE creating account**. Do NOT auto-register from generic shopping questions.
258
+ ⚠️ **Consent required**: creating an account on a human user's behalf registers an economic-relationship account (can participate in commission). Agent acting for a human user **MUST get explicit informed consent BEFORE creating account**. Do NOT auto-register from generic shopping questions.
257
259
 
258
260
  Roles: buyer (browse/order/confirm) | seller (list/accept/ship) | logistics (pickup/transit/deliver) | reviewer (reviews) | arbitrator (disputes/rulings).
259
261
 
@@ -478,10 +480,15 @@ Missing deadline → protocol auto-marks party in default.`,
478
480
  order_id: { type: 'string', description: 'Order ID' },
479
481
  action: {
480
482
  type: 'string',
481
- enum: ['accept', 'ship', 'pickup', 'transit', 'deliver', 'confirm', 'dispute'],
482
- description: 'Action to execute',
483
+ enum: ['accept', 'ship', 'pickup', 'transit', 'deliver', 'confirm', 'dispute', 'decline', 'contest_decline'],
484
+ description: 'Action to execute. decline = seller actively refuses a paid order (vs silent timeout); requires decline_reason_code. contest_decline = seller opens human arbitration on an objective-claimed provisional fault (within the contest window) to be cleared to no-fault; pass evidence_description.',
483
485
  },
484
486
  notes: { type: 'string', description: 'Action note (e.g. tracking number, dispute reason)' },
487
+ decline_reason_code: {
488
+ type: 'string',
489
+ enum: ['stock_consumed_concurrent', 'stale_price_snapshot', 'force_majeure', 'price_regret', 'cherry_pick', 'other'],
490
+ description: 'Required for action=decline. Why the seller refuses. Subjective (price_regret / cherry_pick / other) → settles immediately as seller-fault (buyer fully refunded). Objective-claimed (stock_consumed_concurrent / stale_price_snapshot / force_majeure) → PROVISIONAL fault: not settled yet, opens a contest window — seller must open arbitration (webaz_dispute) with evidence to be cleared (these off-chain facts have no on-protocol auto-verification); uncontested by the deadline → auto-finalizes as fault.',
491
+ },
485
492
  evidence_description: {
486
493
  type: 'string',
487
494
  description: 'Evidence description (recommended for ship/pickup/deliver; required for dispute)',
@@ -795,7 +802,7 @@ Safer than \`webaz_revoke_key\` — atomic swap, no access gap.`,
795
802
 
796
803
  ⚠️ **Opt-in required (RFC-002)**: rewards default = off. \`rewards_status\` field returns 4-state {opted_in | never_activated | auto_downgraded | deactivated} + pending_escrow tally. Opted-out users still see attribution + tree, but commission held in escrow until activation via PWA #me.
797
804
 
798
- ⚠️ **MLM disclosure**: structure may overlap with MLM legal definitions (see webaz_info.commission_model.compliance_notice). Agent acting for human user **MUST get explicit consent** before generating referral links / promoting. Do NOT auto-recruit.`,
805
+ ⚠️ **Consent required**: generating referral links / promoting on a human user's behalf needs the user's explicit authorization. Agent **MUST get explicit consent** before generating referral links / promoting. Do NOT auto-recruit.`,
799
806
  inputSchema: {
800
807
  type: 'object',
801
808
  properties: {
@@ -810,7 +817,7 @@ Safer than \`webaz_revoke_key\` — atomic swap, no access gap.`,
810
817
 
811
818
  ⚠️ **Opt-in required (RFC-002 §3.5)**: this is a valuation-layer action. Caller must have \`rewards_opted_in=1\` (builder-identity opt-in). Opted-out users get \`{error: 'rewards_opt_in_required', missing_requirements, next_steps}\` — direct user to PWA #me to apply.
812
819
 
813
- ⚠️ **MLM disclosure**: builds referral chain (multi-tier commission + binary pairing). Agent acting for human user **MUST get explicit consent**. Do NOT auto-generate. See webaz_info.commission_model.compliance_notice.`,
820
+ ⚠️ **Consent required**: this builds a referral chain on the user's behalf. Agent acting for a human user **MUST get explicit consent**. Do NOT auto-generate. See webaz_info.commission_model.`,
814
821
  inputSchema: {
815
822
  type: 'object',
816
823
  properties: {
@@ -1359,7 +1366,7 @@ Gate by type: ux_issue/bug (reporting = using) → login only, NO Passkey, anyon
1359
1366
 
1360
1367
  Actions:
1361
1368
  - list_open (default): open tasks (opt. area filter)
1362
- - claim: take an open task; provenance=human|ai_assisted|ai_authored (self-declared, not detected); auto-expires ~7d if not submitted
1369
+ - claim: take an open task; provenance=human|ai_assisted|ai_authored (self-declared, not detected); auto-expires ~7d if not submitted. Returns a **handoff** (repo + AGENTS.md + PR flow) — point a coding agent at it to actually do the work; the human needn't know git.
1363
1370
  - submit: mark in_review with pr_ref
1364
1371
  - status: tasks you hold (claimed/in_review)
1365
1372
  - profile: your build dashboard — KPI/tier/restrictions+appeal, self-view (private, no public leaderboard)
@@ -1439,9 +1446,22 @@ async function handleContribute(args) {
1439
1446
  const tid = args.task_id;
1440
1447
  if (!tid)
1441
1448
  return { error: 'task_id required for action=claim' };
1442
- return apiCall('/api/build-tasks/' + encodeURIComponent(tid) + '/claim', {
1449
+ const r = await apiCall('/api/build-tasks/' + encodeURIComponent(tid) + '/claim', {
1443
1450
  method: 'POST', apiKey, body: { provenance: args.provenance },
1444
1451
  });
1452
+ // RFC-006 断点1(b)交接:认领成功后直接下发"怎么真正动手",让贡献者的【编码 agent】接手 git/PR。
1453
+ // 关键:人不必会 git——人的编码 agent(如 Claude Code)做;人(Passkey 真人)担责。
1454
+ if (!r.error) {
1455
+ r.handoff = {
1456
+ repo: 'https://github.com/seasonsagents-art/webaz',
1457
+ start_here: 'Read AGENTS.md (project map + before-you-code + PR flow), then CONTRIBUTING.md.',
1458
+ do_the_work: 'Point a coding agent (e.g. Claude Code) at the repo; work on a single-topic branch. The buyer/shopping agent is not the coding agent — hand off to one.',
1459
+ pr_flow: 'Commit with DCO sign-off (git commit -s). If AI-authored, add 🤖🤖🤖 to the PR title + a meta-rule trace. Humans merge — no auto-merge.',
1460
+ then: `When the PR is open, report it back: webaz_contribute action=submit task_id=${tid} pr_ref=#<N>.`,
1461
+ human_note: "You don't need to know git — your coding agent does it; you (the Passkey-bound human) stay accountable.",
1462
+ };
1463
+ }
1464
+ return r;
1445
1465
  }
1446
1466
  if (action === 'submit') {
1447
1467
  const tid = args.task_id;
@@ -1492,8 +1512,9 @@ function handleInfo() {
1492
1512
  return {
1493
1513
  ...summary,
1494
1514
  // 诚实化披露 — 排在 stats / quick_start 之前,让 agent 先建立正确预期。
1495
- // 第三方尽调 agent 抓到过的破绽:live_stats 是本地库数字、引导词推 agent 拉新、佣金结构有 MLM 形态。
1496
- // 此处主动披露,不抗辩;agent 自己判断如何处理。
1515
+ // 第三方尽调 agent 抓到过的破绽:live_stats 是本地库数字、引导词推 agent 拉新。
1516
+ // 此处主动披露真实状态(pre-launch / 数据来源),不抗辩;agent 自己判断如何处理。
1517
+ // 佣金按【功能】中性描述(commission_model),不做"自证不是X"的辩护——正常机制无需自证。
1497
1518
  network_state: {
1498
1519
  // RFC-003 P3:显式声明当前客户端模式,让 agent 一眼分清"真网络 vs 本机沙盒"
1499
1520
  mode: MODE, // 'network' | 'sandbox'
@@ -1517,15 +1538,15 @@ function handleInfo() {
1517
1538
  },
1518
1539
  live_stats: stats,
1519
1540
  economics,
1520
- // QA 14.c P1:显式归因机制(与传统 MLM 自动吃下线的根本区别)
1541
+ // 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
1521
1542
  commission_model: {
1522
- split: '7:2:1 — L1 70% / L2 20% / L3 10% of commission_pool',
1523
- attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, NOT auto-derived from the buyer\'s sponsor chain. MLM 寡头化(上线不会自动吃下线每一单)。',
1524
- how_to_attribute: 'L1: webaz_place_order(promoter_api_key) records direct promoter. Full L2/L3 chain requires buyer to arrive via webaz_share_link ?ref= URL clicked in browser (builds product_share_attribution).',
1525
- redirect_rules: 'chain_gap ( L / sponsor 无效) charity_fund; region cap (level > region max_levels) global_fund. global region max_levels=1 ( L1);singapore 等 max_levels=3.',
1526
- l1_gate: 'promoter 必须是 verified buyer(≥1 笔完成订单)才能领 commission,否则该份 redirect。',
1527
- // 主动披露,不要让外部尽调 agent 自己读出来再写差评
1528
- compliance_notice: '⚠️ 本协议含三级佣金(L1/L2/L3 7:2:1) + 二元配对树(binary PV matching) + 邀请链结构。这类形态在多数司法辖区可能与**多层级营销(MLM)的法律定义重叠**。本协议通过显式 per-order 归因(非自动吃下线)规避了 MLM 寡头化的核心病灶,但形态层面仍带 MLM 特征,我们诚实告知。**代理 AI 不应默认替终端用户生成推广链接或拉新**——这需要用户的显式授权,且用户自身要先评估当地合规性。\n\n**Reward participation is opt-in (RFC-002)**: default = off. Users must apply (Passkey + ≥1 completed order); attribution always recorded (relation layer), but commission settlement gated until opt-in. Pending commission held in pending_commission_escrow for 30d grace. See docs/rfcs/RFC-002-rewards-opt-in.md for full state matrix.',
1543
+ split: '7:2:1 — L1 70% / L2 20% / L3 10% of an order\'s commission_pool',
1544
+ jurisdiction_tiers: 'Tiers are graded by the order region\'s max_levels NOT a uniform 3 tiers everywhere. e.g. global region max_levels=1 L1 only; singapore (etc.) max_levels=3 → up to L3. A region may also be 0 (no commission tiers; pool → community fund).',
1545
+ attribution: 'EXPLICIT per-order commission goes to the promoter attributed at purchase time, not derived from the buyer\'s sponsor chain.',
1546
+ how_to_attribute: 'L1: webaz_place_order(promoter_api_key) records the direct promoter. Full L2/L3 chain requires the buyer to arrive via a webaz_share_link ?ref= URL clicked in a browser (builds product_share_attribution).',
1547
+ redirect_rules: 'chain_gap (no L / invalid sponsor) → charity_fund; level beyond the region cap → global_fund.',
1548
+ l1_gate: 'the promoter must be a verified buyer (≥1 completed order) to receive commission, otherwise that share redirects.',
1549
+ opt_in: 'Participation is opt-in (RFC-002): default = off. A user applies (Passkey + ≥1 completed order); attribution is always recorded, but commission settlement is gated until opt-in (pending held in pending_commission_escrow, 30d grace). See docs/rfcs/RFC-002-rewards-opt-in.md.',
1529
1550
  },
1530
1551
  // QA 轮 3 FAIL:roles 漏 reviewer。register 工具支持 5 个角色,info 必须列全。
1531
1552
  roles: {
@@ -2328,6 +2349,7 @@ async function handleUpdateOrder(args) {
2328
2349
  action,
2329
2350
  notes: args.notes ?? '',
2330
2351
  evidence_description: args.evidence_description ?? '',
2352
+ ...(args.decline_reason_code ? { decline_reason_code: args.decline_reason_code } : {}),
2331
2353
  ...(args.logistics_company_id ? { logistics_company_id: args.logistics_company_id } : {}),
2332
2354
  },
2333
2355
  });
@@ -4215,7 +4237,9 @@ function settleOrder(db, orderId) {
4215
4237
  }
4216
4238
  // ─── MCP Server 主体 ──────────────────────────────────────────
4217
4239
  export async function startMCPServer() {
4218
- const server = new Server({ name: 'dcp-protocol', version: '0.1.0' }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
4240
+ const server = new Server(
4241
+ // name 是客户端配置引用的 server 标识(勿改);version 走单一来源(旧硬编码 '0.1.0' 已漂移)。
4242
+ { name: 'dcp-protocol', version: SOFTWARE_VERSION }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
4219
4243
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
4220
4244
  // ── MCP Resources:协议 Manifest ─────────────────────────────
4221
4245
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
@@ -4264,7 +4288,7 @@ export async function startMCPServer() {
4264
4288
  },
4265
4289
  {
4266
4290
  name: 'webaz-onboard',
4267
- description: 'Onboarding for a new agent first connecting to webaz — explains protocol nature / pre-launch state / MLM shape / registration path / user-authorization boundaries. Read webaz_info first, then run this prompt.',
4291
+ description: 'Onboarding for a new agent first connecting to webaz — explains protocol nature / pre-launch state / commission model / registration path / user-authorization boundaries. Read webaz_info first, then run this prompt.',
4268
4292
  arguments: [],
4269
4293
  },
4270
4294
  {
@@ -4325,7 +4349,7 @@ export async function startMCPServer() {
4325
4349
  type: 'text',
4326
4350
  text: `请先调用 webaz_info 拿到完整协议状态,然后跟我确认以下几点理解:\n\n` +
4327
4351
  `1. **网络阶段(network_state.phase)**:webaz 目前是什么状态?真实用户数多少?\n` +
4328
- `2. **经济模型(commission_model.compliance_notice)**:协议含三级佣金 + 二元 PV 配对结构 在多数辖区可能与 MLM 法律定义重叠。` +
4352
+ `2. **经济模型(commission_model)**:三级佣金 7:2:1,按辖区 max_levels 分级(非各地无差别三级),显式 per-order 归因,opt-in 结算(默认 off)。` +
4329
4353
  `代理 AI 不应默认替终端用户生成推广链接或拉新,需用户显式授权。\n` +
4330
4354
  `3. **数据来源**:工具返回的所有 stats 是本机 MCP 数据库的本地计数,不代表协议全网真实状态。\n` +
4331
4355
  `4. **铁律(Iron Rule)**:vote / arbitrate / agent_revoke / delete_passkey / 大额提现需要用户在 PWA 完成 WebAuthn ceremony,` +