@pkcprotocol/pkc-js 0.0.27 → 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.
Files changed (24) hide show
  1. package/README.md +11 -7
  2. package/dist/browser/community/community-client-manager.js +1 -2
  3. package/dist/browser/community/community-client-manager.js.map +1 -1
  4. package/dist/browser/generated-version.d.ts +1 -1
  5. package/dist/browser/generated-version.js +1 -1
  6. package/dist/browser/runtime/node/community/challenges/index.d.ts +32 -5
  7. package/dist/browser/runtime/node/community/challenges/index.js +356 -124
  8. package/dist/browser/runtime/node/community/challenges/index.js.map +1 -1
  9. package/dist/browser/runtime/node/community/db-handler.js +4 -1
  10. package/dist/browser/runtime/node/community/db-handler.js.map +1 -1
  11. package/dist/browser/runtime/node/community/local-community.js +11 -4
  12. package/dist/browser/runtime/node/community/local-community.js.map +1 -1
  13. package/dist/node/community/community-client-manager.js +1 -2
  14. package/dist/node/community/community-client-manager.js.map +1 -1
  15. package/dist/node/generated-version.d.ts +1 -1
  16. package/dist/node/generated-version.js +1 -1
  17. package/dist/node/runtime/node/community/challenges/index.d.ts +32 -5
  18. package/dist/node/runtime/node/community/challenges/index.js +356 -124
  19. package/dist/node/runtime/node/community/challenges/index.js.map +1 -1
  20. package/dist/node/runtime/node/community/db-handler.js +4 -1
  21. package/dist/node/runtime/node/community/db-handler.js.map +1 -1
  22. package/dist/node/runtime/node/community/local-community.js +11 -4
  23. package/dist/node/runtime/node/community/local-community.js.map +1 -1
  24. 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
- const getPendingChallengesOrChallengeVerification = async (challengeRequestMessage, community) => {
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 challengeOrChallengeResults = [];
62
- // interate over all challenges of the community, can be more than 1
63
- for (const i in community.settings.challenges) {
64
- const challengeIndex = Number(i);
65
- const communityChallengeSettings = community.settings.challenges[challengeIndex];
66
- if (!communityChallengeSettings.path && !resolveChallengeFactoryByName(communityChallengeSettings.name, community._pkc))
67
- throw Error("You have to provide either path or a stored pkc-js challenge");
68
- // if the challenge is an external file, fetch it and override the communityChallengeSettings values
69
- let ChallengeFileFactory;
70
- try {
71
- ChallengeFileFactory = ChallengeFileFactorySchema.parse(communityChallengeSettings.path
72
- ? (await import(pathToFileURL(communityChallengeSettings.path).href)).default
73
- : resolveChallengeFactoryByName(communityChallengeSettings.name, community._pkc));
74
- validateChallengeFileFactory(ChallengeFileFactory, challengeIndex, community);
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
- catch (e) {
77
- throw new PKCError("ERR_FAILED_TO_IMPORT_CHALLENGE_FILE_FACTORY", {
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
- const challengeFile = ChallengeFileFactory({ challengeSettings: communityChallengeSettings });
85
- validateChallengeFile(challengeFile, challengeIndex, community);
86
- let challengeOrChallengeResult;
87
- try {
88
- // the getChallenge function could throw
89
- challengeOrChallengeResult = await challengeFile.getChallenge({
90
- challengeSettings: communityChallengeSettings,
91
- challengeRequestMessage,
92
- challengeIndex,
93
- community
94
- });
95
- validateChallengeOrChallengeResult(challengeOrChallengeResult, challengeIndex, community);
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
- catch (e) {
98
- throw new PKCError("ERR_INVALID_RESULT_FROM_GET_CHALLENGE_FUNCTION", {
99
- communityChallengeSettings,
100
- challengeName: communityChallengeSettings.name || communityChallengeSettings.path,
101
- challengeRequestMessage,
102
- challengeIndex: challengeIndex + 1,
103
- error: e
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
- challengeOrChallengeResults.push(challengeOrChallengeResult);
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
- // check failures and errors
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 (const i in challengeOrChallengeResults) {
114
- const challengeIndex = Number(i);
115
- const challengeOrChallengeResult = challengeOrChallengeResults[challengeIndex];
116
- const communityChallengeSettings = community.settings.challenges[challengeIndex];
117
- const communityChallenge = await getCommunityChallengeFromCommunityChallengeSettings(communityChallengeSettings, community._pkc);
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[challengeIndex] = challengeOrChallengeResult.error;
274
+ challengeErrors[i] = r.error;
132
275
  }
133
- else if ("success" in challengeOrChallengeResult && challengeOrChallengeResult.success === true) {
134
- if (community.challenges?.[challengeIndex]?.pendingApproval) {
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
- // index is needed to exlude based on other challenge success in getChallengeVerification
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
- // if there are any failures, success is false and pending challenges are ignored
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
- // create return value
155
- if (challengeSuccess === true) {
293
+ if (challengeSuccess === true)
156
294
  return { challengeSuccess, pendingApprovalSuccess };
157
- }
158
- else if (challengeSuccess === false) {
159
- return {
160
- challengeSuccess,
161
- challengeErrors
162
- };
163
- }
164
- else {
165
- return { pendingChallenges, pendingApprovalSuccess };
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
- const challengeResult = challengeResultsWithPendingIndexes[Number(i)];
177
- validateChallengeResult(challengeResult, pendingChallenges[Number(i)].index, community);
316
+ validateChallengeResult({
317
+ challengeResult: challengeResultsWithPendingIndexes[Number(i)],
318
+ challengeIndex: pendingChallenges[Number(i)].index,
319
+ community
320
+ });
178
321
  }
179
- // when filtering only pending challenges, the original indexes get lost so restore them
180
- const challengeResults = [];
181
- const challengeResultToPendingChallenge = [];
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
- challengeResults[pendingChallenges[i].index] = challengeResultsWithPendingIndexes[i];
184
- challengeResultToPendingChallenge[pendingChallenges[i].index] = pendingChallenges[i];
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 in challengeResults) {
190
- const challengeIndex = Number(i);
191
- if (!community.settings?.challenges?.[challengeIndex])
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
- if (challengeResult.success === false) {
418
+ if ("success" in r && r.success === false) {
419
+ if (shouldExcludeChallengeSuccess(loadedCommunityChallenges[i], i, results))
420
+ continue;
203
421
  challengeFailureCount++;
204
- challengeErrors[challengeIndex] = challengeResult.error;
422
+ challengeErrors[i] = r.error;
205
423
  }
206
- else if (challengeResult.success === true && community.settings.challenges[challengeIndex]?.pendingApproval) {
207
- pendingApprovalSuccess = true;
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(res.pendingChallenges, challengeAnswers, community);
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
- exclude: communityChallengeSettings.exclude,
298
- description: communityChallengeSettings.description || challengeFile.description,
299
- challenge,
300
- type,
301
- caseInsensitive: challengeFile.caseInsensitive,
302
- pendingApproval: communityChallengeSettings.pendingApproval
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 };