@wasm-oj/organizer 0.2.0 → 0.2.1

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/dist/index.js CHANGED
@@ -1,9 +1,1919 @@
1
1
  import { constants } from "node:fs";
2
- import { lstat, mkdir, open, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
2
+ import { lstat, mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
+ import { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, BROWSER_PROBLEM_SCHEMA as BROWSER_PROBLEM_SCHEMA$1, assertJudgeDataMatchesPracticePublic, canonicalJsonBytes, deriveContestPublic, deriveJudgeData, derivePracticePublic, encodeJudgePackage, parseProblemCollectionIndex, parseStandaloneProblemBundle, parseStandaloneProblemBundle as parseStandaloneProblemBundle$1, problemCollectionRevision, validateJudgePackage, verifyProblemBundleBytes, verifyProblemCollectionRevision } from "@wasm-oj/core";
6
+ import { isBuiltinLanguage } from "@wasm-oj/contracts";
5
7
  import os from "node:os";
6
- import { BROWSER_COLLECTION_SCHEMA, BROWSER_COLLECTION_SCHEMA as BROWSER_COLLECTION_SCHEMA$1, BROWSER_PROBLEM_SCHEMA, BROWSER_PROBLEM_SCHEMA as BROWSER_PROBLEM_SCHEMA$1, MANAGED_COLLECTION_SCHEMA, MANAGED_COLLECTION_SCHEMA as MANAGED_COLLECTION_SCHEMA$1, assertJudgeDataMatchesPracticePublic, canonicalJsonBytes, contestPublicProjectionBytes, deriveJudgeData, derivePracticePublic, encodeJudgePackage, parseManagedCollectionContract, parseManagedCollectionSource, parseManagedCollectionSource as parseManagedCollectionSource$1, parseManagedCollectionV2, parseManagedCollectionV2 as parseManagedCollectionV2$1, parseManagedCollectionValueV2, parseProblemCollectionIndex, parseProblemCollectionIndex as parseProblemCollectionIndex$1, parseStandaloneProblemBundle, parseStandaloneProblemBundle as parseStandaloneProblemBundle$1, problemCollectionRevision, problemCollectionRevision as problemCollectionRevision$1, validateJudgePackage, verifyProblemBundleBytes, verifyProblemBundleBytes as verifyProblemBundleBytes$1, verifyProblemCollectionRevision, verifyProblemCollectionRevision as verifyProblemCollectionRevision$1 } from "@wasm-oj/core";
8
+ //#region src/core/canonical-json.ts
9
+ var encoder = new TextEncoder();
10
+ function canonicalValue(value, path, ancestors) {
11
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
12
+ if (typeof value === "number") {
13
+ if (!Number.isSafeInteger(value)) throw new TypeError(`${path} must be a safe integer.`);
14
+ return value;
15
+ }
16
+ if (typeof value !== "object" || value === void 0) throw new TypeError(`${path} is not canonical JSON data.`);
17
+ if (ancestors.has(value)) throw new TypeError(`${path} contains a cycle.`);
18
+ const nextAncestors = new Set(ancestors).add(value);
19
+ if (Array.isArray(value)) return value.map((item, index) => canonicalValue(item, `${path}[${index}]`, nextAncestors));
20
+ const prototype = Object.getPrototypeOf(value);
21
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`${path} must be a plain object.`);
22
+ const record = value;
23
+ return Object.fromEntries(Object.keys(record).sort().map((key) => [key, canonicalValue(record[key], `${path}.${key}`, nextAncestors)]));
24
+ }
25
+ /** WASM-OJ canonical JSON: sorted object keys, safe integers, UTF-8, and one trailing newline. */
26
+ function canonicalJsonBytes$1(value) {
27
+ return encoder.encode(`${JSON.stringify(canonicalValue(value, "$", /* @__PURE__ */ new Set()))}\n`);
28
+ }
29
+ //#endregion
30
+ //#region src/online-judge/contest-public.ts
31
+ var CONTEST_PUBLIC_PROJECTION_SCHEMA = "wasm-oj-platform/contest-public-problem-projection/v1";
32
+ /** The single deterministic hidden-data redaction used by author CI and platform validation. */
33
+ function deriveContestPublic$1(practice) {
34
+ if (practice.judgeCases.some((testCase) => testCase.kind !== "sample")) throw new TypeError("Practice-public input contains non-sample judge data.");
35
+ return {
36
+ ...structuredClone(practice),
37
+ editorial: {
38
+ "zh-TW": "",
39
+ en: ""
40
+ },
41
+ judgeCases: practice.judgeCases.filter((testCase) => testCase.kind === "sample").map((testCase) => structuredClone(testCase))
42
+ };
43
+ }
44
+ function createContestPublicProjection(practice) {
45
+ return {
46
+ schema: CONTEST_PUBLIC_PROJECTION_SCHEMA,
47
+ problem: deriveContestPublic$1(practice)
48
+ };
49
+ }
50
+ function contestPublicProjectionBytes(practice) {
51
+ return canonicalJsonBytes$1(createContestPublicProjection(practice));
52
+ }
53
+ //#endregion
54
+ //#region src/online-judge/contracts.ts
55
+ var SUBMISSION_SOURCE_LIMITS = Object.freeze({
56
+ totalBytes: 1048576,
57
+ maximumFiles: 128,
58
+ fileBytes: 262144
59
+ });
60
+ var SUBMISSION_VERDICTS = [
61
+ "accepted",
62
+ "wrong-answer",
63
+ "runtime-error",
64
+ "instruction-limit",
65
+ "memory-limit",
66
+ "output-limit",
67
+ "filesystem-limit",
68
+ "logical-time-limit",
69
+ "wall-time-limit",
70
+ "compile-error",
71
+ "judge-error",
72
+ "cancelled"
73
+ ];
74
+ var MAX_PROMPT_BYTES = 16384;
75
+ var SHA256$2 = /^[0-9a-f]{64}$/;
76
+ var SLUG$2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
77
+ var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
78
+ var SOURCE_PATH$1 = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000-\u001f\u007f]+$/;
79
+ var MAX_DURATION_SECONDS = 31622400;
80
+ var MAX_RULE_INTEGER = 1e9;
81
+ var PENALIZED_ICPC_VERDICTS = /* @__PURE__ */ new Set([
82
+ "wrong-answer",
83
+ "runtime-error",
84
+ "instruction-limit",
85
+ "memory-limit",
86
+ "output-limit",
87
+ "filesystem-limit",
88
+ "logical-time-limit",
89
+ "wall-time-limit",
90
+ "compile-error"
91
+ ]);
92
+ function record$4(value, label) {
93
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
94
+ return value;
95
+ }
96
+ function exact$3(value, keys, label) {
97
+ const actual = Object.keys(value).sort();
98
+ const expected = [...keys].sort();
99
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) throw new TypeError(`${label} has an invalid shape.`);
100
+ }
101
+ function integer(value, label, minimum, maximum = MAX_RULE_INTEGER) {
102
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be an integer between ${minimum} and ${maximum}.`);
103
+ return value;
104
+ }
105
+ function finiteNumber(value, label, minimum, maximum = MAX_RULE_INTEGER) {
106
+ if (typeof value !== "number" || !Number.isFinite(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be a finite number between ${minimum} and ${maximum}.`);
107
+ return value;
108
+ }
109
+ function contestSlug(value, label) {
110
+ if (typeof value !== "string" || value.length > 128 || !SLUG$2.test(value)) throw new TypeError(`${label} is invalid.`);
111
+ return value;
112
+ }
113
+ function timestamp(value, label) {
114
+ if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value) || Number.isNaN(Date.parse(value))) throw new TypeError(`${label} must be an RFC 3339 UTC timestamp.`);
115
+ return value;
116
+ }
117
+ function parseClock(value, label) {
118
+ const input = record$4(value, label);
119
+ if (input.kind === "global") {
120
+ exact$3(input, [
121
+ "durationSeconds",
122
+ "kind",
123
+ "registrationClosesAt",
124
+ "registrationOpensAt",
125
+ "startsAt"
126
+ ], label);
127
+ const registrationOpensAt = timestamp(input.registrationOpensAt, `${label}.registrationOpensAt`);
128
+ const registrationClosesAt = timestamp(input.registrationClosesAt, `${label}.registrationClosesAt`);
129
+ const startsAt = timestamp(input.startsAt, `${label}.startsAt`);
130
+ const durationSeconds = integer(input.durationSeconds, `${label}.durationSeconds`, 1, MAX_DURATION_SECONDS);
131
+ const opensMs = Date.parse(registrationOpensAt);
132
+ const closesMs = Date.parse(registrationClosesAt);
133
+ const startsMs = Date.parse(startsAt);
134
+ if (opensMs >= closesMs || opensMs > startsMs || closesMs > startsMs + durationSeconds * 1e3) throw new TypeError(`${label} registration window must open no later than start and close before the contest ends.`);
135
+ return {
136
+ kind: "global",
137
+ registrationOpensAt,
138
+ registrationClosesAt,
139
+ startsAt,
140
+ durationSeconds
141
+ };
142
+ }
143
+ if (input.kind === "individual") {
144
+ exact$3(input, [
145
+ "durationSeconds",
146
+ "enrollmentClosesAt",
147
+ "enrollmentOpensAt",
148
+ "kind"
149
+ ], label);
150
+ const enrollmentOpensAt = timestamp(input.enrollmentOpensAt, `${label}.enrollmentOpensAt`);
151
+ const enrollmentClosesAt = timestamp(input.enrollmentClosesAt, `${label}.enrollmentClosesAt`);
152
+ if (Date.parse(enrollmentOpensAt) >= Date.parse(enrollmentClosesAt)) throw new TypeError(`${label} enrollment window is invalid.`);
153
+ return {
154
+ kind: "individual",
155
+ enrollmentOpensAt,
156
+ enrollmentClosesAt,
157
+ durationSeconds: integer(input.durationSeconds, `${label}.durationSeconds`, 1, MAX_DURATION_SECONDS)
158
+ };
159
+ }
160
+ throw new TypeError(`${label}.kind is invalid.`);
161
+ }
162
+ function parseCompilerPin(value, label) {
163
+ const input = record$4(value, label);
164
+ exact$3(input, ["configDigest", "configId"], label);
165
+ if (typeof input.configId !== "string" || !IDENTIFIER.test(input.configId)) throw new TypeError(`${label}.configId is invalid.`);
166
+ if (typeof input.configDigest !== "string" || !SHA256$2.test(input.configDigest)) throw new TypeError(`${label}.configDigest must be a lowercase SHA-256 digest.`);
167
+ return {
168
+ configId: input.configId,
169
+ configDigest: input.configDigest
170
+ };
171
+ }
172
+ function parsePromptLimits(value, label) {
173
+ const input = record$4(value, label);
174
+ exact$3(input, [
175
+ "generatedSourceBytes",
176
+ "inputTokens",
177
+ "outputTokens",
178
+ "promptBytes",
179
+ "timeoutSeconds"
180
+ ], label);
181
+ return {
182
+ promptBytes: integer(input.promptBytes, `${label}.promptBytes`, 1, MAX_PROMPT_BYTES),
183
+ inputTokens: integer(input.inputTokens, `${label}.inputTokens`, 1, 1e6),
184
+ outputTokens: integer(input.outputTokens, `${label}.outputTokens`, 1, 1e6),
185
+ generatedSourceBytes: integer(input.generatedSourceBytes, `${label}.generatedSourceBytes`, 1, SUBMISSION_SOURCE_LIMITS.totalBytes),
186
+ timeoutSeconds: integer(input.timeoutSeconds, `${label}.timeoutSeconds`, 1, 3600)
187
+ };
188
+ }
189
+ function parseOfficialTrack(value, label) {
190
+ const input = record$4(value, label);
191
+ if (input.kind === "code") {
192
+ exact$3(input, ["aiAssist", "kind"], label);
193
+ if (input.aiAssist !== "allowed" && input.aiAssist !== "disabled") throw new TypeError(`${label}.aiAssist is invalid.`);
194
+ return {
195
+ kind: "code",
196
+ aiAssist: input.aiAssist
197
+ };
198
+ }
199
+ if (input.kind === "prompt-program") {
200
+ exact$3(input, [
201
+ "attemptPolicy",
202
+ "compiler",
203
+ "disclosure",
204
+ "kind",
205
+ "limits"
206
+ ], label);
207
+ const attemptPolicy = record$4(input.attemptPolicy, `${label}.attemptPolicy`);
208
+ exact$3(attemptPolicy, ["consumeOn", "terminalInfrastructureFailure"], `${label}.attemptPolicy`);
209
+ if (attemptPolicy.consumeOn !== "model-response-received" || attemptPolicy.terminalInfrastructureFailure !== "release-reservation") throw new TypeError(`${label}.attemptPolicy is unsupported.`);
210
+ if (input.disclosure !== "private" && input.disclosure !== "best-after-end") throw new TypeError(`${label}.disclosure is invalid.`);
211
+ return {
212
+ kind: "prompt-program",
213
+ compiler: parseCompilerPin(input.compiler, `${label}.compiler`),
214
+ limits: parsePromptLimits(input.limits, `${label}.limits`),
215
+ attemptPolicy: {
216
+ consumeOn: "model-response-received",
217
+ terminalInfrastructureFailure: "release-reservation"
218
+ },
219
+ disclosure: input.disclosure
220
+ };
221
+ }
222
+ throw new TypeError(`${label}.kind is invalid.`);
223
+ }
224
+ function parseOutputProfile(value, label) {
225
+ const input = record$4(value, label);
226
+ exact$3(input, [
227
+ "entry",
228
+ "language",
229
+ "optimization",
230
+ "target"
231
+ ], label);
232
+ if (typeof input.language !== "string" || !isBuiltinLanguage(input.language)) throw new TypeError(`${label}.language is unsupported.`);
233
+ if (input.target !== "wasip1" && input.target !== "wasix") throw new TypeError(`${label}.target is unsupported.`);
234
+ if (input.optimization !== "debug" && input.optimization !== "release") throw new TypeError(`${label}.optimization is unsupported.`);
235
+ if (typeof input.entry !== "string" || input.entry.length > 512 || !SOURCE_PATH$1.test(input.entry)) throw new TypeError(`${label}.entry is invalid.`);
236
+ return {
237
+ language: input.language,
238
+ target: input.target,
239
+ optimization: input.optimization,
240
+ entry: input.entry
241
+ };
242
+ }
243
+ function parseProblems(value, track, durationSeconds, label) {
244
+ if (!Array.isArray(value) || value.length < 1 || value.length > 100) throw new TypeError(`${label} must contain between 1 and 100 problems.`);
245
+ const slugs = /* @__PURE__ */ new Set();
246
+ const batchCounts = /* @__PURE__ */ new Map();
247
+ const batchReleases = /* @__PURE__ */ new Map();
248
+ return value.map((candidate, index) => {
249
+ const problemLabel = `${label}[${index}]`;
250
+ const input = record$4(candidate, problemLabel);
251
+ exact$3(input, track.kind === "code" ? [
252
+ "attemptLimit",
253
+ "batch",
254
+ "points",
255
+ "releaseAfterSeconds",
256
+ "slug",
257
+ "submissionClosesAfterSeconds"
258
+ ] : [
259
+ "attemptLimit",
260
+ "batch",
261
+ "output",
262
+ "points",
263
+ "releaseAfterSeconds",
264
+ "slug",
265
+ "submissionClosesAfterSeconds"
266
+ ], problemLabel);
267
+ const slug = contestSlug(input.slug, `${problemLabel}.slug`);
268
+ if (slugs.has(slug)) throw new TypeError(`${label} contains duplicate problem '${slug}'.`);
269
+ slugs.add(slug);
270
+ const batch = integer(input.batch, `${problemLabel}.batch`, 1, 100);
271
+ const releaseAfterSeconds = integer(input.releaseAfterSeconds, `${problemLabel}.releaseAfterSeconds`, 0, durationSeconds);
272
+ const submissionClosesAfterSeconds = integer(input.submissionClosesAfterSeconds, `${problemLabel}.submissionClosesAfterSeconds`, 0, durationSeconds);
273
+ if (submissionClosesAfterSeconds <= releaseAfterSeconds) throw new TypeError(`${problemLabel} submission window is empty.`);
274
+ const batchCount = (batchCounts.get(batch) ?? 0) + 1;
275
+ if (batchCount > 8) throw new TypeError(`${label} batch ${batch} exceeds 8 problems.`);
276
+ batchCounts.set(batch, batchCount);
277
+ const existingRelease = batchReleases.get(batch);
278
+ if (existingRelease !== void 0 && existingRelease !== releaseAfterSeconds) throw new TypeError(`${label} batch ${batch} has inconsistent release offsets.`);
279
+ batchReleases.set(batch, releaseAfterSeconds);
280
+ const base = {
281
+ slug,
282
+ batch,
283
+ releaseAfterSeconds,
284
+ submissionClosesAfterSeconds,
285
+ points: finiteNumber(input.points, `${problemLabel}.points`, Number.EPSILON),
286
+ attemptLimit: integer(input.attemptLimit, `${problemLabel}.attemptLimit`, 1, 1e6)
287
+ };
288
+ if (track.kind === "code") return base;
289
+ return {
290
+ ...base,
291
+ output: parseOutputProfile(input.output, `${problemLabel}.output`)
292
+ };
293
+ });
294
+ }
295
+ function uniqueEnum(value, allowed, label) {
296
+ if (!Array.isArray(value)) throw new TypeError(`${label} must be an array.`);
297
+ const result = value.map((candidate, index) => {
298
+ if (typeof candidate !== "string" || !allowed.has(candidate)) throw new TypeError(`${label}[${index}] is invalid.`);
299
+ return candidate;
300
+ });
301
+ if (new Set(result).size !== result.length) throw new TypeError(`${label} contains a duplicate.`);
302
+ return result;
303
+ }
304
+ var SCORE_TIE_BREAKS = /* @__PURE__ */ new Set([
305
+ "fully-passed-cases",
306
+ "deterministic-cost",
307
+ "peak-memory",
308
+ "final-best-achieved-at"
309
+ ]);
310
+ var ICPC_TIE_BREAKS = /* @__PURE__ */ new Set([
311
+ "last-solve-at",
312
+ "deterministic-cost",
313
+ "peak-memory"
314
+ ]);
315
+ var PROGRESS_TIE_BREAKS = /* @__PURE__ */ new Set([
316
+ "fully-passed-cases",
317
+ "deterministic-cost",
318
+ "peak-memory",
319
+ "final-best-achieved-at"
320
+ ]);
321
+ function parseScoring$1(value, label) {
322
+ const input = record$4(value, label);
323
+ if (input.kind === "score") {
324
+ exact$3(input, ["kind", "tieBreaks"], label);
325
+ return {
326
+ kind: "score",
327
+ tieBreaks: uniqueEnum(input.tieBreaks, SCORE_TIE_BREAKS, `${label}.tieBreaks`)
328
+ };
329
+ }
330
+ if (input.kind === "icpc") {
331
+ exact$3(input, [
332
+ "kind",
333
+ "penalizedVerdicts",
334
+ "tieBreaks",
335
+ "wrongAttemptPenaltyMinutes"
336
+ ], label);
337
+ const penalizedVerdicts = uniqueEnum(input.penalizedVerdicts, new Set(SUBMISSION_VERDICTS), `${label}.penalizedVerdicts`);
338
+ if (penalizedVerdicts.some((verdict) => !PENALIZED_ICPC_VERDICTS.has(verdict))) throw new TypeError(`${label}.penalizedVerdicts may contain only contestant-fault verdicts.`);
339
+ return {
340
+ kind: "icpc",
341
+ wrongAttemptPenaltyMinutes: integer(input.wrongAttemptPenaltyMinutes, `${label}.wrongAttemptPenaltyMinutes`, 0, 10080),
342
+ penalizedVerdicts,
343
+ tieBreaks: uniqueEnum(input.tieBreaks, ICPC_TIE_BREAKS, `${label}.tieBreaks`)
344
+ };
345
+ }
346
+ if (input.kind === "progress") {
347
+ exact$3(input, ["kind", "tieBreaks"], label);
348
+ return {
349
+ kind: "progress",
350
+ tieBreaks: uniqueEnum(input.tieBreaks, PROGRESS_TIE_BREAKS, `${label}.tieBreaks`)
351
+ };
352
+ }
353
+ throw new TypeError(`${label}.kind is invalid.`);
354
+ }
355
+ function parseLeaderboard(value, clock, label) {
356
+ const input = record$4(value, label);
357
+ if (input.kind === "live" || input.kind === "hidden-until-end") {
358
+ exact$3(input, ["kind"], label);
359
+ return { kind: input.kind };
360
+ }
361
+ if (input.kind === "freeze") {
362
+ exact$3(input, ["atSeconds", "kind"], label);
363
+ if (clock.kind !== "global") throw new TypeError(`${label} freeze is only valid for a global clock.`);
364
+ return {
365
+ kind: "freeze",
366
+ atSeconds: integer(input.atSeconds, `${label}.atSeconds`, 1, clock.durationSeconds - 1)
367
+ };
368
+ }
369
+ throw new TypeError(`${label}.kind is invalid.`);
370
+ }
371
+ function scopedProblems(scope, atSeconds, problems) {
372
+ if (scope.kind === "all-released") return problems.filter((problem) => problem.releaseAfterSeconds <= atSeconds);
373
+ if (scope.kind === "batch") return problems.filter((problem) => problem.batch === scope.batch);
374
+ const requested = new Set(scope.slugs);
375
+ return problems.filter((problem) => requested.has(problem.slug));
376
+ }
377
+ function parseCheckpoint(value, label) {
378
+ const input = record$4(value, label);
379
+ exact$3(input, [
380
+ "atSeconds",
381
+ "id",
382
+ "ranking",
383
+ "scope",
384
+ "settlement",
385
+ "threshold"
386
+ ], label);
387
+ const scopeInput = record$4(input.scope, `${label}.scope`);
388
+ let scope;
389
+ if (scopeInput.kind === "all-released") {
390
+ exact$3(scopeInput, ["kind"], `${label}.scope`);
391
+ scope = { kind: "all-released" };
392
+ } else if (scopeInput.kind === "batch") {
393
+ exact$3(scopeInput, ["batch", "kind"], `${label}.scope`);
394
+ scope = {
395
+ kind: "batch",
396
+ batch: integer(scopeInput.batch, `${label}.scope.batch`, 1, 100)
397
+ };
398
+ } else if (scopeInput.kind === "problems") {
399
+ exact$3(scopeInput, ["kind", "slugs"], `${label}.scope`);
400
+ if (!Array.isArray(scopeInput.slugs) || scopeInput.slugs.length < 1 || scopeInput.slugs.length > 100) throw new TypeError(`${label}.scope.slugs is invalid.`);
401
+ const slugs = scopeInput.slugs.map((slug, index) => contestSlug(slug, `${label}.scope.slugs[${index}]`));
402
+ if (new Set(slugs).size !== slugs.length) throw new TypeError(`${label}.scope.slugs contains a duplicate.`);
403
+ scope = {
404
+ kind: "problems",
405
+ slugs
406
+ };
407
+ } else throw new TypeError(`${label}.scope.kind is invalid.`);
408
+ const thresholdInput = record$4(input.threshold, `${label}.threshold`);
409
+ exact$3(thresholdInput, ["minimumScore", "minimumSolved"], `${label}.threshold`);
410
+ const minimumSolved = thresholdInput.minimumSolved === null ? null : integer(thresholdInput.minimumSolved, `${label}.threshold.minimumSolved`, 0, 100);
411
+ const minimumScore = thresholdInput.minimumScore === null ? null : finiteNumber(thresholdInput.minimumScore, `${label}.threshold.minimumScore`, 0);
412
+ if (minimumSolved === null && minimumScore === null) throw new TypeError(`${label}.threshold must declare at least one minimum.`);
413
+ let ranking;
414
+ if (input.ranking === null) ranking = null;
415
+ else {
416
+ const rankInput = record$4(input.ranking, `${label}.ranking`);
417
+ if (rankInput.kind === "top-k") {
418
+ exact$3(rankInput, ["count", "kind"], `${label}.ranking`);
419
+ ranking = {
420
+ kind: "top-k",
421
+ count: integer(rankInput.count, `${label}.ranking.count`, 1, MAX_RULE_INTEGER)
422
+ };
423
+ } else if (rankInput.kind === "top-percent") {
424
+ exact$3(rankInput, ["kind", "percent"], `${label}.ranking`);
425
+ ranking = {
426
+ kind: "top-percent",
427
+ percent: finiteNumber(rankInput.percent, `${label}.ranking.percent`, Number.EPSILON, 100)
428
+ };
429
+ } else throw new TypeError(`${label}.ranking.kind is invalid.`);
430
+ }
431
+ if (input.settlement !== "provisional" && input.settlement !== "pause-until-terminal") throw new TypeError(`${label}.settlement is invalid.`);
432
+ return {
433
+ id: contestSlug(input.id, `${label}.id`),
434
+ atSeconds: integer(input.atSeconds, `${label}.atSeconds`, 0, MAX_DURATION_SECONDS),
435
+ scope,
436
+ threshold: {
437
+ minimumSolved,
438
+ minimumScore
439
+ },
440
+ ranking,
441
+ settlement: input.settlement
442
+ };
443
+ }
444
+ function parsePresetProblemSlugs(value, label) {
445
+ if (!Array.isArray(value) || value.length < 1 || value.length > 100) throw new TypeError(`${label} must contain between 1 and 100 problem slugs.`);
446
+ const slugs = value.map((candidate, index) => contestSlug(candidate, `${label}[${index}]`));
447
+ if (new Set(slugs).size !== slugs.length) throw new TypeError(`${label} contains a duplicate.`);
448
+ return slugs;
449
+ }
450
+ function parseAiAssist(value, label) {
451
+ if (value !== "allowed" && value !== "disabled") throw new TypeError(`${label} is invalid.`);
452
+ return value;
453
+ }
454
+ function parsePresetLeaderboard(value, clockValue, label) {
455
+ return parseLeaderboard(value, parseClock(clockValue, `${label} clock`), label);
456
+ }
457
+ function parseContestRules(value, label = "contest rules") {
458
+ const input = record$4(value, label);
459
+ exact$3(input, [
460
+ "checkpoints",
461
+ "clock",
462
+ "evidenceAt",
463
+ "leaderboard",
464
+ "officialTrack",
465
+ "problems",
466
+ "scoring"
467
+ ], label);
468
+ const clock = parseClock(input.clock, `${label}.clock`);
469
+ const officialTrack = parseOfficialTrack(input.officialTrack, `${label}.officialTrack`);
470
+ if (input.evidenceAt !== "input-admitted" && input.evidenceAt !== "generated-source-ready" && input.evidenceAt !== "judge-terminal") throw new TypeError(`${label}.evidenceAt is invalid.`);
471
+ if (input.evidenceAt === "generated-source-ready" && officialTrack.kind !== "prompt-program") throw new TypeError(`${label}.evidenceAt generated-source-ready requires the prompt-program track.`);
472
+ const evidenceAt = input.evidenceAt;
473
+ const problems = parseProblems(input.problems, officialTrack, clock.durationSeconds, `${label}.problems`);
474
+ const scoring = parseScoring$1(input.scoring, `${label}.scoring`);
475
+ if (!Array.isArray(input.checkpoints) || input.checkpoints.length > 100) throw new TypeError(`${label}.checkpoints is invalid.`);
476
+ const checkpoints = input.checkpoints.map((checkpoint, index) => parseCheckpoint(checkpoint, `${label}.checkpoints[${index}]`));
477
+ if (scoring.kind === "progress" && checkpoints.length < 1) throw new TypeError(`${label}.scoring progress requires at least one checkpoint.`);
478
+ const checkpointIds = /* @__PURE__ */ new Set();
479
+ let priorCheckpointAt = -1;
480
+ for (const checkpoint of checkpoints) {
481
+ if (checkpointIds.has(checkpoint.id)) throw new TypeError(`${label}.checkpoints contains duplicate id '${checkpoint.id}'.`);
482
+ checkpointIds.add(checkpoint.id);
483
+ if (checkpoint.atSeconds <= priorCheckpointAt || checkpoint.atSeconds > clock.durationSeconds) throw new TypeError(`${label}.checkpoints must have strictly increasing offsets within the contest duration.`);
484
+ priorCheckpointAt = checkpoint.atSeconds;
485
+ if (clock.kind === "individual" && (checkpoint.ranking !== null || checkpoint.settlement !== "provisional")) throw new TypeError(`${label}.checkpoints for an individual clock must be provisional and cannot rank entrants.`);
486
+ const scoped = scopedProblems(checkpoint.scope, checkpoint.atSeconds, problems);
487
+ if (scoped.length < 1 || scoped.some((problem) => problem.releaseAfterSeconds > checkpoint.atSeconds)) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' scope must contain released problems.`);
488
+ if (checkpoint.scope.kind === "problems" && scoped.length !== checkpoint.scope.slugs.length) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' references an unknown problem.`);
489
+ if (checkpoint.threshold.minimumSolved !== null && checkpoint.threshold.minimumSolved > scoped.length) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' minimumSolved exceeds its scope.`);
490
+ const maximumScore = scoped.reduce((total, problem) => total + problem.points, 0);
491
+ if (checkpoint.threshold.minimumScore !== null && checkpoint.threshold.minimumScore > maximumScore) throw new TypeError(`${label}.checkpoint '${checkpoint.id}' minimumScore exceeds its scope.`);
492
+ }
493
+ const base = {
494
+ clock,
495
+ evidenceAt,
496
+ scoring,
497
+ checkpoints,
498
+ leaderboard: parseLeaderboard(input.leaderboard, clock, `${label}.leaderboard`)
499
+ };
500
+ return officialTrack.kind === "code" ? {
501
+ ...base,
502
+ officialTrack,
503
+ problems
504
+ } : {
505
+ ...base,
506
+ officialTrack,
507
+ problems
508
+ };
509
+ }
510
+ function parseContestRulesPreset(value, label = "contest rules preset") {
511
+ const input = record$4(value, label);
512
+ const preset = input.preset;
513
+ if (preset === "classic-score") {
514
+ exact$3(input, [
515
+ "aiAssist",
516
+ "attemptLimit",
517
+ "clock",
518
+ "leaderboard",
519
+ "pointsPerProblem",
520
+ "preset",
521
+ "problemSlugs"
522
+ ], label);
523
+ return {
524
+ preset,
525
+ clock: parseClock(input.clock, `${label}.clock`),
526
+ problemSlugs: parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`),
527
+ pointsPerProblem: finiteNumber(input.pointsPerProblem, `${label}.pointsPerProblem`, Number.EPSILON),
528
+ attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
529
+ aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
530
+ leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
531
+ };
532
+ }
533
+ if (preset === "icpc") {
534
+ exact$3(input, [
535
+ "aiAssist",
536
+ "attemptLimit",
537
+ "clock",
538
+ "leaderboard",
539
+ "penalizedVerdicts",
540
+ "preset",
541
+ "problemSlugs",
542
+ "wrongAttemptPenaltyMinutes"
543
+ ], label);
544
+ const penalizedVerdicts = uniqueEnum(input.penalizedVerdicts, new Set(SUBMISSION_VERDICTS), `${label}.penalizedVerdicts`);
545
+ if (penalizedVerdicts.some((verdict) => !PENALIZED_ICPC_VERDICTS.has(verdict))) throw new TypeError(`${label}.penalizedVerdicts is invalid.`);
546
+ return {
547
+ preset,
548
+ clock: parseClock(input.clock, `${label}.clock`),
549
+ problemSlugs: parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`),
550
+ attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
551
+ aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
552
+ wrongAttemptPenaltyMinutes: integer(input.wrongAttemptPenaltyMinutes, `${label}.wrongAttemptPenaltyMinutes`, 0, 10080),
553
+ penalizedVerdicts,
554
+ leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
555
+ };
556
+ }
557
+ if (preset === "blitz-batches") {
558
+ exact$3(input, [
559
+ "aiAssist",
560
+ "attemptLimit",
561
+ "batchSize",
562
+ "clock",
563
+ "leaderboard",
564
+ "minimumSolvedPerBatch",
565
+ "pointsPerProblem",
566
+ "preset",
567
+ "problemSlugs",
568
+ "releaseIntervalSeconds"
569
+ ], label);
570
+ const problemSlugs = parsePresetProblemSlugs(input.problemSlugs, `${label}.problemSlugs`);
571
+ const batchSize = integer(input.batchSize, `${label}.batchSize`, 1, 8);
572
+ return {
573
+ preset,
574
+ clock: parseClock(input.clock, `${label}.clock`),
575
+ problemSlugs,
576
+ batchSize,
577
+ releaseIntervalSeconds: integer(input.releaseIntervalSeconds, `${label}.releaseIntervalSeconds`, 1, MAX_DURATION_SECONDS),
578
+ pointsPerProblem: finiteNumber(input.pointsPerProblem, `${label}.pointsPerProblem`, Number.EPSILON),
579
+ attemptLimit: integer(input.attemptLimit, `${label}.attemptLimit`, 1, 1e6),
580
+ minimumSolvedPerBatch: integer(input.minimumSolvedPerBatch, `${label}.minimumSolvedPerBatch`, 0, Math.min(batchSize, problemSlugs.length)),
581
+ aiAssist: parseAiAssist(input.aiAssist, `${label}.aiAssist`),
582
+ leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
583
+ };
584
+ }
585
+ if (preset === "prompt-five-by-three") {
586
+ exact$3(input, [
587
+ "clock",
588
+ "compiler",
589
+ "disclosure",
590
+ "leaderboard",
591
+ "limits",
592
+ "preset",
593
+ "problems"
594
+ ], label);
595
+ if (!Array.isArray(input.problems) || input.problems.length !== 5) throw new TypeError(`${label}.problems must contain exactly five problems.`);
596
+ const problems = input.problems.map((candidate, index) => {
597
+ const problem = record$4(candidate, `${label}.problems[${index}]`);
598
+ exact$3(problem, ["output", "slug"], `${label}.problems[${index}]`);
599
+ return {
600
+ slug: contestSlug(problem.slug, `${label}.problems[${index}].slug`),
601
+ output: parseOutputProfile(problem.output, `${label}.problems[${index}].output`)
602
+ };
603
+ });
604
+ if (new Set(problems.map((problem) => problem.slug)).size !== problems.length) throw new TypeError(`${label}.problems contains a duplicate.`);
605
+ if (input.disclosure !== "private" && input.disclosure !== "best-after-end") throw new TypeError(`${label}.disclosure is invalid.`);
606
+ return {
607
+ preset,
608
+ clock: parseClock(input.clock, `${label}.clock`),
609
+ problems,
610
+ compiler: parseCompilerPin(input.compiler, `${label}.compiler`),
611
+ limits: parsePromptLimits(input.limits, `${label}.limits`),
612
+ disclosure: input.disclosure,
613
+ leaderboard: parsePresetLeaderboard(input.leaderboard, input.clock, `${label}.leaderboard`)
614
+ };
615
+ }
616
+ throw new TypeError(`${label}.preset is invalid.`);
617
+ }
618
+ function expandContestRulesPreset(preset) {
619
+ const close = preset.clock.durationSeconds;
620
+ if (preset.preset === "classic-score") return parseContestRules({
621
+ clock: preset.clock,
622
+ officialTrack: {
623
+ kind: "code",
624
+ aiAssist: preset.aiAssist
625
+ },
626
+ evidenceAt: "input-admitted",
627
+ problems: preset.problemSlugs.map((slug) => ({
628
+ slug,
629
+ batch: 1,
630
+ releaseAfterSeconds: 0,
631
+ submissionClosesAfterSeconds: close,
632
+ points: preset.pointsPerProblem,
633
+ attemptLimit: preset.attemptLimit
634
+ })),
635
+ scoring: {
636
+ kind: "score",
637
+ tieBreaks: [
638
+ "fully-passed-cases",
639
+ "deterministic-cost",
640
+ "peak-memory",
641
+ "final-best-achieved-at"
642
+ ]
643
+ },
644
+ checkpoints: [],
645
+ leaderboard: preset.leaderboard
646
+ });
647
+ if (preset.preset === "icpc") return parseContestRules({
648
+ clock: preset.clock,
649
+ officialTrack: {
650
+ kind: "code",
651
+ aiAssist: preset.aiAssist
652
+ },
653
+ evidenceAt: "judge-terminal",
654
+ problems: preset.problemSlugs.map((slug) => ({
655
+ slug,
656
+ batch: 1,
657
+ releaseAfterSeconds: 0,
658
+ submissionClosesAfterSeconds: close,
659
+ points: 100,
660
+ attemptLimit: preset.attemptLimit
661
+ })),
662
+ scoring: {
663
+ kind: "icpc",
664
+ wrongAttemptPenaltyMinutes: preset.wrongAttemptPenaltyMinutes,
665
+ penalizedVerdicts: preset.penalizedVerdicts,
666
+ tieBreaks: []
667
+ },
668
+ checkpoints: [],
669
+ leaderboard: preset.leaderboard
670
+ });
671
+ if (preset.preset === "blitz-batches") {
672
+ const batchCount = Math.ceil(preset.problemSlugs.length / preset.batchSize);
673
+ const checkpoints = Array.from({ length: Math.max(0, batchCount - 1) }, (_, index) => ({
674
+ id: `batch-${index + 1}`,
675
+ atSeconds: (index + 1) * preset.releaseIntervalSeconds,
676
+ scope: {
677
+ kind: "batch",
678
+ batch: index + 1
679
+ },
680
+ threshold: {
681
+ minimumSolved: preset.minimumSolvedPerBatch,
682
+ minimumScore: null
683
+ },
684
+ ranking: null,
685
+ settlement: "provisional"
686
+ }));
687
+ return parseContestRules({
688
+ clock: preset.clock,
689
+ officialTrack: {
690
+ kind: "code",
691
+ aiAssist: preset.aiAssist
692
+ },
693
+ evidenceAt: "judge-terminal",
694
+ problems: preset.problemSlugs.map((slug, index) => ({
695
+ slug,
696
+ batch: Math.floor(index / preset.batchSize) + 1,
697
+ releaseAfterSeconds: Math.floor(index / preset.batchSize) * preset.releaseIntervalSeconds,
698
+ submissionClosesAfterSeconds: close,
699
+ points: preset.pointsPerProblem,
700
+ attemptLimit: preset.attemptLimit
701
+ })),
702
+ scoring: {
703
+ kind: "progress",
704
+ tieBreaks: [
705
+ "fully-passed-cases",
706
+ "deterministic-cost",
707
+ "final-best-achieved-at"
708
+ ]
709
+ },
710
+ checkpoints,
711
+ leaderboard: preset.leaderboard
712
+ });
713
+ }
714
+ return parseContestRules({
715
+ clock: preset.clock,
716
+ officialTrack: {
717
+ kind: "prompt-program",
718
+ compiler: preset.compiler,
719
+ limits: preset.limits,
720
+ attemptPolicy: {
721
+ consumeOn: "model-response-received",
722
+ terminalInfrastructureFailure: "release-reservation"
723
+ },
724
+ disclosure: preset.disclosure
725
+ },
726
+ evidenceAt: "generated-source-ready",
727
+ problems: preset.problems.map((problem) => ({
728
+ ...problem,
729
+ batch: 1,
730
+ releaseAfterSeconds: 0,
731
+ submissionClosesAfterSeconds: close,
732
+ points: 100,
733
+ attemptLimit: 3
734
+ })),
735
+ scoring: {
736
+ kind: "score",
737
+ tieBreaks: ["deterministic-cost", "final-best-achieved-at"]
738
+ },
739
+ checkpoints: [],
740
+ leaderboard: preset.leaderboard
741
+ });
742
+ }
743
+ function logicalContestSeconds(snapshot, now, durationSeconds) {
744
+ integer(snapshot.generation, "contest clock generation", 1);
745
+ const duration = integer(durationSeconds, "contest durationSeconds", 1, MAX_DURATION_SECONDS);
746
+ const capturedAt = timestamp(snapshot.capturedAt, "contest clock capturedAt");
747
+ const nowTimestamp = timestamp(now, "contest clock now");
748
+ if (snapshot.state !== "running" && snapshot.state !== "paused") throw new TypeError("contest clock state is invalid.");
749
+ if (typeof snapshot.logicalSeconds !== "number" || !Number.isFinite(snapshot.logicalSeconds)) throw new TypeError("contest clock logicalSeconds is invalid.");
750
+ const projected = snapshot.state === "paused" ? snapshot.logicalSeconds : snapshot.logicalSeconds + (Date.parse(nowTimestamp) - Date.parse(capturedAt)) / 1e3;
751
+ return Math.min(duration, Math.max(0, projected));
752
+ }
753
+ function projectContestRules(input) {
754
+ const { rules, clock, entrant } = input;
755
+ const observedAt = timestamp(input.observedAt, "contest projection observedAt");
756
+ const scheduleShiftSeconds = integer(input.scheduleShiftSeconds ?? 0, "contest projection scheduleShiftSeconds", 0);
757
+ const observedMs = Date.parse(observedAt) - scheduleShiftSeconds * 1e3;
758
+ if (clock !== null && (!Number.isSafeInteger(clock.generation) || clock.generation < 1)) throw new TypeError("contest clock generation is invalid.");
759
+ let phase;
760
+ let logicalSeconds = clock === null ? null : logicalContestSeconds(clock, observedAt, rules.clock.durationSeconds);
761
+ if (input.contestEnded === true || entrant?.completed === true) phase = "ended";
762
+ else if (entrant === null || !entrant.joined) {
763
+ const opensAt = rules.clock.kind === "global" ? rules.clock.registrationOpensAt : rules.clock.enrollmentOpensAt;
764
+ const closesAt = rules.clock.kind === "global" ? rules.clock.registrationClosesAt : rules.clock.enrollmentClosesAt;
765
+ if (observedMs >= Date.parse(opensAt) && observedMs < Date.parse(closesAt)) phase = "registration";
766
+ else if (rules.clock.kind === "global" && observedMs >= Date.parse(rules.clock.startsAt) + rules.clock.durationSeconds * 1e3) phase = "ended";
767
+ else phase = "upcoming";
768
+ } else if (entrant.eliminatedAtSeconds !== null) {
769
+ phase = "eliminated";
770
+ logicalSeconds ??= entrant.eliminatedAtSeconds;
771
+ } else if (clock?.state === "paused") phase = "paused";
772
+ else if (rules.clock.kind === "individual" && !entrant.started) phase = "awaiting-start";
773
+ else if (clock === null) phase = "upcoming";
774
+ else if (logicalSeconds !== null && logicalSeconds >= rules.clock.durationSeconds) phase = "ended";
775
+ else phase = "running";
776
+ const attempted = (slug) => {
777
+ const count = input.attemptedByProblem[slug] ?? 0;
778
+ if (!Number.isSafeInteger(count) || count < 0) throw new TypeError(`attempt count for '${slug}' is invalid.`);
779
+ return count;
780
+ };
781
+ const problems = rules.problems.map((problem) => {
782
+ let availability;
783
+ if (entrant === null || !entrant.joined || logicalSeconds === null || logicalSeconds < problem.releaseAfterSeconds) availability = "locked";
784
+ else if (phase === "eliminated" || logicalSeconds >= problem.submissionClosesAfterSeconds || phase === "ended") availability = "closed";
785
+ else availability = "open";
786
+ return {
787
+ slug: problem.slug,
788
+ availability,
789
+ releaseAfterSeconds: problem.releaseAfterSeconds,
790
+ submissionClosesAfterSeconds: problem.submissionClosesAfterSeconds,
791
+ attemptsRemaining: Math.max(0, problem.attemptLimit - attempted(problem.slug))
792
+ };
793
+ });
794
+ let nextBoundarySeconds = null;
795
+ if (logicalSeconds !== null && phase !== "ended" && phase !== "eliminated") {
796
+ const candidates = [
797
+ rules.clock.durationSeconds,
798
+ ...rules.problems.flatMap((problem) => [problem.releaseAfterSeconds, problem.submissionClosesAfterSeconds]),
799
+ ...rules.checkpoints.map((checkpoint) => checkpoint.atSeconds),
800
+ ...rules.leaderboard.kind === "freeze" ? [rules.leaderboard.atSeconds] : []
801
+ ].filter((boundary) => boundary > logicalSeconds);
802
+ nextBoundarySeconds = candidates.length === 0 ? null : Math.min(...candidates);
803
+ }
804
+ return {
805
+ generation: clock?.generation ?? 0,
806
+ phase,
807
+ logicalSeconds,
808
+ nextBoundarySeconds,
809
+ problems
810
+ };
811
+ }
812
+ function decideContestAdmission(input, problemSlug) {
813
+ const problem = input.rules.problems.find((candidate) => candidate.slug === problemSlug);
814
+ if (!problem) return {
815
+ allowed: false,
816
+ reason: "unknown-problem"
817
+ };
818
+ if (input.entrant === null || !input.entrant.joined) return {
819
+ allowed: false,
820
+ reason: "not-joined"
821
+ };
822
+ if (input.rules.clock.kind === "individual" && !input.entrant.started) return {
823
+ allowed: false,
824
+ reason: "not-started"
825
+ };
826
+ if (input.clock?.state === "paused") return {
827
+ allowed: false,
828
+ reason: "paused"
829
+ };
830
+ if (input.entrant.eliminatedAtSeconds !== null) return {
831
+ allowed: false,
832
+ reason: "eliminated"
833
+ };
834
+ const projectedProblem = projectContestRules(input).problems.find((candidate) => candidate.slug === problemSlug);
835
+ if (projectedProblem.availability === "locked") return {
836
+ allowed: false,
837
+ reason: "problem-locked"
838
+ };
839
+ if (projectedProblem.availability === "closed") return {
840
+ allowed: false,
841
+ reason: "problem-closed"
842
+ };
843
+ if (projectedProblem.attemptsRemaining < 1) return {
844
+ allowed: false,
845
+ reason: "attempt-limit"
846
+ };
847
+ return {
848
+ allowed: true,
849
+ problem
850
+ };
851
+ }
852
+ function compareNumber(left, right, direction) {
853
+ return direction === "asc" ? left - right : right - left;
854
+ }
855
+ function compareFactMetric(left, right, tieBreak) {
856
+ switch (tieBreak) {
857
+ case "fully-passed-cases": return compareNumber(left.fullyPassedCases, right.fullyPassedCases, "desc");
858
+ case "deterministic-cost": return compareNumber(left.deterministicCost, right.deterministicCost, "asc");
859
+ case "peak-memory": return compareNumber(left.peakMemoryBytes, right.peakMemoryBytes, "asc");
860
+ case "final-best-achieved-at": return compareNumber(left.logicalSeconds, right.logicalSeconds, "asc");
861
+ }
862
+ }
863
+ function bestScoreFact(facts, tieBreaks) {
864
+ let selected = null;
865
+ for (const fact of facts) {
866
+ if (!fact.eligible) continue;
867
+ if (selected === null) {
868
+ selected = fact;
869
+ continue;
870
+ }
871
+ let comparison = compareNumber(fact.score, selected.score, "desc");
872
+ for (const tieBreak of tieBreaks) {
873
+ if (comparison !== 0) break;
874
+ comparison = compareFactMetric(fact, selected, tieBreak);
875
+ }
876
+ if (comparison < 0 || comparison === 0 && fact.logicalSeconds < selected.logicalSeconds) selected = fact;
877
+ }
878
+ return selected;
879
+ }
880
+ function validateResultFacts(rules, entrantIds, results) {
881
+ if (new Set(entrantIds).size !== entrantIds.length || entrantIds.some((entrantId) => !entrantId)) throw new TypeError("entrantIds must be unique non-empty strings.");
882
+ const entrants = new Set(entrantIds);
883
+ const problems = new Set(rules.problems.map((problem) => problem.slug));
884
+ for (const result of results) {
885
+ if (!entrants.has(result.entrantId)) throw new TypeError(`result references unknown entrant '${result.entrantId}'.`);
886
+ if (!problems.has(result.problemSlug)) throw new TypeError(`result references unknown problem '${result.problemSlug}'.`);
887
+ if (!SUBMISSION_VERDICTS.includes(result.verdict)) throw new TypeError("result verdict is invalid.");
888
+ finiteNumber(result.score, "result score", 0, 100);
889
+ integer(result.fullyPassedCases, "result fullyPassedCases", 0);
890
+ integer(result.deterministicCost, "result deterministicCost", 0);
891
+ integer(result.peakMemoryBytes, "result peakMemoryBytes", 0);
892
+ finiteNumber(result.logicalSeconds, "result logicalSeconds", 0, rules.clock.durationSeconds);
893
+ if (typeof result.eligible !== "boolean") throw new TypeError("result eligible must be boolean.");
894
+ }
895
+ }
896
+ function standingMetric(standing, tieBreak) {
897
+ switch (tieBreak) {
898
+ case "fully-passed-cases": return standing.fullyPassedCases;
899
+ case "deterministic-cost": return standing.deterministicCost;
900
+ case "peak-memory": return standing.peakMemoryBytes;
901
+ case "final-best-achieved-at":
902
+ case "last-solve-at": return standing.achievedAtSeconds;
903
+ }
904
+ }
905
+ function compareStandingMetric(left, right, tieBreak) {
906
+ const direction = tieBreak === "fully-passed-cases" ? "desc" : "asc";
907
+ return compareNumber(standingMetric(left, tieBreak), standingMetric(right, tieBreak), direction);
908
+ }
909
+ function compareCompetitiveStandings(scoring, left, right) {
910
+ if (scoring.kind === "icpc") {
911
+ let comparison = compareNumber(left.solved, right.solved, "desc") || compareNumber(left.penaltyMinutes, right.penaltyMinutes, "asc");
912
+ for (const tieBreak of scoring.tieBreaks) {
913
+ if (comparison !== 0) break;
914
+ comparison = compareStandingMetric(left, right, tieBreak);
915
+ }
916
+ return comparison;
917
+ }
918
+ let comparison = scoring.kind === "progress" ? compareNumber(left.furthestCheckpoint, right.furthestCheckpoint, "desc") || compareNumber(left.solved, right.solved, "desc") || compareNumber(left.score, right.score, "desc") : compareNumber(left.score, right.score, "desc");
919
+ for (const tieBreak of scoring.tieBreaks) {
920
+ if (comparison !== 0) break;
921
+ comparison = compareStandingMetric(left, right, tieBreak);
922
+ }
923
+ return comparison;
924
+ }
925
+ function rankContestResults(rules, entrantIds, results, passedCheckpointCounts) {
926
+ validateResultFacts(rules, entrantIds, results);
927
+ const factsByEntrant = /* @__PURE__ */ new Map();
928
+ for (const entrantId of entrantIds) factsByEntrant.set(entrantId, []);
929
+ for (const result of results) factsByEntrant.get(result.entrantId).push(result);
930
+ const unranked = entrantIds.map((entrantId) => {
931
+ const entrantFacts = factsByEntrant.get(entrantId);
932
+ let solved = 0;
933
+ let score = 0;
934
+ let penaltyMinutes = 0;
935
+ let fullyPassedCases = 0;
936
+ let deterministicCost = 0;
937
+ let peakMemoryBytes = 0;
938
+ let achievedAtSeconds = 0;
939
+ if (rules.scoring.kind === "icpc") {
940
+ const penalized = new Set(rules.scoring.penalizedVerdicts);
941
+ for (const problem of rules.problems) {
942
+ const chronological = entrantFacts.filter((fact) => fact.problemSlug === problem.slug && fact.eligible).sort((left, right) => left.logicalSeconds - right.logicalSeconds);
943
+ let wrongAttempts = 0;
944
+ for (const fact of chronological) {
945
+ if (fact.score === 100) {
946
+ solved += 1;
947
+ score += problem.points;
948
+ penaltyMinutes += Math.floor(fact.logicalSeconds / 60) + wrongAttempts * rules.scoring.wrongAttemptPenaltyMinutes;
949
+ fullyPassedCases += fact.fullyPassedCases;
950
+ deterministicCost += fact.deterministicCost;
951
+ peakMemoryBytes = Math.max(peakMemoryBytes, fact.peakMemoryBytes);
952
+ achievedAtSeconds = Math.max(achievedAtSeconds, fact.logicalSeconds);
953
+ break;
954
+ }
955
+ if (penalized.has(fact.verdict)) wrongAttempts += 1;
956
+ }
957
+ }
958
+ } else {
959
+ const tieBreaks = rules.scoring.tieBreaks;
960
+ for (const problem of rules.problems) {
961
+ const selected = bestScoreFact(entrantFacts.filter((fact) => fact.problemSlug === problem.slug), tieBreaks);
962
+ if (!selected) continue;
963
+ score += problem.points * selected.score / 100;
964
+ if (selected.score === 100) solved += 1;
965
+ fullyPassedCases += selected.fullyPassedCases;
966
+ deterministicCost += selected.deterministicCost;
967
+ peakMemoryBytes = Math.max(peakMemoryBytes, selected.peakMemoryBytes);
968
+ achievedAtSeconds = Math.max(achievedAtSeconds, selected.logicalSeconds);
969
+ }
970
+ }
971
+ const furthestCheckpoint = passedCheckpointCounts?.[entrantId] ?? 0;
972
+ integer(furthestCheckpoint, `passed checkpoint count for '${entrantId}'`, 0, rules.checkpoints.length);
973
+ return {
974
+ entrantId,
975
+ rank: 0,
976
+ solved,
977
+ score,
978
+ penaltyMinutes,
979
+ furthestCheckpoint,
980
+ fullyPassedCases,
981
+ deterministicCost,
982
+ peakMemoryBytes,
983
+ achievedAtSeconds
984
+ };
985
+ });
986
+ unranked.sort((left, right) => compareCompetitiveStandings(rules.scoring, left, right) || left.entrantId.localeCompare(right.entrantId));
987
+ let rank = 1;
988
+ return unranked.map((standing, index) => {
989
+ if (index > 0 && compareCompetitiveStandings(rules.scoring, unranked[index - 1], standing) !== 0) rank = index + 1;
990
+ return {
991
+ ...standing,
992
+ rank
993
+ };
994
+ });
995
+ }
996
+ function evaluateContestCheckpoint(rules, checkpoint, standings, candidates) {
997
+ if (rules.clock.kind === "individual" && checkpoint.ranking !== null) throw new TypeError("individual checkpoints cannot rank entrants.");
998
+ if (checkpoint.settlement === "pause-until-terminal" && candidates.some((candidate) => candidate.pending)) throw new TypeError("pause-until-terminal checkpoint cannot settle while bounded work is pending.");
999
+ const standingByEntrant = new Map(standings.map((standing) => [standing.entrantId, standing]));
1000
+ if (standingByEntrant.size !== standings.length) throw new TypeError("checkpoint standings contain a duplicate entrant.");
1001
+ const candidateIds = /* @__PURE__ */ new Set();
1002
+ for (const candidate of candidates) {
1003
+ if (!candidate.entrantId || candidateIds.has(candidate.entrantId)) throw new TypeError("checkpoint candidates must have unique entrant ids.");
1004
+ candidateIds.add(candidate.entrantId);
1005
+ if (!standingByEntrant.has(candidate.entrantId)) throw new TypeError(`checkpoint candidate '${candidate.entrantId}' has no standing.`);
1006
+ integer(candidate.solved, `checkpoint candidate '${candidate.entrantId}' solved`, 0, 100);
1007
+ finiteNumber(candidate.score, `checkpoint candidate '${candidate.entrantId}' score`, 0);
1008
+ if (typeof candidate.pending !== "boolean") throw new TypeError("checkpoint candidate pending must be boolean.");
1009
+ }
1010
+ const seats = checkpoint.ranking === null ? candidates.length : checkpoint.ranking.kind === "top-k" ? Math.min(candidates.length, checkpoint.ranking.count) : Math.ceil(candidates.length * checkpoint.ranking.percent / 100);
1011
+ return candidates.map((candidate) => {
1012
+ const thresholdPassed = (checkpoint.threshold.minimumSolved === null || candidate.solved >= checkpoint.threshold.minimumSolved) && (checkpoint.threshold.minimumScore === null || candidate.score >= checkpoint.threshold.minimumScore);
1013
+ const rankingPassed = checkpoint.ranking === null || standingByEntrant.get(candidate.entrantId).rank <= seats;
1014
+ const provisional = checkpoint.settlement === "provisional" && candidate.pending;
1015
+ return {
1016
+ entrantId: candidate.entrantId,
1017
+ advances: provisional || thresholdPassed && rankingPassed,
1018
+ provisional
1019
+ };
1020
+ });
1021
+ }
1022
+ Object.freeze({
1023
+ project: projectContestRules,
1024
+ admission: decideContestAdmission,
1025
+ rank: rankContestResults,
1026
+ checkpoint: evaluateContestCheckpoint
1027
+ });
1028
+ //#endregion
1029
+ //#region src/core/resources.ts
1030
+ var MAX_LOGICAL_TIME_LIMIT_MS = Math.floor(Number.MAX_SAFE_INTEGER / 1e6);
1031
+ Object.freeze({
1032
+ instructionBudget: 1e10,
1033
+ logicalTimeLimitMs: 6e4,
1034
+ memoryLimitBytes: 268435456,
1035
+ outputLimitBytes: 4194304,
1036
+ filesystemWriteLimitBytes: 67108864,
1037
+ filesystemEntryLimit: 4096,
1038
+ wallTimeLimitMs: 6e4
1039
+ });
1040
+ //#endregion
1041
+ //#region src/judge/problem-model.ts
1042
+ var PROBLEM_LOCALES = ["zh-TW", "en"];
1043
+ //#endregion
1044
+ //#region src/judge/problem-catalog-loader.ts
1045
+ var BROWSER_PROBLEM_SCHEMA$2 = "wasm-oj-browser-problem-v4";
1046
+ var PROBLEM_STARTER_LIMITS = Object.freeze({
1047
+ filesPerLanguage: 128,
1048
+ bytesPerFile: 262144,
1049
+ totalBytesPerLanguage: 1048576
1050
+ });
1051
+ Object.freeze({
1052
+ provider: "github",
1053
+ owner: "wasm-oj",
1054
+ repository: "problems",
1055
+ ref: "main",
1056
+ indexPath: "collection/index.json"
1057
+ });
1058
+ var LANGUAGES = [
1059
+ "c",
1060
+ "cpp",
1061
+ "rust",
1062
+ "go",
1063
+ "python",
1064
+ "javascript",
1065
+ "typescript"
1066
+ ];
1067
+ var POLICY_IDS = [
1068
+ "baseline",
1069
+ "efficient",
1070
+ "optimal"
1071
+ ];
1072
+ var CALIBRATION_METHOD = "wasm-oj-v2/compiled-average-optimal-rounded/v1";
1073
+ var ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1074
+ var UTF8_ENCODER = new TextEncoder();
1075
+ var ProblemCollectionError = class extends Error {
1076
+ kind;
1077
+ constructor(message, kind, options) {
1078
+ super(message, options);
1079
+ this.kind = kind;
1080
+ this.name = "ProblemCollectionError";
1081
+ }
1082
+ };
1083
+ function parseStandaloneProblemBundle$2(value) {
1084
+ if (!isRecord(value) || !hasExactKeys(value, ["schema", "problem"]) || value.schema !== "wasm-oj-browser-problem-v4") throw schemaError("The problem bundle uses an unsupported schema.");
1085
+ return parseJudgeProblem(value.problem);
1086
+ }
1087
+ function parseJudgeProblem(value) {
1088
+ if (!isRecord(value) || !hasExactKeys(value, [
1089
+ "id",
1090
+ "number",
1091
+ "title",
1092
+ "trackId",
1093
+ "track",
1094
+ "difficulty",
1095
+ "tags",
1096
+ "statement",
1097
+ "editorial",
1098
+ "starterTemplates",
1099
+ "judgeCases",
1100
+ "scoring",
1101
+ "complexities"
1102
+ ])) throw schemaError("The judge problem has an invalid shape.");
1103
+ if (typeof value.id !== "string" || !ID_PATTERN.test(value.id) || !Number.isSafeInteger(value.number) || value.number < 1) throw schemaError("The judge problem has an invalid identity.");
1104
+ const id = value.id;
1105
+ const title = parseLocalizedText(value.title, `problem '${id}' title`);
1106
+ if (typeof value.trackId !== "string" || !ID_PATTERN.test(value.trackId)) throw schemaError(`Problem '${id}' has an invalid track ID.`);
1107
+ const track = parseLocalizedText(value.track, `problem '${id}' track`);
1108
+ const statement = parseLocalizedText(value.statement, `problem '${id}' statement`, false);
1109
+ const editorial = parseLocalizedText(value.editorial, `problem '${id}' editorial`, false);
1110
+ const starterTemplates = parseStarterTemplates(value.starterTemplates, id);
1111
+ const difficulty = parseDifficulty(value.difficulty, `problem '${id}'`);
1112
+ const tags = parseTags(value.tags, `problem '${id}'`);
1113
+ if (!Array.isArray(value.judgeCases) || value.judgeCases.length < 1 || value.judgeCases.length > 1e4) throw schemaError(`Problem '${value.id}' has an invalid case inventory.`);
1114
+ const caseIds = /* @__PURE__ */ new Set();
1115
+ const judgeCases = value.judgeCases.map((testCase, index) => {
1116
+ if (!isRecord(testCase) || !hasExactKeys(testCase, [
1117
+ "id",
1118
+ "kind",
1119
+ "input",
1120
+ "output"
1121
+ ]) || typeof testCase.id !== "string" || !ID_PATTERN.test(testCase.id) || caseIds.has(testCase.id) || ![
1122
+ "sample",
1123
+ "adversarial",
1124
+ "regression"
1125
+ ].includes(String(testCase.kind)) || typeof testCase.input !== "string" || typeof testCase.output !== "string") throw schemaError(`Problem '${value.id}' has an invalid case at position ${index + 1}.`);
1126
+ caseIds.add(testCase.id);
1127
+ return {
1128
+ id: testCase.id,
1129
+ kind: testCase.kind,
1130
+ input: testCase.input,
1131
+ output: testCase.output
1132
+ };
1133
+ });
1134
+ if (judgeCases.filter((testCase) => testCase.kind === "sample").length !== 3) throw schemaError(`Problem '${value.id}' must contain exactly three sample cases.`);
1135
+ const scoring = parseScoring(value.scoring, id);
1136
+ if (!Array.isArray(value.complexities) || value.complexities.length < 2 || value.complexities.length > 32) throw schemaError(`Problem '${value.id}' has an invalid complexity inventory.`);
1137
+ const complexities = value.complexities.map((complexity, index) => parseComplexity(complexity, id, index));
1138
+ if (!complexities.at(-1)?.accepted) throw schemaError(`Problem '${id}' must end with its accepted complexity.`);
1139
+ return {
1140
+ id,
1141
+ number: value.number,
1142
+ title,
1143
+ trackId: value.trackId,
1144
+ track,
1145
+ difficulty,
1146
+ tags,
1147
+ statement,
1148
+ editorial,
1149
+ starterTemplates,
1150
+ judgeCases,
1151
+ scoring,
1152
+ complexities
1153
+ };
1154
+ }
1155
+ function parseStarterTemplates(value, problemId) {
1156
+ if (!isRecord(value) || !hasExactKeys(value, LANGUAGES)) throw schemaError(`Problem '${problemId}' must provide starter templates for every built-in language.`);
1157
+ return Object.fromEntries(LANGUAGES.map((language) => [language, parseStarterTemplate(value[language], problemId, language)]));
1158
+ }
1159
+ function parseStarterTemplate(value, problemId, language) {
1160
+ if (!isRecord(value) || !hasExactKeys(value, ["entry", "files"]) || !isRecord(value.files)) throw schemaError(`Problem '${problemId}' has an invalid '${language}' starter template.`);
1161
+ const entry = parseStarterPath(value.entry, `problem '${problemId}' '${language}' starter entry`);
1162
+ const paths = Object.keys(value.files).sort();
1163
+ if (paths.length < 1 || paths.length > PROBLEM_STARTER_LIMITS.filesPerLanguage) throw schemaError(`Problem '${problemId}' '${language}' starter must contain between 1 and ${PROBLEM_STARTER_LIMITS.filesPerLanguage} files.`);
1164
+ let totalBytes = 0;
1165
+ const entries = [];
1166
+ for (const rawPath of paths) {
1167
+ const path = parseStarterPath(rawPath, `problem '${problemId}' '${language}' starter file`);
1168
+ const content = value.files[rawPath];
1169
+ if (typeof content !== "string" || hasUnpairedSurrogate(content)) throw schemaError(`Problem '${problemId}' '${language}' starter file '${path}' is not valid Unicode source text.`);
1170
+ const bytes = UTF8_ENCODER.encode(content).byteLength;
1171
+ if (bytes > PROBLEM_STARTER_LIMITS.bytesPerFile) throw schemaError(`Problem '${problemId}' '${language}' starter file '${path}' exceeds ${PROBLEM_STARTER_LIMITS.bytesPerFile} UTF-8 bytes.`);
1172
+ totalBytes += bytes;
1173
+ if (totalBytes > PROBLEM_STARTER_LIMITS.totalBytesPerLanguage) throw schemaError(`Problem '${problemId}' '${language}' starter files exceed ${PROBLEM_STARTER_LIMITS.totalBytesPerLanguage} UTF-8 bytes in total.`);
1174
+ entries.push([path, content]);
1175
+ }
1176
+ const files = Object.fromEntries(entries);
1177
+ if (!Object.hasOwn(files, entry) || UTF8_ENCODER.encode(files[entry]).byteLength === 0) throw schemaError(`Problem '${problemId}' '${language}' starter entry must name a non-empty file in its file map.`);
1178
+ return {
1179
+ entry,
1180
+ files
1181
+ };
1182
+ }
1183
+ function parseStarterPath(value, label) {
1184
+ if (typeof value !== "string" || !value || value !== value.trim() || value.length > 4096 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("\0") || value.split("/").some((segment) => !segment || segment === "." || segment === "..")) throw schemaError(`The ${label} is not a normalized relative path.`);
1185
+ return value;
1186
+ }
1187
+ function hasUnpairedSurrogate(value) {
1188
+ for (let index = 0; index < value.length; index += 1) {
1189
+ const unit = value.charCodeAt(index);
1190
+ if (unit >= 55296 && unit <= 56319) {
1191
+ const next = value.charCodeAt(index + 1);
1192
+ if (!(next >= 56320 && next <= 57343)) return true;
1193
+ index += 1;
1194
+ } else if (unit >= 56320 && unit <= 57343) return true;
1195
+ }
1196
+ return false;
1197
+ }
1198
+ function parseScoring(value, problemId) {
1199
+ if (!isRecord(value) || !hasExactKeys(value, [
1200
+ "maximumPoints",
1201
+ "calibration",
1202
+ "policies",
1203
+ "safetyLimits"
1204
+ ]) || value.maximumPoints !== 100) throw schemaError(`Problem '${problemId}' has invalid scoring metadata.`);
1205
+ const calibration = value.calibration;
1206
+ if (!isRecord(calibration) || !hasExactKeys(calibration, ["method", "profiles"]) || calibration.method !== CALIBRATION_METHOD) throw schemaError(`Problem '${problemId}' has invalid calibration metadata.`);
1207
+ const profiles = calibration.profiles;
1208
+ if (!isRecord(profiles) || !hasExactKeys(profiles, LANGUAGES) || LANGUAGES.some((language) => typeof profiles[language] !== "string" || !profiles[language] || profiles[language] !== profiles[language].trim())) throw schemaError(`Problem '${problemId}' has invalid calibration profiles.`);
1209
+ if (!Array.isArray(value.policies) || value.policies.length !== POLICY_IDS.length) throw schemaError(`Problem '${problemId}' has invalid scoring policies.`);
1210
+ const policies = value.policies.map((policy, index) => parsePolicy(policy, problemId, POLICY_IDS[index]));
1211
+ if (policies.reduce((total, policy) => total + policy.points, 0) !== 100) throw schemaError(`Problem '${problemId}' policy points must sum to 100.`);
1212
+ for (let index = 1; index < policies.length; index += 1) {
1213
+ const broad = policies[index - 1].limits;
1214
+ const strict = policies[index].limits;
1215
+ const logicalInvalid = broad.logicalTimeLimitMs !== void 0 && (strict.logicalTimeLimitMs === void 0 || strict.logicalTimeLimitMs > broad.logicalTimeLimitMs);
1216
+ const anyStricter = strict.instructionBudget < broad.instructionBudget || strict.memoryLimitBytes < broad.memoryLimitBytes || broad.logicalTimeLimitMs === void 0 && strict.logicalTimeLimitMs !== void 0 || broad.logicalTimeLimitMs !== void 0 && strict.logicalTimeLimitMs !== void 0 && strict.logicalTimeLimitMs < broad.logicalTimeLimitMs;
1217
+ if (strict.instructionBudget > broad.instructionBudget || strict.memoryLimitBytes > broad.memoryLimitBytes || logicalInvalid || !anyStricter) throw schemaError(`Problem '${problemId}' policies are not broad-to-strict.`);
1218
+ }
1219
+ if (!isRecord(value.safetyLimits) || !hasExactKeys(value.safetyLimits, ["wallTimeLimitMs"]) || !isPositiveSafeInteger(value.safetyLimits.wallTimeLimitMs) || value.safetyLimits.wallTimeLimitMs > 6e5) throw schemaError(`Problem '${problemId}' has invalid safety limits.`);
1220
+ return {
1221
+ maximumPoints: 100,
1222
+ calibration: {
1223
+ method: CALIBRATION_METHOD,
1224
+ profiles
1225
+ },
1226
+ policies,
1227
+ safetyLimits: { wallTimeLimitMs: value.safetyLimits.wallTimeLimitMs }
1228
+ };
1229
+ }
1230
+ function parsePolicy(value, problemId, expectedId) {
1231
+ if (!isRecord(value) || !hasExactKeys(value, [
1232
+ "id",
1233
+ "title",
1234
+ "points",
1235
+ "limits"
1236
+ ]) || value.id !== expectedId || !isPositiveSafeInteger(value.points) || !isRecord(value.limits)) throw schemaError(`Problem '${problemId}' has an invalid '${expectedId}' policy.`);
1237
+ const keys = Object.keys(value.limits).sort();
1238
+ if (JSON.stringify(keys) !== JSON.stringify(keys.includes("logicalTimeLimitMs") ? [
1239
+ "instructionBudget",
1240
+ "logicalTimeLimitMs",
1241
+ "memoryLimitBytes"
1242
+ ] : ["instructionBudget", "memoryLimitBytes"])) throw schemaError(`Problem '${problemId}' policy '${expectedId}' has invalid limits.`);
1243
+ if (!isPositiveSafeInteger(value.limits.instructionBudget) || !isPositiveSafeInteger(value.limits.memoryLimitBytes) || value.limits.memoryLimitBytes < 65536 || value.limits.memoryLimitBytes > 4294967296 || value.limits.memoryLimitBytes % 65536 !== 0 || value.limits.logicalTimeLimitMs !== void 0 && (!isPositiveSafeInteger(value.limits.logicalTimeLimitMs) || value.limits.logicalTimeLimitMs > MAX_LOGICAL_TIME_LIMIT_MS)) throw schemaError(`Problem '${problemId}' policy '${expectedId}' has invalid resource values.`);
1244
+ const limits = {
1245
+ instructionBudget: value.limits.instructionBudget,
1246
+ memoryLimitBytes: value.limits.memoryLimitBytes,
1247
+ ...value.limits.logicalTimeLimitMs === void 0 ? {} : { logicalTimeLimitMs: value.limits.logicalTimeLimitMs }
1248
+ };
1249
+ return {
1250
+ id: expectedId,
1251
+ title: parseLocalizedText(value.title, `problem '${problemId}' policy '${expectedId}' title`),
1252
+ points: value.points,
1253
+ limits
1254
+ };
1255
+ }
1256
+ function parseComplexity(value, problemId, index) {
1257
+ if (!isRecord(value) || !hasExactKeys(value, [
1258
+ "name",
1259
+ "time",
1260
+ "space",
1261
+ "accepted"
1262
+ ]) || typeof value.time !== "string" || !value.time || typeof value.space !== "string" || !value.space || typeof value.accepted !== "boolean") throw schemaError(`Problem '${problemId}' has an invalid complexity at position ${index + 1}.`);
1263
+ return {
1264
+ name: parseLocalizedText(value.name, `problem '${problemId}' complexity name`),
1265
+ time: value.time,
1266
+ space: value.space,
1267
+ accepted: value.accepted
1268
+ };
1269
+ }
1270
+ function parseLocalizedText(value, label, trimmed = true) {
1271
+ if (!isRecord(value) || !hasExactKeys(value, PROBLEM_LOCALES)) throw schemaError(`${label} is invalid.`);
1272
+ for (const locale of PROBLEM_LOCALES) if (typeof value[locale] !== "string" || !value[locale] || trimmed && value[locale] !== value[locale].trim()) throw schemaError(`${label}[${locale}] is invalid.`);
1273
+ return value;
1274
+ }
1275
+ function parseDifficulty(value, label) {
1276
+ if (value !== "easy" && value !== "medium" && value !== "hard") throw schemaError(`${label} has an invalid difficulty.`);
1277
+ return value;
1278
+ }
1279
+ function parseTags(value, label) {
1280
+ if (!Array.isArray(value) || value.length < 1 || value.length > 32 || value.some((tag) => typeof tag !== "string" || !tag || tag !== tag.trim()) || new Set(value).size !== value.length) throw schemaError(`${label} has invalid tags.`);
1281
+ return value;
1282
+ }
1283
+ function hasExactKeys(value, expected) {
1284
+ const actual = Object.keys(value).sort();
1285
+ const expectedKeys = [...expected].sort();
1286
+ return actual.length === expectedKeys.length && actual.every((key, index) => key === expectedKeys[index]);
1287
+ }
1288
+ function isRecord(value) {
1289
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1290
+ }
1291
+ function isPositiveSafeInteger(value) {
1292
+ return Number.isSafeInteger(value) && value > 0;
1293
+ }
1294
+ function schemaError(message) {
1295
+ return new ProblemCollectionError(message, "schema");
1296
+ }
1297
+ //#endregion
1298
+ //#region src/online-judge/public-projection.ts
1299
+ /** Parses the redacted contest object. Its repository descriptor supplies the content identity. */
1300
+ function parseContestPublicProblemProjection(value) {
1301
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError("Contest-public projection must be an object.");
1302
+ const record = value;
1303
+ if (Object.keys(record).sort().join("\0") !== "problem\0schema" || record.schema !== "wasm-oj-platform/contest-public-problem-projection/v1") throw new TypeError("Contest-public projection has an invalid shape or schema.");
1304
+ if (!record.problem || typeof record.problem !== "object" || Array.isArray(record.problem)) throw new TypeError("Contest-public projection contains an invalid problem.");
1305
+ const rawProblem = record.problem;
1306
+ const editorial = rawProblem.editorial;
1307
+ if (!editorial || typeof editorial !== "object" || Array.isArray(editorial) || Object.keys(editorial).sort().join("\0") !== "en\0zh-TW" || editorial["zh-TW"] !== "" || editorial.en !== "") throw new TypeError("Contest-public projection contains non-public problem data.");
1308
+ const parsed = parseStandaloneProblemBundle$2({
1309
+ schema: BROWSER_PROBLEM_SCHEMA$2,
1310
+ problem: {
1311
+ ...rawProblem,
1312
+ editorial: {
1313
+ "zh-TW": "redacted",
1314
+ en: "redacted"
1315
+ }
1316
+ }
1317
+ });
1318
+ if (parsed.judgeCases.some((testCase) => testCase.kind !== "sample")) throw new TypeError("Contest-public projection contains non-public problem data.");
1319
+ return {
1320
+ schema: CONTEST_PUBLIC_PROJECTION_SCHEMA,
1321
+ problem: {
1322
+ ...parsed,
1323
+ editorial: {
1324
+ "zh-TW": "",
1325
+ en: ""
1326
+ }
1327
+ }
1328
+ };
1329
+ }
1330
+ //#endregion
1331
+ //#region src/online-judge/compile-profiles.ts
1332
+ function record$3(value, label) {
1333
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
1334
+ return value;
1335
+ }
1336
+ function parseJudgeAllowedProfiles(value, label = "allowedProfiles") {
1337
+ const profiles = record$3(value, label);
1338
+ const entries = Object.entries(profiles).sort(([left], [right]) => left.localeCompare(right));
1339
+ if (entries.length < 1) throw new TypeError(`${label} must contain at least one compile profile.`);
1340
+ const result = {};
1341
+ for (const [language, candidate] of entries) {
1342
+ if (!isBuiltinLanguage(language)) throw new TypeError(`${label} language '${language}' is unsupported.`);
1343
+ const profile = record$3(candidate, `${label}.${language}`);
1344
+ if (JSON.stringify(Object.keys(profile).sort()) !== JSON.stringify(["optimization", "target"])) throw new TypeError(`${label}.${language} has an invalid shape.`);
1345
+ if (profile.target !== "wasip1" && profile.target !== "wasix") throw new TypeError(`${label}.${language}.target is unsupported.`);
1346
+ if (profile.optimization !== "debug" && profile.optimization !== "release") throw new TypeError(`${label}.${language}.optimization is unsupported.`);
1347
+ result[language] = {
1348
+ target: profile.target,
1349
+ optimization: profile.optimization
1350
+ };
1351
+ }
1352
+ return result;
1353
+ }
1354
+ //#endregion
1355
+ //#region src/online-judge/trusted-judge-wasm.ts
1356
+ var TRUSTED_JUDGE_WASM_MAX_BYTES = 8388608;
1357
+ var I32 = 127;
1358
+ var I64 = 126;
1359
+ var errno = (parameters) => ({
1360
+ parameters,
1361
+ results: [I32]
1362
+ });
1363
+ Object.freeze({
1364
+ args_get: errno([I32, I32]),
1365
+ args_sizes_get: errno([I32, I32]),
1366
+ clock_res_get: errno([I32, I32]),
1367
+ clock_time_get: errno([
1368
+ I32,
1369
+ I64,
1370
+ I32
1371
+ ]),
1372
+ environ_get: errno([I32, I32]),
1373
+ environ_sizes_get: errno([I32, I32]),
1374
+ fd_advise: errno([
1375
+ I32,
1376
+ I64,
1377
+ I64,
1378
+ I32
1379
+ ]),
1380
+ fd_allocate: errno([
1381
+ I32,
1382
+ I64,
1383
+ I64
1384
+ ]),
1385
+ fd_close: errno([I32]),
1386
+ fd_datasync: errno([I32]),
1387
+ fd_fdstat_get: errno([I32, I32]),
1388
+ fd_fdstat_set_flags: errno([I32, I32]),
1389
+ fd_fdstat_set_rights: errno([
1390
+ I32,
1391
+ I64,
1392
+ I64
1393
+ ]),
1394
+ fd_filestat_get: errno([I32, I32]),
1395
+ fd_filestat_set_size: errno([I32, I64]),
1396
+ fd_filestat_set_times: errno([
1397
+ I32,
1398
+ I64,
1399
+ I64,
1400
+ I32
1401
+ ]),
1402
+ fd_pread: errno([
1403
+ I32,
1404
+ I32,
1405
+ I32,
1406
+ I64,
1407
+ I32
1408
+ ]),
1409
+ fd_prestat_dir_name: errno([
1410
+ I32,
1411
+ I32,
1412
+ I32
1413
+ ]),
1414
+ fd_prestat_get: errno([I32, I32]),
1415
+ fd_pwrite: errno([
1416
+ I32,
1417
+ I32,
1418
+ I32,
1419
+ I64,
1420
+ I32
1421
+ ]),
1422
+ fd_read: errno([
1423
+ I32,
1424
+ I32,
1425
+ I32,
1426
+ I32
1427
+ ]),
1428
+ fd_readdir: errno([
1429
+ I32,
1430
+ I32,
1431
+ I32,
1432
+ I64,
1433
+ I32
1434
+ ]),
1435
+ fd_renumber: errno([I32, I32]),
1436
+ fd_seek: errno([
1437
+ I32,
1438
+ I64,
1439
+ I32,
1440
+ I32
1441
+ ]),
1442
+ fd_sync: errno([I32]),
1443
+ fd_tell: errno([I32, I32]),
1444
+ fd_write: errno([
1445
+ I32,
1446
+ I32,
1447
+ I32,
1448
+ I32
1449
+ ]),
1450
+ path_create_directory: errno([
1451
+ I32,
1452
+ I32,
1453
+ I32
1454
+ ]),
1455
+ path_filestat_get: errno([
1456
+ I32,
1457
+ I32,
1458
+ I32,
1459
+ I32,
1460
+ I32
1461
+ ]),
1462
+ path_filestat_set_times: errno([
1463
+ I32,
1464
+ I32,
1465
+ I32,
1466
+ I32,
1467
+ I64,
1468
+ I64,
1469
+ I32
1470
+ ]),
1471
+ path_link: errno([
1472
+ I32,
1473
+ I32,
1474
+ I32,
1475
+ I32,
1476
+ I32,
1477
+ I32,
1478
+ I32
1479
+ ]),
1480
+ path_open: errno([
1481
+ I32,
1482
+ I32,
1483
+ I32,
1484
+ I32,
1485
+ I32,
1486
+ I64,
1487
+ I64,
1488
+ I32,
1489
+ I32
1490
+ ]),
1491
+ path_readlink: errno([
1492
+ I32,
1493
+ I32,
1494
+ I32,
1495
+ I32,
1496
+ I32,
1497
+ I32
1498
+ ]),
1499
+ path_remove_directory: errno([
1500
+ I32,
1501
+ I32,
1502
+ I32
1503
+ ]),
1504
+ path_rename: errno([
1505
+ I32,
1506
+ I32,
1507
+ I32,
1508
+ I32,
1509
+ I32,
1510
+ I32
1511
+ ]),
1512
+ path_symlink: errno([
1513
+ I32,
1514
+ I32,
1515
+ I32,
1516
+ I32,
1517
+ I32
1518
+ ]),
1519
+ path_unlink_file: errno([
1520
+ I32,
1521
+ I32,
1522
+ I32
1523
+ ]),
1524
+ poll_oneoff: errno([
1525
+ I32,
1526
+ I32,
1527
+ I32,
1528
+ I32
1529
+ ]),
1530
+ proc_exit: {
1531
+ parameters: [I32],
1532
+ results: []
1533
+ },
1534
+ proc_raise: errno([I32]),
1535
+ random_get: errno([I32, I32]),
1536
+ sched_yield: errno([])
1537
+ });
1538
+ Object.freeze(/* @__PURE__ */ new Set([
1539
+ "args_get",
1540
+ "args_sizes_get",
1541
+ "clock_res_get",
1542
+ "clock_time_get",
1543
+ "environ_get",
1544
+ "environ_sizes_get",
1545
+ "fd_advise",
1546
+ "fd_allocate",
1547
+ "fd_close",
1548
+ "fd_datasync",
1549
+ "fd_fdstat_get",
1550
+ "fd_fdstat_set_flags",
1551
+ "fd_fdstat_set_rights",
1552
+ "fd_filestat_get",
1553
+ "fd_filestat_set_size",
1554
+ "fd_filestat_set_times",
1555
+ "fd_pread",
1556
+ "fd_prestat_dir_name",
1557
+ "fd_prestat_get",
1558
+ "fd_pwrite",
1559
+ "fd_read",
1560
+ "fd_readdir",
1561
+ "fd_renumber",
1562
+ "fd_seek",
1563
+ "fd_sync",
1564
+ "fd_tell",
1565
+ "fd_write",
1566
+ "path_create_directory",
1567
+ "path_filestat_get",
1568
+ "path_filestat_set_times",
1569
+ "path_link",
1570
+ "path_open",
1571
+ "path_readlink",
1572
+ "path_remove_directory",
1573
+ "path_rename",
1574
+ "path_symlink",
1575
+ "path_unlink_file",
1576
+ "poll_oneoff",
1577
+ "proc_exit",
1578
+ "proc_raise",
1579
+ "random_get",
1580
+ "sched_yield"
1581
+ ]));
1582
+ var TRUSTED_JUDGE_RUNTIME_PROFILES = Object.freeze(/* @__PURE__ */ new Set([
1583
+ "c-wasip1-release",
1584
+ "cpp-wasip1-release",
1585
+ "rust-wasip1-release",
1586
+ "go-wasip1-release"
1587
+ ]));
1588
+ //#endregion
1589
+ //#region src/online-judge/repository-authoring.ts
1590
+ var REPOSITORY_AUTHORING_JUDGES_SCHEMA = "wasm-oj-platform/repository-authoring-judges/v1";
1591
+ var SHA256$1 = /^[0-9a-f]{64}$/;
1592
+ var SLUG$1 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1593
+ var PATH$1 = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000]+$/;
1594
+ var GUEST_PATH = /^\/(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000]+$/;
1595
+ var MAX_ASSET_BYTES = 4194304;
1596
+ var MAX_ASSET_TOTAL_BYTES = 4194304;
1597
+ function record$2(value, label) {
1598
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
1599
+ return value;
1600
+ }
1601
+ function exact$2(value, keys, label) {
1602
+ if (Object.keys(value).sort().join("\0") !== [...keys].sort().join("\0")) throw new TypeError(`${label} has an invalid shape.`);
1603
+ }
1604
+ function sourceObject(value, label, maximum) {
1605
+ const object = record$2(value, label);
1606
+ exact$2(object, [
1607
+ "bytes",
1608
+ "path",
1609
+ "sha256"
1610
+ ], label);
1611
+ if (typeof object.path !== "string" || object.path.length < 1 || object.path.length > 512 || !PATH$1.test(object.path)) throw new TypeError(`${label}.path must be a normalized repository-relative POSIX path.`);
1612
+ if (!Number.isSafeInteger(object.bytes) || object.bytes < 1 || object.bytes > maximum) throw new TypeError(`${label}.bytes is outside its limit.`);
1613
+ if (typeof object.sha256 !== "string" || !SHA256$1.test(object.sha256)) throw new TypeError(`${label}.sha256 is invalid.`);
1614
+ return {
1615
+ path: object.path,
1616
+ bytes: object.bytes,
1617
+ sha256: object.sha256
1618
+ };
1619
+ }
1620
+ function judge(value, slug) {
1621
+ const input = record$2(value, `repository authoring '${slug}' judge`);
1622
+ if (input.kind === "text") {
1623
+ exact$2(input, ["kind"], `repository authoring '${slug}' text judge`);
1624
+ return { kind: "text" };
1625
+ }
1626
+ if (input.kind !== "checker" && input.kind !== "interactive") throw new TypeError(`Repository authoring '${slug}' judge kind is unsupported.`);
1627
+ exact$2(input, input.kind === "checker" ? [
1628
+ "args",
1629
+ "artifact",
1630
+ "assets",
1631
+ "kind"
1632
+ ] : [
1633
+ "args",
1634
+ "artifact",
1635
+ "assets",
1636
+ "inputPath",
1637
+ "kind"
1638
+ ], `repository authoring '${slug}' ${input.kind}`);
1639
+ const artifactValue = record$2(input.artifact, `repository authoring '${slug}' ${input.kind} artifact`);
1640
+ exact$2(artifactValue, [
1641
+ "bytes",
1642
+ "path",
1643
+ "runtimeProfile",
1644
+ "sha256"
1645
+ ], `repository authoring '${slug}' ${input.kind} artifact`);
1646
+ if (typeof artifactValue.runtimeProfile !== "string" || !TRUSTED_JUDGE_RUNTIME_PROFILES.has(artifactValue.runtimeProfile)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} runtimeProfile is unsupported.`);
1647
+ const artifact = {
1648
+ ...sourceObject({
1649
+ bytes: artifactValue.bytes,
1650
+ path: artifactValue.path,
1651
+ sha256: artifactValue.sha256
1652
+ }, `repository authoring '${slug}' ${input.kind} artifact`, TRUSTED_JUDGE_WASM_MAX_BYTES),
1653
+ runtimeProfile: artifactValue.runtimeProfile
1654
+ };
1655
+ if (!artifact.path.endsWith(".wasm")) throw new TypeError(`Repository authoring '${slug}' ${input.kind} artifact path must end in '.wasm'.`);
1656
+ if (!Array.isArray(input.assets) || input.assets.length > 256) throw new TypeError(`Repository authoring '${slug}' ${input.kind} assets are invalid.`);
1657
+ const namespace = input.kind === "checker" ? "/checker/assets/" : "/interactor/assets/";
1658
+ const assets = input.assets.map((candidate, index) => {
1659
+ const asset = record$2(candidate, `repository authoring '${slug}' ${input.kind} asset ${index}`);
1660
+ exact$2(asset, [
1661
+ "bytes",
1662
+ "guestPath",
1663
+ "path",
1664
+ "sha256"
1665
+ ], `repository authoring '${slug}' ${input.kind} asset ${index}`);
1666
+ if (typeof asset.guestPath !== "string" || !GUEST_PATH.test(asset.guestPath) || !asset.guestPath.startsWith(namespace)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} asset ${index} guestPath must be inside '${namespace}'.`);
1667
+ return {
1668
+ ...sourceObject({
1669
+ bytes: asset.bytes,
1670
+ path: asset.path,
1671
+ sha256: asset.sha256
1672
+ }, `repository authoring '${slug}' ${input.kind} asset ${index}`, MAX_ASSET_BYTES),
1673
+ guestPath: asset.guestPath
1674
+ };
1675
+ });
1676
+ const guestPaths = assets.map((asset) => asset.guestPath);
1677
+ const repositoryPaths = [artifact.path, ...assets.map((asset) => asset.path)];
1678
+ if (new Set(guestPaths).size !== guestPaths.length || new Set(repositoryPaths).size !== repositoryPaths.length) throw new TypeError(`Repository authoring '${slug}' ${input.kind} repeats an asset or repository path.`);
1679
+ if (assets.reduce((total, asset) => total + asset.bytes, 0) > MAX_ASSET_TOTAL_BYTES) throw new TypeError(`Repository authoring '${slug}' ${input.kind} assets exceed 4 MiB.`);
1680
+ if (!Array.isArray(input.args) || input.args.length > 64 || input.args.some((argument) => typeof argument !== "string" || argument.includes("\0") || new TextEncoder().encode(argument).byteLength > 4096)) throw new TypeError(`Repository authoring '${slug}' ${input.kind} args are invalid.`);
1681
+ const args = [...input.args];
1682
+ if (input.kind === "checker") return {
1683
+ kind: "checker",
1684
+ artifact,
1685
+ assets,
1686
+ args
1687
+ };
1688
+ if (typeof input.inputPath !== "string" || !GUEST_PATH.test(input.inputPath) || !input.inputPath.startsWith("/interactor/input/")) throw new TypeError(`Repository authoring '${slug}' interactive inputPath must be inside '/interactor/input/'.`);
1689
+ return {
1690
+ kind: "interactive",
1691
+ artifact,
1692
+ assets,
1693
+ args,
1694
+ inputPath: input.inputPath
1695
+ };
1696
+ }
1697
+ /** Author-only input used by collection build; never accepted by the platform sync boundary. */
1698
+ function parseRepositoryAuthoringJudges(value) {
1699
+ const source = record$2(value, "repository authoring judges");
1700
+ exact$2(source, ["problems", "schema"], "repository authoring judges");
1701
+ if (source.schema !== "wasm-oj-platform/repository-authoring-judges/v1") throw new TypeError(`Repository authoring judge schema must be '${REPOSITORY_AUTHORING_JUDGES_SCHEMA}'.`);
1702
+ if (!Array.isArray(source.problems) || source.problems.length < 1 || source.problems.length > 1e3) throw new TypeError("Repository authoring judges must contain between 1 and 1000 problems.");
1703
+ const slugs = /* @__PURE__ */ new Set();
1704
+ return {
1705
+ schema: REPOSITORY_AUTHORING_JUDGES_SCHEMA,
1706
+ problems: source.problems.map((candidate, index) => {
1707
+ const problem = record$2(candidate, `repository authoring judge problem ${index + 1}`);
1708
+ exact$2(problem, [
1709
+ "allowedProfiles",
1710
+ "judge",
1711
+ "slug"
1712
+ ], `repository authoring judge problem ${index + 1}`);
1713
+ if (typeof problem.slug !== "string" || !SLUG$1.test(problem.slug) || slugs.has(problem.slug)) throw new TypeError(`Repository authoring judge problem ${index + 1} has an invalid or duplicate slug.`);
1714
+ slugs.add(problem.slug);
1715
+ return {
1716
+ slug: problem.slug,
1717
+ allowedProfiles: parseJudgeAllowedProfiles(problem.allowedProfiles, `repository authoring '${problem.slug}' allowedProfiles`),
1718
+ judge: judge(problem.judge, problem.slug)
1719
+ };
1720
+ })
1721
+ };
1722
+ }
1723
+ //#endregion
1724
+ //#region src/online-judge/repository-contract.ts
1725
+ var REPOSITORY_SCHEMA = "wasm-oj-platform/repository/v1";
1726
+ var PROBLEMS_SCHEMA = "wasm-oj-platform/problems/v1";
1727
+ var CONTESTS_SCHEMA = "wasm-oj-platform/contests/v2";
1728
+ var MAX_PUBLIC_BUNDLE_BYTES = 8388608;
1729
+ var MAX_JUDGE_PACKAGE_BYTES = 33554432;
1730
+ var SHA256 = /^[0-9a-f]{64}$/;
1731
+ var SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1732
+ var PATH = /^(?!\/)(?!.*(?:^|\/)\.{1,2}(?:\/|$))(?!.*\/\/)(?!.*\\)(?!.*\/$)[^\u0000-\u001f\u007f]+$/;
1733
+ var decoder = new TextDecoder("utf-8", { fatal: true });
1734
+ function record$1(value, label) {
1735
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new TypeError(`${label} must be an object.`);
1736
+ return value;
1737
+ }
1738
+ function exact$1(value, keys, label) {
1739
+ const actual = Object.keys(value).sort();
1740
+ const expected = [...keys].sort();
1741
+ if (actual.join("\0") !== expected.join("\0")) throw new TypeError(`${label} has an invalid shape.`);
1742
+ }
1743
+ function repositoryPath(value, label) {
1744
+ if (typeof value !== "string" || value.length < 1 || value.length > 512 || !PATH.test(value)) throw new TypeError(`${label} must be a normalized repository-relative POSIX path.`);
1745
+ return value;
1746
+ }
1747
+ function slug(value, label) {
1748
+ if (typeof value !== "string" || value.length > 128 || !SLUG.test(value)) throw new TypeError(`${label} is invalid.`);
1749
+ return value;
1750
+ }
1751
+ function boundedText(value, label, maximumBytes) {
1752
+ if (typeof value !== "string" || new TextEncoder().encode(value).byteLength > maximumBytes) throw new TypeError(`${label} exceeds its UTF-8 byte limit.`);
1753
+ return value;
1754
+ }
1755
+ function localizedText(value, label, maximumBytes) {
1756
+ const input = record$1(value, label);
1757
+ exact$1(input, ["en", "zh-TW"], label);
1758
+ return {
1759
+ "zh-TW": boundedText(input["zh-TW"], `${label}.zh-TW`, maximumBytes),
1760
+ en: boundedText(input.en, `${label}.en`, maximumBytes)
1761
+ };
1762
+ }
1763
+ function descriptor(value, label, maximumBytes) {
1764
+ const input = record$1(value, label);
1765
+ exact$1(input, [
1766
+ "bytes",
1767
+ "path",
1768
+ "sha256"
1769
+ ], label);
1770
+ if (!Number.isSafeInteger(input.bytes) || input.bytes < 1 || input.bytes > maximumBytes) throw new TypeError(`${label}.bytes is outside its limit.`);
1771
+ if (typeof input.sha256 !== "string" || !SHA256.test(input.sha256)) throw new TypeError(`${label}.sha256 must be a lowercase SHA-256 digest.`);
1772
+ return {
1773
+ path: repositoryPath(input.path, `${label}.path`),
1774
+ bytes: input.bytes,
1775
+ sha256: input.sha256
1776
+ };
1777
+ }
1778
+ function parseJsonBytes(bytes, label) {
1779
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 2 || bytes.byteLength > 2097152) throw new TypeError(`${label} bytes are outside the 2 MiB limit.`);
1780
+ let text;
1781
+ try {
1782
+ text = decoder.decode(bytes);
1783
+ } catch (error) {
1784
+ throw new TypeError(`${label} must be valid UTF-8.`, { cause: error });
1785
+ }
1786
+ try {
1787
+ return JSON.parse(text);
1788
+ } catch (error) {
1789
+ throw new TypeError(`${label} must be valid JSON.`, { cause: error });
1790
+ }
1791
+ }
1792
+ function parseRepositoryRootValue(value) {
1793
+ const input = record$1(value, "repository manifest");
1794
+ exact$1(input, [
1795
+ "contests",
1796
+ "problems",
1797
+ "schema"
1798
+ ], "repository manifest");
1799
+ if (input.schema !== "wasm-oj-platform/repository/v1") throw new TypeError(`Repository schema must be '${REPOSITORY_SCHEMA}'.`);
1800
+ const problems = repositoryPath(input.problems, "repository manifest problems");
1801
+ const contests = repositoryPath(input.contests, "repository manifest contests");
1802
+ if (problems === contests || problems === "wasm-oj.json" || contests === "wasm-oj.json") throw new TypeError("Repository manifest paths must be distinct from each other and the root manifest.");
1803
+ return {
1804
+ schema: REPOSITORY_SCHEMA,
1805
+ problems,
1806
+ contests
1807
+ };
1808
+ }
1809
+ function parseRepositoryRoot(bytes) {
1810
+ return parseRepositoryRootValue(parseJsonBytes(bytes, "repository manifest"));
1811
+ }
1812
+ function parseRepositoryProblemsValue(value) {
1813
+ const input = record$1(value, "problems manifest");
1814
+ exact$1(input, ["problems", "schema"], "problems manifest");
1815
+ if (input.schema !== "wasm-oj-platform/problems/v1") throw new TypeError(`Problems schema must be '${PROBLEMS_SCHEMA}'.`);
1816
+ if (!Array.isArray(input.problems) || input.problems.length < 1 || input.problems.length > 1e3) throw new TypeError("Problems manifest must contain between 1 and 1000 problems.");
1817
+ const slugs = /* @__PURE__ */ new Set();
1818
+ const orders = /* @__PURE__ */ new Set();
1819
+ const paths = /* @__PURE__ */ new Set();
1820
+ const problems = input.problems.map((candidate, index) => {
1821
+ const problem = record$1(candidate, `problem ${index + 1}`);
1822
+ exact$1(problem, [
1823
+ "contestBundle",
1824
+ "judgePackage",
1825
+ "order",
1826
+ "practiceBundle",
1827
+ "practiceEnabled",
1828
+ "slug",
1829
+ "summary",
1830
+ "title"
1831
+ ], `problem ${index + 1}`);
1832
+ const problemSlug = slug(problem.slug, `problem ${index + 1}.slug`);
1833
+ if (slugs.has(problemSlug)) throw new TypeError(`Problem slug '${problemSlug}' is duplicated.`);
1834
+ if (!Number.isSafeInteger(problem.order) || problem.order < 1 || problem.order > 1e3 || orders.has(problem.order)) throw new TypeError(`Problem '${problemSlug}' order is invalid or duplicated.`);
1835
+ if (typeof problem.practiceEnabled !== "boolean") throw new TypeError(`Problem '${problemSlug}' practiceEnabled must be boolean.`);
1836
+ const practiceBundle = descriptor(problem.practiceBundle, `problem '${problemSlug}' practiceBundle`, MAX_PUBLIC_BUNDLE_BYTES);
1837
+ const contestBundle = descriptor(problem.contestBundle, `problem '${problemSlug}' contestBundle`, MAX_PUBLIC_BUNDLE_BYTES);
1838
+ const judgePackage = descriptor(problem.judgePackage, `problem '${problemSlug}' judgePackage`, MAX_JUDGE_PACKAGE_BYTES);
1839
+ for (const object of [
1840
+ practiceBundle,
1841
+ contestBundle,
1842
+ judgePackage
1843
+ ]) {
1844
+ if (paths.has(object.path)) throw new TypeError(`Repository object path '${object.path}' is declared more than once.`);
1845
+ paths.add(object.path);
1846
+ }
1847
+ slugs.add(problemSlug);
1848
+ orders.add(problem.order);
1849
+ return {
1850
+ slug: problemSlug,
1851
+ order: problem.order,
1852
+ title: localizedText(problem.title, `problem '${problemSlug}' title`, 4096),
1853
+ summary: localizedText(problem.summary, `problem '${problemSlug}' summary`, 16384),
1854
+ practiceEnabled: problem.practiceEnabled,
1855
+ practiceBundle,
1856
+ contestBundle,
1857
+ judgePackage
1858
+ };
1859
+ });
1860
+ problems.sort((left, right) => left.order - right.order);
1861
+ return {
1862
+ schema: PROBLEMS_SCHEMA,
1863
+ problems
1864
+ };
1865
+ }
1866
+ function parseRepositoryProblems(bytes) {
1867
+ return parseRepositoryProblemsValue(parseJsonBytes(bytes, "problems manifest"));
1868
+ }
1869
+ function parseRepositoryContestsValue(value) {
1870
+ const input = record$1(value, "contests manifest");
1871
+ exact$1(input, ["contests", "schema"], "contests manifest");
1872
+ if (input.schema !== "wasm-oj-platform/contests/v2") throw new TypeError(`Contests schema must be '${CONTESTS_SCHEMA}'.`);
1873
+ if (!Array.isArray(input.contests) || input.contests.length > 1e3) throw new TypeError("Contests manifest may contain at most 1000 contests.");
1874
+ const slugs = /* @__PURE__ */ new Set();
1875
+ return {
1876
+ schema: CONTESTS_SCHEMA,
1877
+ contests: input.contests.map((candidate, index) => {
1878
+ const contest = record$1(candidate, `contest ${index + 1}`);
1879
+ exact$1(contest, [
1880
+ "accessMode",
1881
+ "description",
1882
+ "rules",
1883
+ "slug",
1884
+ "status",
1885
+ "title"
1886
+ ], `contest ${index + 1}`);
1887
+ const contestSlug = slug(contest.slug, `contest ${index + 1}.slug`);
1888
+ if (slugs.has(contestSlug)) throw new TypeError(`Contest slug '${contestSlug}' is duplicated.`);
1889
+ if (contest.status !== "draft" && contest.status !== "published" && contest.status !== "archived") throw new TypeError(`Contest '${contestSlug}' status is invalid.`);
1890
+ if (contest.accessMode !== "public" && contest.accessMode !== "invite") throw new TypeError(`Contest '${contestSlug}' accessMode is invalid.`);
1891
+ const rules = parseContestRules(contest.rules, `contest '${contestSlug}' rules`);
1892
+ slugs.add(contestSlug);
1893
+ return {
1894
+ slug: contestSlug,
1895
+ status: contest.status,
1896
+ title: boundedText(contest.title, `contest '${contestSlug}' title`, 4096),
1897
+ description: boundedText(contest.description, `contest '${contestSlug}' description`, 65536),
1898
+ accessMode: contest.accessMode,
1899
+ rules
1900
+ };
1901
+ })
1902
+ };
1903
+ }
1904
+ function parseRepositoryContests(bytes) {
1905
+ return parseRepositoryContestsValue(parseJsonBytes(bytes, "contests manifest"));
1906
+ }
1907
+ function validateRepositoryCatalog(root, problems, contests) {
1908
+ const problemSlugs = new Set(problems.problems.map((problem) => problem.slug));
1909
+ for (const contest of contests.contests) for (const problem of contest.rules.problems) if (!problemSlugs.has(problem.slug)) throw new TypeError(`Contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
1910
+ return {
1911
+ root,
1912
+ problems,
1913
+ contests
1914
+ };
1915
+ }
1916
+ //#endregion
7
1917
  //#region src/path-safety.ts
8
1918
  /** Resolve only the operating system's own temporary-directory alias. */
9
1919
  async function canonicalizeSystemTemporaryPrefix(value) {
@@ -31,516 +1941,338 @@ async function anchoredPathHasNoSymlink(value) {
31
1941
  }
32
1942
  //#endregion
33
1943
  //#region src/collection-cli.ts
34
- var SOURCE_SCHEMA = "wasm-oj-browser-collection-source-v1";
35
- var DEFAULT_INDEX_PATH = "collection/index.json";
36
- var DEFAULT_SOURCE_PATH = "collection/source.json";
37
- var INDEX_MAX_BYTES = 524288;
38
- var COLLECTION_SOURCE_MAX_BYTES = 2097152;
39
- var STATEMENT_MAX_BYTES = 2097152;
40
- var PUBLIC_BUNDLE_MAX_BYTES = 8388608;
41
- var AUTHORING_BUNDLE_MAX_BYTES = 33554432;
42
- var MANAGED_MAX_BYTES = 2097152;
43
- var JUDGE_PACKAGE_MAX_BYTES = 33554432;
1944
+ var AUTHORING_SCHEMA = "wasm-oj-platform/repository-authoring/v1";
1945
+ var SOURCE_PATH = "collection/source.json";
1946
+ var ROOT_PATH = "wasm-oj.json";
1947
+ var PROBLEMS_PATH = "collection/problems.json";
1948
+ var CONTESTS_PATH = "collection/contests.json";
1949
+ var JSON_MAX_BYTES = 2097152;
1950
+ var PUBLIC_MAX_BYTES = 8388608;
1951
+ var PACKAGE_MAX_BYTES = 33554432;
44
1952
  function fail(message) {
45
1953
  throw new Error(message);
46
1954
  }
47
- function normalizedRelativePath(value, label) {
48
- if (typeof value !== "string" || !value || value.startsWith("/") || value.includes("\\") || value.includes("\0") || value.endsWith("/")) return fail(`${label} must be a normalized relative POSIX path.`);
49
- if (value.split("/").some((segment) => !segment || segment === "." || segment === "..")) return fail(`${label} must be a normalized relative POSIX path.`);
50
- return value;
51
- }
52
- function portableOutputPath(value, label) {
53
- const relative = normalizedRelativePath(value, label);
54
- if (relative.split("/").some((segment) => !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(segment))) return fail(`${label} must use only portable ASCII path segments.`);
55
- return relative;
56
- }
57
- function resolveInside(root, relativeValue, label) {
58
- const relative = normalizedRelativePath(relativeValue, label);
59
- return path.join(root, ...relative.split("/"));
60
- }
61
- function collisionKey(value) {
62
- return value.normalize("NFC").toLocaleLowerCase("en-US");
63
- }
64
- async function assertRepositoryRoot(root) {
65
- const anchoredRoot = await canonicalizeSystemTemporaryPrefix(path.resolve(root));
66
- if (!await anchoredPathHasNoSymlink(anchoredRoot)) fail("repository root must not traverse a symbolic link.");
67
- const metadata = await lstat(anchoredRoot);
68
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("repository root must be a real directory.");
69
- }
70
- async function readBoundedRepositoryFile(options, relativeValue, label, maximumBytes, expectedBytes) {
71
- const file = resolveInside(options.root, relativeValue, label);
72
- if (!await anchoredPathHasNoSymlink(file)) fail(`${label} must not traverse a symbolic link.`);
73
- let handle;
74
- try {
75
- handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
76
- } catch (error) {
77
- throw new Error(`${label} must be a readable regular file.`, { cause: error });
78
- }
79
- try {
80
- const metadata = await handle.stat();
81
- if (!metadata.isFile() || metadata.size > maximumBytes || expectedBytes !== void 0 && metadata.size !== expectedBytes) fail(`${label} is outside its allowed byte limit.`);
82
- return new Uint8Array(await handle.readFile());
83
- } finally {
84
- await handle.close();
85
- }
1955
+ function sameLocalizedText(left, right) {
1956
+ return left["zh-TW"] === right["zh-TW"] && left.en === right.en;
86
1957
  }
87
- async function assertOutputPathSafe(options, relativeValue, label) {
88
- const output = resolveInside(options.root, relativeValue, label);
89
- if (!await anchoredPathHasNoSymlink(output)) fail(`${label} must not traverse a symbolic link.`);
90
- let metadata;
91
- try {
92
- metadata = await lstat(output);
93
- } catch (error) {
94
- if (error.code === "ENOENT") return;
95
- throw error;
96
- }
97
- if (metadata.isSymbolicLink() || !metadata.isFile()) fail(`${label} must replace only a regular file.`);
1958
+ function normalizedPath(value, label) {
1959
+ if (typeof value !== "string" || value.length < 1 || value.length > 512 || value.startsWith("/") || value.endsWith("/") || value.includes("\\") || value.includes("\0") || value.split("/").some((part) => !part || part === "." || part === "..")) fail(`${label} must be a normalized repository-relative POSIX path.`);
1960
+ return value;
98
1961
  }
99
- async function existingPathIdentity(options, relativeValue, label) {
100
- const file = resolveInside(options.root, relativeValue, label);
101
- if (!await anchoredPathHasNoSymlink(file)) fail(`${label} must not traverse a symbolic link.`);
102
- try {
103
- const metadata = await lstat(file, { bigint: true });
104
- if (metadata.isSymbolicLink()) fail(`${label} must not be a symbolic link.`);
105
- return `${metadata.dev}:${metadata.ino}`;
106
- } catch (error) {
107
- if (error.code === "ENOENT") return void 0;
108
- throw error;
109
- }
110
- }
111
- async function assertNoFilesystemAliases(options, inputs, outputs) {
112
- const inputIdentities = /* @__PURE__ */ new Map();
113
- for (const input of inputs) {
114
- const identity = await existingPathIdentity(options, input, `declared input '${input}'`);
115
- if (identity !== void 0 && !inputIdentities.has(identity)) inputIdentities.set(identity, input);
116
- }
117
- const outputIdentities = /* @__PURE__ */ new Map();
118
- for (const output of outputs) {
119
- const identity = await existingPathIdentity(options, output, `generated output '${output}'`);
120
- if (identity === void 0) continue;
121
- const input = inputIdentities.get(identity);
122
- if (input !== void 0) fail(`generated output '${output}' aliases declared input '${input}' on this filesystem.`);
123
- const priorOutput = outputIdentities.get(identity);
124
- if (priorOutput !== void 0) fail(`generated outputs '${priorOutput}' and '${output}' alias on this filesystem.`);
125
- outputIdentities.set(identity, output);
126
- }
127
- }
128
- async function ensureOutputParent(options, relativeValue) {
129
- const relativeParent = path.posix.dirname(normalizedRelativePath(relativeValue, "output path"));
130
- if (relativeParent === ".") return;
131
- let current = options.root;
132
- for (const segment of relativeParent.split("/")) {
133
- current = path.join(current, segment);
134
- try {
135
- const metadata = await lstat(current);
136
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
137
- } catch (error) {
138
- if (error.code !== "ENOENT") throw error;
139
- try {
140
- await mkdir(current);
141
- } catch (mkdirError) {
142
- if (mkdirError.code !== "EEXIST") throw mkdirError;
143
- const metadata = await lstat(current);
144
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail(`output directory '${relativeParent}' must contain only real directories.`);
145
- }
146
- }
147
- }
1962
+ function record(value, label) {
1963
+ if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${label} must be an object.`);
1964
+ return value;
148
1965
  }
149
- async function atomicRepositoryWrite(options, relativeValue, bytes) {
150
- const output = resolveInside(options.root, relativeValue, "output path");
151
- await ensureOutputParent(options, relativeValue);
152
- const temporary = `${output}.${process.pid}.${crypto.randomUUID()}.tmp`;
153
- try {
154
- await writeFile(temporary, bytes, {
155
- flag: "wx",
156
- mode: 420
157
- });
158
- await rename(temporary, output);
159
- } finally {
160
- await rm(temporary, { force: true });
161
- }
1966
+ function exact(value, keys, label) {
1967
+ const actual = Object.keys(value).sort();
1968
+ const expected = [...keys].sort();
1969
+ if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail(`${label} has an invalid shape.`);
162
1970
  }
163
1971
  function parseOptions(arguments_) {
164
- const [commandValue, ...rest] = arguments_;
165
- if (commandValue !== "build" && commandValue !== "verify") return fail("Usage: woj organizer collection <build|verify> [repository-root] [--index path] [--source path] [--managed path] [--managed-source path]");
1972
+ const [command, ...rest] = arguments_;
1973
+ if (command !== "build" && command !== "verify") fail("Usage: woj organizer collection <build|verify> [repository-root] [--source path]");
166
1974
  let root = ".";
167
- let indexPath = DEFAULT_INDEX_PATH;
168
- let sourcePath = DEFAULT_SOURCE_PATH;
169
- let managedPath;
170
- let managedSourcePath;
171
- let sawRoot = false;
1975
+ let sourcePath = SOURCE_PATH;
1976
+ let rootSeen = false;
172
1977
  for (let index = 0; index < rest.length; index += 1) {
173
1978
  const argument = rest[index];
174
- if (argument === "--index" || argument === "--source" || argument === "--managed" || argument === "--managed-source") {
1979
+ if (argument === "--source") {
175
1980
  const value = rest[index + 1];
176
- if (!value) return fail(`${argument} requires a path.`);
177
- if (argument === "--index") indexPath = portableOutputPath(value, "index path");
178
- else if (argument === "--source") sourcePath = normalizedRelativePath(value, "source path");
179
- else if (argument === "--managed") managedPath = portableOutputPath(value, "managed contract path");
180
- else managedSourcePath = normalizedRelativePath(value, "managed source path");
1981
+ if (!value) fail("--source requires a path.");
1982
+ sourcePath = normalizedPath(value, "source path");
181
1983
  index += 1;
182
- continue;
1984
+ } else if (argument?.startsWith("-")) fail(`Unknown option '${argument}'.`);
1985
+ else if (!argument || rootSeen) fail("Only one repository root may be provided.");
1986
+ else {
1987
+ root = argument;
1988
+ rootSeen = true;
183
1989
  }
184
- if (argument?.startsWith("-")) return fail(`Unknown option '${argument}'.`);
185
- if (sawRoot || !argument) return fail("Only one repository root may be provided.");
186
- root = argument;
187
- sawRoot = true;
188
- }
189
- if (commandValue !== "build" && managedSourcePath) return fail("--managed-source is only valid for build.");
190
- if (commandValue === "build" && managedPath && !managedSourcePath) return fail("--managed requires --managed-source when building.");
191
- if (commandValue === "build" && collisionKey(indexPath) === collisionKey(sourcePath)) return fail("--index and --source must not be the same input and output path.");
192
- if (commandValue === "build" && managedPath !== void 0 && managedSourcePath !== void 0 && collisionKey(managedPath) === collisionKey(managedSourcePath)) return fail("--managed and --managed-source must not be the same input and output path.");
1990
+ }
193
1991
  return {
194
- command: commandValue,
1992
+ command,
195
1993
  root: path.resolve(root),
196
- indexPath,
197
- sourcePath,
198
- ...managedPath ? { managedPath } : {},
199
- ...managedSourcePath ? { managedSourcePath } : {}
1994
+ sourcePath
200
1995
  };
201
1996
  }
202
- function isRecord(value) {
203
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1997
+ async function assertRoot(root) {
1998
+ const anchored = await canonicalizeSystemTemporaryPrefix(root);
1999
+ if (!await anchoredPathHasNoSymlink(anchored)) fail("repository root must not traverse a symbolic link.");
2000
+ const metadata = await lstat(anchored);
2001
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) fail("repository root must be a real directory.");
204
2002
  }
205
- function exactKeys(value, expected, label) {
206
- const actual = Object.keys(value).sort();
207
- const sortedExpected = [...expected].sort();
208
- if (JSON.stringify(actual) !== JSON.stringify(sortedExpected)) fail(`${label} must contain exactly: ${sortedExpected.join(", ")}.`);
209
- }
210
- function parseAuthoredCollection(value) {
211
- if (!isRecord(value)) return fail("collection/source.json must be an object.");
212
- exactKeys(value, [
213
- "schema",
214
- "localization",
215
- "problems"
216
- ], "collection source");
217
- if (value.schema !== SOURCE_SCHEMA) return fail(`collection source schema must be '${SOURCE_SCHEMA}'.`);
218
- if (!isRecord(value.localization)) return fail("collection localization must be an object.");
219
- exactKeys(value.localization, ["defaultLocale", "supportedLocales"], "collection localization");
220
- if (value.localization.defaultLocale !== "zh-TW" || JSON.stringify(value.localization.supportedLocales) !== JSON.stringify(["zh-TW", "en"])) return fail("collection localization must declare zh-TW followed by en.");
221
- if (!Array.isArray(value.problems) || value.problems.length < 1 || value.problems.length > 1e3) return fail("collection source must contain between 1 and 1000 problems.");
222
- return {
223
- schema: SOURCE_SCHEMA,
224
- localization: {
225
- defaultLocale: "zh-TW",
226
- supportedLocales: ["zh-TW", "en"]
227
- },
228
- problems: value.problems.map((problemValue, index) => {
229
- if (!isRecord(problemValue)) return fail(`collection source problem ${index + 1} must be an object.`);
230
- exactKeys(problemValue, ["statementPaths", "bundlePath"], `collection source problem ${index + 1}`);
231
- if (!isRecord(problemValue.statementPaths)) return fail(`problem ${index + 1} statementPaths must be an object.`);
232
- exactKeys(problemValue.statementPaths, ["zh-TW", "en"], `problem ${index + 1} statementPaths`);
233
- const statementPaths = {
234
- "zh-TW": normalizedRelativePath(problemValue.statementPaths["zh-TW"], `problem ${index + 1} zh-TW statement`),
235
- en: normalizedRelativePath(problemValue.statementPaths.en, `problem ${index + 1} English statement`)
236
- };
237
- if (!statementPaths["zh-TW"].endsWith(".md") || !statementPaths.en.endsWith(".md")) return fail(`problem ${index + 1} statements must be Markdown files.`);
238
- return {
239
- statementPaths,
240
- bundlePath: normalizedRelativePath(problemValue.bundlePath, `problem ${index + 1} bundle`)
241
- };
242
- })
243
- };
2003
+ function resolveInside(root, relative) {
2004
+ return path.join(root, ...normalizedPath(relative, "repository path").split("/"));
244
2005
  }
245
- function parseJson(bytes, label) {
246
- let text;
2006
+ async function readFile(root, relative, maximum, expected) {
2007
+ const absolute = resolveInside(root, relative);
2008
+ if (!await anchoredPathHasNoSymlink(absolute)) fail(`'${relative}' must not traverse a symbolic link.`);
2009
+ const handle = await open(absolute, constants.O_RDONLY | constants.O_NOFOLLOW);
247
2010
  try {
248
- text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
249
- } catch (error) {
250
- throw new Error(`${label} is not valid UTF-8.`, { cause: error });
2011
+ const metadata = await handle.stat();
2012
+ if (!metadata.isFile() || metadata.size < 1 || metadata.size > maximum || expected !== void 0 && metadata.size !== expected) fail(`'${relative}' has an invalid size.`);
2013
+ return new Uint8Array(await handle.readFile());
2014
+ } finally {
2015
+ await handle.close();
251
2016
  }
2017
+ }
2018
+ function parseJson(bytes, label) {
252
2019
  try {
253
- return JSON.parse(text);
2020
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
254
2021
  } catch (error) {
255
- throw new Error(`${label} is not valid JSON.`, { cause: error });
2022
+ throw new Error(`${label} must be valid UTF-8 JSON.`, { cause: error });
256
2023
  }
257
2024
  }
258
- async function sha256Hex(bytes) {
2025
+ async function sha256(bytes) {
259
2026
  const digest = await crypto.subtle.digest("SHA-256", bytes.slice().buffer);
260
- return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
261
- }
262
- async function validatePublishedCollection(options, strict) {
263
- const indexBytes = await readBoundedRepositoryFile(options, options.indexPath, "index path", INDEX_MAX_BYTES);
264
- const index = parseProblemCollectionIndex$1(parseJson(indexBytes, options.indexPath));
265
- await verifyProblemCollectionRevision$1(index);
266
- if (strict && !Buffer.from(indexBytes).equals(Buffer.from(canonicalJsonBytes(index)))) fail(`${options.indexPath} is not canonical; run woj organizer collection build.`);
267
- const indexDirectory = path.posix.dirname(options.indexPath);
268
- for (const entry of index.problems) {
269
- for (const [locale, statementPath] of Object.entries(entry.statementPaths)) if ((await readBoundedRepositoryFile(options, statementPath, `problem '${entry.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${entry.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
270
- const repositoryPath = path.posix.join(indexDirectory, entry.bundle.path);
271
- const bytes = await readBoundedRepositoryFile(options, repositoryPath, `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
272
- const problem = await verifyProblemBundleBytes$1(bytes, entry);
273
- if (strict && !Buffer.from(bytes).equals(Buffer.from(canonicalJsonBytes({
274
- schema: BROWSER_PROBLEM_SCHEMA$1,
275
- problem
276
- })))) fail(`${repositoryPath} is not canonical; run woj organizer collection build.`);
277
- }
278
- if (strict) await rejectUndeclaredContentAddressedBundles(options, index);
279
- if (options.managedPath) await validateManagedContract(options, index);
280
- return index;
281
- }
282
- async function validateManagedContract(options, index) {
283
- const managedPath = options.managedPath;
284
- if (!managedPath) fail("managed contract path is required.");
285
- const contractBytes = await readBoundedRepositoryFile(options, managedPath, "managed contract path", MANAGED_MAX_BYTES);
286
- const contract = parseManagedCollectionV2$1(contractBytes);
287
- if (contract.collectionRevision !== index.revision) fail("managed collection revision does not match collection/index.json.");
288
- if (JSON.stringify(contract.problems.map((problem) => problem.slug)) !== JSON.stringify(index.problems.map((problem) => problem.id))) fail("managed collection problems must exactly match index order.");
289
- const indexDirectory = path.posix.dirname(options.indexPath);
290
- for (const [position, publication] of contract.problems.entries()) {
291
- const entry = index.problems[position];
292
- const practiceBytes = await readBoundedRepositoryFile(options, path.posix.join(indexDirectory, entry.bundle.path), `problem '${entry.id}' bundle`, PUBLIC_BUNDLE_MAX_BYTES, entry.bundle.bytes);
293
- const practice = await verifyProblemBundleBytes$1(practiceBytes, entry);
294
- const contestBytes = await readPublishedObject(options, indexDirectory, publication.contestPublic, `contest-public '${publication.slug}'`, PUBLIC_BUNDLE_MAX_BYTES);
295
- const expectedContestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
296
- if (!Buffer.from(contestBytes).equals(Buffer.from(expectedContestBytes))) fail(`contest-public '${publication.slug}' is not the deterministic projection of its practice bundle.`);
297
- const packageBytes = await readPublishedObject(options, indexDirectory, publication.judgePackage, `judge package '${publication.slug}'`, JUDGE_PACKAGE_MAX_BYTES);
298
- const validatedPackage = await validateJudgePackage(packageBytes, {
299
- expectedBytes: publication.judgePackage.bytes,
300
- expectedSha256: publication.judgePackage.sha256,
301
- memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
302
- });
303
- if (JSON.stringify(validatedPackage.manifest.allowedProfiles) !== JSON.stringify(publication.allowedProfiles)) fail(`judge package '${publication.slug}' allowedProfiles disagree with collection/managed.json.`);
304
- assertJudgeDataMatchesPracticePublic(validatedPackage.judgeData, practice, Object.keys(publication.allowedProfiles));
305
- }
2027
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
306
2028
  }
307
- async function readPublishedObject(options, indexDirectory, object, label, maximumBytes) {
308
- const bytes = await readBoundedRepositoryFile(options, path.posix.join(indexDirectory, object.repositoryPath), label, maximumBytes, object.bytes);
309
- if (await sha256Hex(bytes) !== object.sha256) fail(`${label} failed integrity verification.`);
2029
+ async function readDeclared(root, object, label) {
2030
+ const bytes = await readFile(root, object.path, PACKAGE_MAX_BYTES, object.bytes);
2031
+ if (await sha256(bytes) !== object.sha256) fail(`${label} failed digest verification.`);
310
2032
  return bytes;
311
2033
  }
312
- async function rejectUndeclaredContentAddressedBundles(options, index) {
313
- const indexDirectory = path.posix.dirname(options.indexPath);
314
- const relativeDirectory = path.posix.join(indexDirectory, "problems");
315
- const directory = resolveInside(options.root, relativeDirectory, "bundle directory");
316
- if (!await anchoredPathHasNoSymlink(directory)) fail("bundle directory must not traverse a symbolic link.");
317
- const metadata = await lstat(directory);
318
- if (metadata.isSymbolicLink() || !metadata.isDirectory()) fail("bundle directory must be a real directory.");
319
- const declared = new Set(index.problems.map((entry) => path.posix.basename(entry.bundle.path)));
320
- const entries = await readdir(directory, { withFileTypes: true });
321
- const digestPattern = /\.[0-9a-f]{64}\.json$/;
322
- const undeclared = entries.filter((entry) => entry.isFile() && digestPattern.test(entry.name) && !declared.has(entry.name)).map((entry) => entry.name).sort();
323
- const unsafe = entries.filter((entry) => entry.isSymbolicLink() || !entry.isFile() && !entry.isDirectory()).map((entry) => entry.name).sort();
324
- if (unsafe.length > 0) fail(`bundle directory contains unsafe entries: ${unsafe.join(", ")}.`);
325
- if (undeclared.length > 0) fail(`undeclared content-addressed bundles: ${undeclared.join(", ")}.`);
326
- }
327
- function assertGeneratedSize(bytes, maximumBytes, label) {
328
- if (bytes.byteLength > maximumBytes) fail(`${label} exceeds its allowed byte limit.`);
329
- }
330
- async function prepareCollection(options) {
331
- const source = parseAuthoredCollection(parseJson(await readBoundedRepositoryFile(options, options.sourcePath, "source path", COLLECTION_SOURCE_MAX_BYTES), options.sourcePath));
332
- const indexDirectory = path.posix.dirname(options.indexPath);
333
- const entries = [];
334
- const authoredProblems = [];
335
- const practiceProblems = [];
336
- const inputs = /* @__PURE__ */ new Set([options.sourcePath]);
337
- const outputs = [];
338
- for (const [position, authored] of source.problems.entries()) {
339
- inputs.add(authored.bundlePath);
340
- const authoredBytes = await readBoundedRepositoryFile(options, authored.bundlePath, `problem ${position + 1} source bundle`, AUTHORING_BUNDLE_MAX_BYTES);
341
- const problem = parseStandaloneProblemBundle$1(parseJson(authoredBytes, authored.bundlePath));
342
- if (problem.number !== position + 1) fail(`problem '${problem.id}' must have number ${position + 1}.`);
343
- for (const [locale, statementPath] of Object.entries(authored.statementPaths)) {
344
- inputs.add(statementPath);
345
- if ((await readBoundedRepositoryFile(options, statementPath, `problem '${problem.id}' ${locale} statement`, STATEMENT_MAX_BYTES)).byteLength < 1) fail(`problem '${problem.id}' ${locale} statement must contain between 1 byte and 2 MiB.`);
346
- }
347
- authoredProblems.push(problem);
348
- const practice = derivePracticePublic(problem);
349
- practiceProblems.push(practice);
350
- const bundleBytes = canonicalJsonBytes({
351
- schema: BROWSER_PROBLEM_SCHEMA$1,
352
- problem: practice
353
- });
354
- assertGeneratedSize(bundleBytes, PUBLIC_BUNDLE_MAX_BYTES, `problem '${problem.id}' public bundle`);
355
- const digest = await sha256Hex(bundleBytes);
356
- const bundleName = `${String(problem.number).padStart(3, "0")}-${problem.id}.${digest}.json`;
357
- outputs.push({
358
- path: path.posix.join(indexDirectory, "problems", bundleName),
359
- label: `problem '${problem.id}' public bundle output`,
360
- bytes: bundleBytes
361
- });
362
- entries.push({
363
- id: problem.id,
364
- number: problem.number,
365
- title: problem.title,
366
- trackId: problem.trackId,
367
- track: problem.track,
368
- statementPaths: authored.statementPaths,
369
- difficulty: problem.difficulty,
370
- tags: problem.tags,
371
- caseCount: practice.judgeCases.length,
372
- bundle: {
373
- path: `problems/${bundleName}`,
374
- sha256: digest,
375
- bytes: bundleBytes.byteLength
376
- }
377
- });
378
- }
379
- const withoutRevision = {
380
- schema: BROWSER_COLLECTION_SCHEMA$1,
381
- problemSchema: BROWSER_PROBLEM_SCHEMA$1,
382
- localization: source.localization,
383
- problems: entries
384
- };
385
- const index = parseProblemCollectionIndex$1({
386
- ...withoutRevision,
387
- revision: await problemCollectionRevision$1(withoutRevision)
388
- });
389
- const indexBytes = canonicalJsonBytes(index);
390
- assertGeneratedSize(indexBytes, INDEX_MAX_BYTES, "collection index output");
391
- outputs.push({
392
- path: options.indexPath,
393
- label: "collection index output",
394
- bytes: indexBytes
395
- });
396
- return {
397
- index,
398
- authoredProblems,
399
- practiceProblems,
400
- inputs,
401
- outputs
402
- };
403
- }
404
- async function readDeclaredManagedSourceObject(options, object, label, inputs) {
405
- inputs.add(object.path);
406
- const bytes = await readBoundedRepositoryFile(options, object.path, label, JUDGE_PACKAGE_MAX_BYTES, object.bytes);
407
- if (await sha256Hex(bytes) !== object.sha256) fail(`${label} failed declared size or digest verification.`);
408
- return bytes;
409
- }
410
- async function managedJudgeInput(options, problem, inputs) {
411
- if (problem.judge.kind === "text") return { kind: "text" };
412
- const artifact = await readDeclaredManagedSourceObject(options, problem.judge.artifact, `${problem.judge.kind} artifact '${problem.slug}'`, inputs);
2034
+ async function judgeInput(root, judge) {
2035
+ if (judge.kind === "text") return { kind: "text" };
2036
+ const artifact = await readDeclared(root, judge.artifact, `${judge.kind} artifact`);
413
2037
  const assets = [];
414
- for (const asset of problem.judge.assets) assets.push({
2038
+ for (const asset of judge.assets) assets.push({
415
2039
  guestPath: asset.guestPath,
416
- contents: await readDeclaredManagedSourceObject(options, asset, `${problem.judge.kind} asset '${problem.slug}/${asset.path}'`, inputs)
2040
+ contents: await readDeclared(root, asset, `${judge.kind} asset`)
417
2041
  });
418
- return problem.judge.kind === "checker" ? {
2042
+ return judge.kind === "checker" ? {
419
2043
  kind: "checker",
420
- runtimeProfile: problem.judge.artifact.runtimeProfile,
2044
+ runtimeProfile: judge.artifact.runtimeProfile,
421
2045
  artifact,
422
2046
  assets,
423
- args: problem.judge.args
2047
+ args: judge.args
424
2048
  } : {
425
2049
  kind: "interactive",
426
- runtimeProfile: problem.judge.artifact.runtimeProfile,
2050
+ runtimeProfile: judge.artifact.runtimeProfile,
427
2051
  artifact,
428
2052
  assets,
429
- args: problem.judge.args,
430
- inputPath: problem.judge.inputPath
2053
+ args: judge.args,
2054
+ inputPath: judge.inputPath
2055
+ };
2056
+ }
2057
+ function localized(value, label, empty) {
2058
+ const input = record(value, label);
2059
+ exact(input, ["en", "zh-TW"], label);
2060
+ if (typeof input.en !== "string" || typeof input["zh-TW"] !== "string" || !empty && (!input.en.trim() || !input["zh-TW"].trim())) fail(`${label} must contain zh-TW and en strings.`);
2061
+ return {
2062
+ "zh-TW": input["zh-TW"],
2063
+ en: input.en
2064
+ };
2065
+ }
2066
+ function parseAuthoringSource(value) {
2067
+ const source = record(value, "repository authoring source");
2068
+ exact(source, [
2069
+ "contests",
2070
+ "problems",
2071
+ "schema"
2072
+ ], "repository authoring source");
2073
+ if (source.schema !== AUTHORING_SCHEMA || !Array.isArray(source.problems)) fail(`repository authoring schema must be '${AUTHORING_SCHEMA}'.`);
2074
+ const judgeSource = parseRepositoryAuthoringJudges({
2075
+ schema: "wasm-oj-platform/repository-authoring-judges/v1",
2076
+ problems: source.problems.map((candidate) => {
2077
+ const problem = record(candidate, "authoring problem");
2078
+ return {
2079
+ slug: problem.slug,
2080
+ allowedProfiles: problem.allowedProfiles,
2081
+ judge: problem.judge
2082
+ };
2083
+ })
2084
+ });
2085
+ const problems = source.problems.map((candidate, index) => {
2086
+ const problem = record(candidate, `authoring problem ${index + 1}`);
2087
+ exact(problem, [
2088
+ "allowedProfiles",
2089
+ "authoringBundle",
2090
+ "judge",
2091
+ "order",
2092
+ "practiceEnabled",
2093
+ "slug",
2094
+ "summary",
2095
+ "title"
2096
+ ], `authoring problem ${index + 1}`);
2097
+ if (!Number.isSafeInteger(problem.order) || problem.order !== index + 1 || typeof problem.practiceEnabled !== "boolean") fail(`authoring problem ${index + 1} order or practiceEnabled is invalid.`);
2098
+ const parsedJudge = judgeSource.problems[index];
2099
+ return {
2100
+ slug: parsedJudge.slug,
2101
+ order: problem.order,
2102
+ title: localized(problem.title, `authoring problem '${parsedJudge.slug}' title`, false),
2103
+ summary: localized(problem.summary, `authoring problem '${parsedJudge.slug}' summary`, true),
2104
+ practiceEnabled: problem.practiceEnabled,
2105
+ authoringBundle: normalizedPath(problem.authoringBundle, `authoring problem '${parsedJudge.slug}' bundle`),
2106
+ allowedProfiles: parsedJudge.allowedProfiles,
2107
+ judge: parsedJudge.judge
2108
+ };
2109
+ });
2110
+ if (!Array.isArray(source.contests)) fail("repository authoring source contests must be an array.");
2111
+ const contests = parseRepositoryContestsValue({
2112
+ schema: "wasm-oj-platform/contests/v2",
2113
+ contests: source.contests.map((candidate, index) => {
2114
+ const contest = record(candidate, `authoring contest ${index + 1}`);
2115
+ exact(contest, [
2116
+ "accessMode",
2117
+ "description",
2118
+ "rules",
2119
+ "slug",
2120
+ "status",
2121
+ "title"
2122
+ ], `authoring contest ${index + 1}`);
2123
+ const rulesInput = record(contest.rules, `authoring contest ${index + 1} rules`);
2124
+ const rules = Object.hasOwn(rulesInput, "preset") ? expandContestRulesPreset(parseContestRulesPreset(rulesInput, `authoring contest ${index + 1} rules`)) : parseContestRules(rulesInput, `authoring contest ${index + 1} rules`);
2125
+ return {
2126
+ ...contest,
2127
+ rules
2128
+ };
2129
+ })
2130
+ }).contests;
2131
+ const known = new Set(problems.map((problem) => problem.slug));
2132
+ for (const contest of contests) for (const problem of contest.rules.problems) if (!known.has(problem.slug)) fail(`contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
2133
+ return {
2134
+ problems,
2135
+ contests
431
2136
  };
432
2137
  }
433
- async function prepareManagedCollection(options, built) {
434
- const managedSourcePath = options.managedSourcePath;
435
- if (!managedSourcePath) fail("managed source path is required.");
436
- const inputs = /* @__PURE__ */ new Set([managedSourcePath]);
2138
+ async function atomicWrite(root, output) {
2139
+ const absolute = resolveInside(root, output.path);
2140
+ const parent = path.dirname(absolute);
2141
+ await mkdir(parent, { recursive: true });
2142
+ if (!await anchoredPathHasNoSymlink(parent)) fail(`output parent for '${output.path}' must not traverse a symbolic link.`);
2143
+ const temporary = `${absolute}.${process.pid}.${crypto.randomUUID()}.tmp`;
2144
+ try {
2145
+ await writeFile(temporary, output.bytes, {
2146
+ flag: "wx",
2147
+ mode: 420
2148
+ });
2149
+ await rename(temporary, absolute);
2150
+ } finally {
2151
+ await rm(temporary, { force: true });
2152
+ }
2153
+ }
2154
+ async function build(options) {
2155
+ const source = parseAuthoringSource(parseJson(await readFile(options.root, options.sourcePath, JSON_MAX_BYTES), options.sourcePath));
2156
+ const problems = [];
437
2157
  const outputs = [];
438
- const sourceBytes = await readBoundedRepositoryFile(options, managedSourcePath, "managed source path", MANAGED_MAX_BYTES);
439
- const source = parseManagedCollectionSource$1(parseJson(sourceBytes, managedSourcePath));
440
- if (JSON.stringify(source.problems.map((problem) => problem.slug)) !== JSON.stringify(built.index.problems.map((problem) => problem.id))) fail("managed source problems must exactly match collection/index.json order.");
441
- const indexDirectory = path.posix.dirname(options.indexPath);
442
- const publicationDirectory = path.posix.join(indexDirectory, "managed");
443
- const publications = [];
444
- for (const [position, sourceProblem] of source.problems.entries()) {
445
- const entry = built.index.problems[position];
446
- const practice = built.practiceProblems[position];
447
- const authored = built.authoredProblems[position];
448
- if (!authored || authored.id !== entry.id) fail(`authoring source for '${entry.id}' is unavailable.`);
449
- const contestBytes = contestPublicProjectionBytes(practice, entry.bundle.sha256);
450
- assertGeneratedSize(contestBytes, PUBLIC_BUNDLE_MAX_BYTES, `contest-public '${entry.id}' output`);
451
- const contestSha256 = await sha256Hex(contestBytes);
452
- const contestName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${contestSha256}.contest.json`;
453
- outputs.push({
454
- path: path.posix.join(publicationDirectory, contestName),
455
- label: `contest-public '${entry.id}' output`,
456
- bytes: contestBytes
2158
+ for (const authored of source.problems) {
2159
+ const bundle = parseStandaloneProblemBundle$1(parseJson(await readFile(options.root, authored.authoringBundle, PACKAGE_MAX_BYTES), authored.authoringBundle));
2160
+ if (bundle.id !== authored.slug || bundle.number !== authored.order || !sameLocalizedText(bundle.title, authored.title)) fail(`authoring bundle '${authored.slug}' disagrees with its declarative identity.`);
2161
+ const practice = derivePracticePublic(bundle);
2162
+ const practiceBytes = canonicalJsonBytes({
2163
+ schema: BROWSER_PROBLEM_SCHEMA$1,
2164
+ problem: practice
457
2165
  });
2166
+ const practiceDigest = await sha256(practiceBytes);
2167
+ const contestBytes = contestPublicProjectionBytes(practice);
458
2168
  const encoded = await encodeJudgePackage({
459
- judgeData: deriveJudgeData(authored, Object.keys(sourceProblem.allowedProfiles)),
460
- allowedProfiles: sourceProblem.allowedProfiles,
461
- judge: await managedJudgeInput(options, sourceProblem, inputs)
2169
+ judgeData: deriveJudgeData(bundle, Object.keys(authored.allowedProfiles)),
2170
+ allowedProfiles: authored.allowedProfiles,
2171
+ judge: await judgeInput(options.root, authored.judge)
462
2172
  });
463
- assertGeneratedSize(encoded.bytes, JUDGE_PACKAGE_MAX_BYTES, `judge package '${entry.id}' output`);
464
- const validated = await validateJudgePackage(encoded.bytes, {
465
- expectedBytes: encoded.bytes.byteLength,
466
- expectedSha256: encoded.executionSemanticSha256,
467
- memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
468
- });
469
- assertJudgeDataMatchesPracticePublic(validated.judgeData, practice, Object.keys(sourceProblem.allowedProfiles));
470
- const packageName = `${String(entry.number).padStart(3, "0")}-${entry.id}.${encoded.executionSemanticSha256}.wasmojjudge`;
2173
+ const base = `collection/problems/${String(authored.order).padStart(3, "0")}-${authored.slug}`;
2174
+ const practicePath = `${base}.practice.json`;
2175
+ const contestPath = `${base}.contest.json`;
2176
+ const judgePath = `${base}.wasmojjudge`;
471
2177
  outputs.push({
472
- path: path.posix.join(publicationDirectory, packageName),
473
- label: `judge package '${entry.id}' output`,
2178
+ path: practicePath,
2179
+ bytes: practiceBytes
2180
+ }, {
2181
+ path: contestPath,
2182
+ bytes: contestBytes
2183
+ }, {
2184
+ path: judgePath,
474
2185
  bytes: encoded.bytes
475
2186
  });
476
- publications.push({
477
- slug: entry.id,
478
- allowedProfiles: sourceProblem.allowedProfiles,
479
- contestPublic: {
480
- repositoryPath: `managed/${contestName}`,
2187
+ problems.push({
2188
+ slug: authored.slug,
2189
+ order: authored.order,
2190
+ title: authored.title,
2191
+ summary: authored.summary,
2192
+ practiceEnabled: authored.practiceEnabled,
2193
+ practiceBundle: {
2194
+ path: practicePath,
2195
+ bytes: practiceBytes.byteLength,
2196
+ sha256: practiceDigest
2197
+ },
2198
+ contestBundle: {
2199
+ path: contestPath,
481
2200
  bytes: contestBytes.byteLength,
482
- sha256: contestSha256
2201
+ sha256: await sha256(contestBytes)
483
2202
  },
484
2203
  judgePackage: {
485
- repositoryPath: `managed/${packageName}`,
2204
+ path: judgePath,
486
2205
  bytes: encoded.bytes.byteLength,
487
2206
  sha256: encoded.executionSemanticSha256
488
2207
  }
489
2208
  });
490
2209
  }
491
- const contract = parseManagedCollectionV2$1(canonicalJsonBytes({
492
- schema: MANAGED_COLLECTION_SCHEMA$1,
493
- collectionRevision: built.index.revision,
494
- problems: publications
495
- }));
496
- const managedPath = options.managedPath ?? path.posix.join(indexDirectory, "managed.json");
497
- const contractBytes = canonicalJsonBytes(contract);
498
- assertGeneratedSize(contractBytes, MANAGED_MAX_BYTES, "managed contract output");
2210
+ const problemManifest = {
2211
+ schema: "wasm-oj-platform/problems/v1",
2212
+ problems
2213
+ };
2214
+ parseRepositoryProblemsValue(problemManifest);
2215
+ const contestManifest = {
2216
+ schema: "wasm-oj-platform/contests/v2",
2217
+ contests: source.contests
2218
+ };
2219
+ parseRepositoryContestsValue(contestManifest);
2220
+ const root = {
2221
+ schema: "wasm-oj-platform/repository/v1",
2222
+ problems: PROBLEMS_PATH,
2223
+ contests: CONTESTS_PATH
2224
+ };
2225
+ parseRepositoryRootValue(root);
499
2226
  outputs.push({
500
- path: managedPath,
501
- label: "managed contract output",
502
- bytes: contractBytes
2227
+ path: PROBLEMS_PATH,
2228
+ bytes: canonicalJsonBytes(problemManifest)
2229
+ }, {
2230
+ path: CONTESTS_PATH,
2231
+ bytes: canonicalJsonBytes(contestManifest)
2232
+ }, {
2233
+ path: ROOT_PATH,
2234
+ bytes: canonicalJsonBytes(root)
503
2235
  });
504
- return {
505
- inputs,
506
- outputs
507
- };
2236
+ for (const output of outputs) await atomicWrite(options.root, output);
2237
+ return problems.length;
508
2238
  }
509
- function pathsOverlap(left, right) {
510
- const leftKey = collisionKey(left);
511
- const rightKey = collisionKey(right);
512
- return leftKey === rightKey || leftKey.startsWith(`${rightKey}/`) || rightKey.startsWith(`${leftKey}/`);
2239
+ async function verifiedObject(root, descriptor, maximum) {
2240
+ const bytes = await readFile(root, descriptor.path, maximum, descriptor.bytes);
2241
+ if (await sha256(bytes) !== descriptor.sha256) fail(`'${descriptor.path}' failed digest verification.`);
2242
+ return bytes;
513
2243
  }
514
- async function writePreparedCollection(options, inputs, outputs) {
515
- const outputPaths = /* @__PURE__ */ new Set();
516
- for (const output of outputs) {
517
- portableOutputPath(output.path, output.label);
518
- const key = collisionKey(output.path);
519
- if (outputPaths.has(key)) fail(`generated output '${output.path}' has a case-folded or normalized collision.`);
520
- outputPaths.add(key);
521
- for (const input of inputs) if (pathsOverlap(input, output.path)) fail(`generated output '${output.path}' overlaps declared input '${input}'.`);
2244
+ async function verify(options) {
2245
+ const root = parseRepositoryRootValue(parseJson(await readFile(options.root, ROOT_PATH, JSON_MAX_BYTES), ROOT_PATH));
2246
+ const problems = parseRepositoryProblemsValue(parseJson(await readFile(options.root, root.problems, JSON_MAX_BYTES), root.problems));
2247
+ const contests = parseRepositoryContestsValue(parseJson(await readFile(options.root, root.contests, JSON_MAX_BYTES), root.contests));
2248
+ const known = new Set(problems.problems.map((problem) => problem.slug));
2249
+ for (const contest of contests.contests) for (const problem of contest.rules.problems) if (!known.has(problem.slug)) fail(`contest '${contest.slug}' references unknown problem '${problem.slug}'.`);
2250
+ for (const problem of problems.problems) {
2251
+ const practiceBytes = await verifiedObject(options.root, problem.practiceBundle, PUBLIC_MAX_BYTES);
2252
+ const practice = parseStandaloneProblemBundle$1(parseJson(practiceBytes, problem.practiceBundle.path));
2253
+ if (practice.id !== problem.slug || practice.number !== problem.order || !sameLocalizedText(practice.title, problem.title)) fail(`practice bundle '${problem.slug}' disagrees with problems.json.`);
2254
+ const contest = parseContestPublicProblemProjection(parseJson(await verifiedObject(options.root, problem.contestBundle, PUBLIC_MAX_BYTES), problem.contestBundle.path));
2255
+ const expectedContest = canonicalJsonBytes(deriveContestPublic(practice));
2256
+ if (!Buffer.from(canonicalJsonBytes(contest.problem)).equals(Buffer.from(expectedContest))) fail(`contest bundle '${problem.slug}' is not the public projection of its practice bundle.`);
2257
+ const packageBytes = await verifiedObject(options.root, problem.judgePackage, PACKAGE_MAX_BYTES);
2258
+ const validated = await validateJudgePackage(packageBytes, {
2259
+ expectedBytes: problem.judgePackage.bytes,
2260
+ expectedSha256: problem.judgePackage.sha256,
2261
+ memoryLimitBytes: Math.max(...practice.scoring.policies.map((policy) => policy.limits.memoryLimitBytes))
2262
+ });
2263
+ assertJudgeDataMatchesPracticePublic(validated.judgeData, practice, Object.keys(validated.manifest.allowedProfiles));
522
2264
  }
523
- await assertNoFilesystemAliases(options, inputs, outputs.map((output) => output.path));
524
- await Promise.all(outputs.map((output) => assertOutputPathSafe(options, output.path, output.label)));
525
- for (const output of outputs) await atomicRepositoryWrite(options, output.path, output.bytes);
2265
+ return problems.problems.length;
526
2266
  }
527
2267
  async function runCollectionCli(arguments_) {
528
2268
  const options = parseOptions(arguments_);
529
- await assertRepositoryRoot(options.root);
530
- let index;
531
- if (options.command === "build") {
532
- const initialManagedOutput = options.managedSourcePath ? options.managedPath ?? path.posix.join(path.posix.dirname(options.indexPath), "managed.json") : void 0;
533
- await assertNoFilesystemAliases(options, /* @__PURE__ */ new Set([options.sourcePath, ...options.managedSourcePath ? [options.managedSourcePath] : []]), [options.indexPath, ...initialManagedOutput ? [initialManagedOutput] : []]);
534
- const built = await prepareCollection(options);
535
- index = built.index;
536
- const managed = options.managedSourcePath ? await prepareManagedCollection(options, built) : void 0;
537
- await writePreparedCollection(options, /* @__PURE__ */ new Set([...built.inputs, ...managed?.inputs ?? []]), [...built.outputs, ...managed?.outputs ?? []]);
538
- } else index = await validatePublishedCollection(options, true);
539
- process.stdout.write(`${options.command} ok: ${index.problems.length} problems, revision ${index.revision}\n`);
2269
+ await assertRoot(options.root);
2270
+ const count = options.command === "build" ? await build(options) : await verify(options);
2271
+ process.stdout.write(`${options.command} ok: ${count} problems\n`);
540
2272
  }
541
2273
  if (process.argv[1] ? import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href : false) runCollectionCli(process.argv.slice(2)).catch((error) => {
542
2274
  process.stderr.write(`woj organizer collection: ${error instanceof Error ? error.message : String(error)}\n`);
543
2275
  process.exitCode = 1;
544
2276
  });
545
2277
  //#endregion
546
- export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, MANAGED_COLLECTION_SCHEMA, parseManagedCollectionContract, parseManagedCollectionSource, parseManagedCollectionV2, parseManagedCollectionValueV2, parseProblemCollectionIndex, parseStandaloneProblemBundle, problemCollectionRevision, runCollectionCli, verifyProblemBundleBytes, verifyProblemCollectionRevision };
2278
+ export { BROWSER_COLLECTION_SCHEMA, BROWSER_PROBLEM_SCHEMA, CONTESTS_SCHEMA, PROBLEMS_SCHEMA, REPOSITORY_AUTHORING_JUDGES_SCHEMA, REPOSITORY_SCHEMA, parseProblemCollectionIndex, parseRepositoryAuthoringJudges, parseRepositoryContests, parseRepositoryContestsValue, parseRepositoryProblems, parseRepositoryProblemsValue, parseRepositoryRoot, parseRepositoryRootValue, parseStandaloneProblemBundle, problemCollectionRevision, runCollectionCli, validateRepositoryCatalog, verifyProblemBundleBytes, verifyProblemCollectionRevision };