@seasonkoh/webaz 0.1.24 → 0.1.26

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 (195) hide show
  1. package/README.md +5 -1
  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 +288 -208
  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 +182 -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 +11 -3
  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-discovery.js +55 -0
  35. package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
  36. package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
  37. package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
  38. package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
  39. package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
  40. package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
  41. package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
  42. package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
  43. package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
  44. package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
  45. package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
  46. package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
  47. package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
  48. package/dist/pwa/acp-feed.js +13 -1
  49. package/dist/pwa/admin-bearer-auth.js +21 -0
  50. package/dist/pwa/contract-fingerprint.js +2 -0
  51. package/dist/pwa/email-delivery.js +127 -0
  52. package/dist/pwa/endpoint-actions.js +5 -1
  53. package/dist/pwa/goal-index.js +8 -8
  54. package/dist/pwa/human-presence.js +62 -0
  55. package/dist/pwa/public/app.js +1485 -283
  56. package/dist/pwa/public/i18n.js +297 -59
  57. package/dist/pwa/public/index.html +1 -0
  58. package/dist/pwa/public/openapi.json +5 -5
  59. package/dist/pwa/public/whitepaper/en/index.html +153 -0
  60. package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
  61. package/dist/pwa/rate-limit.js +22 -0
  62. package/dist/pwa/routes/account-deletion.js +15 -13
  63. package/dist/pwa/routes/addresses.js +10 -9
  64. package/dist/pwa/routes/admin-admins.js +13 -14
  65. package/dist/pwa/routes/admin-analytics.js +109 -69
  66. package/dist/pwa/routes/admin-atomic.js +10 -4
  67. package/dist/pwa/routes/admin-catalog.js +13 -11
  68. package/dist/pwa/routes/admin-editor-picks.js +15 -10
  69. package/dist/pwa/routes/admin-events.js +5 -3
  70. package/dist/pwa/routes/admin-health.js +2 -1
  71. package/dist/pwa/routes/admin-moderation.js +50 -29
  72. package/dist/pwa/routes/admin-ops.js +35 -23
  73. package/dist/pwa/routes/admin-protocol-params.js +16 -19
  74. package/dist/pwa/routes/admin-reports.js +23 -21
  75. package/dist/pwa/routes/admin-tokenomics.js +26 -25
  76. package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
  77. package/dist/pwa/routes/admin-users-query.js +65 -53
  78. package/dist/pwa/routes/admin-verifier-flow.js +82 -41
  79. package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
  80. package/dist/pwa/routes/admin-wallet-ops.js +32 -7
  81. package/dist/pwa/routes/agent-buy.js +46 -22
  82. package/dist/pwa/routes/agent-governance.js +52 -56
  83. package/dist/pwa/routes/ai.js +7 -5
  84. package/dist/pwa/routes/analytics.js +43 -41
  85. package/dist/pwa/routes/anchors.js +19 -20
  86. package/dist/pwa/routes/announcements.js +13 -13
  87. package/dist/pwa/routes/arbitrator.js +97 -31
  88. package/dist/pwa/routes/auction.js +157 -116
  89. package/dist/pwa/routes/auth-login.js +6 -4
  90. package/dist/pwa/routes/auth-read.js +21 -10
  91. package/dist/pwa/routes/auth-register.js +111 -26
  92. package/dist/pwa/routes/auth-sessions.js +12 -11
  93. package/dist/pwa/routes/blocklist.js +16 -15
  94. package/dist/pwa/routes/build-feedback.js +10 -9
  95. package/dist/pwa/routes/build-reputation.js +6 -2
  96. package/dist/pwa/routes/build-tasks.js +45 -13
  97. package/dist/pwa/routes/buyer-feeds.js +27 -25
  98. package/dist/pwa/routes/cart.js +16 -15
  99. package/dist/pwa/routes/charity.js +212 -150
  100. package/dist/pwa/routes/chat.js +42 -43
  101. package/dist/pwa/routes/checkin-tasks.js +10 -9
  102. package/dist/pwa/routes/checkout-helpers.js +12 -10
  103. package/dist/pwa/routes/claim-initiators.js +34 -14
  104. package/dist/pwa/routes/claim-verify.js +86 -53
  105. package/dist/pwa/routes/claim-voting.js +43 -18
  106. package/dist/pwa/routes/contribution-identity.js +164 -0
  107. package/dist/pwa/routes/contribution-score.js +19 -0
  108. package/dist/pwa/routes/coupons.js +19 -16
  109. package/dist/pwa/routes/dashboards.js +18 -16
  110. package/dist/pwa/routes/dispute-cases.js +25 -24
  111. package/dist/pwa/routes/disputes-read.js +45 -51
  112. package/dist/pwa/routes/disputes-write.js +124 -61
  113. package/dist/pwa/routes/evidence.js +9 -9
  114. package/dist/pwa/routes/external-anchors.js +13 -12
  115. package/dist/pwa/routes/feedback.js +29 -33
  116. package/dist/pwa/routes/flash-sales.js +18 -16
  117. package/dist/pwa/routes/follows.js +25 -24
  118. package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
  119. package/dist/pwa/routes/governance-onboarding.js +70 -59
  120. package/dist/pwa/routes/group-buys.js +22 -22
  121. package/dist/pwa/routes/growth.js +34 -31
  122. package/dist/pwa/routes/import-product.js +12 -10
  123. package/dist/pwa/routes/kyc.js +9 -8
  124. package/dist/pwa/routes/leaderboard.js +20 -18
  125. package/dist/pwa/routes/listings.js +23 -22
  126. package/dist/pwa/routes/logistics.js +10 -8
  127. package/dist/pwa/routes/manifests.js +27 -27
  128. package/dist/pwa/routes/me-data.js +23 -21
  129. package/dist/pwa/routes/notifications.js +7 -6
  130. package/dist/pwa/routes/offers.js +30 -12
  131. package/dist/pwa/routes/orders-action.js +51 -29
  132. package/dist/pwa/routes/orders-create.js +75 -20
  133. package/dist/pwa/routes/orders-read.js +21 -20
  134. package/dist/pwa/routes/p2p-products.js +30 -18
  135. package/dist/pwa/routes/payments-governance.js +61 -56
  136. package/dist/pwa/routes/peers.js +9 -8
  137. package/dist/pwa/routes/pin-receipts.js +13 -13
  138. package/dist/pwa/routes/products-aliases.js +12 -10
  139. package/dist/pwa/routes/products-claims.js +36 -17
  140. package/dist/pwa/routes/products-create.js +53 -38
  141. package/dist/pwa/routes/products-crud.js +17 -16
  142. package/dist/pwa/routes/products-links.js +49 -26
  143. package/dist/pwa/routes/products-list.js +6 -4
  144. package/dist/pwa/routes/products-meta.js +40 -39
  145. package/dist/pwa/routes/products-update.js +19 -5
  146. package/dist/pwa/routes/profile-credentials.js +20 -19
  147. package/dist/pwa/routes/profile-identity.js +14 -13
  148. package/dist/pwa/routes/profile-location.js +7 -6
  149. package/dist/pwa/routes/profile-placement.js +20 -19
  150. package/dist/pwa/routes/profile-prefs.js +11 -11
  151. package/dist/pwa/routes/promoter.js +58 -66
  152. package/dist/pwa/routes/public-build-tasks.js +19 -0
  153. package/dist/pwa/routes/public-utils.js +108 -46
  154. package/dist/pwa/routes/push.js +16 -15
  155. package/dist/pwa/routes/ratings.js +92 -32
  156. package/dist/pwa/routes/recover-key.js +66 -26
  157. package/dist/pwa/routes/referral.js +37 -52
  158. package/dist/pwa/routes/reputation.js +3 -2
  159. package/dist/pwa/routes/returns.js +76 -73
  160. package/dist/pwa/routes/reviews.js +41 -18
  161. package/dist/pwa/routes/rewards-apply.js +16 -15
  162. package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
  163. package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
  164. package/dist/pwa/routes/rfqs.js +163 -85
  165. package/dist/pwa/routes/search.js +16 -14
  166. package/dist/pwa/routes/secondhand.js +25 -22
  167. package/dist/pwa/routes/seller-quota.js +24 -26
  168. package/dist/pwa/routes/share-redirects.js +60 -55
  169. package/dist/pwa/routes/shareables-interactions.js +34 -35
  170. package/dist/pwa/routes/shareables.js +55 -51
  171. package/dist/pwa/routes/shop-referral.js +58 -0
  172. package/dist/pwa/routes/shops.js +25 -20
  173. package/dist/pwa/routes/signaling.js +10 -9
  174. package/dist/pwa/routes/skill-market.js +16 -16
  175. package/dist/pwa/routes/skills.js +15 -14
  176. package/dist/pwa/routes/snf.js +14 -13
  177. package/dist/pwa/routes/tags.js +10 -9
  178. package/dist/pwa/routes/task-proposals.js +121 -0
  179. package/dist/pwa/routes/trial.js +72 -52
  180. package/dist/pwa/routes/trusted-kpi.js +20 -18
  181. package/dist/pwa/routes/url-claim.js +67 -28
  182. package/dist/pwa/routes/users-public.js +62 -70
  183. package/dist/pwa/routes/variants.js +12 -13
  184. package/dist/pwa/routes/verifier-user.js +61 -21
  185. package/dist/pwa/routes/verify-tasks.js +49 -25
  186. package/dist/pwa/routes/waitlist.js +16 -15
  187. package/dist/pwa/routes/wallet-read.js +75 -37
  188. package/dist/pwa/routes/wallet-write.js +12 -9
  189. package/dist/pwa/routes/webauthn.js +25 -26
  190. package/dist/pwa/routes/webhooks.js +26 -26
  191. package/dist/pwa/routes/welcome.js +45 -50
  192. package/dist/pwa/routes/wishlist-qa.js +29 -32
  193. package/dist/pwa/server.js +304 -90
  194. package/dist/version.js +1 -1
  195. package/package.json +76 -3
@@ -25,6 +25,7 @@
25
25
  */
26
26
  import crypto from 'crypto';
27
27
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
28
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
28
29
  export function initExternalAnchorSchema(db) {
29
30
  db.exec(`
30
31
  CREATE TABLE IF NOT EXISTS external_anchors (
@@ -164,14 +165,15 @@ export function createAnchor(db, args) {
164
165
  `).run(id, args.sellerId, args.productId || null, args.platform, args.externalUrl, canonJson, contentHash, signature, args.sellerNodeUrl || null, fee);
165
166
  return { id, content_hash: contentHash, signature, verification_fee: fee };
166
167
  }
167
- export function verifyAnchorSignature(db, anchorId) {
168
- const r = db.prepare('SELECT seller_id, canonical_json, content_hash, signature FROM external_anchors WHERE id = ?').get(anchorId);
168
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 external-anchors.ts 均 inTx=false,无引擎内写调用)
169
+ export async function verifyAnchorSignature(_db, anchorId) {
170
+ const r = await dbOne('SELECT seller_id, canonical_json, content_hash, signature FROM external_anchors WHERE id = ?', [anchorId]);
169
171
  if (!r)
170
172
  return { ok: false, reason: 'not_found' };
171
173
  const reHash = sha256Hex(r.canonical_json);
172
174
  if (reHash !== r.content_hash)
173
175
  return { ok: false, reason: 'content_hash_mismatch' };
174
- const seller = db.prepare('SELECT api_key FROM users WHERE id = ?').get(r.seller_id);
176
+ const seller = await dbOne('SELECT api_key FROM users WHERE id = ?', [r.seller_id]);
175
177
  if (!seller)
176
178
  return { ok: false, reason: 'seller_not_found' };
177
179
  const sig = crypto.createHmac('sha256', seller.api_key).update(r.canonical_json).digest('hex');
@@ -307,18 +309,18 @@ export function distributeAnchorRewards(db, anchorId) {
307
309
  })();
308
310
  return Math.round(actualPaid * 100) / 100;
309
311
  }
310
- export function getAnchor(db, anchorId) {
311
- const row = db.prepare('SELECT * FROM external_anchors WHERE id = ?').get(anchorId);
312
+ export async function getAnchor(_db, anchorId) {
313
+ const row = await dbOne('SELECT * FROM external_anchors WHERE id = ?', [anchorId]);
312
314
  if (!row)
313
315
  return null;
314
- const verifs = db.prepare('SELECT id, verifier_id, verifier_role, content_matches, token_found, verified_at FROM external_anchor_verifications WHERE anchor_id = ? ORDER BY verified_at DESC LIMIT 20').all(anchorId);
316
+ const verifs = await dbAll('SELECT id, verifier_id, verifier_role, content_matches, token_found, verified_at FROM external_anchor_verifications WHERE anchor_id = ? ORDER BY verified_at DESC LIMIT 20', [anchorId]);
315
317
  return { ...row, verifications: verifs };
316
318
  }
317
- export function listAnchorsByProduct(db, productId) {
318
- return db.prepare(`SELECT id, seller_id, platform, external_url, content_hash, ownership_verified, verify_count, seller_node_url, posted_at, revoked
319
- FROM external_anchors WHERE product_id = ? AND revoked = 0 ORDER BY posted_at DESC`).all(productId);
319
+ export async function listAnchorsByProduct(_db, productId) {
320
+ return await dbAll(`SELECT id, seller_id, platform, external_url, content_hash, ownership_verified, verify_count, seller_node_url, posted_at, revoked
321
+ FROM external_anchors WHERE product_id = ? AND revoked = 0 ORDER BY posted_at DESC`, [productId]);
320
322
  }
321
- export function listAnchorsBySeller(db, sellerId) {
322
- return db.prepare(`SELECT id, product_id, platform, external_url, content_hash, ownership_verified, verify_count, seller_node_url, posted_at, revoked
323
- FROM external_anchors WHERE seller_id = ? ORDER BY revoked ASC, posted_at DESC LIMIT 200`).all(sellerId);
323
+ export async function listAnchorsBySeller(_db, sellerId) {
324
+ return await dbAll(`SELECT id, product_id, platform, external_url, content_hash, ownership_verified, verify_count, seller_node_url, posted_at, revoked
325
+ FROM external_anchors WHERE seller_id = ? ORDER BY revoked ASC, posted_at DESC LIMIT 200`, [sellerId]);
324
326
  }
@@ -6,6 +6,7 @@
6
6
  * PWA 通过 SSE 实时接收;Agent 通过 dcp_notifications 工具轮询。
7
7
  */
8
8
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
9
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
9
10
  // ─── Schema 初始化 ────────────────────────────────────────────
10
11
  export function initNotificationSchema(db) {
11
12
  db.exec(`
@@ -206,14 +207,16 @@ export function createNotification(db, userId, orderId, type, title, body) {
206
207
  return notif;
207
208
  }
208
209
  // ─── 查询 ─────────────────────────────────────────────────────
209
- export function getNotifications(db, userId, onlyUnread = false, limit = 30) {
210
+ // RFC-016 Phase 1:纯读 异步 seam。db 参数保留(签名兼容),内部走 dbAll/dbOne(同实例,setSeamDb)
211
+ // 调用点全部已确认不在 db.transaction 内(notifications.ts:43/60/61 + mcp server.ts:2770/2771)。
212
+ export async function getNotifications(_db, userId, onlyUnread = false, limit = 30) {
210
213
  const sql = `SELECT * FROM notifications WHERE user_id = ?${onlyUnread ? ' AND read = 0' : ''}
211
214
  ORDER BY created_at DESC LIMIT ?`;
212
- return db.prepare(sql).all(userId, limit);
215
+ return await dbAll(sql, [userId, limit]);
213
216
  }
214
- export function getUnreadCount(db, userId) {
215
- const row = db.prepare('SELECT COUNT(*) as n FROM notifications WHERE user_id = ? AND read = 0').get(userId);
216
- return row.n;
217
+ export async function getUnreadCount(_db, userId) {
218
+ const row = await dbOne('SELECT COUNT(*) as n FROM notifications WHERE user_id = ? AND read = 0', [userId]);
219
+ return row?.n ?? 0;
217
220
  }
218
221
  export function markRead(db, userId, notifId) {
219
222
  if (notifId) {
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import crypto from 'crypto';
23
23
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
24
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
24
25
  export const SNF_TTL_DAYS = 30;
25
26
  export const SNF_MAX_PAYLOAD = 32 * 1024; // 32KB 单条上限(大附件走 manifest_registry,SNF 只传 hash)
26
27
  export const SNF_MAX_RETRIES = 5; // pull → nack 累计超过此次数 → 自动 dead-letter
@@ -115,8 +116,9 @@ export function snfSend(db, args) {
115
116
  }
116
117
  // 只读 list — 列出我作为收件人的近期消息(含已 delivered,TTL 内)
117
118
  // 用于 UI 显示。不消费 — 刷新 / 重进页面都能看到。
118
- export function snfListInbox(db, userId, limit = 80, sinceDays = 30) {
119
- const rows = db.prepare(`
119
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 snf.ts inTx=false,无引擎内写调用)
120
+ export async function snfListInbox(_db, userId, limit = 80, sinceDays = 30) {
121
+ const rows = await dbAll(`
120
122
  SELECT id, sender_id, message_type, payload, signature, created_at, delivered_at, priority, related_order_id
121
123
  FROM snf_messages
122
124
  WHERE recipient_id = ?
@@ -125,7 +127,7 @@ export function snfListInbox(db, userId, limit = 80, sinceDays = 30) {
125
127
  AND datetime(created_at) > datetime('now', ?)
126
128
  ORDER BY priority DESC, created_at DESC
127
129
  LIMIT ?
128
- `).all(userId, '-' + sinceDays + ' days', limit);
130
+ `, [userId, '-' + sinceDays + ' days', limit]);
129
131
  return rows.map(r => ({
130
132
  id: r.id,
131
133
  sender_id: r.sender_id,
@@ -221,14 +223,14 @@ export function snfNack(db, userId, msgIds, error) {
221
223
  return { reopened, deadLettered };
222
224
  }
223
225
  // 列出死信消息(人工 review 用 — admin / agent 异常排查)
224
- export function snfListDeadLetter(db, userId, limit = 50) {
225
- return db.prepare(`
226
+ export async function snfListDeadLetter(_db, userId, limit = 50) {
227
+ return await dbAll(`
226
228
  SELECT id, sender_id, message_type, delivery_attempts, last_error, last_attempt_at, created_at, related_order_id
227
229
  FROM snf_messages
228
230
  WHERE recipient_id = ? AND dead_letter = 1
229
231
  ORDER BY last_attempt_at DESC NULLS LAST, created_at DESC
230
232
  LIMIT ?
231
- `).all(userId, limit);
233
+ `, [userId, limit]);
232
234
  }
233
235
  // 死信复活:清零 attempts + dead_letter + delivered_at,重新进 active 队列
234
236
  // 用于:手动审查发现 transient 错误后想再试
@@ -244,18 +246,18 @@ export function snfRevive(db, userId, msgId) {
244
246
  return { ok: true };
245
247
  }
246
248
  // 查看 inbox 未读数(不消费)
247
- export function snfPendingCount(db, userId) {
248
- const r = db.prepare(`SELECT COUNT(*) as n FROM snf_messages WHERE recipient_id = ? AND delivered_at IS NULL AND dead_letter = 0 AND datetime(expires_at) > datetime('now')`).get(userId);
249
- return r.n;
249
+ export async function snfPendingCount(_db, userId) {
250
+ const r = await dbOne(`SELECT COUNT(*) as n FROM snf_messages WHERE recipient_id = ? AND delivered_at IS NULL AND dead_letter = 0 AND datetime(expires_at) > datetime('now')`, [userId]);
251
+ return r?.n ?? 0;
250
252
  }
251
253
  // 验证签名(用 sender 当时的 api_key — 若 key 旋转过则失败)
252
- export function snfVerify(db, msgId) {
253
- const r = db.prepare(`SELECT sender_id, payload, signature FROM snf_messages WHERE id = ?`).get(msgId);
254
+ export async function snfVerify(_db, msgId) {
255
+ const r = await dbOne(`SELECT sender_id, payload, signature FROM snf_messages WHERE id = ?`, [msgId]);
254
256
  if (!r)
255
257
  return { ok: false, reason: 'not_found' };
256
258
  if (!r.signature)
257
259
  return { ok: false, reason: 'no_signature' };
258
- const sender = db.prepare('SELECT api_key FROM users WHERE id = ?').get(r.sender_id);
260
+ const sender = await dbOne('SELECT api_key FROM users WHERE id = ?', [r.sender_id]);
259
261
  if (!sender)
260
262
  return { ok: false, reason: 'sender_gone' };
261
263
  const sig = crypto.createHmac('sha256', sender.api_key).update(r.payload).digest('hex');
@@ -268,8 +270,8 @@ export function snfDesignate(db, userId, peers) {
268
270
  ON CONFLICT(user_id) DO UPDATE SET snf_peers = excluded.snf_peers, updated_at = datetime('now')
269
271
  `).run(userId, JSON.stringify(peers.slice(0, 5))); // 上限 5 个
270
272
  }
271
- export function snfGetDesignation(db, userId) {
272
- const r = db.prepare(`SELECT snf_peers FROM snf_designations WHERE user_id = ?`).get(userId);
273
+ export async function snfGetDesignation(_db, userId) {
274
+ const r = await dbOne(`SELECT snf_peers FROM snf_designations WHERE user_id = ?`, [userId]);
273
275
  if (!r)
274
276
  return [];
275
277
  try {
@@ -1,4 +1,5 @@
1
1
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
2
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
2
3
  // RFC-006 不变量 1:建设贡献记入【独立】build_reputation 池,不再写交易 reputation_scores
3
4
  //(旧:recordRepEvent('feedback_accepted') 会污染 verifier/arbitrator 准入,已隔离)。
4
5
  import { creditBuildReputation, BUILD_POINTS } from '../L2-9-contribution/build-reputation-engine.js';
@@ -139,10 +140,10 @@ function parse(row) {
139
140
  const { scene_json, ...rest } = row;
140
141
  return { ...rest, scene };
141
142
  }
142
- export function listMyBuildFeedback(db, userId) {
143
- const rows = db.prepare(`SELECT id, type, area, severity, subject, body, status, dedup_of, resolution, credited_points, credit_pending_anchor, promoted_task_id, created_at, updated_at
144
- FROM build_feedback WHERE user_id = ? ORDER BY created_at DESC LIMIT 100`).all(userId);
145
- return rows;
143
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 build-feedback.ts:62/68/77 已确认不在 db.transaction 内)
144
+ export async function listMyBuildFeedback(_db, userId) {
145
+ return await dbAll(`SELECT id, type, area, severity, subject, body, status, dedup_of, resolution, credited_points, credit_pending_anchor, promoted_task_id, created_at, updated_at
146
+ FROM build_feedback WHERE user_id = ? ORDER BY created_at DESC LIMIT 100`, [userId]);
146
147
  }
147
148
  /**
148
149
  * RFC-004 体验补:提交者【事后】绑定 Passkey 时,追溯补发此前"已受理但因无锚点跳过记功"的贡献信誉。
@@ -167,19 +168,19 @@ export function grantPendingAnchorCredits(db, userId) {
167
168
  }
168
169
  return { granted, total_points: total };
169
170
  }
170
- export function getBuildFeedback(db, id, userId, isAdmin) {
171
- const row = db.prepare('SELECT * FROM build_feedback WHERE id = ?').get(id);
171
+ export async function getBuildFeedback(_db, id, userId, isAdmin) {
172
+ const row = await dbOne('SELECT * FROM build_feedback WHERE id = ?', [id]);
172
173
  if (!row)
173
174
  return null;
174
175
  if (!isAdmin && row.user_id !== userId)
175
176
  return null;
176
- const events = db.prepare('SELECT from_status, to_status, note, created_at FROM build_feedback_events WHERE feedback_id = ? ORDER BY created_at').all(id);
177
+ const events = await dbAll('SELECT from_status, to_status, note, created_at FROM build_feedback_events WHERE feedback_id = ? ORDER BY created_at', [id]);
177
178
  return { ...parse(row), events };
178
179
  }
179
- export function adminListBuildFeedback(db, status) {
180
+ export async function adminListBuildFeedback(_db, status) {
180
181
  const rows = (status && FB_STATUS.has(status))
181
- ? db.prepare('SELECT * FROM build_feedback WHERE status = ? ORDER BY created_at DESC LIMIT 200').all(status)
182
- : db.prepare('SELECT * FROM build_feedback ORDER BY created_at DESC LIMIT 200').all();
182
+ ? await dbAll('SELECT * FROM build_feedback WHERE status = ? ORDER BY created_at DESC LIMIT 200', [status])
183
+ : await dbAll('SELECT * FROM build_feedback ORDER BY created_at DESC LIMIT 200');
183
184
  return rows.map(parse);
184
185
  }
185
186
  export function adminUpdateBuildFeedback(db, u) {
@@ -188,6 +189,13 @@ export function adminUpdateBuildFeedback(db, u) {
188
189
  return { error: '反馈不存在' };
189
190
  const fromStatus = row.status;
190
191
  const newStatus = u.status && FB_STATUS.has(u.status) ? u.status : fromStatus;
192
+ // Codex #113 P2:promote 成 build_task 语义上是"采纳的 proposal → 来一起建设",通知文案也说"被采纳了"。
193
+ // 因此只允许在本次更新把状态置为 resolved(采纳)时 promote;否则 support admin 误传 promote_to_task=true
194
+ // 会把 received/triaged/rejected 的 proposal 建成 open task 并谎称"被采纳",破坏贡献漏斗语义。
195
+ // 先于任何写返回错误,避免部分副作用。
196
+ if (u.promoteToTask && newStatus !== 'resolved') {
197
+ return { error: 'PROMOTE_REQUIRES_RESOLVED' };
198
+ }
191
199
  // co-build 信誉:仅在置为 resolved + credit 且此前未记功时发放(防重复发放 / gaming)。
192
200
  // 分级门(RFC-004 精确化):信誉只发给【有 Passkey 锚点】的提交者 —— 奖励必须锚真人;
193
201
  // 无 Passkey 的报告者(报问题=用)可受理致谢,但无锚点不记分。
@@ -1,4 +1,5 @@
1
1
  import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
2
+ import { dbOne, dbAll } from '../../layer0-foundation/L0-1-database/db.js'; // RFC-016 异步 seam(纯读)
2
3
  // 建设贡献分值(独立计量,与交易分无关)
3
4
  export const BUILD_POINTS = {
4
5
  feedback_accepted: 8, // RFC-004 反馈/提案被采纳
@@ -36,17 +37,27 @@ export function initBuildReputationSchema(db) {
36
37
  )
37
38
  `);
38
39
  db.exec(`CREATE INDEX IF NOT EXISTS idx_build_rep_events ON build_reputation_events(user_id, created_at DESC)`);
40
+ // Codex #104 P3:DB 级去重 —— 同 (source, ref_id) 只记一次 build event(并发/双击/多 worker 安全;
41
+ // 仅约束 ref_id 非空,无 ref 的事件不去重)。先清历史重复行(每组保留最早 id)再建唯一索引,保证能建成。
42
+ db.exec(`DELETE FROM build_reputation_events WHERE ref_id IS NOT NULL AND id NOT IN (
43
+ SELECT MIN(id) FROM build_reputation_events WHERE ref_id IS NOT NULL GROUP BY source, ref_id)`);
44
+ db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS uniq_build_rep_source_ref ON build_reputation_events(source, ref_id) WHERE ref_id IS NOT NULL`);
39
45
  }
40
46
  // 记入建设信誉(独立池)。防重复:同 (source, ref_id) 只记一次。
41
- // 注:调用方负责校验提交者【有 Passkey 锚点】(奖励锚真人);本函数只管入池。
47
+ // 注:调用方负责校验提交者【有 Passkey 锚点】(可问责真人锚点);本函数只管入池。
42
48
  export function creditBuildReputation(db, userId, source, points, refId, note) {
49
+ // 去重权威靠 DB 级 partial UNIQUE(source, ref_id) + INSERT OR IGNORE:并发/双击/多 worker 下
50
+ // 同一 (source, ref_id) 只入一次 event,且仅当真正插入(changes===1)才加 build_points → 绝不重复计分。
43
51
  if (refId) {
44
- const dup = db.prepare(`SELECT id FROM build_reputation_events WHERE source = ? AND ref_id = ?`).get(source, refId);
45
- if (dup)
46
- return { credited: 0, already: true };
52
+ const ins = db.prepare(`INSERT OR IGNORE INTO build_reputation_events (id, user_id, source, points, ref_id, note) VALUES (?,?,?,?,?,?)`)
53
+ .run(generateId('brev'), userId, source, points, refId, note ?? null);
54
+ if (ins.changes === 0)
55
+ return { credited: 0, already: true }; // 唯一冲突 → 已记过,不重复加分
56
+ }
57
+ else {
58
+ db.prepare(`INSERT INTO build_reputation_events (id, user_id, source, points, ref_id, note) VALUES (?,?,?,?,?,?)`)
59
+ .run(generateId('brev'), userId, source, points, null, note ?? null);
47
60
  }
48
- db.prepare(`INSERT INTO build_reputation_events (id, user_id, source, points, ref_id, note) VALUES (?,?,?,?,?,?)`)
49
- .run(generateId('brev'), userId, source, points, refId ?? null, note ?? null);
50
61
  const existing = db.prepare(`SELECT build_points FROM build_reputation WHERE user_id = ?`).get(userId);
51
62
  if (!existing) {
52
63
  db.prepare(`INSERT INTO build_reputation (user_id, build_points) VALUES (?, ?)`).run(userId, Math.max(0, points));
@@ -63,32 +74,33 @@ export function creditBuildReputation(db, userId, source, points, refId, note) {
63
74
  // 看板这里只【展示】当事人的活跃 strike + 申诉入口(透明先于强制);真人申诉走现成 strikes/:id/appeal。
64
75
  // 贡献者【自查】档案 —— KPI + 等级 + 来源拆分 + provenance + 限制/惩罚 + 申诉入口。
65
76
  // 不变量 3:仅本人可调(路由层 auth);不做公开榜。
66
- export function getBuildProfile(db, userId) {
67
- const num = (sql, ...p) => db.prepare(sql).get(...p).n;
77
+ // RFC-016 Phase 1:纯读 → 异步 seam(db 参数保留签名兼容;调用点 build-reputation.ts:24 已确认不在 db.transaction 内)
78
+ export async function getBuildProfile(_db, userId) {
79
+ const num = async (sql, ...p) => ((await dbOne(sql, p))?.n ?? 0);
68
80
  // KPI(从 build_tasks + build_feedback 实算)
69
81
  const kpi = {
70
- tasks_claimed: num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'claimed'`, userId),
71
- tasks_in_review: num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'in_review'`, userId),
72
- tasks_done: num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'done'`, userId),
73
- tasks_created: num(`SELECT COUNT(*) n FROM build_tasks WHERE created_by = ?`, userId),
74
- feedback_submitted: num(`SELECT COUNT(*) n FROM build_feedback WHERE user_id = ?`, userId),
75
- feedback_accepted: num(`SELECT COUNT(*) n FROM build_feedback WHERE user_id = ? AND status = 'resolved' AND credited_points > 0`, userId),
82
+ tasks_claimed: await num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'claimed'`, userId),
83
+ tasks_in_review: await num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'in_review'`, userId),
84
+ tasks_done: await num(`SELECT COUNT(*) n FROM build_tasks WHERE claimer_id = ? AND status = 'done'`, userId),
85
+ tasks_created: await num(`SELECT COUNT(*) n FROM build_tasks WHERE created_by = ?`, userId),
86
+ feedback_submitted: await num(`SELECT COUNT(*) n FROM build_feedback WHERE user_id = ?`, userId),
87
+ feedback_accepted: await num(`SELECT COUNT(*) n FROM build_feedback WHERE user_id = ? AND status = 'resolved' AND credited_points > 0`, userId),
76
88
  };
77
- const summary = db.prepare(`SELECT build_points FROM build_reputation WHERE user_id = ?`).get(userId);
89
+ const summary = await dbOne(`SELECT build_points FROM build_reputation WHERE user_id = ?`, [userId]);
78
90
  const buildPoints = summary?.build_points ?? 0;
79
91
  const tier = tierFor(buildPoints);
80
- const bySource = db.prepare(`SELECT source, COUNT(*) AS count, COALESCE(SUM(points),0) AS points
81
- FROM build_reputation_events WHERE user_id = ? GROUP BY source`).all(userId);
92
+ const bySource = await dbAll(`SELECT source, COUNT(*) AS count, COALESCE(SUM(points),0) AS points
93
+ FROM build_reputation_events WHERE user_id = ? GROUP BY source`, [userId]);
82
94
  // provenance 透明(自报,非检测):我认领的任务里 human/ai_assisted/ai_authored 各多少
83
- const provenance = db.prepare(`SELECT COALESCE(claimer_provenance,'unspecified') AS provenance, COUNT(*) AS count
84
- FROM build_tasks WHERE claimer_id = ? GROUP BY claimer_provenance`).all(userId);
95
+ const provenance = await dbAll(`SELECT COALESCE(claimer_provenance,'unspecified') AS provenance, COUNT(*) AS count
96
+ FROM build_tasks WHERE claimer_id = ? GROUP BY claimer_provenance`, [userId]);
85
97
  // 限制 / 惩罚(复用现有 agent_strikes;只读 + 申诉入口)。pre-launch 通常为空。
86
- const strikes = db.prepare(`SELECT id, severity, reason_code, reason_detail, issued_at, expires_at, appeal_status
98
+ const strikes = await dbAll(`SELECT id, severity, reason_code, reason_detail, issued_at, expires_at, appeal_status
87
99
  FROM agent_strikes WHERE user_id = ?
88
100
  AND (expires_at IS NULL OR expires_at > datetime('now'))
89
101
  AND COALESCE(appeal_status,'') != 'upheld_removed'
90
- ORDER BY issued_at DESC LIMIT 20`).all(userId);
91
- const hasAnchor = num(`SELECT COUNT(*) n FROM webauthn_credentials WHERE user_id = ?`, userId) > 0;
102
+ ORDER BY issued_at DESC LIMIT 20`, [userId]);
103
+ const hasAnchor = (await num(`SELECT COUNT(*) n FROM webauthn_credentials WHERE user_id = ?`, userId)) > 0;
92
104
  return {
93
105
  user_id: userId,
94
106
  build_points: buildPoints,
@@ -99,7 +111,9 @@ export function getBuildProfile(db, userId) {
99
111
  standing: strikes.length === 0 ? 'ok' : 'flagged',
100
112
  restrictions: strikes, // 当事人看得见自己的扣分 + 原因
101
113
  appeal_hint: strikes.length > 0 ? 'POST /api/me/agents/strikes/:id/appeal' : null,
102
- reward_anchored: hasAnchor, // Passkey → 受理致谢但不记分(奖励锚真人)
114
+ // 是否已绑定 Passkey 锚点(把贡献锚定到可问责的真人)。无锚点 受理致谢但不记入建设信誉。
115
+ // 此处不承诺任何经济回报,只表达"是否锚定了可问责真人"(PR-5A 边界);该字段由旧的 reward-anchor 命名更名而来。
116
+ passkey_anchor_present: hasAnchor,
103
117
  // 不变量提示:此 build_points 仅用于建设分层 + 本看板,绝不喂交易侧(verifier/arbitrator)准入。
104
118
  pool: 'build_reputation (separate from trade reputation — never gates verifier/arbitrator)',
105
119
  };
@@ -0,0 +1,182 @@
1
+ export const RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
2
+ export const AUDIENCES = ['public', 'restricted', 'internal'];
3
+ export const AUTONOMIES = ['autonomous', 'supervised', 'human_in_the_loop', 'human_only'];
4
+ export const CONTEXT_SIZES = ['small', 'medium', 'large'];
5
+ export const AGENT_BUDGETS = ['minimal', 'small', 'moderate', 'large', 'xlarge'];
6
+ export const TASK_TYPES = ['docs', 'i18n', 'tests', 'sdk_example', 'ui', 'code', 'api', 'schema', 'infra', 'governance', 'audit', 'other']; // aligned with spec/agent-task/agent-task.schema.ts
7
+ const HUMAN_AUTONOMY = new Set(['human_in_the_loop', 'human_only']);
8
+ const CREATE_TABLE = `
9
+ CREATE TABLE IF NOT EXISTS build_task_agent_metadata (
10
+ task_id TEXT PRIMARY KEY REFERENCES build_tasks(id),
11
+ task_type TEXT NOT NULL CHECK (task_type IN ('docs','i18n','tests','sdk_example','ui','code','api','schema','infra','governance','audit','other')),
12
+ source_ref TEXT,
13
+ version TEXT,
14
+ allowed_paths TEXT NOT NULL CHECK (allowed_paths <> '' AND allowed_paths <> '[]'),
15
+ forbidden_paths TEXT NOT NULL DEFAULT '[]',
16
+ prohibited_actions TEXT NOT NULL CHECK (prohibited_actions <> '' AND prohibited_actions <> '[]'),
17
+ risk_level TEXT NOT NULL CHECK (risk_level IN ('low','medium','high','critical')),
18
+ audience TEXT NOT NULL CHECK (audience IN ('public','restricted','internal')),
19
+ agent_autonomy TEXT NOT NULL CHECK (agent_autonomy IN ('autonomous','supervised','human_in_the_loop','human_only')),
20
+ auto_claimable INTEGER NOT NULL CHECK (auto_claimable IN (0,1)),
21
+ human_confirmation_points TEXT NOT NULL DEFAULT '[]',
22
+ required_capabilities TEXT NOT NULL CHECK (required_capabilities <> '' AND required_capabilities <> '[]'),
23
+ acceptance_criteria TEXT NOT NULL CHECK (acceptance_criteria <> '' AND acceptance_criteria <> '[]'),
24
+ verification_commands TEXT NOT NULL CHECK (verification_commands <> '' AND verification_commands <> '[]'),
25
+ expected_results TEXT NOT NULL CHECK (length(trim(expected_results)) > 0),
26
+ deliverables TEXT NOT NULL CHECK (deliverables <> '' AND deliverables <> '[]'),
27
+ definition_of_done TEXT NOT NULL CHECK (length(trim(definition_of_done)) > 0),
28
+ estimated_duration_min_minutes INTEGER NOT NULL CHECK (estimated_duration_min_minutes >= 0),
29
+ estimated_duration_max_minutes INTEGER NOT NULL,
30
+ estimated_context_size TEXT NOT NULL CHECK (estimated_context_size IN ('small','medium','large')),
31
+ estimated_agent_budget TEXT NOT NULL CHECK (estimated_agent_budget IN ('minimal','small','moderate','large','xlarge')),
32
+ dependencies TEXT NOT NULL DEFAULT '[]',
33
+ blocking_conditions TEXT NOT NULL DEFAULT '[]',
34
+ value_state TEXT NOT NULL DEFAULT 'uncommitted' CHECK (value_state = 'uncommitted'),
35
+ contribution_type TEXT NOT NULL CHECK (length(trim(contribution_type)) > 0),
36
+ accountable_party_required INTEGER NOT NULL CHECK (accountable_party_required IN (0,1)),
37
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
38
+ CHECK (estimated_duration_max_minutes >= estimated_duration_min_minutes),
39
+ CHECK (risk_level NOT IN ('high','critical') OR auto_claimable = 0),
40
+ CHECK (risk_level NOT IN ('high','critical') OR agent_autonomy IN ('human_in_the_loop','human_only')),
41
+ CHECK (risk_level NOT IN ('high','critical') OR (human_confirmation_points <> '[]' AND human_confirmation_points <> '')),
42
+ CHECK (risk_level <> 'critical' OR audience <> 'public')
43
+ )
44
+ `;
45
+ const CREATE_INDEX = `CREATE INDEX IF NOT EXISTS idx_btam_discovery ON build_task_agent_metadata(risk_level, audience, auto_claimable)`;
46
+ export function initBuildTaskAgentMetadataSchema(db) {
47
+ db.exec(CREATE_TABLE);
48
+ db.exec(CREATE_INDEX);
49
+ }
50
+ // ── JSON list helpers (the ONLY place list fields are stringified/parsed) ──
51
+ function toJsonList(field, v, { allowEmpty }) {
52
+ if (!Array.isArray(v))
53
+ throw new Error(`${field} must be a string[]`);
54
+ // Every entry must be a non-blank string — a list with an empty/whitespace element is an unexecutable
55
+ // boundary (Codex P1: ['']/[' '] previously slipped past the coarse DB CHECK). This holds even for
56
+ // allowEmpty fields: the array may be empty, but it may not contain an empty element.
57
+ for (const x of v) {
58
+ if (typeof x !== 'string' || x.trim().length === 0)
59
+ throw new Error(`${field} entries must be non-empty strings`);
60
+ }
61
+ if (!allowEmpty && v.length === 0)
62
+ throw new Error(`${field} must be non-empty`);
63
+ return JSON.stringify(v);
64
+ }
65
+ export function parseJsonList(v) {
66
+ if (!v)
67
+ return [];
68
+ try {
69
+ const a = JSON.parse(v);
70
+ return Array.isArray(a) ? a.filter(x => typeof x === 'string') : [];
71
+ }
72
+ catch {
73
+ return [];
74
+ }
75
+ }
76
+ function assertEnum(field, v, allowed) {
77
+ if (typeof v !== 'string' || !allowed.includes(v))
78
+ throw new Error(`${field} must be one of ${allowed.join('|')} (got ${String(v)})`);
79
+ return v;
80
+ }
81
+ function assertNonEmptyText(field, v) {
82
+ if (typeof v !== 'string' || v.trim().length === 0)
83
+ throw new Error(`${field} must be non-empty text`);
84
+ return v;
85
+ }
86
+ /* eslint-disable @typescript-eslint/no-explicit-any */
87
+ /**
88
+ * Validate + insert a task's agent metadata. The store is the FIRST guard (throws on any contract
89
+ * violation); the DB CHECK/FK are the backstop. Lists are stringified to JSON here — callers pass arrays,
90
+ * never raw JSON.
91
+ */
92
+ export function insertBuildTaskAgentMetadata(db, taskId, m) {
93
+ if (!taskId)
94
+ throw new Error('taskId required');
95
+ const task_type = assertEnum('task_type', m.task_type, TASK_TYPES);
96
+ const risk_level = assertEnum('risk_level', m.risk_level, RISK_LEVELS);
97
+ const audience = assertEnum('audience', m.audience, AUDIENCES);
98
+ const agent_autonomy = assertEnum('agent_autonomy', m.agent_autonomy, AUTONOMIES);
99
+ const estimated_context_size = assertEnum('estimated_context_size', m.estimated_context_size, CONTEXT_SIZES);
100
+ const estimated_agent_budget = assertEnum('estimated_agent_budget', m.estimated_agent_budget, AGENT_BUDGETS);
101
+ if ((m.value_state ?? 'uncommitted') !== 'uncommitted')
102
+ throw new Error('value_state must be uncommitted');
103
+ const expected_results = assertNonEmptyText('expected_results', m.expected_results);
104
+ const definition_of_done = assertNonEmptyText('definition_of_done', m.definition_of_done);
105
+ const contribution_type = assertNonEmptyText('contribution_type', m.contribution_type);
106
+ const min = m.estimated_duration_min_minutes, max = m.estimated_duration_max_minutes;
107
+ if (!Number.isInteger(min) || min < 0)
108
+ throw new Error('estimated_duration_min_minutes must be a non-negative integer');
109
+ if (!Number.isInteger(max) || max < min)
110
+ throw new Error('estimated_duration_max_minutes must be an integer >= min');
111
+ const isHigh = risk_level === 'high' || risk_level === 'critical';
112
+ if (isHigh && m.auto_claimable === true)
113
+ throw new Error('high/critical risk_level requires auto_claimable=false');
114
+ if (isHigh && !HUMAN_AUTONOMY.has(agent_autonomy))
115
+ throw new Error('high/critical risk_level requires agent_autonomy human_in_the_loop|human_only');
116
+ if (risk_level === 'critical' && audience === 'public')
117
+ throw new Error('critical risk_level cannot have audience=public');
118
+ const hcp = m.human_confirmation_points ?? [];
119
+ if (isHigh && hcp.length === 0)
120
+ throw new Error('high/critical risk_level requires >=1 human_confirmation_points');
121
+ db.prepare(`INSERT INTO build_task_agent_metadata (
122
+ task_id, task_type, source_ref, version, allowed_paths, forbidden_paths, prohibited_actions,
123
+ risk_level, audience, agent_autonomy, auto_claimable, human_confirmation_points, required_capabilities,
124
+ acceptance_criteria, verification_commands, expected_results, deliverables, definition_of_done,
125
+ estimated_duration_min_minutes, estimated_duration_max_minutes, estimated_context_size,
126
+ estimated_agent_budget, dependencies, blocking_conditions, value_state, contribution_type,
127
+ accountable_party_required
128
+ ) VALUES (
129
+ @task_id, @task_type, @source_ref, @version, @allowed_paths, @forbidden_paths, @prohibited_actions,
130
+ @risk_level, @audience, @agent_autonomy, @auto_claimable, @human_confirmation_points, @required_capabilities,
131
+ @acceptance_criteria, @verification_commands, @expected_results, @deliverables, @definition_of_done,
132
+ @estimated_duration_min_minutes, @estimated_duration_max_minutes, @estimated_context_size,
133
+ @estimated_agent_budget, @dependencies, @blocking_conditions, 'uncommitted', @contribution_type,
134
+ @accountable_party_required
135
+ )`).run({
136
+ task_id: taskId,
137
+ task_type,
138
+ source_ref: m.source_ref ?? null,
139
+ version: m.version ?? null,
140
+ allowed_paths: toJsonList('allowed_paths', m.allowed_paths, { allowEmpty: false }),
141
+ forbidden_paths: toJsonList('forbidden_paths', m.forbidden_paths ?? [], { allowEmpty: true }),
142
+ prohibited_actions: toJsonList('prohibited_actions', m.prohibited_actions, { allowEmpty: false }),
143
+ risk_level, audience, agent_autonomy,
144
+ auto_claimable: m.auto_claimable ? 1 : 0,
145
+ human_confirmation_points: toJsonList('human_confirmation_points', hcp, { allowEmpty: true }),
146
+ required_capabilities: toJsonList('required_capabilities', m.required_capabilities, { allowEmpty: false }),
147
+ acceptance_criteria: toJsonList('acceptance_criteria', m.acceptance_criteria, { allowEmpty: false }),
148
+ verification_commands: toJsonList('verification_commands', m.verification_commands, { allowEmpty: false }),
149
+ expected_results,
150
+ deliverables: toJsonList('deliverables', m.deliverables, { allowEmpty: false }),
151
+ definition_of_done,
152
+ estimated_duration_min_minutes: min,
153
+ estimated_duration_max_minutes: max,
154
+ estimated_context_size, estimated_agent_budget,
155
+ dependencies: toJsonList('dependencies', m.dependencies ?? [], { allowEmpty: true }),
156
+ blocking_conditions: toJsonList('blocking_conditions', m.blocking_conditions ?? [], { allowEmpty: true }),
157
+ contribution_type,
158
+ accountable_party_required: m.accountable_party_required ? 1 : 0,
159
+ });
160
+ }
161
+ /** Read + parse a task's agent metadata (list fields parsed back to arrays). */
162
+ export function getBuildTaskAgentMetadata(db, taskId) {
163
+ const row = db.prepare('SELECT * FROM build_task_agent_metadata WHERE task_id = ?').get(taskId);
164
+ if (!row)
165
+ return null;
166
+ for (const k of ['allowed_paths', 'forbidden_paths', 'prohibited_actions', 'human_confirmation_points',
167
+ 'required_capabilities', 'acceptance_criteria', 'verification_commands', 'deliverables', 'dependencies',
168
+ 'blocking_conditions'])
169
+ row[k] = parseJsonList(row[k]);
170
+ row.auto_claimable = row.auto_claimable === 1;
171
+ row.accountable_party_required = row.accountable_party_required === 1;
172
+ return row;
173
+ }
174
+ /**
175
+ * Flip a task's audience (used to PUBLISH an internal draft → 'public'). Validated against AUDIENCES.
176
+ * Returns the number of rows changed (0 = no metadata row for that task).
177
+ */
178
+ export function setBuildTaskAudience(db, taskId, audience) {
179
+ const a = assertEnum('audience', audience, AUDIENCES);
180
+ const r = db.prepare(`UPDATE build_task_agent_metadata SET audience = ? WHERE task_id = ?`).run(a, taskId);
181
+ return r.changes;
182
+ }
@@ -0,0 +1,47 @@
1
+ import { getBuildTaskWithAgentMetadata } from './build-task-read.js';
2
+ import { getCanonicalContributionTarget } from './canonical-contribution-target.js';
3
+ /** Gate a participation action on a task. restricted/internal/missing → 404 no-leak; auto_claimable=false → refuse claim. */
4
+ export function guardParticipation(db, id, action) {
5
+ const task = getBuildTaskWithAgentMetadata(db, id, 'member'); // member scope hides restricted/internal; releaseExpiredClaims runs inside
6
+ if (!task)
7
+ return { ok: false, status: 404, code: 'NOT_FOUND', message: '任务不存在' }; // also covers restricted/internal → no existence leak
8
+ const meta = task.agent_metadata;
9
+ if (action === 'claim' && meta && meta.auto_claimable === false) {
10
+ return { ok: false, status: 409, code: 'NOT_AUTO_CLAIMABLE', message: '该任务不可自助认领,需真人在环(human_in_the_loop / human_only),不能由 agent 自动认领' };
11
+ }
12
+ return { ok: true, task };
13
+ }
14
+ /**
15
+ * Anti GitHub-target-confusion (FAIL-CLOSED). A submitted `pr_ref` is accepted ONLY if it is either:
16
+ * - a canonical PR shorthand: `#123` or `123` (a PR number on the canonical repo), or
17
+ * - a strictly-parsed URL whose hostname is EXACTLY `github.com` AND whose first two path segments equal
18
+ * `canonical_contribution_target.expected_pr_base_repo`.
19
+ * Everything else — a lookalike host (`evilgithub.com`), a non-GitHub host (`gitlab.com`), an unparseable /
20
+ * arbitrary string (`evil/repo#1`), or an empty ref — is REJECTED with a typed code. (Codex P1: substring
21
+ * matching let lookalike hosts and non-URL text through.)
22
+ */
23
+ export function validatePrRefAgainstCanonical(prRef) {
24
+ const ref = String(prRef ?? '').trim();
25
+ const target = getCanonicalContributionTarget();
26
+ const expected = target.expected_pr_base_repo.toLowerCase();
27
+ if (ref === '')
28
+ return { ok: false, code: 'PR_REF_REQUIRED', message: `submit 需要一个指向 canonical repo ${target.expected_pr_base_repo} 的 PR(github.com URL 或 #编号)` };
29
+ if (/^#?\d+$/.test(ref))
30
+ return { ok: true }; // explicit canonical shorthand (#123 / 123)
31
+ let u;
32
+ try {
33
+ u = new URL(ref);
34
+ }
35
+ catch {
36
+ return { ok: false, code: 'INVALID_PR_REF', message: 'PR 必须是 canonical github.com PR URL 或 #编号;不接受任意文本' };
37
+ }
38
+ if (u.hostname.toLowerCase() !== 'github.com') {
39
+ return { ok: false, code: 'WRONG_PR_BASE_REPO', message: `PR host 必须精确等于 github.com(检测到 ${u.hostname});不要把贡献提交到非 WebAZ 仓库` };
40
+ }
41
+ const m = u.pathname.match(/^\/([^/]+)\/([^/]+)(?:\/|$)/);
42
+ const repo = m ? `${m[1]}/${m[2]}`.replace(/\.git$/i, '').toLowerCase() : '';
43
+ if (repo !== expected) {
44
+ return { ok: false, code: 'WRONG_PR_BASE_REPO', message: `PR 必须提交到 canonical repo ${target.expected_pr_base_repo}(检测到 ${repo || '未知'});不要把贡献提交到非 WebAZ 仓库` };
45
+ }
46
+ return { ok: true };
47
+ }