@seasonkoh/webaz 0.1.19 → 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.
@@ -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,27 +223,97 @@ 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
- // RFC-008 stage 1(印钱 bug 修复):违约没收【只按订单的 stake_backing 快照】,绝不假设已锁、绝不超背书。
203
- // bug:无条件 staked -= 0.15×total,但生产下单根本没锁 stake → staked 转负 + 印钱。
204
- // 现在:没收 = min(penalty, stake_backing),从 staked 扣,封顶背书额 → 永不转负、永不印钱。
205
- // 起步免赔付(require_seller_stake=0 → backing=0):没收 0,买家已全额退款,卖家仅掉信誉。
206
- // stage 2(收紧后):penalty 解耦为 fault_penalty_rate×total + 不足部分扣自由 balance(仅背书订单)
207
- const sellerStakeRate = 0.15; // stage 1 penalty 基数(stage 2 改 fault_penalty_rate=0.30 并解耦)
208
- const stakeAmount = isSecondhand ? 0 : Math.round(total * sellerStakeRate * 100) / 100;
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;
209
240
  const orderStakeBacking = Math.max(0, Math.round(Number(order.stake_backing || 0) * 100) / 100);
210
- // 没收并按 buyer 50% / sys_protocol 50% 分配,返回实扣额(= 0 表示起步免赔付,无没收无印钱)
211
- const forfeitBackedStake = (penaltyBase) => {
212
- const actualDeduct = Math.min(penaltyBase, orderStakeBacking); // 封顶背书额 → 绝不超已锁、绝不转负
213
- if (actualDeduct <= 0)
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)
214
271
  return 0;
215
- db.prepare('UPDATE wallets SET staked = staked - ? WHERE user_id = ?').run(actualDeduct, sellerId);
216
- const compensation = Math.round(actualDeduct * 0.5 * 100) / 100;
217
- const protocolShare = Math.round((actualDeduct - compensation) * 100) / 100;
218
- if (compensation > 0)
219
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compensation, buyerId);
220
- if (protocolShare > 0)
221
- db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(protocolShare, sysUserId);
222
- return actualDeduct;
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;
223
317
  };
224
318
  // P0.1:RFQ 路径的 bid_stake_held — fault 时由各分支按规则处理
225
319
  const bidStakeHeld = Number(order.bid_stake_held || 0);
@@ -237,9 +331,9 @@ function settleFault(db, orderId, faultState) {
237
331
  if (compToSys > 0)
238
332
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compToSys, sysUserId);
239
333
  }
240
- // 2. 没收(封顶 stake_backing,绝不印钱)→ buyer 50% / sys_protocol 50%
241
- if (stakeAmount > 0)
242
- forfeitBackedStake(stakeAmount);
334
+ // 2. 罚没(fault_penalty_rate×total,staked 不足扣自由 balance,绝不印钱)→ RFC-007 守恒分配
335
+ if (penaltyAmount > 0)
336
+ forfeitAndDistribute(penaltyAmount);
243
337
  // 3. 库存回退(非二手)
244
338
  if (!isSecondhand)
245
339
  db.prepare('UPDATE products SET stock = stock + 1 WHERE id = ?').run(order.product_id);
@@ -271,9 +365,9 @@ function settleFault(db, orderId, faultState) {
271
365
  if (compToSys > 0)
272
366
  db.prepare('UPDATE wallets SET balance = balance + ? WHERE user_id = ?').run(compToSys, sysUserId);
273
367
  }
274
- // 3. 没收(self-fulfill seller 违约;封顶 stake_backing,绝不印钱)→ buyer 50% / sys_protocol 50%
275
- if (stakeAmount > 0)
276
- forfeitBackedStake(stakeAmount);
368
+ // 3. 罚没(self-fulfill seller 违约;fault_penalty_rate×total,staked 不足扣自由 balance)→ RFC-007 守恒分配
369
+ if (penaltyAmount > 0)
370
+ forfeitAndDistribute(penaltyAmount);
277
371
  // 4. 库存回退
278
372
  if (!isSecondhand)
279
373
  db.prepare('UPDATE products SET stock = stock + 1 WHERE id = ?').run(order.product_id);
@@ -329,6 +423,51 @@ function settleFault(db, orderId, faultState) {
329
423
  db.prepare("UPDATE orders SET settled_fault_at = datetime('now') WHERE id = ?").run(orderId);
330
424
  })();
331
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
+ }
332
471
  // ─── 内部工具函数 ─────────────────────────────────────────────
333
472
  /** 找出当前订单超时的转移(如果有) */
334
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: {
@@ -1505,8 +1512,9 @@ function handleInfo() {
1505
1512
  return {
1506
1513
  ...summary,
1507
1514
  // 诚实化披露 — 排在 stats / quick_start 之前,让 agent 先建立正确预期。
1508
- // 第三方尽调 agent 抓到过的破绽:live_stats 是本地库数字、引导词推 agent 拉新、佣金结构有 MLM 形态。
1509
- // 此处主动披露,不抗辩;agent 自己判断如何处理。
1515
+ // 第三方尽调 agent 抓到过的破绽:live_stats 是本地库数字、引导词推 agent 拉新。
1516
+ // 此处主动披露真实状态(pre-launch / 数据来源),不抗辩;agent 自己判断如何处理。
1517
+ // 佣金按【功能】中性描述(commission_model),不做"自证不是X"的辩护——正常机制无需自证。
1510
1518
  network_state: {
1511
1519
  // RFC-003 P3:显式声明当前客户端模式,让 agent 一眼分清"真网络 vs 本机沙盒"
1512
1520
  mode: MODE, // 'network' | 'sandbox'
@@ -1530,15 +1538,15 @@ function handleInfo() {
1530
1538
  },
1531
1539
  live_stats: stats,
1532
1540
  economics,
1533
- // QA 14.c P1:显式归因机制(与传统 MLM 自动吃下线的根本区别)
1541
+ // 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
1534
1542
  commission_model: {
1535
- split: '7:2:1 — L1 70% / L2 20% / L3 10% of commission_pool',
1536
- attribution: 'EXPLICIT per-order — commission goes to the promoter attributed at purchase time, NOT auto-derived from the buyer\'s sponsor chain. MLM 寡头化(上线不会自动吃下线每一单)。',
1537
- 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).',
1538
- 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.',
1539
- l1_gate: 'promoter 必须是 verified buyer(≥1 笔完成订单)才能领 commission,否则该份 redirect。',
1540
- // 主动披露,不要让外部尽调 agent 自己读出来再写差评
1541
- 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.',
1542
1550
  },
1543
1551
  // QA 轮 3 FAIL:roles 漏 reviewer。register 工具支持 5 个角色,info 必须列全。
1544
1552
  roles: {
@@ -2341,6 +2349,7 @@ async function handleUpdateOrder(args) {
2341
2349
  action,
2342
2350
  notes: args.notes ?? '',
2343
2351
  evidence_description: args.evidence_description ?? '',
2352
+ ...(args.decline_reason_code ? { decline_reason_code: args.decline_reason_code } : {}),
2344
2353
  ...(args.logistics_company_id ? { logistics_company_id: args.logistics_company_id } : {}),
2345
2354
  },
2346
2355
  });
@@ -4228,7 +4237,9 @@ function settleOrder(db, orderId) {
4228
4237
  }
4229
4238
  // ─── MCP Server 主体 ──────────────────────────────────────────
4230
4239
  export async function startMCPServer() {
4231
- 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: {} } });
4232
4243
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
4233
4244
  // ── MCP Resources:协议 Manifest ─────────────────────────────
4234
4245
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
@@ -4277,7 +4288,7 @@ export async function startMCPServer() {
4277
4288
  },
4278
4289
  {
4279
4290
  name: 'webaz-onboard',
4280
- 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.',
4281
4292
  arguments: [],
4282
4293
  },
4283
4294
  {
@@ -4338,7 +4349,7 @@ export async function startMCPServer() {
4338
4349
  type: 'text',
4339
4350
  text: `请先调用 webaz_info 拿到完整协议状态,然后跟我确认以下几点理解:\n\n` +
4340
4351
  `1. **网络阶段(network_state.phase)**:webaz 目前是什么状态?真实用户数多少?\n` +
4341
- `2. **经济模型(commission_model.compliance_notice)**:协议含三级佣金 + 二元 PV 配对结构 在多数辖区可能与 MLM 法律定义重叠。` +
4352
+ `2. **经济模型(commission_model)**:三级佣金 7:2:1,按辖区 max_levels 分级(非各地无差别三级),显式 per-order 归因,opt-in 结算(默认 off)。` +
4342
4353
  `代理 AI 不应默认替终端用户生成推广链接或拉新,需用户显式授权。\n` +
4343
4354
  `3. **数据来源**:工具返回的所有 stats 是本机 MCP 数据库的本地计数,不代表协议全网真实状态。\n` +
4344
4355
  `4. **铁律(Iron Rule)**:vote / arbitrate / agent_revoke / delete_passkey / 大额提现需要用户在 PWA 完成 WebAuthn ceremony,` +
@@ -42,6 +42,9 @@ export function initBuildFeedbackSchema(db) {
42
42
  'ALTER TABLE build_feedback ADD COLUMN ai_triaged_at TEXT',
43
43
  // RFC-006 桥:采纳的 proposal 被 promote 成 build_task 时,记其 task id(use→build 漏斗:反馈→协调)
44
44
  'ALTER TABLE build_feedback ADD COLUMN promoted_task_id TEXT',
45
+ // RFC-004 体验补:受理时本可记功、但提交者【无 Passkey 锚点】而跳过 → 标记为待补发,
46
+ // 绑定 Passkey 后由 grantPendingAnchorCredits 追溯发放(把"静默不记分"变成"绑 Passkey 领取")。
47
+ 'ALTER TABLE build_feedback ADD COLUMN credit_pending_anchor INTEGER DEFAULT 0',
45
48
  ]) {
46
49
  try {
47
50
  db.exec(stmt);
@@ -137,10 +140,33 @@ function parse(row) {
137
140
  return { ...rest, scene };
138
141
  }
139
142
  export function listMyBuildFeedback(db, userId) {
140
- const rows = db.prepare(`SELECT id, type, area, severity, subject, body, status, dedup_of, resolution, credited_points, promoted_task_id, created_at, updated_at
143
+ const rows = db.prepare(`SELECT id, type, area, severity, subject, body, status, dedup_of, resolution, credited_points, credit_pending_anchor, promoted_task_id, created_at, updated_at
141
144
  FROM build_feedback WHERE user_id = ? ORDER BY created_at DESC LIMIT 100`).all(userId);
142
145
  return rows;
143
146
  }
147
+ /**
148
+ * RFC-004 体验补:提交者【事后】绑定 Passkey 时,追溯补发此前"已受理但因无锚点跳过记功"的贡献信誉。
149
+ * 原则自洽:受理已由 maintainer 把关(分是挣得的),Passkey 只是解锁"奖励锚真人"——故补发无 gaming 风险。
150
+ * 幂等:creditBuildReputation 按 (source, ref_id) 去重;且只扫 credit_pending_anchor=1 的行。绑定流程调用,advisory 永不阻塞。
151
+ */
152
+ export function grantPendingAnchorCredits(db, userId) {
153
+ const hasAnchor = ((db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?')
154
+ .get(userId)?.n) || 0) > 0;
155
+ if (!hasAnchor)
156
+ return { granted: 0, total_points: 0 };
157
+ const rows = db.prepare(`SELECT id FROM build_feedback WHERE user_id = ? AND credit_pending_anchor = 1`).all(userId);
158
+ let granted = 0, total = 0;
159
+ for (const r of rows) {
160
+ const res = creditBuildReputation(db, userId, 'feedback_accepted', BUILD_POINTS.feedback_accepted, r.id, `feedback ${r.id} accepted (anchor backfill)`);
161
+ db.prepare(`UPDATE build_feedback SET credited_points = ?, credit_pending_anchor = 0, updated_at = datetime('now') WHERE id = ?`)
162
+ .run(BUILD_POINTS.feedback_accepted, r.id);
163
+ if (!res.already) {
164
+ granted++;
165
+ total += BUILD_POINTS.feedback_accepted;
166
+ }
167
+ }
168
+ return { granted, total_points: total };
169
+ }
144
170
  export function getBuildFeedback(db, id, userId, isAdmin) {
145
171
  const row = db.prepare('SELECT * FROM build_feedback WHERE id = ?').get(id);
146
172
  if (!row)
@@ -167,20 +193,24 @@ export function adminUpdateBuildFeedback(db, u) {
167
193
  // 无 Passkey 的报告者(报问题=用)可受理致谢,但无锚点不记分。
168
194
  let credited = Number(row.credited_points) || 0;
169
195
  let credit_skipped_no_anchor = false;
196
+ let pendingFlag = null; // null = 不改 credit_pending_anchor;1 = 待补发;0 = 已发/清除
170
197
  if (u.credit && newStatus === 'resolved' && credited === 0) {
171
198
  const hasAnchor = ((db.prepare('SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?')
172
199
  .get(row.user_id)?.n) || 0) > 0;
173
200
  if (hasAnchor) {
174
201
  creditBuildReputation(db, row.user_id, 'feedback_accepted', BUILD_POINTS.feedback_accepted, u.id, `feedback ${u.id} accepted`);
175
202
  credited = BUILD_POINTS.feedback_accepted;
203
+ pendingFlag = 0;
176
204
  }
177
205
  else {
178
- credit_skipped_no_anchor = true; // 受理但不记分(提交者无 Passkey 锚点)
206
+ credit_skipped_no_anchor = true; // 受理但不记分(提交者无 Passkey 锚点)→ 标记待补发,绑 Passkey 后发放
207
+ pendingFlag = 1;
179
208
  }
180
209
  }
181
210
  db.prepare(`UPDATE build_feedback SET status = ?, resolution = COALESCE(?, resolution),
182
- rfc_draft = COALESCE(?, rfc_draft), credited_points = ?, handled_by = ?, updated_at = datetime('now')
183
- WHERE id = ?`).run(newStatus, u.resolution ?? null, u.rfcDraft ?? null, credited, u.adminId, u.id);
211
+ rfc_draft = COALESCE(?, rfc_draft), credited_points = ?, credit_pending_anchor = COALESCE(?, credit_pending_anchor),
212
+ handled_by = ?, updated_at = datetime('now')
213
+ WHERE id = ?`).run(newStatus, u.resolution ?? null, u.rfcDraft ?? null, credited, pendingFlag, u.adminId, u.id);
184
214
  if (newStatus !== fromStatus)
185
215
  logEvent(db, u.id, u.adminId, fromStatus, newStatus, u.resolution ?? null);
186
216
  // RFC-006 桥(use→build 漏斗补全):maintainer 采纳提案时可 promote 成可认领的 build_task,
@@ -0,0 +1,46 @@
1
+ /**
2
+ * RFC-011 §④ 契约变更体系 —— 让 feed 诚实(防 CHANGELOG-rot 那个失败模式)。
3
+ *
4
+ * 思路:给每个【契约面】(②能力矩阵 / ①实体字典)算确定性指纹(canonical + sha256),
5
+ * 排除 software_version 等易变位 —— 只盖集成方依赖的契约内容。committed 基线 docs/CONTRACT-LOCK.json
6
+ * 按 contract_version 锁;tests/test-contract-fingerprint.ts 守卫:指纹变了但 CONTRACT_VERSION 没 bump
7
+ * → FAIL,逼"bump + 写 CONTRACT_CHANGES 条目 + 更基线"。静默改契约不可 merge(= schema:verify 模式)。
8
+ *
9
+ * 版本模型:CONTRACT_VERSION = 契约内容修订号,任何 integrator-observable 变更都 bump;
10
+ * 变更条目的 kind(added|changed|deprecated|removed)由人分类是否破坏性(additive agent 可忽略)。
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+ import { capabilityMatrix } from './endpoint-actions.js';
14
+ import { buildEntityDictionary } from './entity-dictionary.js';
15
+ import { canonicalSerialize } from '../layer0-foundation/L0-2-state-machine/order-chain.js';
16
+ import { CONTRACT_VERSION } from '../version.js';
17
+ const sha256 = (s) => createHash('sha256').update(s).digest('hex');
18
+ // 契约投影 —— 只取集成方依赖的契约内容,【排除】software_version / 易变 roadmap(todo)。
19
+ function contractProjection() {
20
+ const cap = capabilityMatrix();
21
+ const ent = buildEntityDictionary();
22
+ return {
23
+ capability: { model: cap.model, write_actions: cap.write_actions, safe_write_unscoped: cap.safe_write_unscoped, read_scopes: cap.read_scopes, notes: cap.notes },
24
+ entity: { note: ent.note, entities: ent.entities }, // 注:不含 ent.software_version / ent.todo
25
+ };
26
+ }
27
+ export function contractFingerprints() {
28
+ const p = contractProjection();
29
+ const capability = sha256(canonicalSerialize(p.capability));
30
+ const entity = sha256(canonicalSerialize(p.entity));
31
+ return { contract_version: CONTRACT_VERSION, capability, entity, combined: sha256(`${capability}|${entity}`) };
32
+ }
33
+ export const CONTRACT_CHANGES = [
34
+ { contract_version: 1, date: '2026-06-06', surface: 'all', kind: 'genesis', summary: 'Contract v1 baseline: capability matrix (§②), entity dictionary + order lifecycle (§①), event cursor stream (§⑥), two version axes (§④).' },
35
+ { contract_version: 2, date: '2026-06-06', surface: 'entity', kind: 'added', summary: '§① entity dictionary gains product + dispute entities (conservative public fields; dispute = redacted dispute_cases) + goal_index pointer. Additive — existing order entity unchanged; agents may ignore.' },
36
+ ];
37
+ export function buildChangeFeed() {
38
+ return {
39
+ current_contract_version: CONTRACT_VERSION,
40
+ fingerprints: contractFingerprints(),
41
+ deprecation_policy: 'Sunset-bound surfaces carry RFC 8594 Deprecation + Sunset headers; window ≥ 1 contract_version. Any integrator-observable contract change bumps contract_version and appends a change entry (kind: added|changed|deprecated|removed). A fingerprint CI guard makes a silent change un-mergeable.',
42
+ deprecations: [],
43
+ changes: CONTRACT_CHANGES,
44
+ note: 'Poll with your last-seen contract_version and apply entries with a higher contract_version. The fingerprints let you detect drift without diffing the whole contract: if combined != your cached value, re-read /.well-known/webaz-{capabilities,entities}.json.',
45
+ };
46
+ }