@seasonkoh/webaz 0.1.26 → 0.1.27

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 (81) hide show
  1. package/LICENSE +2 -2
  2. package/NOTICE +24 -3
  3. package/README.md +74 -330
  4. package/README.zh-CN.md +419 -0
  5. package/dist/layer0-foundation/L0-2-state-machine/genuine-sale.js +21 -0
  6. package/dist/layer0-foundation/L0-5-manifest/manifest.js +8 -3
  7. package/dist/layer1-agent/L1-1-mcp-server/auth.js +13 -1
  8. package/dist/layer1-agent/L1-1-mcp-server/server.js +36 -28
  9. package/dist/layer2-business/L2-9-contribution/admin-coordination-ingestion-engine.js +181 -0
  10. package/dist/layer2-business/L2-9-contribution/admin-coordination-resolver.js +114 -0
  11. package/dist/layer2-business/L2-9-contribution/admin-coordination-store.js +251 -0
  12. package/dist/layer2-business/L2-9-contribution/admin-operator-claim-workflow.js +390 -0
  13. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +24 -0
  14. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +6 -2
  15. package/dist/layer2-business/L2-9-contribution/build-task-quota.js +337 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-read.js +25 -2
  17. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +57 -7
  18. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +1 -1
  19. package/dist/layer2-business/L2-9-contribution/contribution-facts-read.js +66 -0
  20. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +187 -18
  21. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +29 -4
  22. package/dist/ledger.js +1 -1
  23. package/dist/pwa/admin-audit.js +38 -0
  24. package/dist/pwa/anti-abuse-thresholds.js +135 -0
  25. package/dist/pwa/cf-origin-guard.js +33 -0
  26. package/dist/pwa/contract-fingerprint.js +1 -0
  27. package/dist/pwa/data/onboarding-cases.js +2 -2
  28. package/dist/pwa/data/onboarding-quiz.js +1 -1
  29. package/dist/pwa/economic-participation.js +2 -2
  30. package/dist/pwa/integration-contract.js +46 -4
  31. package/dist/pwa/internal/pv-settlement.js +12 -0
  32. package/dist/pwa/internal/wallet-signer.js +26 -0
  33. package/dist/pwa/public/app.js +679 -679
  34. package/dist/pwa/public/i18n.js +15 -28
  35. package/dist/pwa/public/index.html +1 -1
  36. package/dist/pwa/public/openapi.json +4760 -2769
  37. package/dist/pwa/pv-kill-switch.js +31 -0
  38. package/dist/pwa/routes/admin-admins.js +48 -1
  39. package/dist/pwa/routes/admin-analytics.js +1 -10
  40. package/dist/pwa/routes/admin-atomic.js +4 -17
  41. package/dist/pwa/routes/admin-operator-claims.js +280 -0
  42. package/dist/pwa/routes/admin-reports.js +4 -26
  43. package/dist/pwa/routes/admin-tokenomics.js +2 -76
  44. package/dist/pwa/routes/admin-users-lifecycle.js +1 -14
  45. package/dist/pwa/routes/admin-users-query.js +23 -1
  46. package/dist/pwa/routes/admin-wallet-ops.js +1 -1
  47. package/dist/pwa/routes/auth-read.js +1 -5
  48. package/dist/pwa/routes/auth-register.js +3 -13
  49. package/dist/pwa/routes/build-task-quota.js +113 -0
  50. package/dist/pwa/routes/claim-verify.js +15 -11
  51. package/dist/pwa/routes/contribution-facts.js +18 -0
  52. package/dist/pwa/routes/dispute-cases.js +5 -4
  53. package/dist/pwa/routes/growth.js +3 -3
  54. package/dist/pwa/routes/orders-action.js +27 -10
  55. package/dist/pwa/routes/orders-create.js +1 -1
  56. package/dist/pwa/routes/products-meta.js +19 -6
  57. package/dist/pwa/routes/profile-placement.js +1 -1
  58. package/dist/pwa/routes/promoter.js +10 -29
  59. package/dist/pwa/routes/public-build-tasks.js +5 -1
  60. package/dist/pwa/routes/public-utils.js +9 -12
  61. package/dist/pwa/routes/referral.js +5 -26
  62. package/dist/pwa/routes/rewards-apply.js +3 -2
  63. package/dist/pwa/routes/share-redirects.js +1 -1
  64. package/dist/pwa/routes/shareables-interactions.js +2 -1
  65. package/dist/pwa/routes/task-proposals.js +85 -9
  66. package/dist/pwa/routes/users-public.js +1 -4
  67. package/dist/pwa/routes/wallet-read.js +2 -14
  68. package/dist/pwa/routes/webauthn.js +1 -1
  69. package/dist/pwa/server.js +156 -469
  70. package/dist/settlement-math.js +3 -3
  71. package/dist/version.js +6 -4
  72. package/package.json +33 -7
  73. package/dist/index.js +0 -182
  74. package/dist/pwa/public/docs/ECONOMIC-MODEL.md +0 -287
  75. package/dist/pwa/public/docs/INTEGRATOR.md +0 -67
  76. package/dist/pwa/public/docs/META-RULES-FULL.md +0 -543
  77. package/dist/test-dispute.js +0 -153
  78. package/dist/test-manifest.js +0 -61
  79. package/dist/test-mcp-tools.js +0 -135
  80. package/dist/test-reputation.js +0 -116
  81. package/dist/test-skill-market.js +0 -101
@@ -66,12 +66,10 @@ export function registerPublicUtilsRoutes(app, deps) {
66
66
  });
67
67
  app.get('/api/system-flags', async (_req, res) => {
68
68
  const requireRef = (await dbOne("SELECT value FROM system_state WHERE key='require_ref_to_register'"))?.value === '1';
69
- const inviteRotation = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
70
69
  // #1049 Turnstile 公钥(若启用),前端注册表单 widget 用
71
70
  const turnstileSiteKey = process.env.TURNSTILE_SITE_KEY || null;
72
71
  res.json({
73
72
  require_ref_to_register: requireRef,
74
- invite_rotation_enabled: inviteRotation,
75
73
  turnstile_site_key: turnstileSiteKey,
76
74
  });
77
75
  });
@@ -134,13 +132,12 @@ export function registerPublicUtilsRoutes(app, deps) {
134
132
  },
135
133
  // 公开披露文档(#1050) — 协议层"钱怎么流"的源真理(协议外可读)
136
134
  disclosures: {
137
- // 源码仓库 launch 前私有,下列 github 链接可能 404(launch 时开),不是死项目;机器可读 spec 已全在 /.well-known/*。
138
- source_status: 'repo private until W8 public launch — github.com links below may 404 until then (they open at launch); the full spec is already public via /.well-known/*.',
139
- economic_model: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ECONOMIC-MODEL.md',
140
- mlm_compliance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/MLM-COMPLIANCE.md',
141
- agent_governance: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
142
- protocol_compatibility: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/PROTOCOL-COMPATIBILITY-AUDIT-2026-05-30.md',
143
- changelog: 'https://github.com/seasonsagents-art/webaz/blob/main/CHANGELOG.md',
135
+ // 源码仓库已公开(github.com/webaz-protocol/webaz);机器可读 spec 也全在 /.well-known/*。
136
+ source_status: 'repo is public (github.com/webaz-protocol/webaz); the full spec is also available via /.well-known/*.',
137
+ economic_model: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ECONOMIC-MODEL.md',
138
+ mlm_compliance: 'https://github.com/webaz-protocol/webaz/blob/main/docs/PARTICIPATION-ATTRIBUTION-COMPLIANCE.md',
139
+ agent_governance: 'https://github.com/webaz-protocol/webaz/blob/main/docs/AGENT-GOVERNANCE.md',
140
+ changelog: 'https://github.com/webaz-protocol/webaz/blob/main/CHANGELOG.md',
144
141
  // RFC-011 §②:agent 可读能力矩阵(写边界 action-scope + 敏感读 scope),live doc=code
145
142
  capability_matrix: 'https://webaz.xyz/.well-known/webaz-capabilities.json',
146
143
  },
@@ -457,9 +454,9 @@ export function registerPublicUtilsRoutes(app, deps) {
457
454
  eligibility,
458
455
  quiz_pass_score: quizPassScore,
459
456
  spec_urls: {
460
- onboarding: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md',
461
- playbook: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md',
462
- leaderboard: 'https://github.com/seasonsagents-art/webaz/blob/main/docs/GOVERNANCE-LEADERBOARD-SPEC.md',
457
+ onboarding: 'https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-ONBOARDING.md',
458
+ playbook: 'https://github.com/webaz-protocol/webaz/blob/main/docs/ARBITRATION-PLAYBOOK.md',
459
+ leaderboard: 'https://github.com/webaz-protocol/webaz/blob/main/docs/GOVERNANCE-LEADERBOARD-SPEC.md',
463
460
  },
464
461
  });
465
462
  }
@@ -1,7 +1,8 @@
1
- import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
1
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
2
3
  export function registerReferralRoutes(app, deps) {
3
4
  // db 已全量走 RFC-016 异步 seam(dbOne/dbAll/dbRun),不再直接用 deps.db
4
- const { auth, requireProtocolAdmin, logAdminAction, issueInviteSlot, inviteRotationLookup } = deps;
5
+ const { auth } = deps;
5
6
  // B-1: 个人邀请 dashboard
6
7
  app.get('/api/referral/me', async (req, res) => {
7
8
  const user = auth(req, res);
@@ -36,28 +37,6 @@ export function registerReferralRoutes(app, deps) {
36
37
  },
37
38
  });
38
39
  });
39
- // 公开邀请码轮询(开关 ON 时)
40
- app.post('/api/invite/rotate', async (_req, res) => {
41
- const enabled = (await dbOne("SELECT value FROM system_state WHERE key='invite_rotation_enabled'"))?.value === '1';
42
- if (!enabled)
43
- return void res.status(403).json({ error: '邀请码获取暂未开放', enabled: false });
44
- const slot = issueInviteSlot();
45
- const u = inviteRotationLookup(slot);
46
- if (!u)
47
- return void res.status(503).json({ error: `轮询用户未就绪,请联系管理员`, enabled: true });
48
- res.json({ enabled: true, code: u.code });
49
- });
50
- // protocol 开关
51
- app.post('/api/admin/invite-rotation/toggle', async (req, res) => {
52
- const admin = requireProtocolAdmin(req, res);
53
- if (!admin)
54
- return;
55
- const { enabled } = req.body;
56
- const v = enabled ? '1' : '0';
57
- await dbRun("INSERT OR REPLACE INTO system_state (key, value) VALUES ('invite_rotation_enabled', ?)", [v]);
58
- logAdminAction(admin.id, 'invite_rotation_toggle', 'system', 'invite_rotation_enabled', { value: v });
59
- res.json({ success: true, enabled: !!enabled });
60
- });
61
40
  // RFC-003 #1122: 生成商品分享链接(把 MCP webaz_share_link 的本地计算搬到服务端,
62
41
  // 让 MCP NETWORK 模式可代理)。RFC-002 §3.5 valuation-layer gate:需 rewards opt-in。
63
42
  // pre-public 去左右码:不再接受/返回 side,放置侧别由注册时系统自动决定。
@@ -77,7 +56,7 @@ export function registerReferralRoutes(app, deps) {
77
56
  };
78
57
  const minOrders = await getParam('rewards_opt_in.min_completed_orders', 1);
79
58
  const requirePasskey = await getParam('rewards_opt_in.require_passkey', 1);
80
- const totalCompleted = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
59
+ const totalCompleted = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
81
60
  const passkeyCount = (await dbOne("SELECT COUNT(*) as n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
82
61
  const missing = [];
83
62
  if (totalCompleted < minOrders)
@@ -101,7 +80,7 @@ export function registerReferralRoutes(app, deps) {
101
80
  if (!product)
102
81
  return void res.status(404).json({ error: '商品不存在或已下架', error_code: 'PRODUCT_NOT_FOUND' });
103
82
  // pre-public 去左右码:分享链接不再计算/携带 side(放置侧别由注册时系统自动决定)
104
- const completed = (await dbOne("SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
83
+ const completed = (await dbOne(`SELECT COUNT(*) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
105
84
  const override = (await dbOne("SELECT l1_share_override FROM users WHERE id = ?", [userId]))?.l1_share_override ?? 0;
106
85
  const canL1 = override === 1 || (override === 0 && completed > 0);
107
86
  const rate = Number(product.commission_rate ?? 0);
@@ -1,5 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
3
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
3
4
  function sha256_hex(s) {
4
5
  return createHash('sha256').update(s).digest('hex');
5
6
  }
@@ -26,7 +27,7 @@ export function registerRewardsApplyRoutes(app, deps) {
26
27
  state = 'auto_downgraded';
27
28
  else
28
29
  state = 'never_activated';
29
- const completedOrders = (await dbOne("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
30
+ const completedOrders = (await dbOne(`SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
30
31
  const passkeyCount = (await dbOne("SELECT COUNT(*) AS n FROM webauthn_credentials WHERE user_id = ?", [userId])).n;
31
32
  const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
32
33
  const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
@@ -103,7 +104,7 @@ export function registerRewardsApplyRoutes(app, deps) {
103
104
  }
104
105
  // 5. Pre-conditions (re-check inside server)
105
106
  const minOrders = Number(getProtocolParam('rewards_opt_in.min_completed_orders', 1));
106
- const completedOrders = (await dbOne("SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND status = 'completed'", [userId])).n;
107
+ const completedOrders = (await dbOne(`SELECT COUNT(*) AS n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [userId])).n; // 真实成交,排除退款/违约
107
108
  if (completedOrders < minOrders)
108
109
  return void errorRes(res, 403, 'INSUFFICIENT_ORDERS', `需 ${minOrders} 笔已完成订单,目前 ${completedOrders}`);
109
110
  const requirePasskey = Number(getProtocolParam('rewards_opt_in.require_passkey', 1));
@@ -158,7 +158,7 @@ export function registerShareRedirectsRoutes(app, deps) {
158
158
  // 邀请短链 /i/CODE — invite-code ONLY (permanent_code, 兼容旧的 -L/-R 后缀). usr_xxx / @handle / 裸 handle
159
159
  // 一律 404(不再做 handle 解析)。pre-public 去左右码:/i/CODE 与旧 /i/CODE-L、/i/CODE-R 一律
160
160
  // 规范化重定向到 /?ref=CODE(丢弃 side;放置侧别由注册时系统自动决定),旧链接/二维码仍可用。
161
- // 不受 invite_rotation_enabled 影响:已有用户分享出的 /i/CODE 链接和二维码必须始终可用。
161
+ // 不受注册门控开关影响:已有用户分享出的 /i/CODE 链接和二维码必须始终可用。
162
162
  app.get('/i/:code', (req, res) => {
163
163
  const ref = resolveInviteCodeRef(String(req.params.code || ''));
164
164
  if (!ref)
@@ -1,4 +1,5 @@
1
1
  import { dbOne, dbAll, dbRun } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 DB seam
2
+ import { genuineSalePredicate } from '../../layer0-foundation/L0-2-state-machine/genuine-sale.js'; // 真实成交单一真相源
2
3
  export function registerShareablesInteractionsRoutes(app, deps) {
3
4
  const { db, auth, generateId, rateLimitOk, piiSanitize, detectFraud, commentBlocklistHit, llmModerateComment, parseMentions, notifyMentions } = deps;
4
5
  app.post('/api/shareables/:id/click', async (req, res) => {
@@ -21,7 +22,7 @@ export function registerShareablesInteractionsRoutes(app, deps) {
21
22
  if (sh.owner_id === user.id)
22
23
  return void res.json({ error: '不能给自己点赞' });
23
24
  // P1 Sybil 软门槛:至少完成过 1 笔订单(不限购买该商品,只需活跃用户)
24
- const completed = (await dbOne("SELECT COUNT(1) as n FROM orders WHERE buyer_id = ? AND status = 'completed'", [user.id])).n;
25
+ const completed = (await dbOne(`SELECT COUNT(1) as n FROM orders WHERE buyer_id = ? AND ${genuineSalePredicate('orders')}`, [user.id])).n; // 真实成交,排除退款/违约
25
26
  if (completed < 1)
26
27
  return void res.json({ error: '完成首笔购买后才能点赞(防止刷赞)' });
27
28
  // P0 fix:SELECT existing 进 transaction
@@ -1,15 +1,25 @@
1
- import { validateProposalInput, insertTaskProposal, listTaskProposals, reviewTaskProposal } from '../../layer2-business/L2-9-contribution/task-proposal-store.js';
1
+ import { validateProposalInput, insertTaskProposal, listTaskProposals, listMyProposals, reviewTaskProposal } from '../../layer2-business/L2-9-contribution/task-proposal-store.js';
2
2
  import { withUncommittedValueBoundary } from '../../layer2-business/L2-9-contribution/contribution-display-envelope.js';
3
3
  import { getCanonicalContributionTarget } from '../../layer2-business/L2-9-contribution/canonical-contribution-target.js';
4
- import { createDraftFromProposal, listDraftBuildTasks, publishDraftBuildTask } from '../../layer2-business/L2-9-contribution/task-proposal-draft.js';
4
+ import { createDraftFromProposal, listDraftBuildTasks, getDraftBuildTaskDetail, publishDraftBuildTask, discardDraft, withdrawPublishedTask } from '../../layer2-business/L2-9-contribution/task-proposal-draft.js';
5
5
  import { recommendForProposal, insertAiSuggestion, listAiSuggestions, getProposalLite } from '../../layer2-business/L2-9-contribution/task-proposal-ai-store.js';
6
6
  const AI_NOTICE = 'AI suggestion — assistant only, NOT a decision. A human maintainer must explicitly create / publish / reject the formal task. AI never auto-publishes, auto-rejects, hides proposals, or assigns reward / credit.';
7
7
  const PROPOSAL_NOTICE = 'A task proposal is a SUGGESTION in the maintainer review inbox. It is NOT a contribution fact, formal participation, or any reward / payout / score, and it never appears on the public task board until a maintainer reviews and (manually) converts it. source_ref is a reference only; the canonical contribution target is fixed by trusted config.';
8
8
  function withProposalEnvelope(payload) {
9
9
  return withUncommittedValueBoundary({ ...payload, proposal_notice: PROPOSAL_NOTICE, canonical_contribution_target: getCanonicalContributionTarget() });
10
10
  }
11
+ // Agent-readable next-step hint per status (the channel is agent-native both ways).
12
+ function nextActionFor(status) {
13
+ switch (status) {
14
+ case 'new': return 'Awaiting maintainer review — no action needed.';
15
+ case 'needs_info': return 'Maintainer needs more detail. Submit an updated proposal (webaz_feedback type=proposal) referencing this id; see public_reply for what is missing.';
16
+ case 'rejected': return 'Not converted to a task. See public_reply for the reason; you may submit a revised proposal.';
17
+ case 'converted': return 'Converted to a task. See converted_ref for the linked task / PR / decision.';
18
+ default: return 'No action.';
19
+ }
20
+ }
11
21
  export function registerTaskProposalsRoutes(app, deps) {
12
- const { db, errorRes, requireSupportAdmin, rateLimitOk } = deps;
22
+ const { db, errorRes, requireSupportAdmin, rateLimitOk, auth, resolveUser } = deps;
13
23
  // public submit — anonymous; proposer_account_id is never taken from the body (anti-spoof).
14
24
  app.post('/api/public/task-proposals', (req, res) => {
15
25
  // anti-flood: per-IP rate limit (counts every attempt, before validation) then a recent-window dedup.
@@ -19,10 +29,23 @@ export function registerTaskProposalsRoutes(app, deps) {
19
29
  const v = validateProposalInput(req.body);
20
30
  if (!v.ok)
21
31
  return void errorRes(res, 400, v.code, v.message);
22
- const result = insertTaskProposal(db, v.input, null);
32
+ // Link to the submitter when authenticated (account id comes from the session, NEVER the body — anti-spoof);
33
+ // anonymous submit still works (account id null) — it just won't show up in "my proposals".
34
+ const submitter = resolveUser(req);
35
+ const accountId = submitter ? String(submitter.id) : null;
36
+ const result = insertTaskProposal(db, v.input, accountId);
23
37
  if ('duplicate' in result)
24
38
  return void errorRes(res, 409, 'DUPLICATE_PROPOSAL', '相同建议已在收件箱中,请勿重复提交', { existing_id: result.existing_id });
25
- res.json(withProposalEnvelope({ proposal: { id: result.id, status: result.status } }));
39
+ res.json(withProposalEnvelope({ proposal: { id: result.id, status: result.status }, linked_to_account: !!accountId }));
40
+ });
41
+ // proposer-facing read: the caller's OWN proposals + status + public_reply (agent-readable). No review_note; own rows only.
42
+ app.get('/api/me/task-proposals', (req, res) => {
43
+ const user = auth(req, res);
44
+ if (!user)
45
+ return;
46
+ // case_id threads the whole case (proposal → task → PR). For a proposal it is the proposal's own id.
47
+ const proposals = listMyProposals(db, String(user.id)).map(p => ({ ...p, case_id: p.id, next_action: nextActionFor(String(p.status)) }));
48
+ res.json(withProposalEnvelope({ proposals }));
26
49
  });
27
50
  // admin list (maintainer only)
28
51
  app.get('/api/admin/task-proposals', (req, res) => {
@@ -30,15 +53,17 @@ export function registerTaskProposalsRoutes(app, deps) {
30
53
  if (!admin)
31
54
  return;
32
55
  const status = typeof req.query.status === 'string' ? req.query.status : undefined;
33
- res.json(withProposalEnvelope({ proposals: listTaskProposals(db, { status }) }));
56
+ // case_id = the proposal's own id (the case originates at the proposal); shown in the management inbox.
57
+ const proposals = listTaskProposals(db, { status }).map(p => ({ ...p, case_id: p.id }));
58
+ res.json(withProposalEnvelope({ proposals }));
34
59
  });
35
60
  // admin review (maintainer only): needs_info | rejected | converted — no build_task is created here.
36
61
  app.post('/api/admin/task-proposals/:id/review', (req, res) => {
37
62
  const admin = requireSupportAdmin(req, res);
38
63
  if (!admin)
39
64
  return;
40
- const { status, note, converted_ref } = req.body ?? {};
41
- const result = reviewTaskProposal(db, String(req.params.id), admin.id, String(status), note, converted_ref);
65
+ const { status, note, converted_ref, public_reply } = req.body ?? {};
66
+ const result = reviewTaskProposal(db, String(req.params.id), admin.id, String(status), note, converted_ref, public_reply);
42
67
  if ('error' in result) {
43
68
  const code = result.code === 'NOT_FOUND' ? 404 : result.code === 'ALREADY_TERMINAL' ? 409 : 400;
44
69
  return void errorRes(res, code, result.code, result.error);
@@ -90,6 +115,11 @@ export function registerTaskProposalsRoutes(app, deps) {
90
115
  definitionOfDone: b.definition_of_done ?? null,
91
116
  expectedResults: b.expected_results ?? null,
92
117
  autoClaimable: b.auto_claimable === false ? false : undefined,
118
+ // optional real effort estimate (#34/#5); if omitted the draft is a 0–0 placeholder and publish is fail-closed.
119
+ estimatedDurationMinMinutes: typeof b.estimated_duration_min_minutes === 'number' ? b.estimated_duration_min_minutes : undefined,
120
+ estimatedDurationMaxMinutes: typeof b.estimated_duration_max_minutes === 'number' ? b.estimated_duration_max_minutes : undefined,
121
+ estimatedAgentBudget: b.estimated_agent_budget,
122
+ estimatedContextSize: b.estimated_context_size,
93
123
  riskLevel: b.risk_level, taskType: b.task_type, note: b.note ?? null,
94
124
  });
95
125
  if ('error' in r) {
@@ -105,12 +135,30 @@ export function registerTaskProposalsRoutes(app, deps) {
105
135
  return;
106
136
  res.json(withProposalEnvelope({ drafts: listDraftBuildTasks(db) }));
107
137
  });
138
+ // full stored body of ONE unpublished internal draft — for PRE-PUBLISH PREVIEW (publish against visible content).
139
+ app.get('/api/admin/build-task-drafts/:id', (req, res) => {
140
+ const admin = requireSupportAdmin(req, res);
141
+ if (!admin)
142
+ return;
143
+ const draft = getDraftBuildTaskDetail(db, String(req.params.id));
144
+ if (!draft)
145
+ return void errorRes(res, 404, 'NOT_FOUND', 'draft not found (or not an unpublished internal draft)');
146
+ res.json(withProposalEnvelope({ draft }));
147
+ });
108
148
  // PUBLISH a draft → public open task — explicit human/admin action; records the acting admin
109
149
  app.post('/api/admin/build-task-drafts/:id/publish', (req, res) => {
110
150
  const admin = requireSupportAdmin(req, res);
111
151
  if (!admin)
112
152
  return;
113
- const r = publishDraftBuildTask(db, String(req.params.id), admin.id);
153
+ const b = (req.body ?? {});
154
+ // a published task MUST carry a real effort estimate (#34/#5). The maintainer supplies it here when the
155
+ // draft is still a 0–0 placeholder; without it publishDraftBuildTask fails closed (DRAFT_ESTIMATE_REQUIRED).
156
+ const r = publishDraftBuildTask(db, String(req.params.id), admin.id, {
157
+ minMinutes: typeof b.estimated_duration_min_minutes === 'number' ? b.estimated_duration_min_minutes : undefined,
158
+ maxMinutes: typeof b.estimated_duration_max_minutes === 'number' ? b.estimated_duration_max_minutes : undefined,
159
+ budget: b.estimated_agent_budget,
160
+ contextSize: b.estimated_context_size,
161
+ });
114
162
  if ('error' in r) {
115
163
  const code = r.error_code === 'NOT_FOUND' ? 404
116
164
  : (r.error_code === 'PROPOSAL_REJECTED' || r.error_code === 'PROPOSAL_CONVERTED_ELSEWHERE') ? 409 : 400;
@@ -118,4 +166,32 @@ export function registerTaskProposalsRoutes(app, deps) {
118
166
  }
119
167
  res.json(withProposalEnvelope({ published: { task_id: r.task_id, published: true }, published_by: admin.id }));
120
168
  });
169
+ // DISCARD an unpublished internal draft (soft-delete → frees the proposal's draft slot; provenance retained).
170
+ // Fail-closed: refuses a published / claimed draft or an already-converted source proposal. Scope = discard only.
171
+ app.post('/api/admin/build-task-drafts/:id/discard', (req, res) => {
172
+ const admin = requireSupportAdmin(req, res);
173
+ if (!admin)
174
+ return;
175
+ const r = discardDraft(db, String(req.params.id), admin.id);
176
+ if ('error' in r) {
177
+ const code = r.error_code === 'NOT_FOUND' ? 404
178
+ : (r.error_code === 'ALREADY_PUBLISHED' || r.error_code === 'DRAFT_CLAIMED' || r.error_code === 'ALREADY_CONVERTED') ? 409 : 400;
179
+ return void errorRes(res, code, r.error_code, r.error);
180
+ }
181
+ res.json(withProposalEnvelope({ discarded: { task_id: r.task_id, status: 'discarded', already_discarded: !!r.already_discarded }, discarded_by: admin.id }));
182
+ });
183
+ // RECOVERY: withdraw an UNCLAIMED published task off the board + reopen its source proposal (so a corrected
184
+ // draft can be built). Fail-closed: refuses a claimed task or a non-published task. Soft-delete (provenance kept).
185
+ app.post('/api/admin/build-tasks/:id/withdraw', (req, res) => {
186
+ const admin = requireSupportAdmin(req, res);
187
+ if (!admin)
188
+ return;
189
+ const r = withdrawPublishedTask(db, String(req.params.id), admin.id);
190
+ if ('error' in r) {
191
+ const code = r.error_code === 'NOT_FOUND' ? 404
192
+ : (r.error_code === 'TASK_CLAIMED' || r.error_code === 'NOT_PUBLISHED') ? 409 : 400;
193
+ return void errorRes(res, code, r.error_code, r.error);
194
+ }
195
+ res.json(withProposalEnvelope({ withdrawn: { task_id: r.task_id, reopened_proposal_id: r.reopened_proposal_id }, withdrawn_by: admin.id }));
196
+ });
121
197
  }
@@ -62,8 +62,7 @@ export function registerUsersPublicRoutes(app, deps) {
62
62
  const rightChildName = u.right_child_id ? (await dbOne("SELECT name FROM users WHERE id = ?", [u.right_child_id]))?.name : null;
63
63
  const leftPv = Number(u.total_left_pv ?? 0);
64
64
  const rightPv = Number(u.total_right_pv ?? 0);
65
- // pre-public de-MLM: PV 对碰为 pre-launch、未启用 —— public 端口不再暴露弱腿 / 对碰收益指标
66
- // (weak_leg_pv / pending_score / settled_waz / total_hits)。位置 + 左右区 PV 仅作为参与记录保留。
65
+ // 匹配奖励引擎已切除(#401):public 端口只保留位置 + 左右区 PV 作为参与记录,不暴露任何奖励指标。
67
66
  res.json({
68
67
  id: u.id,
69
68
  name: u.name,
@@ -263,13 +262,11 @@ export function registerUsersPublicRoutes(app, deps) {
263
262
  if (isOwner) {
264
263
  const w = await dbOne('SELECT balance, earned FROM wallets WHERE user_id = ?', [me.id]);
265
264
  const pv = await dbOne("SELECT total_left_pv, total_right_pv FROM users WHERE id = ?", [me.id]);
266
- const score = (await dbOne("SELECT COALESCE(SUM(score),0) as s FROM binary_score_records WHERE user_id = ? AND settled_at IS NULL", [me.id])).s;
267
265
  privateStats = {
268
266
  wallet_balance: Number(w?.balance ?? 0),
269
267
  wallet_earned: Number(w?.earned ?? 0),
270
268
  total_left_pv: Number(pv?.total_left_pv ?? 0),
271
269
  total_right_pv: Number(pv?.total_right_pv ?? 0),
272
- pending_score: Number(score),
273
270
  };
274
271
  }
275
272
  // D2 信誉徽章墙
@@ -175,26 +175,14 @@ export function registerWalletReadRoutes(app, deps) {
175
175
  if (commMap[key])
176
176
  commMap[key] = { count: r.cnt, total: Number(r.total.toFixed(2)) };
177
177
  }
178
- const binary = (await dbOne(`
179
- SELECT
180
- COUNT(CASE WHEN settled_at IS NOT NULL THEN 1 END) as settled_cnt,
181
- COALESCE(SUM(CASE WHEN settled_at IS NOT NULL THEN waz_amount END), 0) as settled_waz,
182
- COALESCE(SUM(CASE WHEN settled_at IS NULL THEN score END), 0) as pending_score
183
- FROM binary_score_records WHERE user_id = ?
184
- `, [user.id]));
178
+ // matching-rewards income removed — engine excised (#401). Income = affiliate commission (real sales) + own sales.
185
179
  const sales = (await dbOne(`
186
180
  SELECT COUNT(*) as cnt, COALESCE(SUM(total_amount),0) as total
187
181
  FROM orders WHERE seller_id = ? AND status = 'completed'
188
182
  `, [user.id]));
189
- const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total +
190
- Number(binary.settled_waz) + Number(sales.total);
183
+ const totalIncome = commMap.l1.total + commMap.l2.total + commMap.l3.total + Number(sales.total);
191
184
  res.json({
192
185
  commissions: commMap,
193
- binary: {
194
- settled_count: binary.settled_cnt,
195
- settled_waz: Number(Number(binary.settled_waz).toFixed(2)),
196
- pending_score: Number(Number(binary.pending_score).toFixed(2)),
197
- },
198
186
  sales: { count: sales.cnt, total: Number(Number(sales.total).toFixed(2)) },
199
187
  total_income: Number(totalIncome.toFixed(2)),
200
188
  });
@@ -73,7 +73,7 @@ export function registerWebauthnRoutes(app, deps) {
73
73
  if (!user)
74
74
  return;
75
75
  const purpose = String(req.body?.purpose || '').trim();
76
- const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim']);
76
+ const allowed = new Set(['withdraw', 'change-password', 'reveal-key', 'region', 'delete_passkey', 'governance_apply', 'governance_activate', 'governance_resign', 'governance_appeal_resolve', 'rewards_apply', 'rewards_deactivate', 'identity_claim', 'operator_claim_unlink']);
77
77
  if (!allowed.has(purpose))
78
78
  return void res.status(400).json({ error: 'invalid purpose' });
79
79
  const purpose_data = req.body?.purpose_data ?? null;