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