@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
@@ -0,0 +1,99 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { generateId } from '../../layer0-foundation/L0-1-database/schema.js';
3
+ export function initTaskProposalAiSchema(db) {
4
+ // CREATE only (no ALTER) — additive, never blocks an existing fresh-DB boot.
5
+ db.exec(`CREATE TABLE IF NOT EXISTS task_proposal_ai_suggestions (
6
+ id TEXT PRIMARY KEY,
7
+ proposal_id TEXT NOT NULL,
8
+ reviewer_type TEXT NOT NULL DEFAULT 'ai',
9
+ model TEXT,
10
+ provider TEXT,
11
+ input_hash TEXT,
12
+ input_summary TEXT,
13
+ output_json TEXT NOT NULL,
14
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
15
+ )`);
16
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_tpai_proposal ON task_proposal_ai_suggestions(proposal_id, created_at DESC)`);
17
+ }
18
+ /** Read the fields the recommender needs (keeps the route free of direct SQL). */
19
+ export function getProposalLite(db, id) {
20
+ return db.prepare('SELECT id, title, summary, suggested_area, source_ref, expected_outcome FROM task_proposals WHERE id = ?').get(id) ?? null;
21
+ }
22
+ const CATEGORY_KEYWORDS = {
23
+ docs: ['doc', 'readme', '文档', 'guide'], i18n: ['i18n', 'translat', '翻译', 'locale'],
24
+ tests: ['test', '测试', 'spec'], ui: ['ui', 'page', 'button', '界面', 'pwa', 'css'],
25
+ api: ['api', 'endpoint', 'route'], schema: ['schema', 'migration', 'table', '字段'],
26
+ infra: ['ci', 'deploy', 'infra', 'pipeline', 'docker'], governance: ['governance', 'charter', '治理', 'rfc'],
27
+ audit: ['audit', 'security', '审计', '安全'], code: ['fix', 'bug', 'refactor', 'implement', '实现', '修复'],
28
+ };
29
+ const HIGH_RISK_KEYWORDS = ['wallet', 'withdraw', 'fund', 'money', 'settle', 'escrow', 'payment', 'reward', 'schema', 'migration', 'auth', 'admin', 'key', '资金', '钱包', '提现', '结算', '权限', '密钥'];
30
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9一-鿿]+/g, ' ').trim();
31
+ /**
32
+ * Deterministic local heuristic recommendation (stub for a future model). Read-only; never decides.
33
+ */
34
+ export function recommendForProposal(db, p) {
35
+ const text = norm(`${p.title} ${p.summary} ${p.suggested_area ?? ''}`);
36
+ // category
37
+ let category = 'other';
38
+ let best = 0;
39
+ for (const [cat, kws] of Object.entries(CATEGORY_KEYWORDS)) {
40
+ const hits = kws.filter(k => text.includes(k)).length;
41
+ if (hits > best) {
42
+ best = hits;
43
+ category = cat;
44
+ }
45
+ }
46
+ // risk
47
+ const risk = HIGH_RISK_KEYWORDS.some(k => text.includes(k)) ? 'high' : (p.summary.length > 400 ? 'medium' : 'low');
48
+ // effort by summary size
49
+ const effort = p.summary.length > 600 ? 'large' : p.summary.length > 200 ? 'medium' : 'small';
50
+ // missing info flags
51
+ const missing_info = [];
52
+ if (p.summary.trim().length < 40)
53
+ missing_info.push('summary too short — needs concrete scope / acceptance');
54
+ if (!p.source_ref)
55
+ missing_info.push('no source_ref (file / RFC / issue reference)');
56
+ if (!p.expected_outcome)
57
+ missing_info.push('no expected_outcome (definition of done)');
58
+ // duplicate likelihood: normalized-title overlap vs existing proposals + build_tasks
59
+ const titleNorm = norm(p.title);
60
+ const titleWords = new Set(titleNorm.split(' ').filter(w => w.length >= 3));
61
+ let dupScore = 0;
62
+ if (titleWords.size > 0) {
63
+ const others = db.prepare(`SELECT title FROM task_proposals WHERE id != ? UNION ALL SELECT title FROM build_tasks`).all(p.id);
64
+ for (const o of others) {
65
+ const ow = new Set(norm(o.title).split(' ').filter(w => w.length >= 3));
66
+ let inter = 0;
67
+ for (const w of titleWords)
68
+ if (ow.has(w))
69
+ inter++;
70
+ const jac = inter / (titleWords.size + ow.size - inter || 1);
71
+ if (jac > dupScore)
72
+ dupScore = jac;
73
+ }
74
+ }
75
+ const duplicate_likelihood = dupScore >= 0.6 ? 'high' : dupScore >= 0.3 ? 'medium' : 'low';
76
+ const recommendation = {
77
+ category, risk, effort, missing_info, duplicate_likelihood,
78
+ suggested: {
79
+ title: p.title,
80
+ area: p.suggested_area ?? category,
81
+ description: p.summary,
82
+ acceptance_criteria: p.expected_outcome ? [String(p.expected_outcome).slice(0, 500)] : [],
83
+ verification_commands: [],
84
+ },
85
+ };
86
+ return { recommendation, model: 'heuristic-v1', provider: 'local' };
87
+ }
88
+ /** Persist an AI suggestion as evidence (accountability metadata). Returns the row id. */
89
+ export function insertAiSuggestion(db, args) {
90
+ const id = generateId('tpai');
91
+ const inputHash = createHash('sha256').update(args.inputSummary).digest('hex');
92
+ db.prepare(`INSERT INTO task_proposal_ai_suggestions (id, proposal_id, reviewer_type, model, provider, input_hash, input_summary, output_json)
93
+ VALUES (?,?,?,?,?,?,?,?)`).run(id, args.proposalId, args.reviewerType ?? 'ai', args.model ?? null, args.provider ?? null, inputHash, args.inputSummary.slice(0, 1000), args.outputJson);
94
+ return { id };
95
+ }
96
+ export function listAiSuggestions(db, proposalId) {
97
+ return db.prepare(`SELECT id, proposal_id, reviewer_type, model, provider, input_summary, output_json, created_at
98
+ FROM task_proposal_ai_suggestions WHERE proposal_id = ? ORDER BY created_at DESC LIMIT 20`).all(proposalId);
99
+ }
@@ -0,0 +1,191 @@
1
+ import { createBuildTask, logTaskEvent } from './build-tasks-engine.js';
2
+ import { insertBuildTaskAgentMetadata, getBuildTaskAgentMetadata, setBuildTaskAudience, RISK_LEVELS, TASK_TYPES } from './build-task-agent-metadata-store.js';
3
+ import { reviewTaskProposal } from './task-proposal-store.js';
4
+ /** Source-proposal ↔ draft-task link (lets a draft preserve its origin WITHOUT marking the proposal terminal). */
5
+ export function initTaskProposalDraftLinkSchema(db) {
6
+ db.exec(`CREATE TABLE IF NOT EXISTS task_proposal_draft_links (
7
+ task_id TEXT PRIMARY KEY,
8
+ proposal_id TEXT NOT NULL,
9
+ created_by TEXT NOT NULL,
10
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
11
+ )`);
12
+ // UNIQUE: one draft per source proposal (aligns the DB with the createDraftFromProposal business rule).
13
+ db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_tpdl_proposal ON task_proposal_draft_links(proposal_id)`);
14
+ }
15
+ const strList = (v) => Array.isArray(v) ? v.slice(0, 50).map(s => String(s).slice(0, 500)).filter(s => s.trim()) : [];
16
+ const txt = (v) => (v ?? '').trim();
17
+ /** Handoff fields an executable agent task must carry. Returns the list of missing ones (empty = complete). */
18
+ function missingHandoff(f) {
19
+ const missing = [];
20
+ if (!txt(f.title))
21
+ missing.push('title');
22
+ if (!txt(f.description))
23
+ missing.push('summary/description (reason)');
24
+ if (f.allowed_paths.length === 0)
25
+ missing.push('allowed_paths (execution boundary)');
26
+ if (f.prohibited_actions.length === 0)
27
+ missing.push('forbidden_actions');
28
+ if (f.acceptance_criteria.length === 0)
29
+ missing.push('acceptance_criteria');
30
+ if (f.verification_commands.length === 0)
31
+ missing.push('verification_commands');
32
+ if (f.deliverables.length === 0)
33
+ missing.push('deliverables');
34
+ if (!txt(f.expected_results))
35
+ missing.push('expected_results');
36
+ if (!txt(f.definition_of_done))
37
+ missing.push('definition_of_done');
38
+ return missing;
39
+ }
40
+ /**
41
+ * Create an UNPUBLISHED draft (internal build_task) from a non-terminal proposal and record the
42
+ * source-proposal ↔ draft link. Does NOT mark the proposal 'converted' (acceptance happens at publish).
43
+ * Returns the new draft task id.
44
+ */
45
+ export function createDraftFromProposal(db, a) {
46
+ // 1) proposal must exist + be non-terminal (only new / needs_info → draft). Pre-check before creating anything.
47
+ const prop = db.prepare('SELECT id, status FROM task_proposals WHERE id = ?').get(a.proposalId);
48
+ if (!prop)
49
+ return { error: 'proposal not found', error_code: 'PROPOSAL_NOT_FOUND' };
50
+ if (prop.status === 'rejected' || prop.status === 'converted')
51
+ return { error: `proposal already ${prop.status}`, error_code: 'PROPOSAL_TERMINAL' };
52
+ // one draft per proposal (without using a terminal proposal status as the guard)
53
+ const existingLink = db.prepare('SELECT task_id FROM task_proposal_draft_links WHERE proposal_id = ?').get(a.proposalId);
54
+ if (existingLink)
55
+ return { error: `a task draft already exists for this proposal (${existingLink.task_id})`, error_code: 'PROPOSAL_HAS_DRAFT' };
56
+ // draft risk stays low/medium (high/critical metadata rules are out of scope for this PR)
57
+ const riskLevel = (a.riskLevel === 'medium') ? 'medium' : 'low';
58
+ if (a.riskLevel && !RISK_LEVELS.includes(a.riskLevel))
59
+ return { error: 'invalid risk_level', error_code: 'BAD_RISK_LEVEL' };
60
+ if (a.riskLevel === 'high' || a.riskLevel === 'critical')
61
+ return { error: 'draft risk_level must be low or medium (high/critical deferred)', error_code: 'RISK_TOO_HIGH_FOR_DRAFT' };
62
+ const taskType = (a.taskType && TASK_TYPES.includes(a.taskType)) ? a.taskType : 'other';
63
+ // 2) assemble the agent-handoff fields and VALIDATE completeness BEFORE creating anything — the existing
64
+ // task model requires these to be executable, so we never create an incomplete/orphan task.
65
+ const allowed = strList(a.allowedPaths);
66
+ const prohibited = strList(a.forbiddenActions);
67
+ const accept = strList(a.acceptanceCriteria);
68
+ const verify = strList(a.verificationCommands);
69
+ const deliver = strList(a.deliverables);
70
+ const description = txt(a.description);
71
+ const dod = txt(a.definitionOfDone).slice(0, 1000);
72
+ const expected = (txt(a.expectedResults) || description).slice(0, 1000);
73
+ const missing = missingHandoff({ title: txt(a.title), description, allowed_paths: allowed, prohibited_actions: prohibited, acceptance_criteria: accept, verification_commands: verify, deliverables: deliver, expected_results: expected, definition_of_done: dod });
74
+ if (missing.length > 0)
75
+ return { error: `draft incomplete — provide: ${missing.join(', ')}`, error_code: 'DRAFT_INCOMPLETE', missing };
76
+ // 3) create the formal task via the trusted path (creator = admin → accountability)
77
+ const created = createBuildTask(db, { creatorId: a.adminId, title: a.title, area: a.area ?? undefined, description: a.description ?? undefined, rfcRef: a.sourceRef ?? undefined });
78
+ if ('error' in created)
79
+ return created;
80
+ const taskId = created.id;
81
+ // 4) attach agent_metadata as an INTERNAL draft (hidden + unclaimable via the existing read/participation
82
+ // guards). auto_claimable defaults TRUE so once published the task enters the normal agent claim flow
83
+ // (maintainer may opt human-only with autoClaimable:false). While audience='internal' it is unclaimable.
84
+ const caps = strList(a.requiredCapabilities);
85
+ if (caps.length === 0)
86
+ caps.push('general');
87
+ const meta = {
88
+ task_type: taskType,
89
+ source_ref: a.sourceRef ?? null,
90
+ allowed_paths: allowed,
91
+ forbidden_paths: strList(a.forbiddenPaths),
92
+ prohibited_actions: prohibited,
93
+ risk_level: riskLevel,
94
+ audience: 'internal',
95
+ agent_autonomy: 'human_in_the_loop',
96
+ auto_claimable: a.autoClaimable !== false,
97
+ required_capabilities: caps,
98
+ acceptance_criteria: accept,
99
+ verification_commands: verify,
100
+ expected_results: expected,
101
+ deliverables: deliver,
102
+ definition_of_done: dod,
103
+ estimated_duration_min_minutes: 0,
104
+ estimated_duration_max_minutes: 0,
105
+ estimated_context_size: 'small',
106
+ estimated_agent_budget: 'minimal',
107
+ value_state: 'uncommitted',
108
+ contribution_type: 'task',
109
+ accountable_party_required: true,
110
+ };
111
+ insertBuildTaskAgentMetadata(db, taskId, meta);
112
+ // 5) record the source-proposal ↔ draft link (accountability) — WITHOUT marking the proposal converted;
113
+ // the proposal stays non-terminal until an explicit human publish.
114
+ db.prepare('INSERT INTO task_proposal_draft_links (task_id, proposal_id, created_by) VALUES (?,?,?)').run(taskId, a.proposalId, a.adminId);
115
+ return { draft_task_id: taskId };
116
+ }
117
+ /** Admin list of UNPUBLISHED drafts (internal, open) + their source proposal id (via the draft-link table). */
118
+ export function listDraftBuildTasks(db) {
119
+ return db.prepare(`
120
+ SELECT t.id, t.title, t.area, t.description, t.rfc_ref, t.status, t.created_by, t.created_at,
121
+ m.risk_level, m.audience, m.auto_claimable,
122
+ l.proposal_id AS source_proposal_id
123
+ FROM build_tasks t
124
+ JOIN build_task_agent_metadata m ON m.task_id = t.id
125
+ LEFT JOIN task_proposal_draft_links l ON l.task_id = t.id
126
+ WHERE m.audience = 'internal' AND t.status = 'open'
127
+ ORDER BY t.created_at DESC LIMIT 200
128
+ `).all();
129
+ }
130
+ /**
131
+ * Validate that a draft carries enough agent-handoff info to be executed by another participant's agent.
132
+ * Returns the list of missing fields (empty = complete). Gate for publish — never publish an incomplete task.
133
+ */
134
+ export function validateDraftForPublish(task, meta) {
135
+ return missingHandoff({
136
+ title: txt(task.title), description: txt(task.description),
137
+ allowed_paths: meta.allowed_paths ?? [], prohibited_actions: meta.prohibited_actions ?? [],
138
+ acceptance_criteria: meta.acceptance_criteria ?? [], verification_commands: meta.verification_commands ?? [],
139
+ deliverables: meta.deliverables ?? [], expected_results: txt(meta.expected_results), definition_of_done: txt(meta.definition_of_done),
140
+ });
141
+ }
142
+ /**
143
+ * PUBLISH a draft → audience 'public' (now on the board / claimable via the existing flow). Explicit
144
+ * human/admin action only; validates agent-handoff completeness first (never publishes an incomplete task).
145
+ * This is also where the source proposal is marked 'converted' (converted_ref=task id, reviewer=actor) —
146
+ * acceptance happens at publish, not at draft creation. The canonical repo / PR target is the protocol-wide
147
+ * canonical contribution target enforced by the existing submit path — it is not stored per-task.
148
+ *
149
+ * Evidence-chain guard: the linked proposal is validated BEFORE anything is published. If it was rejected (or
150
+ * converted to a different task) since draft creation, publish is refused (409) and the task stays internal —
151
+ * so a public, claimable task can never coexist with a rejected source proposal. The audience flip + proposal
152
+ * conversion run in one transaction (both or neither).
153
+ */
154
+ export function publishDraftBuildTask(db, taskId, adminId) {
155
+ const meta = getBuildTaskAgentMetadata(db, taskId);
156
+ if (!meta)
157
+ return { error: 'task has no draft metadata', error_code: 'NOT_A_DRAFT' };
158
+ if (meta.audience !== 'internal')
159
+ return { error: 'task is not an internal draft', error_code: 'NOT_DRAFT_AUDIENCE' };
160
+ const t = db.prepare('SELECT status, title, description FROM build_tasks WHERE id = ?').get(taskId);
161
+ if (!t)
162
+ return { error: 'task not found', error_code: 'NOT_FOUND' };
163
+ const missing = validateDraftForPublish(t, meta);
164
+ if (missing.length > 0)
165
+ return { error: `draft incomplete — fill before publish: ${missing.join(', ')}`, error_code: 'DRAFT_INCOMPLETE', missing };
166
+ // validate the linked proposal FIRST — do not publish on top of a rejected / elsewhere-converted proposal.
167
+ const link = db.prepare('SELECT proposal_id FROM task_proposal_draft_links WHERE task_id = ?').get(taskId);
168
+ let proposalToConvert = null;
169
+ if (link) {
170
+ const prop = db.prepare('SELECT status, converted_ref FROM task_proposals WHERE id = ?').get(link.proposal_id);
171
+ if (prop) {
172
+ if (prop.status === 'rejected')
173
+ return { error: 'source proposal was rejected — cannot publish', error_code: 'PROPOSAL_REJECTED' };
174
+ if (prop.status === 'converted' && prop.converted_ref !== taskId)
175
+ return { error: 'source proposal already converted to a different task', error_code: 'PROPOSAL_CONVERTED_ELSEWHERE' };
176
+ if (prop.status === 'new' || prop.status === 'needs_info')
177
+ proposalToConvert = link.proposal_id;
178
+ }
179
+ }
180
+ // all validation passed → publish atomically: flip audience + (if applicable) mark the proposal converted.
181
+ db.transaction(() => {
182
+ setBuildTaskAudience(db, taskId, 'public');
183
+ logTaskEvent(db, taskId, adminId, t.status, t.status, 'published from draft (audience → public)');
184
+ if (proposalToConvert) {
185
+ const rv = reviewTaskProposal(db, proposalToConvert, adminId, 'converted', `published as formal task ${taskId}`, taskId);
186
+ if ('error' in rv)
187
+ throw new Error(`proposal conversion failed: ${rv.code}`); // rollback the audience flip
188
+ }
189
+ })();
190
+ return { ok: true, task_id: taskId };
191
+ }
@@ -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
  }