@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.
- package/README.md +5 -1
- package/dist/layer0-foundation/L0-1-database/db-backends/pg-backend.js +51 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-dialect-datetime.js +437 -0
- package/dist/layer0-foundation/L0-1-database/db-backends/sql-placeholders.js +98 -0
- package/dist/layer0-foundation/L0-1-database/db.js +65 -0
- package/dist/layer0-foundation/L0-2-state-machine/order-chain.js +13 -11
- package/dist/layer0-foundation/L0-2-state-machine/transitions.js +1 -1
- package/dist/layer0-foundation/L0-5-manifest/manifest.js +13 -11
- package/dist/layer1-agent/L1-1-mcp-server/server.js +288 -208
- package/dist/layer1-agent/L1-2-external-anchor/anchor-engine.js +14 -12
- package/dist/layer2-business/L2-6-notifications/notification-engine.js +8 -5
- package/dist/layer2-business/L2-7-snf/snf-engine.js +16 -14
- package/dist/layer2-business/L2-8-feedback/build-feedback-engine.js +18 -10
- package/dist/layer2-business/L2-9-contribution/build-reputation-engine.js +37 -23
- package/dist/layer2-business/L2-9-contribution/build-task-agent-metadata-store.js +182 -0
- package/dist/layer2-business/L2-9-contribution/build-task-participation.js +47 -0
- package/dist/layer2-business/L2-9-contribution/build-task-read.js +222 -0
- package/dist/layer2-business/L2-9-contribution/build-tasks-engine.js +11 -3
- package/dist/layer2-business/L2-9-contribution/canonical-contribution-target.js +16 -0
- package/dist/layer2-business/L2-9-contribution/contribution-display-envelope.js +40 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-contract.js +36 -0
- package/dist/layer2-business/L2-9-contribution/contribution-score-evidence.js +61 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/canonical.js +60 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-credential.schema.js +140 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/github-fetch-adapter.js +437 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/self-consistency.js +38 -0
- package/dist/layer2-business/L2-9-contribution/github-credential/verifier.js +231 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-ingestion-engine.js +145 -0
- package/dist/layer2-business/L2-9-contribution/github-credential-store.js +115 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-engine.js +134 -0
- package/dist/layer2-business/L2-9-contribution/identity-binding-store.js +101 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-engine.js +126 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-challenge-store.js +30 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-discovery.js +55 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-engine.js +109 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-fact-precondition.js +22 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-proof-verifier.js +97 -0
- package/dist/layer2-business/L2-9-contribution/identity-claim-read.js +59 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-ai-store.js +99 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-draft.js +191 -0
- package/dist/layer2-business/L2-9-contribution/task-proposal-store.js +129 -0
- package/dist/layer2-business/L2-notes/note-photo-storage.js +4 -2
- package/dist/layer3-trust/L3-1-dispute-engine/dispute-engine.js +17 -15
- package/dist/layer3-trust/L3-1-dispute-engine/evidence-storage.js +11 -8
- package/dist/layer4-economics/L4-3-reputation/reputation-engine.js +9 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-engine.js +11 -8
- package/dist/layer4-economics/L4-4-skill-market/skill-listing-engine.js +22 -16
- package/dist/pwa/acp-feed.js +13 -1
- package/dist/pwa/admin-bearer-auth.js +21 -0
- package/dist/pwa/contract-fingerprint.js +2 -0
- package/dist/pwa/email-delivery.js +127 -0
- package/dist/pwa/endpoint-actions.js +5 -1
- package/dist/pwa/goal-index.js +8 -8
- package/dist/pwa/human-presence.js +62 -0
- package/dist/pwa/public/app.js +1485 -283
- package/dist/pwa/public/i18n.js +297 -59
- package/dist/pwa/public/index.html +1 -0
- package/dist/pwa/public/openapi.json +5 -5
- package/dist/pwa/public/whitepaper/en/index.html +153 -0
- package/dist/pwa/public/whitepaper/zh-CN/index.html +153 -0
- package/dist/pwa/rate-limit.js +22 -0
- package/dist/pwa/routes/account-deletion.js +15 -13
- package/dist/pwa/routes/addresses.js +10 -9
- package/dist/pwa/routes/admin-admins.js +13 -14
- package/dist/pwa/routes/admin-analytics.js +109 -69
- package/dist/pwa/routes/admin-atomic.js +10 -4
- package/dist/pwa/routes/admin-catalog.js +13 -11
- package/dist/pwa/routes/admin-editor-picks.js +15 -10
- package/dist/pwa/routes/admin-events.js +5 -3
- package/dist/pwa/routes/admin-health.js +2 -1
- package/dist/pwa/routes/admin-moderation.js +50 -29
- package/dist/pwa/routes/admin-ops.js +35 -23
- package/dist/pwa/routes/admin-protocol-params.js +16 -19
- package/dist/pwa/routes/admin-reports.js +23 -21
- package/dist/pwa/routes/admin-tokenomics.js +26 -25
- package/dist/pwa/routes/admin-users-lifecycle.js +37 -40
- package/dist/pwa/routes/admin-users-query.js +65 -53
- package/dist/pwa/routes/admin-verifier-flow.js +82 -41
- package/dist/pwa/routes/admin-verifier-whitelist.js +55 -27
- package/dist/pwa/routes/admin-wallet-ops.js +32 -7
- package/dist/pwa/routes/agent-buy.js +46 -22
- package/dist/pwa/routes/agent-governance.js +52 -56
- package/dist/pwa/routes/ai.js +7 -5
- package/dist/pwa/routes/analytics.js +43 -41
- package/dist/pwa/routes/anchors.js +19 -20
- package/dist/pwa/routes/announcements.js +13 -13
- package/dist/pwa/routes/arbitrator.js +97 -31
- package/dist/pwa/routes/auction.js +157 -116
- package/dist/pwa/routes/auth-login.js +6 -4
- package/dist/pwa/routes/auth-read.js +21 -10
- package/dist/pwa/routes/auth-register.js +111 -26
- package/dist/pwa/routes/auth-sessions.js +12 -11
- package/dist/pwa/routes/blocklist.js +16 -15
- package/dist/pwa/routes/build-feedback.js +10 -9
- package/dist/pwa/routes/build-reputation.js +6 -2
- package/dist/pwa/routes/build-tasks.js +45 -13
- package/dist/pwa/routes/buyer-feeds.js +27 -25
- package/dist/pwa/routes/cart.js +16 -15
- package/dist/pwa/routes/charity.js +212 -150
- package/dist/pwa/routes/chat.js +42 -43
- package/dist/pwa/routes/checkin-tasks.js +10 -9
- package/dist/pwa/routes/checkout-helpers.js +12 -10
- package/dist/pwa/routes/claim-initiators.js +34 -14
- package/dist/pwa/routes/claim-verify.js +86 -53
- package/dist/pwa/routes/claim-voting.js +43 -18
- package/dist/pwa/routes/contribution-identity.js +164 -0
- package/dist/pwa/routes/contribution-score.js +19 -0
- package/dist/pwa/routes/coupons.js +19 -16
- package/dist/pwa/routes/dashboards.js +18 -16
- package/dist/pwa/routes/dispute-cases.js +25 -24
- package/dist/pwa/routes/disputes-read.js +45 -51
- package/dist/pwa/routes/disputes-write.js +124 -61
- package/dist/pwa/routes/evidence.js +9 -9
- package/dist/pwa/routes/external-anchors.js +13 -12
- package/dist/pwa/routes/feedback.js +29 -33
- package/dist/pwa/routes/flash-sales.js +18 -16
- package/dist/pwa/routes/follows.js +25 -24
- package/dist/pwa/routes/governance-auto-deactivate.js +21 -9
- package/dist/pwa/routes/governance-onboarding.js +70 -59
- package/dist/pwa/routes/group-buys.js +22 -22
- package/dist/pwa/routes/growth.js +34 -31
- package/dist/pwa/routes/import-product.js +12 -10
- package/dist/pwa/routes/kyc.js +9 -8
- package/dist/pwa/routes/leaderboard.js +20 -18
- package/dist/pwa/routes/listings.js +23 -22
- package/dist/pwa/routes/logistics.js +10 -8
- package/dist/pwa/routes/manifests.js +27 -27
- package/dist/pwa/routes/me-data.js +23 -21
- package/dist/pwa/routes/notifications.js +7 -6
- package/dist/pwa/routes/offers.js +30 -12
- package/dist/pwa/routes/orders-action.js +51 -29
- package/dist/pwa/routes/orders-create.js +75 -20
- package/dist/pwa/routes/orders-read.js +21 -20
- package/dist/pwa/routes/p2p-products.js +30 -18
- package/dist/pwa/routes/payments-governance.js +61 -56
- package/dist/pwa/routes/peers.js +9 -8
- package/dist/pwa/routes/pin-receipts.js +13 -13
- package/dist/pwa/routes/products-aliases.js +12 -10
- package/dist/pwa/routes/products-claims.js +36 -17
- package/dist/pwa/routes/products-create.js +53 -38
- package/dist/pwa/routes/products-crud.js +17 -16
- package/dist/pwa/routes/products-links.js +49 -26
- package/dist/pwa/routes/products-list.js +6 -4
- package/dist/pwa/routes/products-meta.js +40 -39
- package/dist/pwa/routes/products-update.js +19 -5
- package/dist/pwa/routes/profile-credentials.js +20 -19
- package/dist/pwa/routes/profile-identity.js +14 -13
- package/dist/pwa/routes/profile-location.js +7 -6
- package/dist/pwa/routes/profile-placement.js +20 -19
- package/dist/pwa/routes/profile-prefs.js +11 -11
- package/dist/pwa/routes/promoter.js +58 -66
- package/dist/pwa/routes/public-build-tasks.js +19 -0
- package/dist/pwa/routes/public-utils.js +108 -46
- package/dist/pwa/routes/push.js +16 -15
- package/dist/pwa/routes/ratings.js +92 -32
- package/dist/pwa/routes/recover-key.js +66 -26
- package/dist/pwa/routes/referral.js +37 -52
- package/dist/pwa/routes/reputation.js +3 -2
- package/dist/pwa/routes/returns.js +76 -73
- package/dist/pwa/routes/reviews.js +41 -18
- package/dist/pwa/routes/rewards-apply.js +16 -15
- package/dist/pwa/routes/rewards-auto-downgrade.js +9 -7
- package/dist/pwa/routes/rewards-escrow-expire.js +7 -5
- package/dist/pwa/routes/rfqs.js +163 -85
- package/dist/pwa/routes/search.js +16 -14
- package/dist/pwa/routes/secondhand.js +25 -22
- package/dist/pwa/routes/seller-quota.js +24 -26
- package/dist/pwa/routes/share-redirects.js +60 -55
- package/dist/pwa/routes/shareables-interactions.js +34 -35
- package/dist/pwa/routes/shareables.js +55 -51
- package/dist/pwa/routes/shop-referral.js +58 -0
- package/dist/pwa/routes/shops.js +25 -20
- package/dist/pwa/routes/signaling.js +10 -9
- package/dist/pwa/routes/skill-market.js +16 -16
- package/dist/pwa/routes/skills.js +15 -14
- package/dist/pwa/routes/snf.js +14 -13
- package/dist/pwa/routes/tags.js +10 -9
- package/dist/pwa/routes/task-proposals.js +121 -0
- package/dist/pwa/routes/trial.js +72 -52
- package/dist/pwa/routes/trusted-kpi.js +20 -18
- package/dist/pwa/routes/url-claim.js +67 -28
- package/dist/pwa/routes/users-public.js +62 -70
- package/dist/pwa/routes/variants.js +12 -13
- package/dist/pwa/routes/verifier-user.js +61 -21
- package/dist/pwa/routes/verify-tasks.js +49 -25
- package/dist/pwa/routes/waitlist.js +16 -15
- package/dist/pwa/routes/wallet-read.js +75 -37
- package/dist/pwa/routes/wallet-write.js +12 -9
- package/dist/pwa/routes/webauthn.js +25 -26
- package/dist/pwa/routes/webhooks.js +26 -26
- package/dist/pwa/routes/welcome.js +45 -50
- package/dist/pwa/routes/wishlist-qa.js +29 -32
- package/dist/pwa/server.js +304 -90
- package/dist/version.js +1 -1
- 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
|
-
|
|
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 =
|
|
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
|
-
|
|
650
|
-
|
|
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
|
-
|
|
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
|
-
?
|
|
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(
|
|
677
|
-
return
|
|
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
|
-
|
|
687
|
+
`, [disputeId])) ?? null;
|
|
686
688
|
}
|
|
687
|
-
export function getOrderDispute(
|
|
688
|
-
return
|
|
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
|
-
|
|
699
|
+
`, [orderId])) ?? null;
|
|
698
700
|
}
|
|
699
|
-
export function getOpenDisputes(
|
|
700
|
-
return
|
|
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
|
-
`)
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
227
|
-
const ev =
|
|
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
|
-
|
|
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(
|
|
291
|
-
const score =
|
|
292
|
-
|
|
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(
|
|
297
|
-
const score =
|
|
298
|
-
|
|
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
|
}
|