@seasonkoh/webaz 0.1.24 → 0.1.25

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 (187) hide show
  1. package/README.md +2 -0
  2. package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
  3. package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
  4. package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
  5. package/dist/layer0-foundation/L0-1-database/db.js +65 -0
  6. package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
  7. package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
  8. package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
  9. package/dist/layer1-agent/L1-1-mcp-server/server.js +165 -64
  10. package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
  11. package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
  12. package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
  13. package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
  14. package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
  15. package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +173 -0
  16. package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
  17. package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
  18. package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +10 -2
  19. package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
  20. package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
  21. package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
  22. package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
  23. package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
  24. package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
  25. package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
  26. package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
  27. package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
  28. package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
  29. package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
  30. package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
  31. package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
  32. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
  33. package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
  34. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  38. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  39. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  40. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  41. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  42. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  43. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  44. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  45. package/dist/pwa/acp-feed.js +13 -1
  46. package/dist/pwa/contract-fingerprint.js +2 -0
  47. package/dist/pwa/endpoint-actions.js +5 -1
  48. package/dist/pwa/goal-index.js +8 -8
  49. package/dist/pwa/human-presence.js +62 -0
  50. package/dist/pwa/public/app.js +575 -68
  51. package/dist/pwa/public/i18n.js +29 -20
  52. package/dist/pwa/public/index.html +1 -0
  53. package/dist/pwa/public/openapi.json +2 -2
  54. package/dist/pwa/rate-limit.js +22 -0
  55. package/dist/pwa/routes/account-deletion.js +15 -13
  56. package/dist/pwa/routes/addresses.js +10 -9
  57. package/dist/pwa/routes/admin-admins.js +13 -14
  58. package/dist/pwa/routes/admin-analytics.js +109 -69
  59. package/dist/pwa/routes/admin-catalog.js +13 -11
  60. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  61. package/dist/pwa/routes/admin-events.js +5 -3
  62. package/dist/pwa/routes/admin-health.js +2 -1
  63. package/dist/pwa/routes/admin-moderation.js +26 -29
  64. package/dist/pwa/routes/admin-ops.js +22 -21
  65. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  66. package/dist/pwa/routes/admin-reports.js +23 -21
  67. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  68. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  69. package/dist/pwa/routes/admin-users-query.js +54 -53
  70. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  71. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  72. package/dist/pwa/routes/admin-wallet-ops.js +7 -5
  73. package/dist/pwa/routes/agent-buy.js +46 -22
  74. package/dist/pwa/routes/agent-governance.js +52 -56
  75. package/dist/pwa/routes/ai.js +7 -5
  76. package/dist/pwa/routes/analytics.js +43 -41
  77. package/dist/pwa/routes/anchors.js +19 -20
  78. package/dist/pwa/routes/announcements.js +13 -13
  79. package/dist/pwa/routes/arbitrator.js +97 -31
  80. package/dist/pwa/routes/auction.js +153 -114
  81. package/dist/pwa/routes/auth-login.js +6 -4
  82. package/dist/pwa/routes/auth-read.js +11 -9
  83. package/dist/pwa/routes/auth-register.js +35 -20
  84. package/dist/pwa/routes/auth-sessions.js +12 -11
  85. package/dist/pwa/routes/blocklist.js +16 -15
  86. package/dist/pwa/routes/build-feedback.js +10 -9
  87. package/dist/pwa/routes/build-reputation.js +6 -2
  88. package/dist/pwa/routes/build-tasks.js +45 -13
  89. package/dist/pwa/routes/buyer-feeds.js +27 -25
  90. package/dist/pwa/routes/cart.js +16 -15
  91. package/dist/pwa/routes/charity.js +212 -150
  92. package/dist/pwa/routes/chat.js +42 -43
  93. package/dist/pwa/routes/checkin-tasks.js +10 -9
  94. package/dist/pwa/routes/checkout-helpers.js +12 -10
  95. package/dist/pwa/routes/claim-initiators.js +34 -14
  96. package/dist/pwa/routes/claim-verify.js +86 -53
  97. package/dist/pwa/routes/claim-voting.js +43 -18
  98. package/dist/pwa/routes/contribution-identity.js +147 -0
  99. package/dist/pwa/routes/contribution-score.js +19 -0
  100. package/dist/pwa/routes/coupons.js +19 -16
  101. package/dist/pwa/routes/dashboards.js +18 -16
  102. package/dist/pwa/routes/dispute-cases.js +25 -24
  103. package/dist/pwa/routes/disputes-read.js +45 -51
  104. package/dist/pwa/routes/disputes-write.js +124 -61
  105. package/dist/pwa/routes/evidence.js +9 -9
  106. package/dist/pwa/routes/external-anchors.js +13 -12
  107. package/dist/pwa/routes/feedback.js +29 -33
  108. package/dist/pwa/routes/flash-sales.js +18 -16
  109. package/dist/pwa/routes/follows.js +25 -24
  110. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  111. package/dist/pwa/routes/governance-onboarding.js +70 -59
  112. package/dist/pwa/routes/group-buys.js +22 -22
  113. package/dist/pwa/routes/growth.js +33 -30
  114. package/dist/pwa/routes/import-product.js +12 -10
  115. package/dist/pwa/routes/kyc.js +9 -8
  116. package/dist/pwa/routes/leaderboard.js +20 -18
  117. package/dist/pwa/routes/listings.js +23 -22
  118. package/dist/pwa/routes/logistics.js +10 -8
  119. package/dist/pwa/routes/manifests.js +27 -27
  120. package/dist/pwa/routes/me-data.js +23 -21
  121. package/dist/pwa/routes/notifications.js +7 -6
  122. package/dist/pwa/routes/offers.js +30 -12
  123. package/dist/pwa/routes/orders-action.js +33 -17
  124. package/dist/pwa/routes/orders-create.js +75 -20
  125. package/dist/pwa/routes/orders-read.js +21 -20
  126. package/dist/pwa/routes/p2p-products.js +30 -18
  127. package/dist/pwa/routes/payments-governance.js +61 -56
  128. package/dist/pwa/routes/peers.js +9 -8
  129. package/dist/pwa/routes/pin-receipts.js +13 -13
  130. package/dist/pwa/routes/products-aliases.js +12 -10
  131. package/dist/pwa/routes/products-claims.js +36 -17
  132. package/dist/pwa/routes/products-create.js +53 -38
  133. package/dist/pwa/routes/products-crud.js +17 -16
  134. package/dist/pwa/routes/products-links.js +49 -26
  135. package/dist/pwa/routes/products-list.js +6 -4
  136. package/dist/pwa/routes/products-meta.js +40 -39
  137. package/dist/pwa/routes/products-update.js +19 -5
  138. package/dist/pwa/routes/profile-credentials.js +14 -16
  139. package/dist/pwa/routes/profile-identity.js +14 -13
  140. package/dist/pwa/routes/profile-location.js +7 -6
  141. package/dist/pwa/routes/profile-placement.js +19 -17
  142. package/dist/pwa/routes/profile-prefs.js +11 -11
  143. package/dist/pwa/routes/promoter.js +55 -49
  144. package/dist/pwa/routes/public-build-tasks.js +19 -0
  145. package/dist/pwa/routes/public-utils.js +108 -46
  146. package/dist/pwa/routes/push.js +16 -15
  147. package/dist/pwa/routes/ratings.js +30 -30
  148. package/dist/pwa/routes/recover-key.js +13 -12
  149. package/dist/pwa/routes/referral.js +37 -32
  150. package/dist/pwa/routes/reputation.js +3 -2
  151. package/dist/pwa/routes/returns.js +76 -73
  152. package/dist/pwa/routes/reviews.js +41 -18
  153. package/dist/pwa/routes/rewards-apply.js +16 -15
  154. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  155. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  156. package/dist/pwa/routes/rfqs.js +163 -85
  157. package/dist/pwa/routes/search.js +16 -14
  158. package/dist/pwa/routes/secondhand.js +25 -22
  159. package/dist/pwa/routes/seller-quota.js +24 -26
  160. package/dist/pwa/routes/share-redirects.js +59 -55
  161. package/dist/pwa/routes/shareables-interactions.js +34 -35
  162. package/dist/pwa/routes/shareables.js +55 -51
  163. package/dist/pwa/routes/shop-referral.js +57 -0
  164. package/dist/pwa/routes/shops.js +20 -18
  165. package/dist/pwa/routes/signaling.js +10 -9
  166. package/dist/pwa/routes/skill-market.js +16 -16
  167. package/dist/pwa/routes/skills.js +15 -14
  168. package/dist/pwa/routes/snf.js +14 -13
  169. package/dist/pwa/routes/tags.js +10 -9
  170. package/dist/pwa/routes/task-proposals.js +45 -0
  171. package/dist/pwa/routes/trial.js +69 -51
  172. package/dist/pwa/routes/trusted-kpi.js +20 -18
  173. package/dist/pwa/routes/url-claim.js +67 -28
  174. package/dist/pwa/routes/users-public.js +62 -60
  175. package/dist/pwa/routes/variants.js +12 -13
  176. package/dist/pwa/routes/verifier-user.js +61 -21
  177. package/dist/pwa/routes/verify-tasks.js +49 -25
  178. package/dist/pwa/routes/waitlist.js +16 -15
  179. package/dist/pwa/routes/wallet-read.js +74 -36
  180. package/dist/pwa/routes/wallet-write.js +12 -9
  181. package/dist/pwa/routes/webauthn.js +25 -26
  182. package/dist/pwa/routes/webhooks.js +26 -26
  183. package/dist/pwa/routes/welcome.js +45 -50
  184. package/dist/pwa/routes/wishlist-qa.js +29 -32
  185. package/dist/pwa/server.js +237 -81
  186. package/dist/version.js +1 -1
  187. package/package.json +47 -2
@@ -0,0 +1,129 @@
1
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
2
+ export const PROPOSAL_STATUSES = ['new', 'needs_info', 'rejected', 'converted'];
3
+ export const REVIEW_TARGETS = ['needs_info', 'rejected', 'converted']; // 'new' is the initial state only
4
+ const TITLE_MIN = 3, TITLE_MAX = 200, SUMMARY_MAX = 2000, AREA_MAX = 64, OUTCOME_MAX = 2000, SOURCE_MAX = 500, LOGIN_MAX = 100, NOTE_MAX = 2000, CONVERTED_REF_MAX = 500;
5
+ const CREATE_TABLE = `
6
+ CREATE TABLE IF NOT EXISTS task_proposals (
7
+ id TEXT PRIMARY KEY,
8
+ title TEXT NOT NULL CHECK (length(trim(title)) >= 3 AND length(title) <= 200),
9
+ summary TEXT NOT NULL CHECK (length(trim(summary)) >= 1 AND length(summary) <= 2000),
10
+ suggested_area TEXT CHECK (suggested_area IS NULL OR length(suggested_area) <= 64),
11
+ expected_outcome TEXT CHECK (expected_outcome IS NULL OR length(expected_outcome) <= 2000),
12
+ source_ref TEXT CHECK (source_ref IS NULL OR length(source_ref) <= 500),
13
+ proposer_account_id TEXT,
14
+ proposer_github_login TEXT CHECK (proposer_github_login IS NULL OR length(proposer_github_login) <= 100),
15
+ status TEXT NOT NULL DEFAULT 'new' CHECK (status IN ('new','needs_info','rejected','converted')),
16
+ reviewer_id TEXT,
17
+ review_note TEXT CHECK (review_note IS NULL OR length(review_note) <= 2000),
18
+ converted_ref TEXT CHECK (converted_ref IS NULL OR length(converted_ref) <= 500),
19
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
20
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
21
+ )
22
+ `;
23
+ const CREATE_INDEX = `CREATE INDEX IF NOT EXISTS idx_task_proposals_status ON task_proposals(status, created_at DESC)`;
24
+ export function initTaskProposalSchema(db) {
25
+ db.exec(CREATE_TABLE);
26
+ db.exec(CREATE_INDEX);
27
+ }
28
+ /** Validate a public submission — fail-closed: bad/oversized fields are rejected, never silently truncated. */
29
+ export function validateProposalInput(body) {
30
+ const b = (body ?? {});
31
+ const bad = (code, message) => ({ ok: false, code, message });
32
+ const asStr = (v) => (typeof v === 'string' ? v : v == null ? null : '__invalid__');
33
+ const titleRaw = asStr(b.title);
34
+ if (titleRaw === '__invalid__')
35
+ return bad('INVALID_TITLE', 'title must be a string');
36
+ const title = (titleRaw ?? '').trim();
37
+ if (title.length < TITLE_MIN)
38
+ return bad('TITLE_TOO_SHORT', `title must be at least ${TITLE_MIN} chars`);
39
+ if (title.length > TITLE_MAX)
40
+ return bad('TITLE_TOO_LONG', `title must be at most ${TITLE_MAX} chars`);
41
+ const summaryRaw = asStr(b.summary ?? b.reason);
42
+ if (summaryRaw === '__invalid__')
43
+ return bad('INVALID_SUMMARY', 'summary must be a string');
44
+ const summary = (summaryRaw ?? '').trim();
45
+ if (summary.length < 1)
46
+ return bad('SUMMARY_REQUIRED', 'summary (reason) is required');
47
+ if (summary.length > SUMMARY_MAX)
48
+ return bad('SUMMARY_TOO_LONG', `summary must be at most ${SUMMARY_MAX} chars`);
49
+ const optLen = (v, max, code) => {
50
+ if (v == null)
51
+ return null;
52
+ if (typeof v !== 'string')
53
+ return { code };
54
+ if (v.length > max)
55
+ return { code };
56
+ return v;
57
+ };
58
+ const area = optLen(b.suggested_area, AREA_MAX, 'SUGGESTED_AREA_TOO_LONG');
59
+ if (area && typeof area === 'object')
60
+ return bad(area.code, 'suggested_area invalid/too long');
61
+ const outcome = optLen(b.expected_outcome, OUTCOME_MAX, 'EXPECTED_OUTCOME_TOO_LONG');
62
+ if (outcome && typeof outcome === 'object')
63
+ return bad(outcome.code, 'expected_outcome invalid/too long');
64
+ const source = optLen(b.source_ref, SOURCE_MAX, 'SOURCE_REF_TOO_LONG');
65
+ if (source && typeof source === 'object')
66
+ return bad(source.code, 'source_ref invalid/too long');
67
+ const login = optLen(b.proposer_github_login, LOGIN_MAX, 'GITHUB_LOGIN_TOO_LONG');
68
+ if (login && typeof login === 'object')
69
+ return bad(login.code, 'proposer_github_login invalid/too long');
70
+ return { ok: true, input: { title, summary, suggested_area: area, expected_outcome: outcome, source_ref: source, proposer_github_login: login } };
71
+ }
72
+ const DEDUP_WINDOW = '-1 hours'; // SQLite datetime modifier — a recent-window duplicate guard
73
+ /* eslint-disable @typescript-eslint/no-explicit-any */
74
+ /**
75
+ * Insert a new proposal (status='new'). proposer_account_id is NEVER taken from the body (anti-spoof).
76
+ * Dedup (anti-flood): if an identical proposal already exists in the recent window — same non-empty
77
+ * `source_ref`, OR same (title, summary) — NO new row is inserted; returns `{ duplicate, existing_id }`.
78
+ */
79
+ export function insertTaskProposal(db, input, proposerAccountId = null) {
80
+ const src = input.source_ref ?? null;
81
+ const dup = db.prepare(`SELECT id FROM task_proposals
82
+ WHERE created_at > datetime('now', ?)
83
+ AND (((? IS NOT NULL) AND source_ref = ?) OR (title = ? AND summary = ?))
84
+ ORDER BY created_at DESC LIMIT 1`).get(DEDUP_WINDOW, src, src, input.title, input.summary);
85
+ if (dup)
86
+ return { duplicate: true, existing_id: dup.id };
87
+ const id = generateId('tp');
88
+ db.prepare(`INSERT INTO task_proposals (id, title, summary, suggested_area, expected_outcome, source_ref, proposer_account_id, proposer_github_login, status)
89
+ VALUES (@id, @title, @summary, @suggested_area, @expected_outcome, @source_ref, @proposer_account_id, @proposer_github_login, 'new')`).run({
90
+ id, title: input.title, summary: input.summary,
91
+ suggested_area: input.suggested_area ?? null, expected_outcome: input.expected_outcome ?? null,
92
+ source_ref: src, proposer_account_id: proposerAccountId,
93
+ proposer_github_login: input.proposer_github_login ?? null,
94
+ });
95
+ return { id, status: 'new' };
96
+ }
97
+ export function listTaskProposals(db, filter = {}) {
98
+ const where = [];
99
+ const params = [];
100
+ if (filter.status && PROPOSAL_STATUSES.includes(filter.status)) {
101
+ where.push('status = ?');
102
+ params.push(filter.status);
103
+ }
104
+ return db.prepare(`SELECT id, title, summary, suggested_area, expected_outcome, source_ref, proposer_account_id,
105
+ proposer_github_login, status, reviewer_id, review_note, converted_ref, created_at, updated_at FROM task_proposals
106
+ ${where.length ? 'WHERE ' + where.join(' AND ') : ''} ORDER BY created_at DESC LIMIT 200`).all(...params);
107
+ }
108
+ /**
109
+ * Review a proposal: needs_info / rejected / converted. NO build_task is created here (conversion is a
110
+ * later manual maintainer step — deferred). Terminal states (rejected/converted) cannot be re-reviewed.
111
+ * `convertedRef` (only meaningful for `converted`) records the link to the real task / PR / release /
112
+ * product decision — the non-code contribution evidence chain { proposer → reviewer → converted_ref }.
113
+ * Recording only; no reward/score is computed.
114
+ */
115
+ export function reviewTaskProposal(db, id, reviewerId, status, note, convertedRef) {
116
+ if (!REVIEW_TARGETS.includes(status))
117
+ return { error: 'status must be needs_info | rejected | converted', code: 'BAD_STATUS' };
118
+ if (convertedRef != null && (typeof convertedRef !== 'string' || convertedRef.length > CONVERTED_REF_MAX))
119
+ return { error: 'converted_ref invalid/too long', code: 'CONVERTED_REF_TOO_LONG' };
120
+ const cur = db.prepare('SELECT status FROM task_proposals WHERE id = ?').get(id);
121
+ if (!cur)
122
+ return { error: 'proposal not found', code: 'NOT_FOUND' };
123
+ if (cur.status === 'rejected' || cur.status === 'converted')
124
+ return { error: `proposal already ${cur.status}`, code: 'ALREADY_TERMINAL' };
125
+ const ref = status === 'converted' && typeof convertedRef === 'string' && convertedRef.trim() ? convertedRef.trim() : null;
126
+ db.prepare(`UPDATE task_proposals SET status = ?, reviewer_id = ?, review_note = ?, converted_ref = ?, updated_at = datetime('now') WHERE id = ?`)
127
+ .run(status, reviewerId, note ? String(note).slice(0, NOTE_MAX) : null, ref, id);
128
+ return { id, status, converted_ref: ref };
129
+ }
@@ -4,6 +4,7 @@ import * as fs from 'fs';
4
4
  import * as path from 'path';
5
5
  import * as os from 'os';
6
6
  import { createHash } from 'crypto';
7
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
7
8
  const NOTE_PHOTO_DIR = path.join(os.homedir(), '.webaz', 'note-photos');
8
9
  if (!fs.existsSync(NOTE_PHOTO_DIR))
9
10
  fs.mkdirSync(NOTE_PHOTO_DIR, { recursive: true });
@@ -71,7 +72,8 @@ function sniffImageMime(b) {
71
72
  // - 再 POST /api/shareables 把 hash 写进 note_photo_index
72
73
  // - 中间窗口(典型 几十秒)误删会让上传白费
73
74
  // 由 cron 每天调一次(与 evidence cleanup 同节奏)
74
- export function cleanupOrphanNotePhotos(db, graceMs = 60 * 60 * 1000) {
75
+ // RFC-016 Phase 1:cron 清理 —— db 部分是纯读(查引用),迁异步 seam;文件删除为 fs 操作(非 db)。调用点 server.ts:9262 cron,inTx=false。
76
+ export async function cleanupOrphanNotePhotos(_db, graceMs = 60 * 60 * 1000) {
75
77
  if (!fs.existsSync(NOTE_PHOTO_DIR))
76
78
  return { swept: 0, bytes: 0 };
77
79
  let swept = 0;
@@ -118,7 +120,7 @@ export function cleanupOrphanNotePhotos(db, graceMs = 60 * 60 * 1000) {
118
120
  const age = Math.max(0, now - fstat.mtimeMs);
119
121
  if (age < graceMs)
120
122
  continue; // grace 窗口内保留
121
- const row = db.prepare(`SELECT 1 FROM note_photo_index WHERE hash = ?`).get(fname);
123
+ const row = await dbOne(`SELECT 1 FROM note_photo_index WHERE hash = ?`, [fname]);
122
124
  if (!row) {
123
125
  try {
124
126
  bytes += fstat.size;
@@ -13,6 +13,7 @@
13
13
  * arbitrate 是 Iron-Rule 真人动作(需 Passkey) · 协议级改动审批见 CHARTER §3.2
14
14
  */
15
15
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
16
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
16
17
  import { transition } from '../../layer0-foundation/L0-2-state-machine/engine.js';
17
18
  // RFC-014 PR5 — 争议资金处置走整数 base-units + 绝对值落库 + allocate 精确拆分。
18
19
  import { toUnits, toDecimal, mulRate, allocate } from '../../money.js';
@@ -646,21 +647,22 @@ export function submitEvidenceForRequest(db, requestId, submitterId, evidenceTyp
646
647
  /**
647
648
  * 查询争议的所有证据请求(含已提交内容)
648
649
  */
649
- export function getEvidenceRequests(db, disputeId) {
650
- const rows = db.prepare(`
650
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容,内部走 dbAll/dbOne;调用点全部已确认不在 db.transaction 内)
651
+ export async function getEvidenceRequests(_db, disputeId) {
652
+ const rows = await dbAll(`
651
653
  SELECT r.*, u.name as requested_from_name, u.role as requested_from_role
652
654
  FROM dispute_evidence_requests r
653
655
  LEFT JOIN users u ON r.requested_from_id = u.id
654
656
  WHERE r.dispute_id = ?
655
657
  ORDER BY r.created_at ASC
656
- `).all(disputeId);
657
- return rows.map(r => {
658
+ `, [disputeId]);
659
+ return await Promise.all(rows.map(async (r) => {
658
660
  const ids = JSON.parse(r.submitted_evidence_ids || '[]');
659
661
  const items = ids.length
660
- ? db.prepare(`SELECT * FROM evidence WHERE id IN (${ids.map(() => '?').join(',')})`).all(...ids)
662
+ ? await dbAll(`SELECT * FROM evidence WHERE id IN (${ids.map(() => '?').join(',')})`, ids)
661
663
  : [];
662
664
  return { ...r, submitted_items: items };
663
- });
665
+ }));
664
666
  }
665
667
  /** 生成锚定哈希(Phase 0 模拟;Phase 2 用 IPFS/链上替换) */
666
668
  function generateAnchorHash(content) {
@@ -673,8 +675,8 @@ function generateAnchorHash(content) {
673
675
  return `0x${h.toString(16).padStart(8, '0')}${ts}`;
674
676
  }
675
677
  // ─── 查询函数 ─────────────────────────────────────────────────
676
- export function getDisputeDetails(db, disputeId) {
677
- return db.prepare(`
678
+ export async function getDisputeDetails(_db, disputeId) {
679
+ return (await dbOne(`
678
680
  SELECT d.*,
679
681
  u1.name as initiator_name, u1.role as initiator_role,
680
682
  u2.name as defendant_name, u2.role as defendant_role
@@ -682,10 +684,10 @@ export function getDisputeDetails(db, disputeId) {
682
684
  LEFT JOIN users u1 ON d.initiator_id = u1.id
683
685
  LEFT JOIN users u2 ON d.defendant_id = u2.id
684
686
  WHERE d.id = ?
685
- `).get(disputeId);
687
+ `, [disputeId])) ?? null;
686
688
  }
687
- export function getOrderDispute(db, orderId) {
688
- return db.prepare(`
689
+ export async function getOrderDispute(_db, orderId) {
690
+ return (await dbOne(`
689
691
  SELECT d.*,
690
692
  u1.name as initiator_name, u1.role as initiator_role,
691
693
  u2.name as defendant_name, u2.role as defendant_role
@@ -694,10 +696,10 @@ export function getOrderDispute(db, orderId) {
694
696
  LEFT JOIN users u2 ON d.defendant_id = u2.id
695
697
  WHERE d.order_id = ? AND d.status NOT IN ('resolved', 'dismissed')
696
698
  ORDER BY d.created_at DESC LIMIT 1
697
- `).get(orderId);
699
+ `, [orderId])) ?? null;
698
700
  }
699
- export function getOpenDisputes(db) {
700
- return db.prepare(`
701
+ export async function getOpenDisputes(_db) {
702
+ return await dbAll(`
701
703
  SELECT d.*,
702
704
  u1.name as initiator_name, u1.role as initiator_role,
703
705
  u2.name as defendant_name, u2.role as defendant_role,
@@ -708,7 +710,7 @@ export function getOpenDisputes(db) {
708
710
  LEFT JOIN orders o ON d.order_id = o.id
709
711
  WHERE d.status IN ('open', 'in_review')
710
712
  ORDER BY d.created_at ASC
711
- `).all();
713
+ `);
712
714
  }
713
715
  // ─── 工具函数 ─────────────────────────────────────────────────
714
716
  function addHours(date, hours) {
@@ -5,6 +5,7 @@ import * as path from 'path';
5
5
  import * as os from 'os';
6
6
  import { createHash, createHmac } from 'crypto';
7
7
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
8
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
8
9
  const EVIDENCE_DIR = path.join(os.homedir(), '.webaz', 'evidence');
9
10
  if (!fs.existsSync(EVIDENCE_DIR))
10
11
  fs.mkdirSync(EVIDENCE_DIR, { recursive: true });
@@ -135,8 +136,10 @@ export function uploadEvidence(db, args) {
135
136
  }
136
137
  return { id: eid, hash: actualHash, sig, dedup: blobExists, size: args.blob.length };
137
138
  }
138
- export function readEvidenceBlob(db, evidenceId, requesterId) {
139
- const ev = db.prepare('SELECT id, dispute_id, file_hash, mime, filename, withdrawn_at FROM evidence WHERE id = ?').get(evidenceId);
139
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点均 inTx=false)
140
+ // 注:isPartyOrArbitrator(被同步写 uploadEvidence 复用)保持同步,留 Phase 3;async 读内同步调用它 OK(其 db.prepare 仍同步)
141
+ export async function readEvidenceBlob(db, evidenceId, requesterId) {
142
+ const ev = await dbOne('SELECT id, dispute_id, file_hash, mime, filename, withdrawn_at FROM evidence WHERE id = ?', [evidenceId]);
140
143
  if (!ev)
141
144
  throw new Error('evidence_not_found');
142
145
  if (ev.withdrawn_at)
@@ -178,16 +181,16 @@ export function withdrawEvidence(db, evidenceId, uploaderId) {
178
181
  db.prepare('UPDATE disputes SET party_evidence_ids = ? WHERE id = ?').run(JSON.stringify(filtered), ev.dispute_id);
179
182
  }
180
183
  }
181
- export function listEvidence(db, disputeId, requesterId) {
184
+ export async function listEvidence(db, disputeId, requesterId) {
182
185
  if (!isPartyOrArbitrator(db, disputeId, requesterId)) {
183
186
  throw new Error('not_dispute_party');
184
187
  }
185
- return db.prepare(`
188
+ return await dbAll(`
186
189
  SELECT id, uploader_id, type, description, file_hash, size, mime, sig, filename, created_at, withdrawn_at
187
190
  FROM evidence
188
191
  WHERE dispute_id = ?
189
192
  ORDER BY created_at ASC
190
- `).all(disputeId);
193
+ `, [disputeId]);
191
194
  }
192
195
  export function markEvidenceExpiry(db, disputeId) {
193
196
  const exp = new Date(Date.now() + EVIDENCE_TTL_DAYS_AFTER_RESOLVED * 24 * 3600 * 1000).toISOString();
@@ -223,13 +226,13 @@ export function cleanupExpiredEvidence(db) {
223
226
  }
224
227
  return { swept, bytes };
225
228
  }
226
- export function verifyEvidenceSig(db, evidenceId) {
227
- const ev = db.prepare(`
229
+ export async function verifyEvidenceSig(_db, evidenceId) {
230
+ const ev = await dbOne(`
228
231
  SELECT e.id, e.uploader_id, e.dispute_id, e.file_hash, e.size, e.mime, e.description, e.sig,
229
232
  u.api_key
230
233
  FROM evidence e JOIN users u ON u.id = e.uploader_id
231
234
  WHERE e.id = ?
232
- `).get(evidenceId);
235
+ `, [evidenceId]);
233
236
  if (!ev)
234
237
  return { ok: false, reason: 'evidence_not_found' };
235
238
  if (!ev.sig)
@@ -15,6 +15,7 @@
15
15
  * -25 争议败诉 -40 超时违约
16
16
  */
17
17
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
18
+ import { dbOne } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
18
19
  // ─── Schema ───────────────────────────────────────────────────
19
20
  export function initReputationSchema(db) {
20
21
  db.exec(`
@@ -286,15 +287,15 @@ export function getReputation(db, userId) {
286
287
  export function getReputationByUserId(db, userId) {
287
288
  return getReputation(db, userId);
288
289
  }
290
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容,内部走 dbOne,同实例 setSeamDb)。
291
+ // 调用点全部已确认不在 db.transaction 内(mcp 1967/2210 · products-create.ts:122 · url-claim.ts:120)。
289
292
  /** 获取用户的搜索加成(用于商品排序) */
290
- export function getSearchBoost(db, userId) {
291
- const score = db.prepare('SELECT total_points FROM reputation_scores WHERE user_id = ?').get(userId);
292
- const points = score?.total_points ?? 0;
293
- return getLevel(points).searchBoost;
293
+ export async function getSearchBoost(_db, userId) {
294
+ const score = await dbOne('SELECT total_points FROM reputation_scores WHERE user_id = ?', [userId]);
295
+ return getLevel(score?.total_points ?? 0).searchBoost;
294
296
  }
295
297
  /** 获取用户的质押折扣(用于上架商品时的质押计算) */
296
- export function getStakeDiscount(db, userId) {
297
- const score = db.prepare('SELECT total_points FROM reputation_scores WHERE user_id = ?').get(userId);
298
- const points = score?.total_points ?? 0;
299
- return getLevel(points).stakeDiscount;
298
+ export async function getStakeDiscount(_db, userId) {
299
+ const score = await dbOne('SELECT total_points FROM reputation_scores WHERE user_id = ?', [userId]);
300
+ return getLevel(score?.total_points ?? 0).stakeDiscount;
300
301
  }
@@ -16,6 +16,7 @@
16
16
  * - 其他技能目前免费(增强曝光,间接提升成交)
17
17
  */
18
18
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
19
+ import { dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
19
20
  import { toUnits, toDecimal } from '../../money.js';
20
21
  import { applyWalletDelta } from '../../ledger.js';
21
22
  // ─── Schema ───────────────────────────────────────────────────
@@ -135,7 +136,9 @@ export function publishSkill(db, input) {
135
136
  `).run(id, input.sellerId, input.name, input.description, input.category ?? 'general', input.skillType, config, input.pricePerUse ?? 0);
136
137
  return getSkillById(db, id);
137
138
  }
138
- export function listSkills(db, filter = {}) {
139
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点均确认不在 db.transaction )
140
+ // 注:getSkillById(被同步写 publishSkill 内部调用)+ shouldAutoAccept(orders-create tx 内)留 Phase 3。
141
+ export async function listSkills(_db, filter = {}) {
139
142
  const params = [];
140
143
  let sql = `
141
144
  SELECT s.*, u.name as seller_name,
@@ -161,7 +164,7 @@ export function listSkills(db, filter = {}) {
161
164
  }
162
165
  sql += ` ORDER BY s.total_uses DESC, s.rating DESC LIMIT ?`;
163
166
  params.push(filter.limit ?? 20);
164
- return db.prepare(sql).all(...params);
167
+ return await dbAll(sql, params);
165
168
  }
166
169
  export function getSkillById(db, skillId) {
167
170
  return db.prepare(`
@@ -170,12 +173,12 @@ export function getSkillById(db, skillId) {
170
173
  FROM skills s JOIN users u ON s.seller_id = u.id WHERE s.id = ?
171
174
  `).get(skillId);
172
175
  }
173
- export function getMySkills(db, sellerId) {
174
- return db.prepare(`
176
+ export async function getMySkills(_db, sellerId) {
177
+ return await dbAll(`
175
178
  SELECT s.*,
176
179
  (SELECT COUNT(*) FROM skill_subscriptions ss WHERE ss.skill_id = s.id AND ss.active = 1) as subscriber_count
177
180
  FROM skills s WHERE s.seller_id = ? ORDER BY s.created_at DESC
178
- `).all(sellerId);
181
+ `, [sellerId]);
179
182
  }
180
183
  // ─── 订阅 / 取消订阅 ──────────────────────────────────────────
181
184
  export function subscribeSkill(db, userId, skillId, config = {}) {
@@ -195,8 +198,8 @@ export function subscribeSkill(db, userId, skillId, config = {}) {
195
198
  export function unsubscribeSkill(db, userId, skillId) {
196
199
  db.prepare('UPDATE skill_subscriptions SET active = 0 WHERE skill_id = ? AND user_id = ?').run(skillId, userId);
197
200
  }
198
- export function getMySubscriptions(db, userId) {
199
- return db.prepare(`
201
+ export async function getMySubscriptions(_db, userId) {
202
+ return await dbAll(`
200
203
  SELECT s.*, u.name as seller_name, 1 as subscribed,
201
204
  (SELECT COUNT(*) FROM skill_subscriptions ss WHERE ss.skill_id = s.id AND ss.active = 1) as subscriber_count
202
205
  FROM skill_subscriptions sub
@@ -204,7 +207,7 @@ export function getMySubscriptions(db, userId) {
204
207
  JOIN users u ON s.seller_id = u.id
205
208
  WHERE sub.user_id = ? AND sub.active = 1
206
209
  ORDER BY sub.created_at DESC
207
- `).all(userId);
210
+ `, [userId]);
208
211
  }
209
212
  // ─── 使用记录 + 佣金 ──────────────────────────────────────────
210
213
  /**
@@ -18,6 +18,7 @@
18
18
  * 绝不进入 PV 二元匹配 / 推土机三级佣金 / fund_base —— 保持双引擎解耦、规避 PV 合规问题。
19
19
  */
20
20
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
21
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
21
22
  import { toUnits, toDecimal } from '../../money.js';
22
23
  import { applyWalletDelta } from '../../ledger.js';
23
24
  export const SKILL_KINDS = ['template', 'prompt', 'guide', 'checklist'];
@@ -170,7 +171,9 @@ function getListingRaw(db, id) {
170
171
  return db.prepare('SELECT * FROM skill_listings WHERE id = ?').get(id);
171
172
  }
172
173
  /** 公开列表:仅 approved + active 的技能,不含 content */
173
- export function listMarket(db, filter = {}) {
174
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 skill-market.ts 均 inTx=false)
175
+ // 注:hasUnlock(被同步写 purchaseListing/readContent 复用)留 Phase 3,故 getMarketDetail 的 owned 检查内联为 dbOne。
176
+ export async function listMarket(_db, filter = {}) {
174
177
  const params = [];
175
178
  let ownedSelect = '';
176
179
  if (filter.viewerId) {
@@ -198,29 +201,32 @@ export function listMarket(db, filter = {}) {
198
201
  }
199
202
  sql += ' ORDER BY l.total_sales DESC, l.rating DESC, l.created_at DESC LIMIT ?';
200
203
  params.push(filter.limit ?? 30);
201
- return db.prepare(sql).all(...params);
204
+ return await dbAll(sql, params);
202
205
  }
203
206
  /** 公开详情(不含 content);附加 owned 标记。未上架的仅作者本人可见,防 metadata 泄露 */
204
- export function getMarketDetail(db, id, viewerId) {
205
- const row = db.prepare(`
207
+ export async function getMarketDetail(_db, id, viewerId) {
208
+ const row = await dbOne(`
206
209
  SELECT ${PUBLIC_COLS}, u.name as author_name
207
210
  FROM skill_listings l JOIN users u ON l.author_id = u.id
208
211
  WHERE l.id = ?
209
- `).get(id);
212
+ `, [id]);
210
213
  if (!row)
211
214
  return null;
212
215
  if (row.status !== 'approved' && row.author_id !== viewerId)
213
216
  return null;
214
- if (viewerId)
215
- row.owned = hasUnlock(db, viewerId, id) ? 1 : 0;
217
+ if (viewerId) {
218
+ // owned 检查内联(hasUnlock 被同步写路径复用,留 Phase 3;此处直接走 seam,与 hasUnlock 同 SQL)
219
+ const unlocked = await dbOne(`SELECT 1 FROM skill_orders o WHERE o.listing_id = ? AND o.buyer_id = ? AND o.billing_mode IN ('free','one_time') LIMIT 1`, [id, viewerId]);
220
+ row.owned = unlocked ? 1 : 0;
221
+ }
216
222
  return row;
217
223
  }
218
224
  /** 作者视角:自己的全部技能(含各状态、含 content 由调用方决定是否回传) */
219
- export function getMyListings(db, authorId) {
220
- return db.prepare(`
225
+ export async function getMyListings(_db, authorId) {
226
+ return await dbAll(`
221
227
  SELECT ${PUBLIC_COLS}, l.audit_note, l.reviewed_at, l.total_revenue
222
228
  FROM skill_listings l WHERE l.author_id = ? ORDER BY l.created_at DESC
223
- `).all(authorId);
229
+ `, [authorId]);
224
230
  }
225
231
  // ─── 访问权 / 解锁 ────────────────────────────────────────────
226
232
  /** one_time/free 是否已永久解锁(per_use 不算永久解锁) */
@@ -303,8 +309,8 @@ export function readContent(db, userId, listingId, feeRate) {
303
309
  return { content: listing.content, billing_mode: listing.billing_mode };
304
310
  }
305
311
  /** 我的技能库:已解锁(free/one_time)的技能 + per_use 使用过的技能 */
306
- export function getMyLibrary(db, userId) {
307
- return db.prepare(`
312
+ export async function getMyLibrary(_db, userId) {
313
+ return await dbAll(`
308
314
  SELECT ${PUBLIC_COLS}, u.name as author_name, 1 as owned,
309
315
  MAX(o.created_at) as last_used
310
316
  FROM skill_orders o
@@ -313,15 +319,15 @@ export function getMyLibrary(db, userId) {
313
319
  WHERE o.buyer_id = ?
314
320
  GROUP BY l.id
315
321
  ORDER BY last_used DESC
316
- `).all(userId);
322
+ `, [userId]);
317
323
  }
318
324
  // ─── 审计(WebAZ content admin)─────────────────────────────────
319
- export function listPendingAudit(db, limit = 100) {
320
- return db.prepare(`
325
+ export async function listPendingAudit(_db, limit = 100) {
326
+ return await dbAll(`
321
327
  SELECT l.*, u.name as author_name
322
328
  FROM skill_listings l JOIN users u ON l.author_id = u.id
323
329
  WHERE l.status = 'submitted' ORDER BY l.created_at ASC LIMIT ?
324
- `).all(limit);
330
+ `, [limit]);
325
331
  }
326
332
  export function auditListing(db, listingId, reviewerId, decision, note) {
327
333
  const cur = getListingRaw(db, listingId);
@@ -87,11 +87,23 @@ export function buildAcpProductFeed(db, opts = {}) {
87
87
  generated_at: new Date().toISOString(),
88
88
  source: 'WebAZ',
89
89
  spec: {
90
- based_on: 'OpenAI Agentic Commerce Protocol — product feed',
90
+ feed_kind: 'acp-inspired-discovery',
91
+ based_on: 'OpenAI Agentic Commerce Protocol — product feed (SHAPE only; this is a discovery projection, not a strict ACP-ingestable merchant feed)',
91
92
  api_version_observed: '2025-09-12',
92
93
  reference: 'https://developers.openai.com/commerce/specs/feed',
93
94
  rfc: `${BASE}/docs/INTEGRATOR.md`,
94
95
  },
96
+ // Explicit non-compliance so an ACP ingester does NOT treat this as a strict feed (Codex #151).
97
+ compatibility: {
98
+ is_strict_acp_ingestable: false,
99
+ summary: 'Discovery projection that borrows the ACP product-feed shape. It is intentionally NOT a strict ACP-ingestable feed.',
100
+ non_compliant_points: [
101
+ 'price.currency is SIMULATED WAZ (pre-launch internal unit), not an ISO 4217 fiat currency.',
102
+ 'is_eligible_checkout is false for every item: ACP checkout (card + PSP) is not wired.',
103
+ 'Merchant-level required fields (target_countries, store_country) are not emitted.',
104
+ ],
105
+ strict_export: 'A strict/export feed (ISO 4217 + merchant required fields, compliant items only) is deferred to a later RFC-015 phase, after fiat pricing + ACP checkout exist — it would be empty today.',
106
+ },
95
107
  _disclosures: {
96
108
  phase: 'pre-launch',
97
109
  currency: "Each item's price.currency is the protocol's SIMULATED pre-launch internal unit (WAZ, or legacy 'DCP'), NOT an ISO 4217 fiat currency. No real money is involved.",
@@ -33,6 +33,8 @@ export function contractFingerprints() {
33
33
  export const CONTRACT_CHANGES = [
34
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
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
+ { contract_version: 3, date: '2026-06-09', surface: 'entity', kind: 'changed', summary: '§① order lifecycle: corrected declined_nofault state meaning text — it is NOT terminal (transitions declined_nofault→completed on settlement). Dropped the contradictory "(terminal)" label that conflicted with the auto-derived terminal:false. Semantics/state-machine unchanged; text-only clarification for agents reading the entity dictionary.' },
37
+ { contract_version: 4, date: '2026-06-09', surface: 'capability', kind: 'changed', summary: '§② capability matrix: POST /api/reviews/:type/:id/claim now requires the new "review_claim" action scope instead of being SAFE (unscoped). The review-claim path locks a 5 WAZ stake (escrow), so it belongs under default-deny accountability like other value writes. GET reviews endpoints stay open.', migration: 'A declared agent that calls review claim must add the "review_claim" scope to its api_key (or hold a Passkey, or declare "*"). Passkey-bound humans and "*" agents are unaffected; GET reviews unchanged.' },
36
38
  ];
37
39
  export function buildChangeFeed() {
38
40
  return {
@@ -31,6 +31,9 @@ export const WRITE_RULES = [
31
31
  { method: 'POST', re: /^\/api\/skill-market\/[^/]+\/purchase/, action: 'purchase' },
32
32
  { method: 'POST', re: /^\/api\/secondhand\/[^/]+\/order/, action: 'buy_secondhand' },
33
33
  { method: 'POST', re: /^\/api\/group-buys\/[^/]+\/join/, action: 'group_buy_join' },
34
+ // Codex #98 P1:review claim 锁 5 WAZ stake(扣 balance + escrowed)—— 资金写,绝不能落 SAFE,纳入 default-deny 问责门。
35
+ // 只命中 .../:type/:id/claim;其余 reviews 写无规则 → 落通用 'write'(仍 default-deny),GET reviews 由 endpointToAction(GET) 返回 null。
36
+ { method: 'POST', re: /^\/api\/reviews\/[^/]+\/[^/]+\/claim$/, action: 'review_claim' },
34
37
  // #1115 P1:写 PII(收货地址)。原为 (addresses OR profile/default-address);拆两条等价规则,顺序保持。
35
38
  { method: 'WRITE', re: /^\/api\/addresses(\/|$)/, action: 'set_address' },
36
39
  { method: 'WRITE', exact: '/api/profile/default-address', action: 'set_address' },
@@ -55,7 +58,8 @@ export const SAFE_WRITE = [
55
58
  /^\/api\/me\/(delete-cancel|notify-claim-tasks)/,
56
59
  /^\/api\/peers\//, /^\/api\/signaling\//,
57
60
  /^\/api\/product-share\/touch$/, /^\/api\/anchor\/[^/]+\/touch$/,
58
- /^\/api\/reviews\//,
61
+ // Codex #98 P1:不再整段放行 /api/reviews/ —— claim 是 5 WAZ 质押资金写,已上提到 WRITE_RULES(review_claim);
62
+ // GET reviews(recent / :type/:id/claims)无须写 scope(GET 由 endpointToAction 返回 null,不依赖 SAFE)。
59
63
  ];
60
64
  function methodMatches(m, method) {
61
65
  if (m === 'POST')
@@ -12,29 +12,29 @@ import { SOFTWARE_VERSION, CONTRACT_VERSION } from '../version.js';
12
12
  import { capabilityMatrix } from './endpoint-actions.js';
13
13
  const GOALS = [
14
14
  // ── discover / buy ──
15
- { goal: 'Find a specific product by title/SKU/exact desc', when: 'buyer knows what they want (strict match)', action: 'open', endpoint: 'GET /api/search?query=...', mcp_tool: 'webaz_search', pwa: '#buy', see: '② read scope "search"', notes: 'STRICT no fuzzy fallback; 0 hits → guide user to #discover (fuzzy is a human action, not agent-automated).' },
16
- { goal: 'Match a pasted external link (taobao/douyin/xhs/jd/...)', when: 'buyer pastes an off-site product URL', action: 'open', endpoint: 'GET /api/search?external_link=...', mcp_tool: 'webaz_search', pwa: '#buy', notes: 'matches the anchor registry product fingerprint.' },
15
+ { goal: 'Find a specific product by title/SKU/exact desc', when: 'buyer knows what they want (strict match)', action: 'open', endpoint: 'GET /api/products?q=...', mcp_tool: 'webaz_search', pwa: '#buy', see: '② read scope "search"', notes: 'protocol-level STRICT alias match (mode=agent), no fuzzy fallback; 0 hits → guide user to #discover (fuzzy is a human action, not agent-automated).' },
16
+ { goal: 'Match a pasted external link (taobao/douyin/xhs/jd/...)', when: 'buyer pastes an off-site product URL or share text', action: 'open', endpoint: 'POST /api/search-by-link', mcp_tool: 'webaz_search', pwa: '#buy', notes: 'body: { text } (raw paste of share text/URL) OR { external_link: { platform, external_id, external_title, canonical_url } }. Matches the anchor registry product fingerprint.' },
17
17
  { goal: "Browse what's popular near me / same city", when: 'geo discovery, no keyword', action: 'search', endpoint: 'GET /api/nearby', mcp_tool: 'webaz_nearby', pwa: '#nearby', see: '② read scope "search"', notes: 'k-anonymity ≥3.' },
18
18
  { goal: 'Find used / pre-owned / secondhand items', when: 'pre-owned, separate space from new catalog', action: 'open', endpoint: 'GET /api/secondhand', mcp_tool: 'webaz_secondhand', pwa: '#secondhand', notes: 'webaz_search does NOT return secondhand.' },
19
19
  { goal: 'Verify a price before buying', when: 'BEFORE every purchase', action: 'open', endpoint: 'GET /api/products/:id (+ verify)', mcp_tool: 'webaz_verify_price', pwa: '#buy', notes: 'defeats flash-sale/hidden-fee race; protocol only liable for the verified T0 price.' },
20
20
  { goal: 'Place an order (buy a catalog product)', when: 'buyer commits to purchase', action: 'place_order', endpoint: 'POST /api/orders', mcp_tool: 'webaz_place_order', pwa: '#buy', see: '① entity order · ⑧ value flow', notes: 'pass expected_price (T0 guard, 409 on drift).' },
21
21
  { goal: 'Buy a secondhand item', when: 'order a pre-owned listing', action: 'buy_secondhand', endpoint: 'POST /api/secondhand/:id/order', mcp_tool: 'webaz_secondhand', pwa: '#secondhand' },
22
22
  { goal: 'Buy a knowledge skill', when: 'purchase a prompt/template/checklist', action: 'purchase', endpoint: 'POST /api/skill-market/:id/purchase', mcp_tool: 'webaz_skill_market', pwa: '#skill-market', notes: 'content market — distinct from webaz_skill behavior plugins.' },
23
- { goal: 'Bid in an auction', when: 'time-windowed price discovery on listed item', action: 'bid', endpoint: 'POST /api/auctions/:id/bid', mcp_tool: 'webaz_bid', pwa: '#auctions', notes: 'anti-snipe time extension.' },
23
+ { goal: 'Bid in an auction', when: 'time-windowed price discovery on listed item', action: 'bid', endpoint: 'POST /api/auctions/:id/bids', mcp_tool: 'webaz_bid', pwa: '#auctions', notes: 'anti-snipe time extension.' },
24
24
  { goal: 'Post a buy request (RFQ) for sellers to quote', when: 'no good match / bulk / custom / wants competing quotes', action: 'rfq', endpoint: 'POST /api/rfqs', mcp_tool: 'webaz_rfq', pwa: '#rfqs', notes: 'reverse match — buyer posts need + 1% stake.' },
25
25
  // ── sell / fulfill ──
26
- { goal: 'List / update a product', when: 'seller publishes, edits, delists own listing', action: 'list_product', endpoint: 'POST|PUT /api/products', mcp_tool: 'webaz_list_product', pwa: '#me', see: '① entity product', notes: 'system suggests stake ~15% of price (buyer protection).' },
27
- { goal: 'Fulfill an order (accept / ship / deliver / pickup)', when: 'seller or logistics advances fulfilment', action: 'fulfill', endpoint: 'POST /api/orders/:id/{accept|ship|deliver|pickup|transit}', mcp_tool: 'webaz_update_order', pwa: '#me', see: '① order lifecycle · ⑦ liability', notes: 'missing a deadline → auto fault (see order lifecycle).' },
28
- { goal: 'Confirm receipt (buyer closes the order)', when: 'buyer received the goods', action: 'confirm_order', endpoint: 'POST /api/orders/:id/confirm', mcp_tool: 'webaz_update_order', pwa: '#me', notes: 'auto-confirm on confirm_deadline timeout.' },
26
+ { goal: 'List / update a product', when: 'seller publishes, edits, delists own listing', action: 'list_product', endpoint: 'POST /api/products · PUT /api/products/:id', mcp_tool: 'webaz_list_product', pwa: '#me', see: '① entity product', notes: 'POST creates, PUT /:id edits/delists. System suggests stake ~15% of price (buyer protection).' },
27
+ { goal: 'Fulfill an order (accept / ship / deliver / pickup)', when: 'seller or logistics advances fulfilment', action: 'fulfill', endpoint: 'POST /api/orders/:id/action', mcp_tool: 'webaz_update_order', pwa: '#me', see: '① order lifecycle · ⑦ liability', notes: 'single state-machine endpoint; body { action } ∈ {accept|ship|pickup|transit|deliver|confirm|dispute}. Missing a deadline → auto fault.' },
28
+ { goal: 'Confirm receipt (buyer closes the order)', when: 'buyer received the goods', action: 'confirm_order', endpoint: 'POST /api/orders/:id/action', mcp_tool: 'webaz_update_order', pwa: '#me', notes: 'body { action: "confirm" }. Auto-confirm on confirm_deadline timeout.' },
29
29
  // ── dispute / verify ──
30
30
  { goal: 'Respond to a dispute as a party', when: 'a counterparty opened a dispute on your order', action: 'dispute_respond', endpoint: 'POST /api/disputes/:id/respond', mcp_tool: 'webaz_dispute', pwa: '#me', see: '① entity dispute · ⑦ liability' },
31
31
  { goal: 'Look up public dispute precedents', when: 'assess a seller / understand likely ruling', action: 'open', endpoint: 'GET /api/disputes/cases (+ /by-product/:id)', mcp_tool: 'webaz_dispute', pwa: '#disputes', see: '① entity dispute', notes: 'redacted post-ruling cases; amount is bucketed.' },
32
32
  { goal: 'Verify an agent passport / external anchor / AP2 mandate', when: 'check a counterparty/data is genuine', action: 'open', endpoint: 'GET /.well-known/webaz-verifiability.json', mcp_tool: null, pwa: '(n/a)', see: '⑤ verifiability index', notes: 'offline-verifiable where signed; order-chain is integrity-only.' },
33
33
  // ── participate / social ──
34
34
  { goal: 'Become a value participant (earn/pay/stake)', when: 'integrate as seller/logistics/verifier/insurer/etc.', action: 'open', endpoint: 'GET /.well-known/webaz-economic.json', mcp_tool: null, pwa: '(n/a)', see: '⑧ economic participation', notes: 'roles + live rates + collateral + conserved liability.' },
35
- { goal: 'Communicate with a trade counterparty', when: 'ask seller a question / coordinate an order', action: 'chat', endpoint: 'POST /api/conversations', mcp_tool: 'webaz_chat', pwa: '#messages', notes: 'every message attaches to a trade context — not general LLM chat.' },
35
+ { goal: 'Communicate with a trade counterparty', when: 'ask seller a question / coordinate an order', action: 'chat', endpoint: 'POST /api/conversations/start', mcp_tool: 'webaz_chat', pwa: '#messages', notes: 'start a thread; then POST /api/conversations/:id/messages. Every message attaches to a trade context — not general LLM chat.' },
36
36
  { goal: 'Share / refer a product for commission', when: 'promote a listing; attributed sales pay commission', action: 'share', endpoint: 'POST /api/shareables', mcp_tool: 'webaz_shareables', pwa: '#me', see: '⑧ promoter role' },
37
- { goal: 'Publish or fund a charity wish / community fund', when: 'community mutual-aid', action: 'charity', endpoint: 'POST /api/wishes · POST /api/charity', mcp_tool: 'webaz_charity', pwa: '#charity', notes: 'distinct from place_order donation_pct.' },
37
+ { goal: 'Publish or fund a charity wish / community fund', when: 'community mutual-aid', action: 'charity', endpoint: 'POST /api/wishes', mcp_tool: 'webaz_charity', pwa: '#charity', notes: 'publish a wish; fund the shared pool via POST /api/charity/fund/donate. Distinct from place_order donation_pct.' },
38
38
  { goal: 'Donate to the community fund', when: 'contribute to the shared fund', action: 'donate', endpoint: 'POST /api/charity/fund/donate', mcp_tool: 'webaz_charity', pwa: '#charity' },
39
39
  // ── self state ──
40
40
  { goal: 'Set a shipping address (PII write)', when: 'before a shipped order', action: 'set_address', endpoint: 'POST /api/addresses', mcp_tool: 'webaz_default_address', pwa: '#me', see: '② write_action set_address (元规则#3 PII gate)' },