chainlesschain 0.47.8 → 0.49.0
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/bin/chainlesschain.js +0 -0
- package/package.json +10 -8
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AppLayout-6SPt_8Y_.js → AppLayout-Rvi759IS.js} +1 -1
- package/src/assets/web-panel/assets/Dashboard-BS-tzGNj.css +1 -0
- package/src/assets/web-panel/assets/{Dashboard-Br7kCwKJ.js → Dashboard-DBhFxXYQ.js} +2 -2
- package/src/assets/web-panel/assets/{index-tN-8TosE.js → index-uL0cZ8N_.js} +2 -2
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/activitypub.js +533 -0
- package/src/commands/codegen.js +303 -0
- package/src/commands/collab.js +482 -0
- package/src/commands/compliance.js +597 -6
- package/src/commands/crosschain.js +382 -0
- package/src/commands/dbevo.js +388 -0
- package/src/commands/dev.js +411 -0
- package/src/commands/federation.js +427 -0
- package/src/commands/fusion.js +332 -0
- package/src/commands/governance.js +505 -0
- package/src/commands/hardening.js +110 -0
- package/src/commands/incentive.js +373 -0
- package/src/commands/inference.js +304 -0
- package/src/commands/infra.js +361 -0
- package/src/commands/kg.js +371 -0
- package/src/commands/marketplace.js +326 -0
- package/src/commands/matrix.js +283 -0
- package/src/commands/mcp.js +441 -18
- package/src/commands/nlprog.js +329 -0
- package/src/commands/nostr.js +196 -7
- package/src/commands/ops.js +408 -0
- package/src/commands/perception.js +385 -0
- package/src/commands/pqc.js +34 -0
- package/src/commands/privacy.js +345 -0
- package/src/commands/quantization.js +280 -0
- package/src/commands/recommend.js +336 -0
- package/src/commands/reputation.js +349 -0
- package/src/commands/runtime.js +500 -0
- package/src/commands/sla.js +352 -0
- package/src/commands/social.js +265 -0
- package/src/commands/stress.js +252 -0
- package/src/commands/tech.js +268 -0
- package/src/commands/tenant.js +576 -0
- package/src/commands/trust.js +366 -0
- package/src/harness/mcp-client.js +330 -54
- package/src/index.js +114 -0
- package/src/lib/activitypub-bridge.js +623 -0
- package/src/lib/aiops.js +523 -0
- package/src/lib/autonomous-developer.js +524 -0
- package/src/lib/code-agent.js +442 -0
- package/src/lib/collaboration-governance.js +556 -0
- package/src/lib/community-governance.js +649 -0
- package/src/lib/compliance-framework-reporter.js +600 -0
- package/src/lib/content-recommendation.js +600 -0
- package/src/lib/cross-chain.js +669 -0
- package/src/lib/dbevo.js +669 -0
- package/src/lib/decentral-infra.js +445 -0
- package/src/lib/federation-hardening.js +587 -0
- package/src/lib/hardening-manager.js +409 -0
- package/src/lib/inference-network.js +407 -0
- package/src/lib/knowledge-graph.js +530 -0
- package/src/lib/matrix-bridge.js +252 -0
- package/src/lib/mcp-client.js +3 -0
- package/src/lib/mcp-registry.js +347 -0
- package/src/lib/mcp-scaffold.js +385 -0
- package/src/lib/multimodal.js +698 -0
- package/src/lib/nl-programming.js +595 -0
- package/src/lib/nostr-bridge.js +214 -38
- package/src/lib/perception.js +500 -0
- package/src/lib/pqc-manager.js +141 -9
- package/src/lib/privacy-computing.js +575 -0
- package/src/lib/protocol-fusion.js +535 -0
- package/src/lib/quantization.js +362 -0
- package/src/lib/reputation-optimizer.js +509 -0
- package/src/lib/skill-marketplace.js +397 -0
- package/src/lib/sla-manager.js +484 -0
- package/src/lib/social-graph.js +408 -0
- package/src/lib/stix-parser.js +167 -0
- package/src/lib/stress-tester.js +383 -0
- package/src/lib/tech-learning-engine.js +651 -0
- package/src/lib/tenant-saas.js +831 -0
- package/src/lib/threat-intel.js +268 -0
- package/src/lib/token-incentive.js +513 -0
- package/src/lib/topic-classifier.js +400 -0
- package/src/lib/trust-security.js +473 -0
- package/src/lib/ueba.js +403 -0
- package/src/lib/universal-runtime.js +771 -0
- package/src/assets/web-panel/assets/Dashboard-CKeMmCoT.css +0 -1
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Community Governance — CLI port of Phase 54 AI社区治理系统
|
|
3
|
+
* (docs/design/modules/26_社区治理系统.md).
|
|
4
|
+
*
|
|
5
|
+
* The Desktop build drives governance with LLM-powered impact analysis
|
|
6
|
+
* (context engineering + Ollama), real-time vote prediction, and a
|
|
7
|
+
* GovernancePage.vue with risk/benefit radar charts. The CLI can't host
|
|
8
|
+
* LLM inference or interactive UI, so this port ships:
|
|
9
|
+
*
|
|
10
|
+
* - ProposalStore: create/list/show/activate/close/expire.
|
|
11
|
+
* - VoteStore: cast/list per proposal (unique voter+proposal).
|
|
12
|
+
* - Tally: quorum + pass-threshold calculation.
|
|
13
|
+
* - Heuristic impact analysis: type-based risk/benefit scoring.
|
|
14
|
+
* - Heuristic vote prediction: extrapolation from current votes.
|
|
15
|
+
* - Catalogs: 4 proposal types, 5 statuses, 4 impact levels.
|
|
16
|
+
*
|
|
17
|
+
* What does NOT port: LLM-powered impact analysis (context engineering +
|
|
18
|
+
* Ollama), real-time prediction updates, GovernancePage.vue, Pinia store.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import crypto from "crypto";
|
|
22
|
+
|
|
23
|
+
/* ── Constants ─────────────────────────────────────────────── */
|
|
24
|
+
|
|
25
|
+
export const PROPOSAL_TYPES = Object.freeze({
|
|
26
|
+
PARAMETER_CHANGE: Object.freeze({
|
|
27
|
+
id: "parameter_change",
|
|
28
|
+
name: "Parameter Change",
|
|
29
|
+
description: "参数变更",
|
|
30
|
+
}),
|
|
31
|
+
FEATURE_REQUEST: Object.freeze({
|
|
32
|
+
id: "feature_request",
|
|
33
|
+
name: "Feature Request",
|
|
34
|
+
description: "功能请求",
|
|
35
|
+
}),
|
|
36
|
+
POLICY_UPDATE: Object.freeze({
|
|
37
|
+
id: "policy_update",
|
|
38
|
+
name: "Policy Update",
|
|
39
|
+
description: "策略更新",
|
|
40
|
+
}),
|
|
41
|
+
BUDGET_ALLOCATION: Object.freeze({
|
|
42
|
+
id: "budget_allocation",
|
|
43
|
+
name: "Budget Allocation",
|
|
44
|
+
description: "预算分配",
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export const PROPOSAL_STATUS = Object.freeze({
|
|
49
|
+
DRAFT: "draft",
|
|
50
|
+
ACTIVE: "active",
|
|
51
|
+
PASSED: "passed",
|
|
52
|
+
REJECTED: "rejected",
|
|
53
|
+
EXPIRED: "expired",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
export const IMPACT_LEVELS = Object.freeze({
|
|
57
|
+
LOW: Object.freeze({ id: "low", name: "Low", description: "低影响" }),
|
|
58
|
+
MEDIUM: Object.freeze({
|
|
59
|
+
id: "medium",
|
|
60
|
+
name: "Medium",
|
|
61
|
+
description: "中等影响",
|
|
62
|
+
}),
|
|
63
|
+
HIGH: Object.freeze({ id: "high", name: "High", description: "高影响" }),
|
|
64
|
+
CRITICAL: Object.freeze({
|
|
65
|
+
id: "critical",
|
|
66
|
+
name: "Critical",
|
|
67
|
+
description: "关键影响",
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export const VOTE_VALUES = Object.freeze(["yes", "no", "abstain"]);
|
|
72
|
+
|
|
73
|
+
// Default config (from design doc §6)
|
|
74
|
+
const DEFAULT_CONFIG = {
|
|
75
|
+
votingDurationMs: 7 * 24 * 60 * 60 * 1000, // 7 days
|
|
76
|
+
quorumThreshold: 0.5,
|
|
77
|
+
passThreshold: 0.6,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/* ── State ─────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
const _proposals = new Map(); // id → proposal
|
|
83
|
+
const _votes = new Map(); // id → vote
|
|
84
|
+
const _proposalVotes = new Map(); // proposalId → Set<voteId>
|
|
85
|
+
let _seq = 0;
|
|
86
|
+
|
|
87
|
+
/* ── Schema ────────────────────────────────────────────────── */
|
|
88
|
+
|
|
89
|
+
export function ensureGovernanceTables(db) {
|
|
90
|
+
if (!db) return;
|
|
91
|
+
db.exec(`
|
|
92
|
+
CREATE TABLE IF NOT EXISTS governance_proposals (
|
|
93
|
+
id TEXT PRIMARY KEY,
|
|
94
|
+
title TEXT NOT NULL,
|
|
95
|
+
description TEXT,
|
|
96
|
+
type TEXT DEFAULT 'feature_request',
|
|
97
|
+
proposer_did TEXT,
|
|
98
|
+
status TEXT DEFAULT 'draft',
|
|
99
|
+
impact_level TEXT,
|
|
100
|
+
impact_analysis TEXT,
|
|
101
|
+
vote_yes INTEGER DEFAULT 0,
|
|
102
|
+
vote_no INTEGER DEFAULT 0,
|
|
103
|
+
vote_abstain INTEGER DEFAULT 0,
|
|
104
|
+
voting_starts_at INTEGER,
|
|
105
|
+
voting_ends_at INTEGER,
|
|
106
|
+
metadata TEXT,
|
|
107
|
+
created_at INTEGER NOT NULL
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
db.exec(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS governance_votes (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
proposal_id TEXT NOT NULL,
|
|
114
|
+
voter_did TEXT NOT NULL,
|
|
115
|
+
vote TEXT NOT NULL,
|
|
116
|
+
reason TEXT,
|
|
117
|
+
weight REAL DEFAULT 1.0,
|
|
118
|
+
created_at INTEGER NOT NULL
|
|
119
|
+
)
|
|
120
|
+
`);
|
|
121
|
+
db.exec(
|
|
122
|
+
`CREATE INDEX IF NOT EXISTS idx_governance_proposals_status ON governance_proposals(status)`,
|
|
123
|
+
);
|
|
124
|
+
db.exec(
|
|
125
|
+
`CREATE INDEX IF NOT EXISTS idx_governance_proposals_type ON governance_proposals(type)`,
|
|
126
|
+
);
|
|
127
|
+
db.exec(
|
|
128
|
+
`CREATE INDEX IF NOT EXISTS idx_governance_votes_proposal ON governance_votes(proposal_id)`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* ── Catalogs ──────────────────────────────────────────────── */
|
|
133
|
+
|
|
134
|
+
export function listProposalTypes() {
|
|
135
|
+
return Object.values(PROPOSAL_TYPES).map((t) => ({ ...t }));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function listProposalStatuses() {
|
|
139
|
+
return Object.values(PROPOSAL_STATUS).map((s) => s);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function listImpactLevels() {
|
|
143
|
+
return Object.values(IMPACT_LEVELS).map((l) => ({ ...l }));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _strip(row) {
|
|
147
|
+
const { _seq: _omit, ...rest } = row;
|
|
148
|
+
void _omit;
|
|
149
|
+
return rest;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── Proposals ─────────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
function _persistProposal(db, proposal) {
|
|
155
|
+
if (!db) return;
|
|
156
|
+
db.prepare(
|
|
157
|
+
`INSERT OR REPLACE INTO governance_proposals
|
|
158
|
+
(id, title, description, type, proposer_did, status, impact_level,
|
|
159
|
+
impact_analysis, vote_yes, vote_no, vote_abstain,
|
|
160
|
+
voting_starts_at, voting_ends_at, metadata, created_at)
|
|
161
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
162
|
+
).run(
|
|
163
|
+
proposal.id,
|
|
164
|
+
proposal.title,
|
|
165
|
+
proposal.description || null,
|
|
166
|
+
proposal.type,
|
|
167
|
+
proposal.proposerDid || null,
|
|
168
|
+
proposal.status,
|
|
169
|
+
proposal.impactLevel || null,
|
|
170
|
+
proposal.impactAnalysis ? JSON.stringify(proposal.impactAnalysis) : null,
|
|
171
|
+
proposal.voteYes,
|
|
172
|
+
proposal.voteNo,
|
|
173
|
+
proposal.voteAbstain,
|
|
174
|
+
proposal.votingStartsAt || null,
|
|
175
|
+
proposal.votingEndsAt || null,
|
|
176
|
+
proposal.metadata ? JSON.stringify(proposal.metadata) : null,
|
|
177
|
+
proposal.createdAt,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function createProposal(db, config = {}) {
|
|
182
|
+
const title = String(config.title || "").trim();
|
|
183
|
+
if (!title) throw new Error("proposal title is required");
|
|
184
|
+
|
|
185
|
+
const type = String(config.type || "feature_request");
|
|
186
|
+
const validTypes = Object.values(PROPOSAL_TYPES).map((t) => t.id);
|
|
187
|
+
if (!validTypes.includes(type)) {
|
|
188
|
+
throw new Error(`Unknown proposal type: ${type}`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const now = Number(config.now ?? Date.now());
|
|
192
|
+
const id = config.id || crypto.randomUUID();
|
|
193
|
+
|
|
194
|
+
if (_proposals.has(id)) {
|
|
195
|
+
throw new Error(`Proposal already exists: ${id}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const proposal = {
|
|
199
|
+
id,
|
|
200
|
+
title,
|
|
201
|
+
description: config.description || null,
|
|
202
|
+
type,
|
|
203
|
+
proposerDid: config.proposerDid || null,
|
|
204
|
+
status: PROPOSAL_STATUS.DRAFT,
|
|
205
|
+
impactLevel: null,
|
|
206
|
+
impactAnalysis: null,
|
|
207
|
+
voteYes: 0,
|
|
208
|
+
voteNo: 0,
|
|
209
|
+
voteAbstain: 0,
|
|
210
|
+
votingStartsAt: null,
|
|
211
|
+
votingEndsAt: null,
|
|
212
|
+
metadata: config.metadata || null,
|
|
213
|
+
createdAt: now,
|
|
214
|
+
_seq: ++_seq,
|
|
215
|
+
};
|
|
216
|
+
_proposals.set(id, proposal);
|
|
217
|
+
_proposalVotes.set(id, new Set());
|
|
218
|
+
_persistProposal(db, proposal);
|
|
219
|
+
return _strip(proposal);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getProposal(id) {
|
|
223
|
+
const p = _proposals.get(String(id || ""));
|
|
224
|
+
return p ? _strip(p) : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function listProposals(options = {}) {
|
|
228
|
+
const rows = Array.from(_proposals.values());
|
|
229
|
+
let filtered = rows;
|
|
230
|
+
if (options.status) {
|
|
231
|
+
filtered = filtered.filter((p) => p.status === options.status);
|
|
232
|
+
}
|
|
233
|
+
if (options.type) {
|
|
234
|
+
filtered = filtered.filter((p) => p.type === options.type);
|
|
235
|
+
}
|
|
236
|
+
if (options.proposerDid) {
|
|
237
|
+
filtered = filtered.filter((p) => p.proposerDid === options.proposerDid);
|
|
238
|
+
}
|
|
239
|
+
filtered.sort((a, b) => b.createdAt - a.createdAt);
|
|
240
|
+
const limit =
|
|
241
|
+
Number.isInteger(options.limit) && options.limit > 0
|
|
242
|
+
? options.limit
|
|
243
|
+
: filtered.length;
|
|
244
|
+
return filtered.slice(0, limit).map(_strip);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function activateProposal(db, id, options = {}) {
|
|
248
|
+
const proposal = _proposals.get(String(id || ""));
|
|
249
|
+
if (!proposal) throw new Error(`Proposal not found: ${id}`);
|
|
250
|
+
if (proposal.status !== PROPOSAL_STATUS.DRAFT) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
`Only draft proposals can be activated (current: ${proposal.status})`,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
const now = Number(options.now ?? Date.now());
|
|
256
|
+
const durationMs = Number(
|
|
257
|
+
options.durationMs ?? DEFAULT_CONFIG.votingDurationMs,
|
|
258
|
+
);
|
|
259
|
+
proposal.status = PROPOSAL_STATUS.ACTIVE;
|
|
260
|
+
proposal.votingStartsAt = now;
|
|
261
|
+
proposal.votingEndsAt = now + durationMs;
|
|
262
|
+
_persistProposal(db, proposal);
|
|
263
|
+
return _strip(proposal);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function closeProposal(db, id, options = {}) {
|
|
267
|
+
const proposal = _proposals.get(String(id || ""));
|
|
268
|
+
if (!proposal) throw new Error(`Proposal not found: ${id}`);
|
|
269
|
+
if (proposal.status !== PROPOSAL_STATUS.ACTIVE) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`Only active proposals can be closed (current: ${proposal.status})`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const quorum = options.quorum ?? DEFAULT_CONFIG.quorumThreshold;
|
|
276
|
+
const threshold = options.threshold ?? DEFAULT_CONFIG.passThreshold;
|
|
277
|
+
const totalVoters = options.totalVoters;
|
|
278
|
+
|
|
279
|
+
const tally = tallyVotes(id, { quorum, threshold, totalVoters });
|
|
280
|
+
proposal.status = tally.passed
|
|
281
|
+
? PROPOSAL_STATUS.PASSED
|
|
282
|
+
: PROPOSAL_STATUS.REJECTED;
|
|
283
|
+
_persistProposal(db, proposal);
|
|
284
|
+
return { proposal: _strip(proposal), tally };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function expireProposal(db, id) {
|
|
288
|
+
const proposal = _proposals.get(String(id || ""));
|
|
289
|
+
if (!proposal) throw new Error(`Proposal not found: ${id}`);
|
|
290
|
+
if (
|
|
291
|
+
proposal.status !== PROPOSAL_STATUS.DRAFT &&
|
|
292
|
+
proposal.status !== PROPOSAL_STATUS.ACTIVE
|
|
293
|
+
) {
|
|
294
|
+
throw new Error(
|
|
295
|
+
`Cannot expire ${proposal.status} proposals (only draft/active)`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
proposal.status = PROPOSAL_STATUS.EXPIRED;
|
|
299
|
+
_persistProposal(db, proposal);
|
|
300
|
+
return _strip(proposal);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/* ── Votes ─────────────────────────────────────────────────── */
|
|
304
|
+
|
|
305
|
+
function _persistVote(db, vote) {
|
|
306
|
+
if (!db) return;
|
|
307
|
+
db.prepare(
|
|
308
|
+
`INSERT OR REPLACE INTO governance_votes
|
|
309
|
+
(id, proposal_id, voter_did, vote, reason, weight, created_at)
|
|
310
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
311
|
+
).run(
|
|
312
|
+
vote.id,
|
|
313
|
+
vote.proposalId,
|
|
314
|
+
vote.voterDid,
|
|
315
|
+
vote.vote,
|
|
316
|
+
vote.reason || null,
|
|
317
|
+
vote.weight,
|
|
318
|
+
vote.createdAt,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function castVote(db, proposalId, voterDid, voteValue, options = {}) {
|
|
323
|
+
const proposal = _proposals.get(String(proposalId || ""));
|
|
324
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
325
|
+
if (proposal.status !== PROPOSAL_STATUS.ACTIVE) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Can only vote on active proposals (current: ${proposal.status})`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
voterDid = String(voterDid || "").trim();
|
|
331
|
+
if (!voterDid) throw new Error("voter DID is required");
|
|
332
|
+
if (!VOTE_VALUES.includes(voteValue)) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`Invalid vote: ${voteValue} (expected ${VOTE_VALUES.join(" | ")})`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const weight = Number(options.weight ?? 1.0);
|
|
339
|
+
if (!Number.isFinite(weight) || weight < 0) {
|
|
340
|
+
throw new Error("Vote weight must be a non-negative finite number");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for duplicate voter on same proposal — replace previous vote
|
|
344
|
+
const existingVotes = _proposalVotes.get(proposal.id) || new Set();
|
|
345
|
+
for (const vid of existingVotes) {
|
|
346
|
+
const existing = _votes.get(vid);
|
|
347
|
+
if (existing && existing.voterDid === voterDid) {
|
|
348
|
+
// Remove old vote counts
|
|
349
|
+
_updateVoteCounts(proposal, existing.vote, existing.weight, -1);
|
|
350
|
+
_votes.delete(vid);
|
|
351
|
+
existingVotes.delete(vid);
|
|
352
|
+
if (db) {
|
|
353
|
+
db.prepare("DELETE FROM governance_votes WHERE id = ?").run(vid);
|
|
354
|
+
}
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const now = Number(options.now ?? Date.now());
|
|
360
|
+
const id = options.id || crypto.randomUUID();
|
|
361
|
+
|
|
362
|
+
const vote = {
|
|
363
|
+
id,
|
|
364
|
+
proposalId: proposal.id,
|
|
365
|
+
voterDid,
|
|
366
|
+
vote: voteValue,
|
|
367
|
+
reason: options.reason || null,
|
|
368
|
+
weight,
|
|
369
|
+
createdAt: now,
|
|
370
|
+
_seq: ++_seq,
|
|
371
|
+
};
|
|
372
|
+
_votes.set(id, vote);
|
|
373
|
+
existingVotes.add(id);
|
|
374
|
+
_proposalVotes.set(proposal.id, existingVotes);
|
|
375
|
+
|
|
376
|
+
_updateVoteCounts(proposal, voteValue, weight, 1);
|
|
377
|
+
_persistProposal(db, proposal);
|
|
378
|
+
_persistVote(db, vote);
|
|
379
|
+
return _strip(vote);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function _updateVoteCounts(proposal, voteValue, weight, direction) {
|
|
383
|
+
const delta = weight * direction;
|
|
384
|
+
if (voteValue === "yes") proposal.voteYes += delta;
|
|
385
|
+
else if (voteValue === "no") proposal.voteNo += delta;
|
|
386
|
+
else if (voteValue === "abstain") proposal.voteAbstain += delta;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function listVotes(proposalId, options = {}) {
|
|
390
|
+
const ids = _proposalVotes.get(String(proposalId || "")) || new Set();
|
|
391
|
+
const rows = Array.from(ids)
|
|
392
|
+
.map((vid) => _votes.get(vid))
|
|
393
|
+
.filter(Boolean);
|
|
394
|
+
rows.sort((a, b) => b.createdAt - a.createdAt);
|
|
395
|
+
const limit =
|
|
396
|
+
Number.isInteger(options.limit) && options.limit > 0
|
|
397
|
+
? options.limit
|
|
398
|
+
: rows.length;
|
|
399
|
+
return rows.slice(0, limit).map(_strip);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ── Tally ─────────────────────────────────────────────────── */
|
|
403
|
+
|
|
404
|
+
export function tallyVotes(proposalId, options = {}) {
|
|
405
|
+
const proposal = _proposals.get(String(proposalId || ""));
|
|
406
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
407
|
+
|
|
408
|
+
const quorum = Number(options.quorum ?? DEFAULT_CONFIG.quorumThreshold);
|
|
409
|
+
const threshold = Number(options.threshold ?? DEFAULT_CONFIG.passThreshold);
|
|
410
|
+
const totalVoters = options.totalVoters;
|
|
411
|
+
|
|
412
|
+
const voteIds = _proposalVotes.get(proposal.id) || new Set();
|
|
413
|
+
const voteCount = voteIds.size;
|
|
414
|
+
|
|
415
|
+
const yesWeight = proposal.voteYes;
|
|
416
|
+
const noWeight = proposal.voteNo;
|
|
417
|
+
const abstainWeight = proposal.voteAbstain;
|
|
418
|
+
const totalWeight = yesWeight + noWeight + abstainWeight;
|
|
419
|
+
|
|
420
|
+
// Quorum: based on totalVoters if provided, else just check there are votes
|
|
421
|
+
let quorumMet;
|
|
422
|
+
if (totalVoters !== undefined && totalVoters > 0) {
|
|
423
|
+
quorumMet = voteCount / totalVoters >= quorum;
|
|
424
|
+
} else {
|
|
425
|
+
quorumMet = voteCount > 0;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Pass threshold: yes / (yes + no), abstain excluded
|
|
429
|
+
const decisiveWeight = yesWeight + noWeight;
|
|
430
|
+
const yesRatio = decisiveWeight > 0 ? yesWeight / decisiveWeight : 0;
|
|
431
|
+
const passed = quorumMet && yesRatio >= threshold;
|
|
432
|
+
|
|
433
|
+
return {
|
|
434
|
+
proposalId: proposal.id,
|
|
435
|
+
voteCount,
|
|
436
|
+
yesWeight,
|
|
437
|
+
noWeight,
|
|
438
|
+
abstainWeight,
|
|
439
|
+
totalWeight,
|
|
440
|
+
yesRatio: Math.round(yesRatio * 10000) / 10000,
|
|
441
|
+
quorum,
|
|
442
|
+
quorumMet,
|
|
443
|
+
threshold,
|
|
444
|
+
passed,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* ── Heuristic Impact Analysis ──────────────────────────────── */
|
|
449
|
+
|
|
450
|
+
// Risk/benefit heuristics per proposal type (no LLM needed).
|
|
451
|
+
const TYPE_RISK_MAP = {
|
|
452
|
+
parameter_change: { riskBase: 0.3, benefitBase: 0.5, effort: "small" },
|
|
453
|
+
feature_request: { riskBase: 0.4, benefitBase: 0.7, effort: "medium" },
|
|
454
|
+
policy_update: { riskBase: 0.5, benefitBase: 0.6, effort: "medium" },
|
|
455
|
+
budget_allocation: { riskBase: 0.6, benefitBase: 0.8, effort: "large" },
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const HIGH_RISK_KEYWORDS = [
|
|
459
|
+
"security",
|
|
460
|
+
"delete",
|
|
461
|
+
"remove",
|
|
462
|
+
"migration",
|
|
463
|
+
"breaking",
|
|
464
|
+
"downtime",
|
|
465
|
+
"encryption",
|
|
466
|
+
"auth",
|
|
467
|
+
"permission",
|
|
468
|
+
"安全",
|
|
469
|
+
"删除",
|
|
470
|
+
"迁移",
|
|
471
|
+
"停机",
|
|
472
|
+
"加密",
|
|
473
|
+
"权限",
|
|
474
|
+
];
|
|
475
|
+
|
|
476
|
+
const COMPONENT_KEYWORDS = {
|
|
477
|
+
database: ["database", "db", "sql", "migration", "数据库", "迁移"],
|
|
478
|
+
security: [
|
|
479
|
+
"security",
|
|
480
|
+
"auth",
|
|
481
|
+
"encrypt",
|
|
482
|
+
"permission",
|
|
483
|
+
"安全",
|
|
484
|
+
"加密",
|
|
485
|
+
"权限",
|
|
486
|
+
],
|
|
487
|
+
network: ["network", "p2p", "api", "endpoint", "网络", "接口"],
|
|
488
|
+
ui: ["ui", "frontend", "page", "component", "界面", "前端", "页面"],
|
|
489
|
+
ai: ["ai", "llm", "model", "embedding", "模型", "向量"],
|
|
490
|
+
storage: ["storage", "file", "disk", "cache", "存储", "文件", "缓存"],
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
export function analyzeImpact(proposalId) {
|
|
494
|
+
const proposal = _proposals.get(String(proposalId || ""));
|
|
495
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
496
|
+
|
|
497
|
+
const typeInfo =
|
|
498
|
+
TYPE_RISK_MAP[proposal.type] || TYPE_RISK_MAP.feature_request;
|
|
499
|
+
const text = `${proposal.title} ${proposal.description || ""}`.toLowerCase();
|
|
500
|
+
|
|
501
|
+
// Risk boost from keywords
|
|
502
|
+
let riskBoost = 0;
|
|
503
|
+
for (const kw of HIGH_RISK_KEYWORDS) {
|
|
504
|
+
if (text.includes(kw)) riskBoost += 0.08;
|
|
505
|
+
}
|
|
506
|
+
const riskScore = Math.min(1, typeInfo.riskBase + riskBoost);
|
|
507
|
+
|
|
508
|
+
// Detect affected components
|
|
509
|
+
const affectedComponents = [];
|
|
510
|
+
for (const [component, keywords] of Object.entries(COMPONENT_KEYWORDS)) {
|
|
511
|
+
if (keywords.some((kw) => text.includes(kw))) {
|
|
512
|
+
affectedComponents.push(component);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (affectedComponents.length === 0) affectedComponents.push("general");
|
|
516
|
+
|
|
517
|
+
// Benefit scales with description length (more detail → higher confidence)
|
|
518
|
+
const descLength = (proposal.description || "").length;
|
|
519
|
+
const detailBonus = Math.min(0.15, descLength / 2000);
|
|
520
|
+
const benefitScore = Math.min(1, typeInfo.benefitBase + detailBonus);
|
|
521
|
+
|
|
522
|
+
// Impact level from risk score
|
|
523
|
+
let impactLevel;
|
|
524
|
+
if (riskScore >= 0.7) impactLevel = "critical";
|
|
525
|
+
else if (riskScore >= 0.5) impactLevel = "high";
|
|
526
|
+
else if (riskScore >= 0.3) impactLevel = "medium";
|
|
527
|
+
else impactLevel = "low";
|
|
528
|
+
|
|
529
|
+
const analysis = {
|
|
530
|
+
impactLevel,
|
|
531
|
+
affectedComponents,
|
|
532
|
+
riskScore: Math.round(riskScore * 10000) / 10000,
|
|
533
|
+
benefitScore: Math.round(benefitScore * 10000) / 10000,
|
|
534
|
+
estimatedEffort: typeInfo.effort,
|
|
535
|
+
communitySentiment: riskScore > 0.5 ? "cautious" : "positive",
|
|
536
|
+
recommendations: _generateRecommendations(
|
|
537
|
+
proposal,
|
|
538
|
+
riskScore,
|
|
539
|
+
affectedComponents,
|
|
540
|
+
),
|
|
541
|
+
analyzedAt: Date.now(),
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// Store analysis on the proposal
|
|
545
|
+
proposal.impactLevel = impactLevel;
|
|
546
|
+
proposal.impactAnalysis = analysis;
|
|
547
|
+
return analysis;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function _generateRecommendations(proposal, riskScore, components) {
|
|
551
|
+
const recs = [];
|
|
552
|
+
if (riskScore >= 0.5) {
|
|
553
|
+
recs.push("Consider a phased rollout to minimize risk");
|
|
554
|
+
}
|
|
555
|
+
if (components.includes("security")) {
|
|
556
|
+
recs.push("Security review required before implementation");
|
|
557
|
+
}
|
|
558
|
+
if (components.includes("database")) {
|
|
559
|
+
recs.push("Ensure database migration is reversible");
|
|
560
|
+
}
|
|
561
|
+
if (proposal.type === "budget_allocation") {
|
|
562
|
+
recs.push("Include ROI projections in the proposal description");
|
|
563
|
+
}
|
|
564
|
+
if (recs.length === 0) {
|
|
565
|
+
recs.push("Standard review process is sufficient");
|
|
566
|
+
}
|
|
567
|
+
return recs;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── Heuristic Vote Prediction ──────────────────────────────── */
|
|
571
|
+
|
|
572
|
+
export function predictVote(proposalId) {
|
|
573
|
+
const proposal = _proposals.get(String(proposalId || ""));
|
|
574
|
+
if (!proposal) throw new Error(`Proposal not found: ${proposalId}`);
|
|
575
|
+
|
|
576
|
+
const totalWeight = proposal.voteYes + proposal.voteNo + proposal.voteAbstain;
|
|
577
|
+
|
|
578
|
+
if (totalWeight === 0) {
|
|
579
|
+
// No votes yet — predict from impact analysis if available
|
|
580
|
+
const analysis = proposal.impactAnalysis;
|
|
581
|
+
const benefit = analysis ? analysis.benefitScore : 0.6;
|
|
582
|
+
const risk = analysis ? analysis.riskScore : 0.4;
|
|
583
|
+
const yesProb = benefit * 0.7 + (1 - risk) * 0.3;
|
|
584
|
+
return {
|
|
585
|
+
proposalId: proposal.id,
|
|
586
|
+
predictedOutcome: yesProb >= 0.5 ? "pass" : "reject",
|
|
587
|
+
confidence: 0.3, // Low confidence with no votes
|
|
588
|
+
yesProb: Math.round(yesProb * 10000) / 10000,
|
|
589
|
+
noProb: Math.round((1 - yesProb) * 10000) / 10000,
|
|
590
|
+
abstainProb: 0,
|
|
591
|
+
basedOn: "heuristic",
|
|
592
|
+
sampleSize: 0,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const yesPct = proposal.voteYes / totalWeight;
|
|
597
|
+
const noPct = proposal.voteNo / totalWeight;
|
|
598
|
+
const abstainPct = proposal.voteAbstain / totalWeight;
|
|
599
|
+
|
|
600
|
+
// Confidence grows with sample size (log scale)
|
|
601
|
+
const voteIds = _proposalVotes.get(proposal.id) || new Set();
|
|
602
|
+
const sampleSize = voteIds.size;
|
|
603
|
+
const confidence = Math.min(0.95, 0.3 + Math.log2(sampleSize + 1) * 0.15);
|
|
604
|
+
|
|
605
|
+
const decisiveWeight = proposal.voteYes + proposal.voteNo;
|
|
606
|
+
const yesRatio = decisiveWeight > 0 ? proposal.voteYes / decisiveWeight : 0;
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
proposalId: proposal.id,
|
|
610
|
+
predictedOutcome:
|
|
611
|
+
yesRatio >= DEFAULT_CONFIG.passThreshold ? "pass" : "reject",
|
|
612
|
+
confidence: Math.round(confidence * 10000) / 10000,
|
|
613
|
+
yesProb: Math.round(yesPct * 10000) / 10000,
|
|
614
|
+
noProb: Math.round(noPct * 10000) / 10000,
|
|
615
|
+
abstainProb: Math.round(abstainPct * 10000) / 10000,
|
|
616
|
+
basedOn: "votes",
|
|
617
|
+
sampleSize,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* ── Stats ─────────────────────────────────────────────────── */
|
|
622
|
+
|
|
623
|
+
export function getGovernanceStats() {
|
|
624
|
+
const proposals = Array.from(_proposals.values());
|
|
625
|
+
const byStatus = {};
|
|
626
|
+
for (const s of Object.values(PROPOSAL_STATUS)) byStatus[s] = 0;
|
|
627
|
+
const byType = {};
|
|
628
|
+
for (const t of Object.values(PROPOSAL_TYPES)) byType[t.id] = 0;
|
|
629
|
+
for (const p of proposals) {
|
|
630
|
+
byStatus[p.status] = (byStatus[p.status] || 0) + 1;
|
|
631
|
+
byType[p.type] = (byType[p.type] || 0) + 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return {
|
|
635
|
+
proposalCount: proposals.length,
|
|
636
|
+
voteCount: _votes.size,
|
|
637
|
+
byStatus,
|
|
638
|
+
byType,
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* ── Test Helpers ──────────────────────────────────────────── */
|
|
643
|
+
|
|
644
|
+
export function _resetState() {
|
|
645
|
+
_proposals.clear();
|
|
646
|
+
_votes.clear();
|
|
647
|
+
_proposalVotes.clear();
|
|
648
|
+
_seq = 0;
|
|
649
|
+
}
|