@pkcprotocol/pkc-js 0.0.26 → 0.0.28
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 +11 -7
- package/dist/browser/clients/base-client-manager.js +1 -1
- package/dist/browser/community/community-client-manager.js +1 -2
- package/dist/browser/community/community-client-manager.js.map +1 -1
- package/dist/browser/generated-version.d.ts +1 -1
- package/dist/browser/generated-version.js +1 -1
- package/dist/browser/helia/helia-for-pkc.js +5 -7
- package/dist/browser/helia/helia-for-pkc.js.map +1 -1
- package/dist/browser/helia/util.d.ts +11 -0
- package/dist/browser/helia/util.js +24 -3
- package/dist/browser/helia/util.js.map +1 -1
- package/dist/browser/publications/comment/comment.d.ts +1 -0
- package/dist/browser/publications/comment/comment.js +35 -2
- package/dist/browser/publications/comment/comment.js.map +1 -1
- package/dist/browser/publications/publication.d.ts +1 -0
- package/dist/browser/publications/publication.js +13 -3
- package/dist/browser/publications/publication.js.map +1 -1
- package/dist/browser/runtime/node/community/challenges/index.d.ts +32 -5
- package/dist/browser/runtime/node/community/challenges/index.js +356 -124
- package/dist/browser/runtime/node/community/challenges/index.js.map +1 -1
- package/dist/browser/runtime/node/community/db-handler.js +4 -1
- package/dist/browser/runtime/node/community/db-handler.js.map +1 -1
- package/dist/browser/runtime/node/community/local-community.js +11 -4
- package/dist/browser/runtime/node/community/local-community.js.map +1 -1
- package/dist/node/clients/base-client-manager.js +1 -1
- package/dist/node/community/community-client-manager.js +1 -2
- package/dist/node/community/community-client-manager.js.map +1 -1
- package/dist/node/generated-version.d.ts +1 -1
- package/dist/node/generated-version.js +1 -1
- package/dist/node/helia/helia-for-pkc.js +5 -7
- package/dist/node/helia/helia-for-pkc.js.map +1 -1
- package/dist/node/helia/util.d.ts +11 -0
- package/dist/node/helia/util.js +24 -3
- package/dist/node/helia/util.js.map +1 -1
- package/dist/node/publications/comment/comment.d.ts +1 -0
- package/dist/node/publications/comment/comment.js +35 -2
- package/dist/node/publications/comment/comment.js.map +1 -1
- package/dist/node/publications/publication.d.ts +1 -0
- package/dist/node/publications/publication.js +13 -3
- package/dist/node/publications/publication.js.map +1 -1
- package/dist/node/runtime/node/community/challenges/index.d.ts +32 -5
- package/dist/node/runtime/node/community/challenges/index.js +356 -124
- package/dist/node/runtime/node/community/challenges/index.js.map +1 -1
- package/dist/node/runtime/node/community/db-handler.js +4 -1
- package/dist/node/runtime/node/community/db-handler.js.map +1 -1
- package/dist/node/runtime/node/community/local-community.js +11 -4
- package/dist/node/runtime/node/community/local-community.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,9 +10,9 @@ import * as remeda from "remeda";
|
|
|
10
10
|
import { ChallengeFileFactorySchema, ChallengeFileSchema, CommunityChallengeSettingSchema } from "../../../../community/schema.js";
|
|
11
11
|
import { PKCError } from "../../../../pkc-error.js";
|
|
12
12
|
import { pathToFileURL } from "node:url";
|
|
13
|
-
const resolveChallengeFactoryByName = (name, pkc) => {
|
|
13
|
+
const resolveChallengeFactoryByName = ({ name, pkc }) => {
|
|
14
14
|
// User-defined shadows built-ins
|
|
15
|
-
return pkc?.settings?.challenges?.[name] ?? pkcJsChallenges[name];
|
|
15
|
+
return { factory: pkc?.settings?.challenges?.[name] ?? pkcJsChallenges[name] };
|
|
16
16
|
};
|
|
17
17
|
const pkcJsChallenges = {
|
|
18
18
|
"text-math": textMath,
|
|
@@ -22,190 +22,412 @@ const pkcJsChallenges = {
|
|
|
22
22
|
question: question,
|
|
23
23
|
"publication-match": publicationMatch
|
|
24
24
|
};
|
|
25
|
-
const validateChallengeFileFactory = (challengeFileFactory, challengeIndex, community) => {
|
|
25
|
+
const validateChallengeFileFactory = ({ challengeFileFactory, challengeIndex, community }) => {
|
|
26
26
|
const communityChallengeSettings = community?.settings?.challenges?.[challengeIndex];
|
|
27
27
|
if (typeof challengeFileFactory !== "function") {
|
|
28
28
|
throw Error(`invalid challenge file factory export from community challenge '${communityChallengeSettings?.name || communityChallengeSettings?.path}' (challenge #${challengeIndex + 1})`);
|
|
29
29
|
}
|
|
30
|
+
return { ok: true };
|
|
30
31
|
};
|
|
31
|
-
const validateChallengeFile = (challengeFile, challengeIndex, community) => {
|
|
32
|
+
const validateChallengeFile = ({ challengeFile, challengeIndex, community }) => {
|
|
32
33
|
const communityChallengeSettings = community.settings?.challenges?.[challengeIndex];
|
|
33
34
|
if (typeof challengeFile?.getChallenge !== "function") {
|
|
34
35
|
throw Error(`invalid challenge file from community challenge '${communityChallengeSettings?.name || communityChallengeSettings?.path}' (challenge #${challengeIndex + 1})`);
|
|
35
36
|
}
|
|
37
|
+
return { ok: true };
|
|
36
38
|
};
|
|
37
|
-
const validateChallengeResult = (challengeResult, challengeIndex, community) => {
|
|
39
|
+
const validateChallengeResult = ({ challengeResult, challengeIndex, community }) => {
|
|
38
40
|
const communityChallengeSettings = community.settings?.challenges?.[challengeIndex];
|
|
39
41
|
const error = `invalid challenge result from community challenge '${communityChallengeSettings?.name || communityChallengeSettings?.path}' (challenge #${challengeIndex + 1})`;
|
|
40
42
|
if (typeof challengeResult?.success !== "boolean") {
|
|
41
43
|
throw Error(error);
|
|
42
44
|
}
|
|
45
|
+
return { ok: true };
|
|
43
46
|
};
|
|
44
|
-
const validateChallengeOrChallengeResult = (challengeOrChallengeResult, challengeIndex, community) => {
|
|
47
|
+
const validateChallengeOrChallengeResult = ({ challengeOrChallengeResult, challengeIndex, community }) => {
|
|
45
48
|
if ("success" in challengeOrChallengeResult) {
|
|
46
|
-
validateChallengeResult(challengeOrChallengeResult, challengeIndex, community);
|
|
49
|
+
validateChallengeResult({ challengeResult: challengeOrChallengeResult, challengeIndex, community });
|
|
47
50
|
}
|
|
48
51
|
else if (typeof challengeOrChallengeResult?.["challenge"] !== "string" ||
|
|
49
52
|
typeof challengeOrChallengeResult?.["type"] !== "string" ||
|
|
50
53
|
typeof challengeOrChallengeResult?.["verify"] !== "function") {
|
|
51
54
|
throw Error("The challenge does not contain the correct {challenge, type, verify}");
|
|
52
55
|
}
|
|
56
|
+
return { ok: true };
|
|
53
57
|
};
|
|
54
|
-
|
|
58
|
+
// load and validate a challenge factory + ChallengeFile for one challenge index
|
|
59
|
+
const loadChallengeFile = async ({ communityChallengeSettings, challengeIndex, community }) => {
|
|
60
|
+
if (!communityChallengeSettings.path &&
|
|
61
|
+
!resolveChallengeFactoryByName({ name: communityChallengeSettings.name, pkc: community._pkc }).factory)
|
|
62
|
+
throw Error("You have to provide either path or a stored pkc-js challenge");
|
|
63
|
+
let ChallengeFileFactory;
|
|
64
|
+
try {
|
|
65
|
+
ChallengeFileFactory = ChallengeFileFactorySchema.parse(communityChallengeSettings.path
|
|
66
|
+
? (await import(pathToFileURL(communityChallengeSettings.path).href)).default
|
|
67
|
+
: resolveChallengeFactoryByName({ name: communityChallengeSettings.name, pkc: community._pkc }).factory);
|
|
68
|
+
validateChallengeFileFactory({ challengeFileFactory: ChallengeFileFactory, challengeIndex, community });
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
throw new PKCError("ERR_FAILED_TO_IMPORT_CHALLENGE_FILE_FACTORY", {
|
|
72
|
+
path: communityChallengeSettings.path,
|
|
73
|
+
communityChallengeSettings,
|
|
74
|
+
error: e,
|
|
75
|
+
challengeIndex
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const challengeFile = ChallengeFileFactory({ challengeSettings: communityChallengeSettings });
|
|
79
|
+
validateChallengeFile({ challengeFile, challengeIndex, community });
|
|
80
|
+
return { challengeFile };
|
|
81
|
+
};
|
|
82
|
+
// invoke getChallenge() with shared error handling and result validation
|
|
83
|
+
const callGetChallenge = async ({ challengeFile, communityChallengeSettings, challengeRequestMessage, challengeIndex, community }) => {
|
|
84
|
+
let challengeOrChallengeResult;
|
|
85
|
+
try {
|
|
86
|
+
challengeOrChallengeResult = await challengeFile.getChallenge({
|
|
87
|
+
challengeSettings: communityChallengeSettings,
|
|
88
|
+
challengeRequestMessage,
|
|
89
|
+
challengeIndex,
|
|
90
|
+
community
|
|
91
|
+
});
|
|
92
|
+
validateChallengeOrChallengeResult({ challengeOrChallengeResult, challengeIndex, community });
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
throw new PKCError("ERR_INVALID_RESULT_FROM_GET_CHALLENGE_FUNCTION", {
|
|
96
|
+
communityChallengeSettings,
|
|
97
|
+
challengeName: communityChallengeSettings.name || communityChallengeSettings.path,
|
|
98
|
+
challengeRequestMessage,
|
|
99
|
+
challengeIndex: challengeIndex + 1,
|
|
100
|
+
error: e
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return { challengeOrChallengeResult };
|
|
104
|
+
};
|
|
105
|
+
const evaluatePhase2Exclusion = ({ i, communityChallenges, decided, results }) => {
|
|
106
|
+
const communityChallenge = communityChallenges[i];
|
|
107
|
+
if (!communityChallenge.exclude?.length)
|
|
108
|
+
return { decision: "ready" };
|
|
109
|
+
let allRulesEvaluable = true;
|
|
110
|
+
let anyDeferRule = false;
|
|
111
|
+
for (const item of communityChallenge.exclude) {
|
|
112
|
+
if (!item.challenges?.length)
|
|
113
|
+
continue;
|
|
114
|
+
let allDecided = true;
|
|
115
|
+
for (const j of item.challenges) {
|
|
116
|
+
if (!decided[j]) {
|
|
117
|
+
allDecided = false;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (!allDecided) {
|
|
122
|
+
allRulesEvaluable = false;
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let allSuccessOrPending = true;
|
|
126
|
+
let anyPending = false;
|
|
127
|
+
const pendingDeps = [];
|
|
128
|
+
for (const j of item.challenges) {
|
|
129
|
+
const r = results[j];
|
|
130
|
+
if (r === undefined) {
|
|
131
|
+
allSuccessOrPending = false;
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
if ("success" in r) {
|
|
135
|
+
if (r.success !== true) {
|
|
136
|
+
allSuccessOrPending = false;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
anyPending = true;
|
|
142
|
+
pendingDeps.push(j);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!allSuccessOrPending)
|
|
146
|
+
continue;
|
|
147
|
+
if (!anyPending)
|
|
148
|
+
return { decision: "exclude" }; // all deps succeeded → definitively exclude i
|
|
149
|
+
// Tentative match (some pending). Defer only if no pending dep references i back —
|
|
150
|
+
// otherwise calling i could affect the pending dep's exclusion, so we should call.
|
|
151
|
+
let allOneWay = true;
|
|
152
|
+
for (const j of pendingDeps) {
|
|
153
|
+
const cj = communityChallenges[j];
|
|
154
|
+
const referencesBack = cj.exclude?.some((it) => it.challenges?.includes(i));
|
|
155
|
+
if (referencesBack) {
|
|
156
|
+
allOneWay = false;
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (allOneWay)
|
|
161
|
+
anyDeferRule = true;
|
|
162
|
+
}
|
|
163
|
+
if (anyDeferRule)
|
|
164
|
+
return { decision: "defer" };
|
|
165
|
+
return { decision: allRulesEvaluable ? "ready" : "skip" };
|
|
166
|
+
};
|
|
167
|
+
const getPendingChallengesOrChallengeVerification = async ({ challengeRequestMessage, community }) => {
|
|
55
168
|
// if community has no challenges, no need to send a challenge
|
|
56
169
|
if (!Array.isArray(community.settings?.challenges))
|
|
57
170
|
return {
|
|
58
171
|
challengeSuccess: true,
|
|
59
172
|
pendingApprovalSuccess: false
|
|
60
173
|
};
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
174
|
+
const challengeCount = community.settings.challenges.length;
|
|
175
|
+
const results = new Array(challengeCount);
|
|
176
|
+
const decided = new Array(challengeCount).fill(false);
|
|
177
|
+
// Phase 0: load CommunityChallenge records for every index in parallel.
|
|
178
|
+
const communityChallenges = await Promise.all(community.settings.challenges.map(async (s) => (await getCommunityChallengeFromCommunityChallengeSettings({
|
|
179
|
+
communityChallengeSettings: s,
|
|
180
|
+
pkc: community._pkc
|
|
181
|
+
})).communityChallenge));
|
|
182
|
+
// Phase 0b: load challenge files (factories) for every index in parallel — needed for getChallenge calls
|
|
183
|
+
// in phases 2/3. Loading them upfront avoids re-importing during the dependency-ordered loop.
|
|
184
|
+
const challengeFiles = await Promise.all(community.settings.challenges.map(async (s, i) => (await loadChallengeFile({ communityChallengeSettings: s, challengeIndex: i, community })).challengeFile));
|
|
185
|
+
// Phase 1: request-only excludes (parallel). Indexes excluded here never reach getChallenge.
|
|
186
|
+
await Promise.all(communityChallenges.map(async (communityChallenge, i) => {
|
|
187
|
+
if (shouldExcludePublication(communityChallenge, challengeRequestMessage, community)) {
|
|
188
|
+
decided[i] = true;
|
|
189
|
+
return;
|
|
75
190
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
path: communityChallengeSettings.path,
|
|
79
|
-
communityChallengeSettings,
|
|
80
|
-
error: e,
|
|
81
|
-
challengeIndex
|
|
82
|
-
});
|
|
191
|
+
if (await shouldExcludeChallengeCommentCids(communityChallenge, challengeRequestMessage, community._pkc)) {
|
|
192
|
+
decided[i] = true;
|
|
83
193
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
|
|
194
|
+
}));
|
|
195
|
+
// Phase 2 + 3: dependency-ordered resolution with incremental cycle-break.
|
|
196
|
+
// `deferred[i] = true` means index i was decided but its result is not yet known — it will be
|
|
197
|
+
// resolved at verify time once pending challenges produce verify outcomes.
|
|
198
|
+
const deferred = new Array(challengeCount).fill(false);
|
|
199
|
+
while (true) {
|
|
200
|
+
let progress = false;
|
|
201
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
202
|
+
if (decided[i])
|
|
203
|
+
continue;
|
|
204
|
+
const { decision } = evaluatePhase2Exclusion({ i, communityChallenges, decided, results });
|
|
205
|
+
if (decision === "skip")
|
|
206
|
+
continue;
|
|
207
|
+
if (decision === "exclude") {
|
|
208
|
+
decided[i] = true;
|
|
209
|
+
}
|
|
210
|
+
else if (decision === "defer") {
|
|
211
|
+
decided[i] = true;
|
|
212
|
+
deferred[i] = true;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
results[i] = (await callGetChallenge({
|
|
216
|
+
challengeFile: challengeFiles[i],
|
|
217
|
+
communityChallengeSettings: community.settings.challenges[i],
|
|
218
|
+
challengeRequestMessage,
|
|
219
|
+
challengeIndex: i,
|
|
220
|
+
community
|
|
221
|
+
})).challengeOrChallengeResult;
|
|
222
|
+
decided[i] = true;
|
|
223
|
+
}
|
|
224
|
+
progress = true;
|
|
96
225
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
226
|
+
if (progress)
|
|
227
|
+
continue;
|
|
228
|
+
// Stalled. Decide between phase 3 (cycle-break) and phase 4 (defer remaining).
|
|
229
|
+
const hasPending = results.some((r) => r !== undefined && !("success" in r));
|
|
230
|
+
const firstUndecided = decided.findIndex((d) => !d);
|
|
231
|
+
if (firstUndecided === -1)
|
|
232
|
+
break;
|
|
233
|
+
if (hasPending) {
|
|
234
|
+
// Defer all remaining undecided to verify time.
|
|
235
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
236
|
+
if (!decided[i]) {
|
|
237
|
+
decided[i] = true;
|
|
238
|
+
deferred[i] = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
break;
|
|
105
242
|
}
|
|
106
|
-
|
|
243
|
+
// Phase 3: cycle-break by calling getChallenge for the lowest-index undecided challenge.
|
|
244
|
+
results[firstUndecided] = (await callGetChallenge({
|
|
245
|
+
challengeFile: challengeFiles[firstUndecided],
|
|
246
|
+
communityChallengeSettings: community.settings.challenges[firstUndecided],
|
|
247
|
+
challengeRequestMessage,
|
|
248
|
+
challengeIndex: firstUndecided,
|
|
249
|
+
community
|
|
250
|
+
})).challengeOrChallengeResult;
|
|
251
|
+
decided[firstUndecided] = true;
|
|
252
|
+
}
|
|
253
|
+
// Phase 4: collect deferred-bucket entries.
|
|
254
|
+
const deferredChallenges = [];
|
|
255
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
256
|
+
if (deferred[i])
|
|
257
|
+
deferredChallenges.push({ index: i, communityChallenge: communityChallenges[i] });
|
|
107
258
|
}
|
|
108
|
-
//
|
|
259
|
+
// Phase 5: classification — tally failures, pending, pendingApproval over the populated result
|
|
260
|
+
// array. Mirrors the original two-pass orchestrator: shouldExcludeChallengeSuccess is applied
|
|
261
|
+
// uniformly to every populated slot before classifying its result.
|
|
109
262
|
let challengeFailureCount = 0;
|
|
110
263
|
let pendingChallenges = [];
|
|
111
264
|
const challengeErrors = {};
|
|
112
265
|
let pendingApprovalSuccess = false;
|
|
113
|
-
for (
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// exclude author from challenge based on the community minimum karma settings
|
|
119
|
-
if (shouldExcludePublication(communityChallenge, challengeRequestMessage, community)) {
|
|
266
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
267
|
+
const r = results[i];
|
|
268
|
+
if (r === undefined)
|
|
269
|
+
continue; // excluded earlier (phase 1 or phase 2)
|
|
270
|
+
if (shouldExcludeChallengeSuccess(communityChallenges[i], i, results))
|
|
120
271
|
continue;
|
|
121
|
-
|
|
122
|
-
if (await shouldExcludeChallengeCommentCids(communityChallenge, challengeRequestMessage, community._pkc)) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
// exclude based on other challenges successes
|
|
126
|
-
if (shouldExcludeChallengeSuccess(communityChallenge, challengeIndex, challengeOrChallengeResults)) {
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if ("success" in challengeOrChallengeResult && challengeOrChallengeResult.success === false) {
|
|
272
|
+
if ("success" in r && r.success === false) {
|
|
130
273
|
challengeFailureCount++;
|
|
131
|
-
challengeErrors[
|
|
274
|
+
challengeErrors[i] = r.error;
|
|
132
275
|
}
|
|
133
|
-
else if ("success" in
|
|
134
|
-
if (
|
|
276
|
+
else if ("success" in r && r.success === true) {
|
|
277
|
+
if (communityChallenges[i].pendingApproval)
|
|
135
278
|
pendingApprovalSuccess = true;
|
|
136
|
-
}
|
|
137
279
|
}
|
|
138
280
|
else {
|
|
139
|
-
|
|
140
|
-
pendingChallenges.push({ ...challengeOrChallengeResult, index: challengeIndex });
|
|
281
|
+
pendingChallenges.push({ ...r, index: i });
|
|
141
282
|
}
|
|
142
283
|
}
|
|
143
|
-
// challenge success can be undefined if there are pending challenges
|
|
144
284
|
let challengeSuccess = undefined;
|
|
145
|
-
//
|
|
285
|
+
// any failure short-circuits pending and deferred
|
|
146
286
|
if (challengeFailureCount > 0) {
|
|
147
287
|
challengeSuccess = false;
|
|
148
288
|
pendingChallenges = [];
|
|
149
289
|
}
|
|
150
|
-
// if there are no pending challenges and no failures, success is true
|
|
151
290
|
if (pendingChallenges.length === 0 && challengeFailureCount === 0) {
|
|
152
291
|
challengeSuccess = true;
|
|
153
292
|
}
|
|
154
|
-
|
|
155
|
-
if (challengeSuccess === true) {
|
|
293
|
+
if (challengeSuccess === true)
|
|
156
294
|
return { challengeSuccess, pendingApprovalSuccess };
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
295
|
+
if (challengeSuccess === false)
|
|
296
|
+
return { challengeSuccess, challengeErrors };
|
|
297
|
+
return {
|
|
298
|
+
pendingChallenges,
|
|
299
|
+
pendingApprovalSuccess,
|
|
300
|
+
deferredChallenges: deferredChallenges.length > 0 ? deferredChallenges : undefined,
|
|
301
|
+
partialResults: results,
|
|
302
|
+
communityChallenges
|
|
303
|
+
};
|
|
167
304
|
};
|
|
168
|
-
const getChallengeVerificationFromChallengeAnswers = async (pendingChallenges, challengeAnswers, community) => {
|
|
305
|
+
const getChallengeVerificationFromChallengeAnswers = async ({ pendingChallenges, challengeAnswers, community, challengeRequestMessage, deferredChallenges, partialResults, communityChallenges }) => {
|
|
306
|
+
if (!Array.isArray(community.settings?.challenges))
|
|
307
|
+
throw Error("community.settings?.challenges is not defined");
|
|
308
|
+
const challengeCount = community.settings.challenges.length;
|
|
309
|
+
// Run verify() for every pending challenge in parallel.
|
|
169
310
|
const verifyChallengePromises = [];
|
|
170
311
|
for (const i in pendingChallenges) {
|
|
171
312
|
verifyChallengePromises.push(Promise.resolve(pendingChallenges[i].verify(challengeAnswers[i])));
|
|
172
313
|
}
|
|
173
314
|
const challengeResultsWithPendingIndexes = await Promise.all(verifyChallengePromises);
|
|
174
|
-
// validate results
|
|
175
315
|
for (const i in challengeResultsWithPendingIndexes) {
|
|
176
|
-
|
|
177
|
-
|
|
316
|
+
validateChallengeResult({
|
|
317
|
+
challengeResult: challengeResultsWithPendingIndexes[Number(i)],
|
|
318
|
+
challengeIndex: pendingChallenges[Number(i)].index,
|
|
319
|
+
community
|
|
320
|
+
});
|
|
178
321
|
}
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
322
|
+
// Hydrate the result array from the persisted partial state. The pre-verify orchestrator marks
|
|
323
|
+
// excluded slots as undefined, resolved-immediate slots with their {success} object, and the
|
|
324
|
+
// pending slots with their {challenge, verify, type} object — replace those pending slots with
|
|
325
|
+
// the verify result here.
|
|
326
|
+
const results = partialResults ? partialResults.slice() : new Array(challengeCount);
|
|
182
327
|
for (const i in challengeResultsWithPendingIndexes) {
|
|
183
|
-
|
|
184
|
-
|
|
328
|
+
results[pendingChallenges[i].index] = challengeResultsWithPendingIndexes[i];
|
|
329
|
+
}
|
|
330
|
+
// Lazily reload CommunityChallenge records if the caller didn't pass them through (back-compat).
|
|
331
|
+
const loadedCommunityChallenges = communityChallenges ??
|
|
332
|
+
(await Promise.all(community.settings.challenges.map(async (s) => (await getCommunityChallengeFromCommunityChallengeSettings({
|
|
333
|
+
communityChallengeSettings: s,
|
|
334
|
+
pkc: community._pkc
|
|
335
|
+
})).communityChallenge)));
|
|
336
|
+
// Resolve the deferred bucket. By this point all originally-pending challenges have a verify
|
|
337
|
+
// result, so the dependency-ordered fixpoint can make progress for any deferred challenge whose
|
|
338
|
+
// exclude.challenges deps are all decided.
|
|
339
|
+
const deferred = deferredChallenges ?? [];
|
|
340
|
+
if (deferred.length > 0) {
|
|
341
|
+
if (!challengeRequestMessage)
|
|
342
|
+
throw Error("getChallengeVerificationFromChallengeAnswers requires challengeRequestMessage when deferredChallenges is non-empty");
|
|
343
|
+
const decided = new Array(challengeCount).fill(true);
|
|
344
|
+
for (const d of deferred)
|
|
345
|
+
decided[d.index] = false;
|
|
346
|
+
// Reload challenge files lazily — only for deferred indexes whose getChallenge actually fires.
|
|
347
|
+
const challengeFilesByIndex = {};
|
|
348
|
+
const ensureChallengeFile = async (i) => {
|
|
349
|
+
if (!challengeFilesByIndex[i]) {
|
|
350
|
+
challengeFilesByIndex[i] = (await loadChallengeFile({
|
|
351
|
+
communityChallengeSettings: community.settings.challenges[i],
|
|
352
|
+
challengeIndex: i,
|
|
353
|
+
community
|
|
354
|
+
})).challengeFile;
|
|
355
|
+
}
|
|
356
|
+
return challengeFilesByIndex[i];
|
|
357
|
+
};
|
|
358
|
+
while (true) {
|
|
359
|
+
let progress = false;
|
|
360
|
+
for (const d of deferred) {
|
|
361
|
+
const i = d.index;
|
|
362
|
+
if (decided[i])
|
|
363
|
+
continue;
|
|
364
|
+
const { decision } = evaluatePhase2Exclusion({
|
|
365
|
+
i,
|
|
366
|
+
communityChallenges: loadedCommunityChallenges,
|
|
367
|
+
decided,
|
|
368
|
+
results
|
|
369
|
+
});
|
|
370
|
+
if (decision === "skip")
|
|
371
|
+
continue;
|
|
372
|
+
if (decision === "exclude" || decision === "defer") {
|
|
373
|
+
// No more pending challenges at verify time, so "defer" collapses to "exclude":
|
|
374
|
+
// a tentatively-matching rule has all-decided deps and ≥1 was pending — but
|
|
375
|
+
// those have verify results now, so the rule no longer matches via pending. Treat
|
|
376
|
+
// as exclude to be safe: the slot stays undefined, which is the same as today's
|
|
377
|
+
// "filtered out" semantics.
|
|
378
|
+
decided[i] = true;
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
const file = await ensureChallengeFile(i);
|
|
382
|
+
results[i] = (await callGetChallenge({
|
|
383
|
+
challengeFile: file,
|
|
384
|
+
communityChallengeSettings: community.settings.challenges[i],
|
|
385
|
+
challengeRequestMessage,
|
|
386
|
+
challengeIndex: i,
|
|
387
|
+
community
|
|
388
|
+
})).challengeOrChallengeResult;
|
|
389
|
+
decided[i] = true;
|
|
390
|
+
}
|
|
391
|
+
progress = true;
|
|
392
|
+
}
|
|
393
|
+
if (progress)
|
|
394
|
+
continue;
|
|
395
|
+
const firstUndecided = decided.findIndex((d) => !d);
|
|
396
|
+
if (firstUndecided === -1)
|
|
397
|
+
break;
|
|
398
|
+
// Cycle-break: lowest-index undecided deferred index.
|
|
399
|
+
const file = await ensureChallengeFile(firstUndecided);
|
|
400
|
+
results[firstUndecided] = (await callGetChallenge({
|
|
401
|
+
challengeFile: file,
|
|
402
|
+
communityChallengeSettings: community.settings.challenges[firstUndecided],
|
|
403
|
+
challengeRequestMessage,
|
|
404
|
+
challengeIndex: firstUndecided,
|
|
405
|
+
community
|
|
406
|
+
})).challengeOrChallengeResult;
|
|
407
|
+
decided[firstUndecided] = true;
|
|
408
|
+
}
|
|
185
409
|
}
|
|
410
|
+
// Classification over the final result array.
|
|
186
411
|
let challengeFailureCount = 0;
|
|
187
412
|
const challengeErrors = {};
|
|
188
413
|
let pendingApprovalSuccess = false;
|
|
189
|
-
for (let i
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
throw Error("community.settings.challenges[challengeIndex] does not exist");
|
|
193
|
-
const challengeResult = challengeResults[challengeIndex];
|
|
194
|
-
// the challenge results that were filtered out were already successful
|
|
195
|
-
if (challengeResult === undefined) {
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
// exclude based on other challenges successes
|
|
199
|
-
if (shouldExcludeChallengeSuccess(community.settings.challenges[challengeIndex], challengeIndex, challengeResults)) {
|
|
414
|
+
for (let i = 0; i < challengeCount; i++) {
|
|
415
|
+
const r = results[i];
|
|
416
|
+
if (r === undefined)
|
|
200
417
|
continue;
|
|
201
|
-
|
|
202
|
-
|
|
418
|
+
if ("success" in r && r.success === false) {
|
|
419
|
+
if (shouldExcludeChallengeSuccess(loadedCommunityChallenges[i], i, results))
|
|
420
|
+
continue;
|
|
203
421
|
challengeFailureCount++;
|
|
204
|
-
challengeErrors[
|
|
422
|
+
challengeErrors[i] = r.error;
|
|
205
423
|
}
|
|
206
|
-
else if (
|
|
207
|
-
|
|
424
|
+
else if ("success" in r && r.success === true) {
|
|
425
|
+
if (shouldExcludeChallengeSuccess(loadedCommunityChallenges[i], i, results))
|
|
426
|
+
continue;
|
|
427
|
+
if (loadedCommunityChallenges[i].pendingApproval)
|
|
428
|
+
pendingApprovalSuccess = true;
|
|
208
429
|
}
|
|
430
|
+
// Any pending shape left at this point would be unexpected; ignore.
|
|
209
431
|
}
|
|
210
432
|
if (challengeFailureCount > 0) {
|
|
211
433
|
return {
|
|
@@ -218,7 +440,7 @@ const getChallengeVerificationFromChallengeAnswers = async (pendingChallenges, c
|
|
|
218
440
|
pendingApprovalSuccess
|
|
219
441
|
};
|
|
220
442
|
};
|
|
221
|
-
const getChallengeVerification = async (challengeRequestMessage, community, getChallengeAnswers) => {
|
|
443
|
+
const getChallengeVerification = async ({ challengeRequestMessage, community, getChallengeAnswers }) => {
|
|
222
444
|
if (!challengeRequestMessage) {
|
|
223
445
|
throw Error(`getChallengeVerification invalid challengeRequestMessage argument '${challengeRequestMessage}'`);
|
|
224
446
|
}
|
|
@@ -230,13 +452,21 @@ const getChallengeVerification = async (challengeRequestMessage, community, getC
|
|
|
230
452
|
}
|
|
231
453
|
if (!Array.isArray(community.settings?.challenges))
|
|
232
454
|
throw Error("community.settings?.challenges is not defined");
|
|
233
|
-
const res = await getPendingChallengesOrChallengeVerification(challengeRequestMessage, community);
|
|
455
|
+
const res = await getPendingChallengesOrChallengeVerification({ challengeRequestMessage, community });
|
|
234
456
|
let pendingApprovalSuccess = "pendingApprovalSuccess" in res ? res.pendingApprovalSuccess : false;
|
|
235
457
|
let challengeVerification;
|
|
236
458
|
// was able to verify without asking author for challenges
|
|
237
459
|
if ("pendingChallenges" in res) {
|
|
238
460
|
const challengeAnswers = await getChallengeAnswers(res.pendingChallenges.map((challenge) => remeda.omit(challenge, ["index", "verify"])));
|
|
239
|
-
const verificationFromPending = await getChallengeVerificationFromChallengeAnswers(
|
|
461
|
+
const verificationFromPending = await getChallengeVerificationFromChallengeAnswers({
|
|
462
|
+
pendingChallenges: res.pendingChallenges,
|
|
463
|
+
challengeAnswers,
|
|
464
|
+
community,
|
|
465
|
+
challengeRequestMessage,
|
|
466
|
+
deferredChallenges: res.deferredChallenges,
|
|
467
|
+
partialResults: res.partialResults,
|
|
468
|
+
communityChallenges: res.communityChallenges
|
|
469
|
+
});
|
|
240
470
|
if ("pendingApprovalSuccess" in verificationFromPending) {
|
|
241
471
|
pendingApprovalSuccess = pendingApprovalSuccess || verificationFromPending.pendingApprovalSuccess;
|
|
242
472
|
challengeVerification = remeda.omit(verificationFromPending, ["pendingApprovalSuccess"]);
|
|
@@ -264,7 +494,7 @@ const getChallengeVerification = async (challengeRequestMessage, community, getC
|
|
|
264
494
|
return challengeVerification;
|
|
265
495
|
};
|
|
266
496
|
// get the data to be published publicly to community.challenges
|
|
267
|
-
const getCommunityChallengeFromCommunityChallengeSettings = async (communityChallengeSettings, pkc) => {
|
|
497
|
+
const getCommunityChallengeFromCommunityChallengeSettings = async ({ communityChallengeSettings, pkc }) => {
|
|
268
498
|
communityChallengeSettings = CommunityChallengeSettingSchema.parse(communityChallengeSettings);
|
|
269
499
|
// if the challenge is an external file, fetch it and override the communityChallengeSettings values
|
|
270
500
|
let challengeFile = undefined;
|
|
@@ -287,19 +517,21 @@ const getCommunityChallengeFromCommunityChallengeSettings = async (communityChal
|
|
|
287
517
|
}
|
|
288
518
|
// else, the challenge is included with pkc-js or user-defined
|
|
289
519
|
else if (communityChallengeSettings.name) {
|
|
290
|
-
const ChallengeFileFactory = ChallengeFileFactorySchema.parse(resolveChallengeFactoryByName(communityChallengeSettings.name, pkc));
|
|
520
|
+
const ChallengeFileFactory = ChallengeFileFactorySchema.parse(resolveChallengeFactoryByName({ name: communityChallengeSettings.name, pkc }).factory);
|
|
291
521
|
challengeFile = ChallengeFileSchema.parse(ChallengeFileFactory({ challengeSettings: communityChallengeSettings }));
|
|
292
522
|
}
|
|
293
523
|
if (!challengeFile)
|
|
294
524
|
throw Error("Failed to load challenge file");
|
|
295
525
|
const { challenge, type } = challengeFile;
|
|
296
526
|
return {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
527
|
+
communityChallenge: {
|
|
528
|
+
exclude: communityChallengeSettings.exclude,
|
|
529
|
+
description: communityChallengeSettings.description || challengeFile.description,
|
|
530
|
+
challenge,
|
|
531
|
+
type,
|
|
532
|
+
caseInsensitive: challengeFile.caseInsensitive,
|
|
533
|
+
pendingApproval: communityChallengeSettings.pendingApproval
|
|
534
|
+
}
|
|
303
535
|
};
|
|
304
536
|
};
|
|
305
537
|
export { pkcJsChallenges, getPendingChallengesOrChallengeVerification, getChallengeVerificationFromChallengeAnswers, getChallengeVerification, getCommunityChallengeFromCommunityChallengeSettings };
|