@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.
- package/dist/layer0-foundation/L0-2-state-machine/engine.js +165 -26
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +23 -0
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +65 -2
- package/dist/layer1-agent/L1-1-mcp-server/server.js +31 -20
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +34 -4
- package/dist/pwa/contract-fingerprint.js +46 -0
- package/dist/pwa/economic-participation.js +122 -0
- package/dist/pwa/endpoint-actions.js +112 -0
- package/dist/pwa/entity-dictionary.js +125 -0
- package/dist/pwa/goal-index.js +60 -0
- package/dist/pwa/integration-contract.js +64 -0
- package/dist/pwa/limits.js +30 -0
- package/dist/pwa/negative-space.js +64 -0
- package/dist/pwa/public/app.js +5 -0
- package/dist/pwa/public/docs/ECONOMIC-MODEL.md +287 -0
- package/dist/pwa/public/docs/INTEGRATOR.md +67 -0
- package/dist/pwa/public/docs/META-RULES-FULL.md +543 -0
- package/dist/pwa/public/i18n.js +4 -0
- package/dist/pwa/routes/disputes-write.js +68 -0
- package/dist/pwa/routes/orders-action.js +93 -1
- package/dist/pwa/routes/orders-read.js +18 -0
- package/dist/pwa/routes/public-utils.js +131 -1
- package/dist/pwa/routes/webauthn.js +9 -1
- package/dist/pwa/server.js +30 -119
- package/dist/pwa/verifiability-index.js +63 -0
- package/dist/version.js +32 -0
- 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
|
-
|
|
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
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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.
|
|
241
|
-
if (
|
|
242
|
-
|
|
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.
|
|
275
|
-
if (
|
|
276
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
⚠️ **
|
|
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
|
-
⚠️ **
|
|
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
|
|
1509
|
-
//
|
|
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
|
-
//
|
|
1541
|
+
// 佣金机制 —— 纯功能性描述(怎么运作),不做"自证清白"式辩护。
|
|
1534
1542
|
commission_model: {
|
|
1535
|
-
split: '7:2:1 — L1 70% / L2 20% / L3 10% of commission_pool',
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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(
|
|
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 /
|
|
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
|
|
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 = ?,
|
|
183
|
-
|
|
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
|
+
}
|