@seasonkoh/webaz 0.1.16 → 0.1.17

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 (34) hide show
  1. package/README.md +60 -5
  2. package/dist/layer0-foundation/L0-2-state-machine/engine.js +3 -0
  3. package/dist/layer1-agent/L1-1-mcp-server/server.js +836 -716
  4. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +169 -0
  5. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +16 -0
  6. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +1 -0
  7. package/dist/mcp.js +7 -3
  8. package/dist/pwa/data/onboarding-cases.js +345 -0
  9. package/dist/pwa/data/onboarding-quiz.js +247 -0
  10. package/dist/pwa/public/app.js +1410 -96
  11. package/dist/pwa/public/i18n.js +280 -2
  12. package/dist/pwa/public/icon-192.png +0 -0
  13. package/dist/pwa/public/icon-512.png +0 -0
  14. package/dist/pwa/public/manifest.json +5 -2
  15. package/dist/pwa/public/openapi.json +1 -1
  16. package/dist/pwa/public/sw.js +1 -1
  17. package/dist/pwa/routes/admin-protocol-params.js +80 -2
  18. package/dist/pwa/routes/admin-reports.js +14 -9
  19. package/dist/pwa/routes/auth-read.js +3 -1
  20. package/dist/pwa/routes/build-feedback.js +67 -0
  21. package/dist/pwa/routes/disputes-write.js +149 -1
  22. package/dist/pwa/routes/governance-auto-deactivate.js +108 -0
  23. package/dist/pwa/routes/governance-onboarding.js +785 -0
  24. package/dist/pwa/routes/leaderboard.js +10 -2
  25. package/dist/pwa/routes/orders-action.js +5 -1
  26. package/dist/pwa/routes/products-meta.js +30 -0
  27. package/dist/pwa/routes/profile-identity.js +1 -1
  28. package/dist/pwa/routes/public-utils.js +44 -0
  29. package/dist/pwa/routes/rewards-apply.js +210 -0
  30. package/dist/pwa/routes/rewards-auto-downgrade.js +65 -0
  31. package/dist/pwa/routes/rewards-escrow-expire.js +48 -0
  32. package/dist/pwa/routes/webauthn.js +1 -1
  33. package/dist/pwa/server.js +570 -63
  34. package/package.json +6 -3
@@ -1,6 +1,6 @@
1
1
  import express from 'express';
2
2
  export function registerDisputesWriteRoutes(app, deps) {
3
- const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend } = deps;
3
+ const { db, auth, generateId, detectFraud, errorRes, isEligibleArbitrator, requireHumanPresence, getDisputeDetails, respondToDispute, arbitrateDispute, addPartyEvidence, requestEvidence, markEvidenceExpiry, uploadEvidence, EVIDENCE_MAX_BYTES, EVIDENCE_ALLOWED_MIME, appendOrderEvent, FUND_BASE_RATE, settleCommission, depositToFund, calculatePv, recordDisputeReputation, issueAgentStrike, publishDisputeCase, logAdminAction, snfSend, getProtocolParam } = deps;
4
4
  // 被诉方反驳
5
5
  app.post('/api/disputes/:id/respond', (req, res) => {
6
6
  const user = auth(req, res);
@@ -472,4 +472,152 @@ export function registerDisputesWriteRoutes(app, deps) {
472
472
  return void res.json({ error: result.error });
473
473
  res.json({ success: true, request_id: result.requestId });
474
474
  });
475
+ // ─── task #1093 stage 6: arbitrator_pause / resume auto_judge ─────
476
+ // Spec: docs/ARBITRATION-PLAYBOOK.md §2.1 (clock conflict resolution)
477
+ // Freezes the 48h respondent-silence + arbitrate_deadline clocks while
478
+ // arbitrator legitimately needs more time(e.g., evidence collection).
479
+ //
480
+ // Both endpoints require caller is one of dispute.assigned_arbitrators.
481
+ // Repause(extend) allowed — each pause writes an audit_log entry.
482
+ // No Iron-Rule Passkey: routine arbitrator action, fully audit-traceable.
483
+ function isAssignedArbitrator(disputeId, userId) {
484
+ const row = db.prepare(`SELECT assigned_arbitrators FROM disputes WHERE id = ?`).get(disputeId);
485
+ if (!row)
486
+ return false;
487
+ let arr = [];
488
+ try {
489
+ arr = JSON.parse(row.assigned_arbitrators || '[]');
490
+ }
491
+ catch {
492
+ arr = [];
493
+ }
494
+ return arr.includes(userId);
495
+ }
496
+ function appendAuditLog(disputeId, entry) {
497
+ // Append-only JSON array. Reads existing audit_log, appends, writes back.
498
+ const row = db.prepare(`SELECT audit_log FROM disputes WHERE id = ?`).get(disputeId);
499
+ let arr = [];
500
+ try {
501
+ arr = JSON.parse(row?.audit_log || '[]');
502
+ }
503
+ catch {
504
+ arr = [];
505
+ }
506
+ arr.push({ ...entry, at: Math.floor(Date.now() / 1000) });
507
+ db.prepare(`UPDATE disputes SET audit_log = ? WHERE id = ?`).run(JSON.stringify(arr), disputeId);
508
+ }
509
+ // task #1093 stage 6 P0 fix:pause 必须扩展 deadline,否则 cron 解冻后立即 auto-judge
510
+ // 且 /respond 端点硬查 deadline,暂停期间 respondent 提交反驳会被拒
511
+ function extendIsoDeadlineBySeconds(text, secondsToAdd) {
512
+ if (!text || secondsToAdd <= 0)
513
+ return text;
514
+ const ms = new Date(text).getTime();
515
+ if (isNaN(ms))
516
+ return text; // unparseable — leave as is
517
+ return new Date(ms + secondsToAdd * 1000).toISOString();
518
+ }
519
+ app.post('/api/disputes/:id/arbitrator-pause-auto-judge', (req, res) => {
520
+ const user = auth(req, res);
521
+ if (!user)
522
+ return;
523
+ const userId = user.id;
524
+ const disputeId = req.params.id;
525
+ const body = req.body || {};
526
+ const reason = String(body.reason || '').trim();
527
+ const untilTs = Number(body.until_ts || 0);
528
+ if (!reason || reason.length < 10) {
529
+ return void errorRes(res, 400, 'REASON_TOO_SHORT', '暂停理由至少 10 字符(写入 audit_log 公示)');
530
+ }
531
+ if (!untilTs || untilTs <= Math.floor(Date.now() / 1000)) {
532
+ return void errorRes(res, 400, 'INVALID_UNTIL_TS', 'until_ts 必须是未来 epoch 秒');
533
+ }
534
+ const maxHours = Number(getProtocolParam('arbitration_max_pause_hours', 168));
535
+ const maxAllowed = Math.floor(Date.now() / 1000) + maxHours * 3600;
536
+ if (untilTs > maxAllowed) {
537
+ return void errorRes(res, 400, 'EXCEEDS_MAX_HOURS', `until_ts 超过最大暂停窗口 ${maxHours}h(playbook §2.1)`);
538
+ }
539
+ const dispute = db.prepare(`SELECT id, status, ruling_type, assigned_arbitrators, auto_judge_paused_until, respond_deadline, arbitrate_deadline FROM disputes WHERE id = ?`).get(disputeId);
540
+ if (!dispute)
541
+ return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
542
+ if (dispute.ruling_type) {
543
+ return void errorRes(res, 409, 'ALREADY_RULED', '已裁决的 dispute 不能暂停自动判定时钟');
544
+ }
545
+ if (dispute.status !== 'open' && dispute.status !== 'in_review') {
546
+ return void errorRes(res, 409, 'WRONG_STATUS', `status='${dispute.status}',只能 pause open / in_review`);
547
+ }
548
+ if (!isAssignedArbitrator(disputeId, userId)) {
549
+ return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可暂停自动判定时钟');
550
+ }
551
+ // P0 fix:计算 deadline 扩展秒数
552
+ // - 首次 pause:increment = untilTs - now
553
+ // - repause(已经 paused):increment = untilTs - existing_paused_until(可能 < 0,clamp 0)
554
+ // 这样多次 pause 累加正确;repause 缩短无效果(只 audit_log 记)
555
+ const nowSec = Math.floor(Date.now() / 1000);
556
+ const baseline = dispute.auto_judge_paused_until && dispute.auto_judge_paused_until > nowSec
557
+ ? dispute.auto_judge_paused_until
558
+ : nowSec;
559
+ const incrementSec = Math.max(0, untilTs - baseline);
560
+ db.transaction(() => {
561
+ // 扩展 deadline(若 increment > 0)
562
+ let newRespondDeadline = dispute.respond_deadline;
563
+ let newArbitrateDeadline = dispute.arbitrate_deadline;
564
+ if (incrementSec > 0) {
565
+ newRespondDeadline = extendIsoDeadlineBySeconds(dispute.respond_deadline, incrementSec);
566
+ newArbitrateDeadline = extendIsoDeadlineBySeconds(dispute.arbitrate_deadline, incrementSec);
567
+ db.prepare(`UPDATE disputes SET respond_deadline = ?, arbitrate_deadline = ? WHERE id = ?`)
568
+ .run(newRespondDeadline, newArbitrateDeadline, disputeId);
569
+ }
570
+ db.prepare(`UPDATE disputes SET auto_judge_paused_until = ?, auto_judge_pause_reason = ? WHERE id = ?`)
571
+ .run(untilTs, reason, disputeId);
572
+ appendAuditLog(disputeId, {
573
+ event: 'arbitrator_pause_auto_judge',
574
+ actor: userId,
575
+ reason,
576
+ until_ts: untilTs,
577
+ deadline_extended_seconds: incrementSec,
578
+ is_repause: dispute.auto_judge_paused_until !== null && dispute.auto_judge_paused_until > nowSec,
579
+ spec_ref: 'playbook §2.1',
580
+ });
581
+ })();
582
+ res.json({
583
+ success: true,
584
+ dispute_id: disputeId,
585
+ paused_until: untilTs,
586
+ paused_until_iso: new Date(untilTs * 1000).toISOString(),
587
+ max_hours: maxHours,
588
+ deadline_extended_seconds: incrementSec,
589
+ note: incrementSec > 0
590
+ ? `自动判定时钟已冻结,respond/arbitrate deadline 已延后 ${Math.round(incrementSec / 3600)}h。补证据期满或证据齐全后请显式 resume。`
591
+ : 'pause 已记录(repause 缩短无 deadline 变化)。',
592
+ });
593
+ });
594
+ app.post('/api/disputes/:id/arbitrator-resume-auto-judge', (req, res) => {
595
+ const user = auth(req, res);
596
+ if (!user)
597
+ return;
598
+ const userId = user.id;
599
+ const disputeId = req.params.id;
600
+ const dispute = db.prepare(`SELECT id, ruling_type, auto_judge_paused_until FROM disputes WHERE id = ?`).get(disputeId);
601
+ if (!dispute)
602
+ return void errorRes(res, 404, 'NOT_FOUND', 'dispute 不存在');
603
+ if (dispute.ruling_type) {
604
+ return void errorRes(res, 409, 'ALREADY_RULED', '已裁决的 dispute 不需 resume');
605
+ }
606
+ if (!dispute.auto_judge_paused_until) {
607
+ return void errorRes(res, 409, 'NOT_PAUSED', '当前未暂停,无需 resume');
608
+ }
609
+ if (!isAssignedArbitrator(disputeId, userId)) {
610
+ return void errorRes(res, 403, 'NOT_ASSIGNED_ARBITRATOR', '仅 assigned_arbitrators 可 resume');
611
+ }
612
+ db.transaction(() => {
613
+ db.prepare(`UPDATE disputes SET auto_judge_paused_until = NULL, auto_judge_pause_reason = NULL WHERE id = ?`)
614
+ .run(disputeId);
615
+ appendAuditLog(disputeId, {
616
+ event: 'arbitrator_resume_auto_judge',
617
+ actor: userId,
618
+ spec_ref: 'playbook §2.1',
619
+ });
620
+ })();
621
+ res.json({ success: true, dispute_id: disputeId, note: '自动判定时钟已解冻' });
622
+ });
475
623
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Run one auto-deactivate sweep. Returns deactivation report for caller
3
+ * (cron caller logs to console; admin endpoint can call directly for query).
4
+ */
5
+ export function runAutoDeactivateSweep(deps) {
6
+ const { db, generateId, getProtocolParam } = deps;
7
+ const thresholdCount = Number(getProtocolParam('governance_auto_deactivate_threshold_count', 5));
8
+ const thresholdPct = Number(getProtocolParam('governance_auto_deactivate_threshold_pct', 0.3));
9
+ const minSample = Number(getProtocolParam('governance_auto_deactivate_min_sample', 10));
10
+ const cooldownDays = Number(getProtocolParam('governance_resign_cooldown_days', 30));
11
+ // Candidates: users with role 'verifier' in users.roles JSON
12
+ // + verifier_stats with tasks_done ≥ min_sample
13
+ // + tasks_wrong ≥ threshold_count
14
+ // + tasks_wrong/tasks_done ≥ threshold_pct
15
+ // (verifier_stats is the source-of-truth for confirmed_wrong; server.ts:5387 increments it
16
+ // on overturn, admin-verifier-flow.ts:130 decrements it on appeal success — exactly the
17
+ // "confirmed_wrong" signal playbook §6.2 requires.)
18
+ const candidates = db.prepare(`
19
+ SELECT u.id AS user_id, u.roles,
20
+ vs.tasks_done, vs.tasks_wrong
21
+ FROM users u
22
+ JOIN verifier_stats vs ON vs.user_id = u.id
23
+ WHERE u.roles IS NOT NULL
24
+ AND u.roles LIKE '%"verifier"%'
25
+ AND vs.tasks_done >= ?
26
+ AND vs.tasks_wrong >= ?
27
+ AND (CAST(vs.tasks_wrong AS REAL) / CAST(vs.tasks_done AS REAL)) >= ?
28
+ `).all(minSample, thresholdCount, thresholdPct);
29
+ const result = { scanned: candidates.length, deactivated: [] };
30
+ const cooldownUntil = Math.floor(Date.now() / 1000) + cooldownDays * 86400;
31
+ for (const c of candidates) {
32
+ // Recheck role inside transaction (idempotency + race safety)
33
+ try {
34
+ let didDeactivate = false;
35
+ db.transaction(() => {
36
+ const u = db.prepare("SELECT roles FROM users WHERE id = ?").get(c.user_id);
37
+ let roles = [];
38
+ try {
39
+ roles = JSON.parse(u?.roles || '[]');
40
+ }
41
+ catch {
42
+ roles = [];
43
+ }
44
+ if (!roles.includes('verifier'))
45
+ return; // already deactivated
46
+ const wrongPct = c.tasks_wrong / c.tasks_done;
47
+ const reason = `confirmed_wrong_count=${c.tasks_wrong}/${c.tasks_done} (${(wrongPct * 100).toFixed(1)}%) ≥ threshold (count=${thresholdCount}, pct=${(thresholdPct * 100).toFixed(0)}%, min_sample=${minSample})`;
48
+ // 1. UPDATE all active rows → inactive
49
+ db.prepare("UPDATE governance_applications SET status = 'inactive' WHERE user_id = ? AND role = 'verifier' AND status = 'active'").run(c.user_id);
50
+ // 2. INSERT auto_deactivate row(audit + appeal source)
51
+ const id = generateId('gapp');
52
+ db.prepare(`
53
+ INSERT INTO governance_applications
54
+ (id, user_id, role, action, status, cooldown_until, appeal_reason)
55
+ VALUES (?, ?, 'verifier', 'auto_deactivate', 'inactive', ?, ?)
56
+ `).run(id, c.user_id, cooldownUntil, reason);
57
+ // Note: appeal_reason field stores the trigger reason (overloaded but keeps schema flat).
58
+ // The user's appeal text(if they file one)goes on a separate appeal row(action='appeal').
59
+ // 3. Remove from users.roles JSON
60
+ const newRoles = roles.filter(r => r !== 'verifier');
61
+ db.prepare("UPDATE users SET roles = ? WHERE id = ?").run(JSON.stringify(newRoles), c.user_id);
62
+ // 4. Notify user with appeal link
63
+ try {
64
+ db.prepare(`INSERT INTO notifications (id, user_id, type, title, body, order_id) VALUES (?,?,?,?,?,?)`)
65
+ .run(generateId('ntf'), c.user_id, 'governance', '⚠️ 你的 verifier 资格已被自动卸任 / Verifier role auto-deactivated', `${reason}\n\nspec docs/GOVERNANCE-ONBOARDING.md §6.2 §7.2:14 天内可在 #governance-me 提交申诉。\n14-day appeal window opens. Submit appeal at #governance-me.`, null);
66
+ }
67
+ catch (_e) { /* notification failure must not block deactivate */ }
68
+ didDeactivate = true;
69
+ result.deactivated.push({
70
+ user_id: c.user_id,
71
+ role: 'verifier',
72
+ tasks_done: c.tasks_done,
73
+ tasks_wrong: c.tasks_wrong,
74
+ wrong_pct: wrongPct,
75
+ reason,
76
+ });
77
+ })();
78
+ void didDeactivate;
79
+ }
80
+ catch (e) {
81
+ console.error('[governance-auto-deactivate] error for user', c.user_id, e);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+ /**
87
+ * Boot the cron. Registers an interval timer based on the
88
+ * governance_auto_deactivate_cron_hours protocol param value.
89
+ *
90
+ * Does NOT run an immediate sweep on boot — phase A solo maintainer
91
+ * wants to observe explicitly via admin endpoint first.
92
+ */
93
+ export function startAutoDeactivateCron(deps) {
94
+ const hours = Number(deps.getProtocolParam('governance_auto_deactivate_cron_hours', 24));
95
+ const ms = Math.max(1, hours) * 60 * 60 * 1000;
96
+ setInterval(() => {
97
+ try {
98
+ const r = runAutoDeactivateSweep(deps);
99
+ if (r.deactivated.length > 0) {
100
+ console.log(`[gov-auto-deactivate] swept ${r.scanned} candidates, deactivated ${r.deactivated.length}:`, r.deactivated.map(d => `${d.user_id}(${d.tasks_wrong}/${d.tasks_done})`).join(', '));
101
+ }
102
+ }
103
+ catch (e) {
104
+ console.error('[gov-auto-deactivate-cron]', e);
105
+ }
106
+ }, ms);
107
+ console.log(`⚖️ governance auto-deactivate cron 已启动 (每 ${hours}h, anchor=confirmed_wrong per playbook §6.2)`);
108
+ }