@joystyk/content 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/NOTICE +36 -0
- package/README.md +113 -2
- package/dist/banks.d.ts +14 -0
- package/dist/banks.js +55 -0
- package/dist/banks.js.map +1 -0
- package/dist/extract-json.d.ts +12 -0
- package/dist/extract-json.js +59 -0
- package/dist/extract-json.js.map +1 -0
- package/dist/filter.d.ts +56 -0
- package/dist/filter.js +127 -0
- package/dist/filter.js.map +1 -0
- package/dist/games.d.ts +9 -0
- package/dist/games.js +40 -0
- package/dist/games.js.map +1 -0
- package/dist/hash.d.ts +4 -0
- package/dist/hash.js +17 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +16 -0
- package/dist/log.d.ts +26 -0
- package/dist/log.js +58 -0
- package/dist/log.js.map +1 -0
- package/dist/model.d.ts +77 -0
- package/dist/model.js +268 -0
- package/dist/model.js.map +1 -0
- package/dist/packs.d.ts +144 -0
- package/dist/packs.js +466 -0
- package/dist/packs.js.map +1 -0
- package/dist/pipeline.d.ts +252 -0
- package/dist/pipeline.js +558 -0
- package/dist/pipeline.js.map +1 -0
- package/dist/publish.d.ts +21 -0
- package/dist/publish.js +56 -0
- package/dist/publish.js.map +1 -0
- package/dist/ranked.d.ts +12 -0
- package/dist/ranked.js +40 -0
- package/dist/ranked.js.map +1 -0
- package/dist/screen.d.ts +25 -0
- package/dist/screen.js +46 -0
- package/dist/screen.js.map +1 -0
- package/dist/store.d.ts +136 -0
- package/dist/store.js +300 -0
- package/dist/store.js.map +1 -0
- package/dist/text.d.ts +31 -0
- package/dist/text.js +111 -0
- package/dist/text.js.map +1 -0
- package/dist/vocabulary.d.ts +16 -0
- package/dist/vocabulary.js +44 -0
- package/dist/vocabulary.js.map +1 -0
- package/lists/SCOWL-COPYRIGHT +252 -0
- package/lists/blocklist.txt +214 -0
- package/lists/common-extra.txt +13 -0
- package/lists/scowl-common.txt +70763 -0
- package/lists/scowl-ranked.txt +72076 -0
- package/lists/scowl-vocabulary.txt +55636 -0
- package/lists/sensitive.txt +280 -0
- package/package.json +53 -3
package/dist/pipeline.js
ADDED
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import { withDailyBank } from "./banks.js";
|
|
2
|
+
import { contentHash, stableIndex } from "./hash.js";
|
|
3
|
+
import { silentLogger } from "./log.js";
|
|
4
|
+
import { ENDPOINT_ERRORS } from "./model.js";
|
|
5
|
+
import { SCREEN_PROMPT_VERSION, modelScreen } from "./screen.js";
|
|
6
|
+
import { addDays, puzzleId, releaseInstant, utcDate } from "@joystyk/daily";
|
|
7
|
+
import { err, ok } from "@joystyk/contracts";
|
|
8
|
+
//#region src/pipeline.ts
|
|
9
|
+
/** @internal Shared with packs.ts. */
|
|
10
|
+
var defaultSleep = (ms, signal) => new Promise((resolve) => {
|
|
11
|
+
if (ms <= 0 || signal?.aborted === true) return resolve();
|
|
12
|
+
const timer = setTimeout(done, ms);
|
|
13
|
+
function done() {
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
signal?.removeEventListener("abort", done);
|
|
16
|
+
resolve();
|
|
17
|
+
}
|
|
18
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
19
|
+
});
|
|
20
|
+
/** @internal Shared with packs.ts. */
|
|
21
|
+
var callReport = (call, error) => ({
|
|
22
|
+
name: call.name,
|
|
23
|
+
ok: error === null,
|
|
24
|
+
error: error === null ? null : `${error.kind}: ${error.message}`,
|
|
25
|
+
latencyMs: call.latencyMs,
|
|
26
|
+
requests: call.requests,
|
|
27
|
+
mode: call.mode,
|
|
28
|
+
promptTokens: call.usage?.promptTokens ?? null,
|
|
29
|
+
completionTokens: call.usage?.completionTokens ?? null,
|
|
30
|
+
totalTokens: call.usage?.totalTokens ?? null
|
|
31
|
+
});
|
|
32
|
+
/** Wraps a model client so every call, successful or not, lands in `calls`. */
|
|
33
|
+
/** @internal Shared with packs.ts. */
|
|
34
|
+
var recording = (model, calls) => ({
|
|
35
|
+
model: model.model,
|
|
36
|
+
complete: async (request) => {
|
|
37
|
+
const result = await model.complete(request);
|
|
38
|
+
calls.push(result.ok ? callReport(result.value.call, null) : callReport(result.error.call, result.error));
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
/** @internal Shared with packs.ts. */
|
|
43
|
+
var outcomeOf = (error) => error.kind === "aborted" ? "aborted" : ENDPOINT_ERRORS.has(error.kind) ? "endpoint" : error.kind === "schema" ? "schema" : "reply";
|
|
44
|
+
/** @internal Shared with packs.ts. */
|
|
45
|
+
var issuesOf = (error) => [error.message, ...error.issues ?? []];
|
|
46
|
+
/** @internal Shared with packs.ts. */
|
|
47
|
+
var decodeConfig = async (schema, value) => {
|
|
48
|
+
const result = await schema["~standard"].validate(value);
|
|
49
|
+
if (result.issues === void 0) return ok(result.value);
|
|
50
|
+
return err(result.issues.map((issue) => {
|
|
51
|
+
const path = (issue.path ?? []).map((segment) => typeof segment === "object" ? String(segment.key) : String(segment)).join(".");
|
|
52
|
+
return path === "" ? issue.message : `${path}: ${issue.message}`;
|
|
53
|
+
}));
|
|
54
|
+
};
|
|
55
|
+
/** Answers within the generator’s window, other than this slot’s non-withdrawn item. */
|
|
56
|
+
var usedNear = (store, gameId, date, puzzleId$1, days = 365) => store.usedAnswers(gameId, addDays(date, -days), addDays(date, days)).filter((row) => puzzleId(gameId, row.date) !== puzzleId$1 || row.status === "withdrawn");
|
|
57
|
+
/** Nearby text keys decoded through the game schema; corrections on the same date may retain clues. */
|
|
58
|
+
var recentContentKeys = async (store, generator, date) => {
|
|
59
|
+
if (generator.repeatKeys === void 0 || generator.repeatWindowDays === void 0) return [];
|
|
60
|
+
const out = [];
|
|
61
|
+
for (const row of store.versions(generator.gameId, addDays(date, -generator.repeatWindowDays), addDays(date, generator.repeatWindowDays))) {
|
|
62
|
+
if (row.date === date || row.config === null) continue;
|
|
63
|
+
const config = await decodeConfig(generator.configSchema, row.config);
|
|
64
|
+
if (!config.ok) throw new Error(`Cannot check repeat history: stored ${row.puzzleId} version ${String(row.id)} is malformed.`);
|
|
65
|
+
for (const key of generator.repeatKeys(config.value)) out.push({
|
|
66
|
+
date: row.date,
|
|
67
|
+
key
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Checks a config the way every item is checked: the schema, the game validators, the
|
|
74
|
+
* duplicate rule and the word lists. The model screen is separate (it needs the endpoint).
|
|
75
|
+
*/
|
|
76
|
+
var checkDailyConfig = async (generator, value, context) => {
|
|
77
|
+
const decoded = await decodeConfig(generator.configSchema, value);
|
|
78
|
+
if (!decoded.ok) return {
|
|
79
|
+
outcome: "schema",
|
|
80
|
+
issues: decoded.error,
|
|
81
|
+
config: null,
|
|
82
|
+
filter: null
|
|
83
|
+
};
|
|
84
|
+
const config = decoded.value;
|
|
85
|
+
if (generator.modelFree === true && generator.texts(config).length > 0) return {
|
|
86
|
+
outcome: "invalid",
|
|
87
|
+
issues: ["A model-free generator must contain no generated player-visible text."],
|
|
88
|
+
config,
|
|
89
|
+
filter: null
|
|
90
|
+
};
|
|
91
|
+
const issues = [...generator.validate(config, {
|
|
92
|
+
date: context.date,
|
|
93
|
+
puzzleId: context.puzzleId,
|
|
94
|
+
inputs: context.inputs
|
|
95
|
+
})];
|
|
96
|
+
if (issues.length > 0) return {
|
|
97
|
+
outcome: "invalid",
|
|
98
|
+
issues,
|
|
99
|
+
config,
|
|
100
|
+
filter: null
|
|
101
|
+
};
|
|
102
|
+
const answer = generator.answerOf(config);
|
|
103
|
+
const clash = answer === null ? void 0 : context.used.find((row) => row.answer === answer);
|
|
104
|
+
if (clash !== void 0) return {
|
|
105
|
+
outcome: "duplicate",
|
|
106
|
+
issues: [`The answer was already used on ${clash.date}.`],
|
|
107
|
+
config,
|
|
108
|
+
filter: null
|
|
109
|
+
};
|
|
110
|
+
const repeat = generator.repeatKeys?.(config).map((key) => context.recentKeys?.find((row) => row.key === key)).find((row) => row !== void 0);
|
|
111
|
+
if (repeat !== void 0) return {
|
|
112
|
+
outcome: "duplicate",
|
|
113
|
+
issues: [`Puzzle text was already used on ${repeat.date}: "${repeat.key}".`],
|
|
114
|
+
config,
|
|
115
|
+
filter: null
|
|
116
|
+
};
|
|
117
|
+
const filtered = context.filter.check(generator.texts(config));
|
|
118
|
+
if (!filtered.ok) {
|
|
119
|
+
const words = [...new Set(filtered.hits.map((hit) => hit.term))].join(", ");
|
|
120
|
+
return {
|
|
121
|
+
outcome: "filtered",
|
|
122
|
+
issues: [`Text in ${[...new Set(filtered.hits.map((hit) => hit.path))].join(", ")} touches a topic or word that isn't allowed (${words}).`],
|
|
123
|
+
config,
|
|
124
|
+
filter: filtered
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
outcome: "validated",
|
|
129
|
+
issues: [],
|
|
130
|
+
config,
|
|
131
|
+
filter: filtered
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
var runPipeline = async (options) => {
|
|
135
|
+
const { store, filter, publisher } = options;
|
|
136
|
+
const now = options.now ?? Date.now;
|
|
137
|
+
const log = options.log ?? silentLogger;
|
|
138
|
+
const sleep = options.sleep ?? defaultSleep;
|
|
139
|
+
const maxAttempts = options.maxAttempts ?? 3;
|
|
140
|
+
const breakerThreshold = options.breakerThreshold ?? 5;
|
|
141
|
+
const deadlineMs = (options.deadlineHours ?? 48) * 36e5;
|
|
142
|
+
const backoffMs = options.backoffMs ?? 2e3;
|
|
143
|
+
const screenOn = options.screen ?? true;
|
|
144
|
+
const signal = options.signal;
|
|
145
|
+
const today = utcDate(now());
|
|
146
|
+
const listThrough = addDays(today, options.listAheadDays ?? 1);
|
|
147
|
+
const dates = options.dates ?? Array.from({ length: (options.daysAhead ?? 1) + 1 }, (_, i) => addDays(today, i));
|
|
148
|
+
const generators = options.generators.filter((g) => options.games === void 0 || options.games.includes(g.gameId)).map((generator) => withDailyBank(store, generator));
|
|
149
|
+
const runId = store.startRun(options.trigger ?? "manual", now());
|
|
150
|
+
log.info("run_started", {
|
|
151
|
+
runId,
|
|
152
|
+
today,
|
|
153
|
+
dates,
|
|
154
|
+
games: generators.map((g) => g.gameId),
|
|
155
|
+
model: options.model?.model ?? null,
|
|
156
|
+
dryRun: publisher === null
|
|
157
|
+
});
|
|
158
|
+
let consecutiveEndpointErrors = 0;
|
|
159
|
+
let breakerOpen = false;
|
|
160
|
+
let aborted = false;
|
|
161
|
+
const slots = [];
|
|
162
|
+
const published = [];
|
|
163
|
+
const slotReport = (row, base, action, attempts) => ({
|
|
164
|
+
...base,
|
|
165
|
+
action,
|
|
166
|
+
status: row?.status ?? "none",
|
|
167
|
+
source: row?.source ?? null,
|
|
168
|
+
answer: row?.answer ?? null,
|
|
169
|
+
inputs: row?.inputs ?? null,
|
|
170
|
+
attempts,
|
|
171
|
+
contentHash: row?.contentHash ?? null,
|
|
172
|
+
config: row?.config ?? null
|
|
173
|
+
});
|
|
174
|
+
const useBank = async (generator, row, base) => {
|
|
175
|
+
const used = usedNear(store, generator.gameId, base.date, base.puzzleId, generator.historyDays);
|
|
176
|
+
const usedSet = new Set(used.map((u) => u.answer));
|
|
177
|
+
const size = generator.bank.size;
|
|
178
|
+
const start = stableIndex(base.puzzleId, size);
|
|
179
|
+
for (let k = 0; k < size; k++) {
|
|
180
|
+
const index = (start + k) % size;
|
|
181
|
+
const candidate = generator.bank.config(index, base.puzzleId);
|
|
182
|
+
const answer = generator.answerOf(candidate);
|
|
183
|
+
if (answer !== null && usedSet.has(answer)) continue;
|
|
184
|
+
const verdict = await checkDailyConfig(generator, candidate, {
|
|
185
|
+
...base,
|
|
186
|
+
inputs: null,
|
|
187
|
+
used,
|
|
188
|
+
filter,
|
|
189
|
+
recentKeys: await recentContentKeys(store, generator, base.date)
|
|
190
|
+
});
|
|
191
|
+
if (verdict.outcome !== "validated" || verdict.config === null) {
|
|
192
|
+
log.warn("bank_item_rejected", {
|
|
193
|
+
puzzleId: base.puzzleId,
|
|
194
|
+
index,
|
|
195
|
+
outcome: verdict.outcome,
|
|
196
|
+
issues: verdict.issues
|
|
197
|
+
});
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
log.warn("bank_used", {
|
|
201
|
+
puzzleId: base.puzzleId,
|
|
202
|
+
index,
|
|
203
|
+
answer
|
|
204
|
+
});
|
|
205
|
+
return store.updateItem(row.id, {
|
|
206
|
+
status: "validated",
|
|
207
|
+
source: "bank",
|
|
208
|
+
answer,
|
|
209
|
+
config: verdict.config,
|
|
210
|
+
contentHash: contentHash(verdict.config),
|
|
211
|
+
model: null,
|
|
212
|
+
promptVersion: `bank:${String(index)}`
|
|
213
|
+
}, now(), {
|
|
214
|
+
kind: "bank",
|
|
215
|
+
detail: { index }
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
log.error("bank_exhausted", {
|
|
219
|
+
puzzleId: base.puzzleId,
|
|
220
|
+
size
|
|
221
|
+
});
|
|
222
|
+
return null;
|
|
223
|
+
};
|
|
224
|
+
const fillSlot = async (generator, date) => {
|
|
225
|
+
const puzzleId$2 = puzzleId(generator.gameId, date);
|
|
226
|
+
const base = {
|
|
227
|
+
gameId: generator.gameId,
|
|
228
|
+
date,
|
|
229
|
+
puzzleId: puzzleId$2
|
|
230
|
+
};
|
|
231
|
+
let row = store.current(puzzleId$2);
|
|
232
|
+
if (row !== null && (row.status === "validated" || row.status === "published")) return slotReport(row, base, "existing", []);
|
|
233
|
+
if (row === null) {
|
|
234
|
+
const scheduled = generator.schedule({
|
|
235
|
+
date,
|
|
236
|
+
puzzleId: puzzleId$2,
|
|
237
|
+
used: usedNear(store, generator.gameId, date, puzzleId$2, generator.historyDays)
|
|
238
|
+
});
|
|
239
|
+
row = store.createItem({
|
|
240
|
+
gameId: generator.gameId,
|
|
241
|
+
puzzleId: puzzleId$2,
|
|
242
|
+
date,
|
|
243
|
+
inputs: scheduled.inputs,
|
|
244
|
+
seed: scheduled.seed,
|
|
245
|
+
answer: scheduled.answer,
|
|
246
|
+
now: now()
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
const attempts = [];
|
|
250
|
+
const pastDeadline = now() >= releaseInstant(date) - deadlineMs;
|
|
251
|
+
const model = generator.modelFree === true ? {
|
|
252
|
+
model: "deterministic",
|
|
253
|
+
complete: async () => err({
|
|
254
|
+
kind: "network",
|
|
255
|
+
message: "A model-free generator must not call a model.",
|
|
256
|
+
call: {
|
|
257
|
+
name: "forbidden",
|
|
258
|
+
latencyMs: 0,
|
|
259
|
+
requests: 0,
|
|
260
|
+
mode: "json_schema",
|
|
261
|
+
finishReason: null,
|
|
262
|
+
usage: null
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
} : options.model;
|
|
266
|
+
if (model !== null && (!breakerOpen || generator.modelFree === true)) {
|
|
267
|
+
row = store.updateItem(row.id, {
|
|
268
|
+
status: "generating",
|
|
269
|
+
model: model.model,
|
|
270
|
+
promptVersion: generator.promptVersion
|
|
271
|
+
}, now());
|
|
272
|
+
let feedback = [];
|
|
273
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
274
|
+
if (signal?.aborted === true) {
|
|
275
|
+
aborted = true;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
if (attempt > 1) await sleep(backoffMs * 2 ** (attempt - 2), signal);
|
|
279
|
+
const calls = [];
|
|
280
|
+
const recorded = recording(model, calls);
|
|
281
|
+
const checks = {};
|
|
282
|
+
const finish = (report) => {
|
|
283
|
+
const full = {
|
|
284
|
+
attempt,
|
|
285
|
+
calls,
|
|
286
|
+
...report,
|
|
287
|
+
...Object.keys(checks).length > 0 ? { checks } : {}
|
|
288
|
+
};
|
|
289
|
+
attempts.push(full);
|
|
290
|
+
log.info("attempt", {
|
|
291
|
+
puzzleId: puzzleId$2,
|
|
292
|
+
attempt,
|
|
293
|
+
outcome: full.outcome,
|
|
294
|
+
issues: full.issues.slice(0, 8),
|
|
295
|
+
calls: calls.map((c) => ({
|
|
296
|
+
name: c.name,
|
|
297
|
+
ok: c.ok,
|
|
298
|
+
ms: c.latencyMs,
|
|
299
|
+
tokens: c.totalTokens
|
|
300
|
+
}))
|
|
301
|
+
});
|
|
302
|
+
return full;
|
|
303
|
+
};
|
|
304
|
+
const generated = await generator.generate({
|
|
305
|
+
date,
|
|
306
|
+
puzzleId: puzzleId$2,
|
|
307
|
+
inputs: row.inputs ?? {},
|
|
308
|
+
seed: row.seed ?? puzzleId$2,
|
|
309
|
+
attempt,
|
|
310
|
+
feedback,
|
|
311
|
+
model: recorded,
|
|
312
|
+
recentKeys: await recentContentKeys(store, generator, date),
|
|
313
|
+
catalogs: generator.reusesCatalogs === true ? store.versions(generator.gameId, "0000-01-01", "9999-12-31").filter((item) => item.status === "published" && item.puzzleId.startsWith(`${generator.gameId}:pack:`) && item.config !== null).map((item) => item.config) : [],
|
|
314
|
+
record: (name, detail) => {
|
|
315
|
+
checks[name] = detail;
|
|
316
|
+
},
|
|
317
|
+
...signal === void 0 ? {} : { signal }
|
|
318
|
+
});
|
|
319
|
+
if (!generated.ok) {
|
|
320
|
+
const failure = generated.error;
|
|
321
|
+
const outcome = failure.kind === "model" ? outcomeOf(failure.error) : "rejected";
|
|
322
|
+
const issues = failure.kind === "model" ? issuesOf(failure.error) : [...failure.issues];
|
|
323
|
+
finish({
|
|
324
|
+
outcome,
|
|
325
|
+
issues,
|
|
326
|
+
filter: null,
|
|
327
|
+
screen: null
|
|
328
|
+
});
|
|
329
|
+
if (outcome === "aborted") {
|
|
330
|
+
aborted = true;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
if (outcome === "endpoint") {
|
|
334
|
+
consecutiveEndpointErrors++;
|
|
335
|
+
if (consecutiveEndpointErrors >= breakerThreshold) {
|
|
336
|
+
breakerOpen = true;
|
|
337
|
+
log.error("circuit_breaker_open", { consecutiveEndpointErrors });
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
} else consecutiveEndpointErrors = 0;
|
|
341
|
+
feedback = outcome === "endpoint" ? feedback : issues;
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
consecutiveEndpointErrors = 0;
|
|
345
|
+
const used = usedNear(store, generator.gameId, date, puzzleId$2, generator.historyDays);
|
|
346
|
+
const verdict = await checkDailyConfig(generator, generated.value, {
|
|
347
|
+
date,
|
|
348
|
+
puzzleId: puzzleId$2,
|
|
349
|
+
inputs: row.inputs,
|
|
350
|
+
used,
|
|
351
|
+
filter,
|
|
352
|
+
recentKeys: await recentContentKeys(store, generator, date)
|
|
353
|
+
});
|
|
354
|
+
if (verdict.outcome !== "validated" || verdict.config === null) {
|
|
355
|
+
finish({
|
|
356
|
+
outcome: verdict.outcome,
|
|
357
|
+
issues: verdict.issues,
|
|
358
|
+
filter: verdict.filter,
|
|
359
|
+
screen: null
|
|
360
|
+
});
|
|
361
|
+
feedback = verdict.issues;
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
let screen = null;
|
|
365
|
+
if (screenOn && generator.texts(verdict.config).length > 0) {
|
|
366
|
+
const screened = await modelScreen(recorded, generator.texts(verdict.config), signal);
|
|
367
|
+
if (!screened.ok) {
|
|
368
|
+
const outcome = outcomeOf(screened.error);
|
|
369
|
+
finish({
|
|
370
|
+
outcome,
|
|
371
|
+
issues: issuesOf(screened.error),
|
|
372
|
+
filter: verdict.filter,
|
|
373
|
+
screen: null
|
|
374
|
+
});
|
|
375
|
+
if (outcome === "aborted") {
|
|
376
|
+
aborted = true;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
if (outcome === "endpoint" && ++consecutiveEndpointErrors >= breakerThreshold) {
|
|
380
|
+
breakerOpen = true;
|
|
381
|
+
log.error("circuit_breaker_open", { consecutiveEndpointErrors });
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
screen = {
|
|
387
|
+
safe: screened.value.verdict.safe,
|
|
388
|
+
reasons: screened.value.verdict.reasons
|
|
389
|
+
};
|
|
390
|
+
if (!screen.safe) {
|
|
391
|
+
const reasons = screen.reasons.length > 0 ? screen.reasons : ["The safety screen refused the text."];
|
|
392
|
+
finish({
|
|
393
|
+
outcome: "screened",
|
|
394
|
+
issues: reasons.map((reason) => `Safety screen: ${reason}`),
|
|
395
|
+
filter: verdict.filter,
|
|
396
|
+
screen
|
|
397
|
+
});
|
|
398
|
+
feedback = reasons.map((reason) => `A safety reviewer objected: ${reason}`);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
const report = finish({
|
|
403
|
+
outcome: "validated",
|
|
404
|
+
issues: [],
|
|
405
|
+
filter: verdict.filter,
|
|
406
|
+
screen
|
|
407
|
+
});
|
|
408
|
+
row = store.updateItem(row.id, {
|
|
409
|
+
status: "validated",
|
|
410
|
+
source: "generated",
|
|
411
|
+
answer: generator.answerOf(verdict.config),
|
|
412
|
+
config: verdict.config,
|
|
413
|
+
contentHash: contentHash(verdict.config),
|
|
414
|
+
attempts: row.attempts + attempts.length,
|
|
415
|
+
report: [...row.report ?? [], ...attempts.map((a) => a)]
|
|
416
|
+
}, now(), {
|
|
417
|
+
kind: "validated",
|
|
418
|
+
detail: {
|
|
419
|
+
attempt: report.attempt,
|
|
420
|
+
screen: screen === null ? null : SCREEN_PROMPT_VERSION
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
return slotReport(row, base, "generated", attempts);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
const history = [...row.report ?? [], ...attempts.map((a) => a)];
|
|
427
|
+
if (aborted) {
|
|
428
|
+
row = store.updateItem(row.id, {
|
|
429
|
+
status: "scheduled",
|
|
430
|
+
attempts: row.attempts + attempts.length,
|
|
431
|
+
report: history
|
|
432
|
+
}, now(), { kind: "interrupted" });
|
|
433
|
+
return slotReport(row, base, "aborted", attempts);
|
|
434
|
+
}
|
|
435
|
+
row = store.updateItem(row.id, {
|
|
436
|
+
status: "failed",
|
|
437
|
+
attempts: row.attempts + attempts.length,
|
|
438
|
+
report: history
|
|
439
|
+
}, now(), attempts.length > 0 ? {
|
|
440
|
+
kind: "failed",
|
|
441
|
+
detail: { attempts: attempts.length }
|
|
442
|
+
} : void 0);
|
|
443
|
+
if (!pastDeadline || options.bankFallback === false) {
|
|
444
|
+
log.warn("slot_deferred", {
|
|
445
|
+
puzzleId: puzzleId$2,
|
|
446
|
+
attempts: attempts.length,
|
|
447
|
+
breakerOpen
|
|
448
|
+
});
|
|
449
|
+
return slotReport(row, base, attempts.length > 0 ? "failed" : "deferred", attempts);
|
|
450
|
+
}
|
|
451
|
+
const banked = await useBank(generator, row, base);
|
|
452
|
+
if (banked === null) return slotReport(row, base, "failed", attempts);
|
|
453
|
+
return slotReport(banked, base, "bank", attempts);
|
|
454
|
+
};
|
|
455
|
+
for (const generator of generators) {
|
|
456
|
+
for (const date of [...dates].sort()) {
|
|
457
|
+
if (signal?.aborted === true) {
|
|
458
|
+
aborted = true;
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
const report = await fillSlot(generator, date);
|
|
462
|
+
slots.push(report);
|
|
463
|
+
log.info("slot", {
|
|
464
|
+
puzzleId: report.puzzleId,
|
|
465
|
+
action: report.action,
|
|
466
|
+
status: report.status,
|
|
467
|
+
source: report.source,
|
|
468
|
+
attempts: report.attempts.length
|
|
469
|
+
});
|
|
470
|
+
if (aborted) break;
|
|
471
|
+
}
|
|
472
|
+
if (aborted) break;
|
|
473
|
+
}
|
|
474
|
+
if (publisher !== null) for (const generator of generators) {
|
|
475
|
+
for (const row of store.validatedThrough(generator.gameId, listThrough)) {
|
|
476
|
+
if (row.config === null) continue;
|
|
477
|
+
const file = publisher.writePuzzle(generator.gameId, row.date, row.config);
|
|
478
|
+
store.updateItem(row.id, {
|
|
479
|
+
status: "published",
|
|
480
|
+
contentHash: file.hash,
|
|
481
|
+
size: file.size,
|
|
482
|
+
publishedAt: new Date(now()).toISOString()
|
|
483
|
+
}, now(), {
|
|
484
|
+
kind: "published",
|
|
485
|
+
detail: { file: file.file }
|
|
486
|
+
});
|
|
487
|
+
published.push({
|
|
488
|
+
gameId: generator.gameId,
|
|
489
|
+
date: row.date,
|
|
490
|
+
file: file.file,
|
|
491
|
+
hash: file.hash,
|
|
492
|
+
size: file.size
|
|
493
|
+
});
|
|
494
|
+
log.info("published", {
|
|
495
|
+
puzzleId: row.puzzleId,
|
|
496
|
+
file: file.file,
|
|
497
|
+
size: file.size
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
rebuildIndex(store, publisher, generator.gameId, today, {
|
|
501
|
+
listAheadDays: options.listAheadDays ?? 1,
|
|
502
|
+
indexBackDays: options.indexBackDays ?? 14
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
const unfinished = slots.filter((slot) => slot.status !== "validated" && slot.status !== "published");
|
|
506
|
+
const status = aborted ? "aborted" : unfinished.some((slot) => slot.date <= listThrough) ? unfinished.length === slots.length ? "failed" : "partial" : "ok";
|
|
507
|
+
const summary = {
|
|
508
|
+
today,
|
|
509
|
+
status,
|
|
510
|
+
breakerOpen,
|
|
511
|
+
slots: slots.map((slot) => ({
|
|
512
|
+
puzzleId: slot.puzzleId,
|
|
513
|
+
action: slot.action,
|
|
514
|
+
status: slot.status,
|
|
515
|
+
source: slot.source,
|
|
516
|
+
attempts: slot.attempts.length
|
|
517
|
+
})),
|
|
518
|
+
published: published.map((p) => p.file)
|
|
519
|
+
};
|
|
520
|
+
store.finishRun(runId, status, summary, now());
|
|
521
|
+
log.info("run_finished", {
|
|
522
|
+
runId,
|
|
523
|
+
...summary
|
|
524
|
+
});
|
|
525
|
+
return {
|
|
526
|
+
runId,
|
|
527
|
+
status,
|
|
528
|
+
today,
|
|
529
|
+
slots,
|
|
530
|
+
published,
|
|
531
|
+
breakerOpen
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
/** Rewrites a game's index from the store: published items from `indexBackDays` ago through today + `listAheadDays`. */
|
|
535
|
+
var rebuildIndex = (store, publisher, gameId, today, { listAheadDays = 1, indexBackDays = 14 } = {}) => {
|
|
536
|
+
const rows = store.published(gameId, addDays(today, -indexBackDays), addDays(today, listAheadDays));
|
|
537
|
+
publisher.writeIndex(gameId, rows.filter((row) => row.contentHash !== null && row.size !== null).map((row) => ({
|
|
538
|
+
date: row.date,
|
|
539
|
+
hash: row.contentHash,
|
|
540
|
+
size: row.size
|
|
541
|
+
})));
|
|
542
|
+
};
|
|
543
|
+
/**
|
|
544
|
+
* Takes an item down (content-pipeline.md, "Corrections"): the item is withdrawn with a
|
|
545
|
+
* reason code and actor, the game's index drops it at once, and the next run fills the slot
|
|
546
|
+
* again, from the model or the bank. The withdrawn answer counts as used, so the replacement
|
|
547
|
+
* gets a different one. Returns the withdrawn item, or null if no item held the puzzle.
|
|
548
|
+
*/
|
|
549
|
+
var withdrawItem = (options) => {
|
|
550
|
+
const at = options.now ?? Date.now();
|
|
551
|
+
const row = options.store.withdraw(puzzleId(options.gameId, options.date), options.reason, options.actor, at, options.note);
|
|
552
|
+
if (row !== null && options.publisher !== null) rebuildIndex(options.store, options.publisher, options.gameId, utcDate(at), { listAheadDays: options.listAheadDays ?? 1 });
|
|
553
|
+
return row;
|
|
554
|
+
};
|
|
555
|
+
//#endregion
|
|
556
|
+
export { callReport, checkDailyConfig, decodeConfig, defaultSleep, issuesOf, outcomeOf, rebuildIndex, recentContentKeys, recording, runPipeline, withdrawItem };
|
|
557
|
+
|
|
558
|
+
//# sourceMappingURL=pipeline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pipeline.js","names":["makePuzzleId","puzzleId"],"sources":["../src/pipeline.ts"],"sourcesContent":["// The generic daily pipeline (content-pipeline.md): for each game and date, schedule the\n// slot's deterministic inputs, generate with the model, then parse, judge and publish.\n//\n// schedule ─▶ generate ─▶ schema ─▶ game validators ─▶ blocklist + sensitive list ─▶ model screen ─▶ validated\n// ▲ │\n// └──────────── up to 3 attempts per item per run, with backoff and feedback ───────────┤\n// ▼\n// past the deadline and still not validated ─▶ bank item publish (≤ today + 1)\n//\n// Runs are idempotent: a slot that is validated or published is left alone, so rerunning a\n// day changes nothing, and a run cut short resumes where it stopped. Nothing waits for a\n// person (D-44). Every attempt's calls, issues and filter outcomes go into the item's\n// report, which is its provenance.\nimport { err, ok, type Json, type Result, type StandardSchemaV1 } from '@joystyk/contracts';\nimport { addDays, puzzleId as makePuzzleId, releaseInstant, utcDate } from '@joystyk/daily';\nimport type { FieldText, FilterHit, WordFilter } from './filter.ts';\nimport { contentHash, stableIndex } from './hash.ts';\nimport { silentLogger, type Logger } from './log.ts';\nimport { ENDPOINT_ERRORS, type CallInfo, type CompletionFailure, type ModelClient } from './model.ts';\nimport type { Publisher } from './publish.ts';\nimport { modelScreen, SCREEN_PROMPT_VERSION } from './screen.ts';\nimport type { ContentStore, ItemRow, WithdrawReason } from './store.ts';\n\n// ---------------------------------------------------------------------------------------------\n// What a game supplies\n\nexport interface ScheduleContext {\n readonly date: string;\n readonly puzzleId: string;\n /** Answers used or reserved within the requested history window, withdrawn ones included. */\n readonly used: readonly { readonly date: string; readonly answer: string }[];\n}\n\nexport interface Scheduled<Inputs extends Json> {\n readonly inputs: Inputs;\n readonly seed: string;\n /** The answer key the inputs commit to, if the schedule picks it (Word of the Day's word). */\n readonly answer: string | null;\n}\n\nexport interface GenerateContext<Inputs extends Json> {\n readonly date: string;\n readonly puzzleId: string;\n readonly inputs: Inputs;\n readonly seed: string;\n /** 1-based, within this run. */\n readonly attempt: number;\n /** What was wrong with the previous attempt, in words the prompt can repeat. Empty on the first. */\n readonly feedback: readonly string[];\n readonly model: ModelClient;\n /** Normalized text keys used on nearby dates, withdrawn content included. */\n readonly recentKeys?: readonly { readonly date: string; readonly key: string }[];\n /** Published reusable catalogs for this game; each generator decodes its own schema. */\n readonly catalogs?: readonly Json[];\n readonly signal?: AbortSignal;\n /**\n * Keeps a game's own check results (for example Crossword's fact-check verdicts) in the\n * attempt's report, under `checks[name]`, whether the attempt passes or fails. A later\n * call with the same name replaces the earlier one. Absent when a test calls `generate`\n * directly.\n */\n readonly record?: (name: string, detail: Json) => void;\n}\n\nexport type GenerateFailure =\n /** A model call failed: the endpoint, or a reply that didn't parse or match its schema. */\n | { readonly kind: 'model'; readonly error: CompletionFailure }\n /** The game's own model checks refused the draft (for example a blind answer check). */\n | { readonly kind: 'rejected'; readonly issues: readonly string[] };\n\nexport interface DailyGenerator<Config extends Json = Json, Inputs extends Json = Json> {\n readonly gameId: string;\n /** Recorded with every item, e.g. \"wotd-v1\". */\n readonly promptVersion: string;\n /** Scheduling history on either side of the date; defaults to 365 days. */\n readonly historyDays?: number;\n /** Numeric/card/grid configurations with no generated player-visible text. They can\n * publish without a model endpoint; `texts` must be empty. */\n readonly modelFree?: boolean;\n /** Optional additional no-repeat rule, independent of the answer schedule. */\n readonly repeatWindowDays?: number;\n readonly reusesCatalogs?: boolean;\n repeatKeys?(config: Config): readonly string[];\n /** The game's `schemas.config`: every config is decoded through it before anything else. */\n readonly configSchema: StandardSchemaV1<unknown, Config>;\n /** Deterministic inputs for a slot: the same date and history always give the same inputs. */\n schedule(context: ScheduleContext): Scheduled<Inputs>;\n generate(context: GenerateContext<Inputs>): Promise<Result<Config, GenerateFailure>>;\n /** The game validators: pure; an empty list means the config passes. */\n validate(\n config: Config,\n context: { readonly date: string; readonly puzzleId: string; readonly inputs: Inputs | null },\n ): readonly string[];\n /** Every player-visible text, for the all-ages filter and the model screen. */\n texts(config: Config): readonly FieldText[];\n /** The answer key for duplicate checks. */\n answerOf(config: Config): string | null;\n /** The reviewed fallback bank: `size` complete items, each turned into a config for a date. */\n readonly bank: { readonly size: number; config(index: number, puzzleId: string): Config };\n}\n\n// ---------------------------------------------------------------------------------------------\n// Reports\n\nexport interface CallReport {\n readonly name: string;\n readonly ok: boolean;\n readonly error: string | null;\n readonly latencyMs: number;\n readonly requests: number;\n readonly mode: string;\n readonly promptTokens: number | null;\n readonly completionTokens: number | null;\n readonly totalTokens: number | null;\n}\n\nexport type AttemptOutcome =\n | 'validated'\n | 'endpoint'\n | 'reply'\n | 'schema'\n | 'rejected'\n | 'invalid'\n | 'duplicate'\n | 'filtered'\n | 'screened'\n | 'aborted';\n\nexport interface AttemptReport {\n readonly attempt: number;\n readonly outcome: AttemptOutcome;\n readonly issues: readonly string[];\n readonly calls: readonly CallReport[];\n /** Null when the attempt never reached the filter. */\n readonly filter: { readonly ok: boolean; readonly hits: readonly FilterHit[] } | null;\n /** Null when the attempt never reached the screen (or screening is off). */\n readonly screen: { readonly safe: boolean; readonly reasons: readonly string[] } | null;\n /** What the game's own checks recorded (`GenerateContext.record`); absent when nothing was. */\n readonly checks?: Readonly<Record<string, Json>>;\n}\n\nexport type SlotAction = 'existing' | 'generated' | 'bank' | 'failed' | 'deferred' | 'aborted';\n\nexport interface SlotReport {\n readonly gameId: string;\n readonly date: string;\n readonly puzzleId: string;\n readonly action: SlotAction;\n readonly status: string;\n readonly source: string | null;\n readonly answer: string | null;\n /** The slot's scheduled inputs (a bank item may replace what they asked for). */\n readonly inputs: Json | null;\n readonly attempts: readonly AttemptReport[];\n readonly contentHash: string | null;\n readonly config: Json | null;\n}\n\nexport interface PublishReport {\n readonly gameId: string;\n readonly date: string;\n readonly file: string;\n readonly hash: string;\n readonly size: number;\n}\n\nexport interface RunReport {\n readonly runId: number;\n readonly status: 'ok' | 'partial' | 'failed' | 'aborted';\n readonly today: string;\n readonly slots: readonly SlotReport[];\n readonly published: readonly PublishReport[];\n readonly breakerOpen: boolean;\n}\n\n// ---------------------------------------------------------------------------------------------\n// The run\n\nexport interface PipelineOptions {\n readonly store: ContentStore;\n readonly generators: readonly DailyGenerator[];\n /** Null runs without a model: existing items publish and late slots take bank items. */\n readonly model: ModelClient | null;\n readonly filter: WordFilter;\n /** Null for a dry run: nothing is written to the published-files folder. */\n readonly publisher: Publisher | null;\n readonly now?: () => number;\n /** The dates to fill. Default: today and the next `daysAhead` days. */\n readonly dates?: readonly string[];\n /** Default 1: today and tomorrow (D-46). */\n readonly daysAhead?: number;\n /** Only these games. */\n readonly games?: readonly string[];\n readonly maxAttempts?: number;\n /** Consecutive endpoint errors that end generation for the run. Default 5. */\n readonly breakerThreshold?: number;\n /** A slot not validated this long before its release takes a bank item. Default 48 hours. */\n readonly deadlineHours?: number;\n /** Give a late slot a bank item (default true). `content bank`, which grows the banks, turns it off. */\n readonly bankFallback?: boolean;\n /** The newest date published and listed, as days after today. Default 1 (D-46). */\n readonly listAheadDays?: number;\n /** How many past days the index keeps listing. Default 14. */\n readonly indexBackDays?: number;\n /** Run the model screen (layer 3). Default true. */\n readonly screen?: boolean;\n /** Pause before attempt n + 1 is `backoffMs * 2^(n-1)`. Default 2000. */\n readonly backoffMs?: number;\n readonly sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;\n readonly signal?: AbortSignal;\n readonly log?: Logger;\n /** Recorded on the run: \"schedule\", \"manual\", \"live-validation\"… */\n readonly trigger?: string;\n}\n\n/** @internal Shared with packs.ts. */\nexport const defaultSleep = (ms: number, signal?: AbortSignal): Promise<void> =>\n new Promise((resolve) => {\n if (ms <= 0 || signal?.aborted === true) return resolve();\n const timer = setTimeout(done, ms);\n function done() {\n clearTimeout(timer);\n signal?.removeEventListener('abort', done);\n resolve();\n }\n signal?.addEventListener('abort', done, { once: true });\n });\n\n/** @internal Shared with packs.ts. */\nexport const callReport = (call: CallInfo, error: CompletionFailure | null): CallReport => ({\n name: call.name,\n ok: error === null,\n error: error === null ? null : `${error.kind}: ${error.message}`,\n latencyMs: call.latencyMs,\n requests: call.requests,\n mode: call.mode,\n promptTokens: call.usage?.promptTokens ?? null,\n completionTokens: call.usage?.completionTokens ?? null,\n totalTokens: call.usage?.totalTokens ?? null,\n});\n\n/** Wraps a model client so every call, successful or not, lands in `calls`. */\n/** @internal Shared with packs.ts. */\nexport const recording = (model: ModelClient, calls: CallReport[]): ModelClient => ({\n model: model.model,\n complete: async (request) => {\n const result = await model.complete(request);\n calls.push(result.ok ? callReport(result.value.call, null) : callReport(result.error.call, result.error));\n return result;\n },\n});\n\n/** @internal Shared with packs.ts. */\nexport const outcomeOf = (error: CompletionFailure): AttemptOutcome =>\n error.kind === 'aborted'\n ? 'aborted'\n : ENDPOINT_ERRORS.has(error.kind)\n ? 'endpoint'\n : error.kind === 'schema'\n ? 'schema'\n : 'reply';\n\n/** @internal Shared with packs.ts. */\nexport const issuesOf = (error: CompletionFailure): string[] => [error.message, ...(error.issues ?? [])];\n\n/** @internal Shared with packs.ts. */\nexport const decodeConfig = async <Config>(\n schema: StandardSchemaV1<unknown, Config>,\n value: unknown,\n): Promise<Result<Config, string[]>> => {\n const result = await schema['~standard'].validate(value);\n if (result.issues === undefined) return ok(result.value);\n return err(\n result.issues.map((issue) => {\n const path = (issue.path ?? [])\n .map((segment) => (typeof segment === 'object' ? String(segment.key) : String(segment)))\n .join('.');\n return path === '' ? issue.message : `${path}: ${issue.message}`;\n }),\n );\n};\n\n/** Answers within the generator’s window, other than this slot’s non-withdrawn item. */\nconst usedNear = (store: ContentStore, gameId: string, date: string, puzzleId: string, days = 365) =>\n store\n .usedAnswers(gameId, addDays(date, -days), addDays(date, days))\n .filter((row) => makePuzzleId(gameId, row.date) !== puzzleId || row.status === 'withdrawn');\n\n/** Nearby text keys decoded through the game schema; corrections on the same date may retain clues. */\nexport const recentContentKeys = async (\n store: ContentStore,\n generator: DailyGenerator,\n date: string,\n): Promise<{ date: string; key: string }[]> => {\n if (generator.repeatKeys === undefined || generator.repeatWindowDays === undefined) return [];\n const out: { date: string; key: string }[] = [];\n for (const row of store.versions(\n generator.gameId,\n addDays(date, -generator.repeatWindowDays),\n addDays(date, generator.repeatWindowDays),\n )) {\n if (row.date === date || row.config === null) continue;\n const config = await decodeConfig(generator.configSchema, row.config);\n if (!config.ok)\n throw new Error(`Cannot check repeat history: stored ${row.puzzleId} version ${String(row.id)} is malformed.`);\n for (const key of generator.repeatKeys(config.value)) out.push({ date: row.date, key });\n }\n return out;\n};\n\n/**\n * Checks a config the way every item is checked: the schema, the game validators, the\n * duplicate rule and the word lists. The model screen is separate (it needs the endpoint).\n */\nexport const checkDailyConfig = async (\n generator: DailyGenerator,\n value: unknown,\n context: {\n date: string;\n puzzleId: string;\n inputs: Json | null;\n used: readonly { date: string; answer: string }[];\n filter: WordFilter;\n recentKeys?: readonly { date: string; key: string }[];\n },\n): Promise<{ outcome: AttemptOutcome; issues: string[]; config: Json | null; filter: AttemptReport['filter'] }> => {\n const decoded = await decodeConfig(generator.configSchema, value);\n if (!decoded.ok) return { outcome: 'schema', issues: decoded.error, config: null, filter: null };\n const config = decoded.value;\n if (generator.modelFree === true && generator.texts(config).length > 0)\n return {\n outcome: 'invalid',\n issues: ['A model-free generator must contain no generated player-visible text.'],\n config,\n filter: null,\n };\n const issues = [\n ...generator.validate(config, { date: context.date, puzzleId: context.puzzleId, inputs: context.inputs }),\n ];\n if (issues.length > 0) return { outcome: 'invalid', issues, config, filter: null };\n const answer = generator.answerOf(config);\n const clash = answer === null ? undefined : context.used.find((row) => row.answer === answer);\n if (clash !== undefined)\n return { outcome: 'duplicate', issues: [`The answer was already used on ${clash.date}.`], config, filter: null };\n const repeat = generator\n .repeatKeys?.(config)\n .map((key) => context.recentKeys?.find((row) => row.key === key))\n .find((row) => row !== undefined);\n if (repeat !== undefined)\n return {\n outcome: 'duplicate',\n issues: [`Puzzle text was already used on ${repeat.date}: \"${repeat.key}\".`],\n config,\n filter: null,\n };\n const filtered = context.filter.check(generator.texts(config));\n if (!filtered.ok) {\n const words = [...new Set(filtered.hits.map((hit) => hit.term))].join(', ');\n return {\n outcome: 'filtered',\n issues: [\n `Text in ${[...new Set(filtered.hits.map((hit) => hit.path))].join(', ')} touches a topic or word that isn't allowed (${words}).`,\n ],\n config,\n filter: filtered,\n };\n }\n return { outcome: 'validated', issues: [], config, filter: filtered };\n};\n\nexport const runPipeline = async (options: PipelineOptions): Promise<RunReport> => {\n const { store, filter, publisher } = options;\n const now = options.now ?? Date.now;\n const log = options.log ?? silentLogger;\n const sleep = options.sleep ?? defaultSleep;\n const maxAttempts = options.maxAttempts ?? 3;\n const breakerThreshold = options.breakerThreshold ?? 5;\n const deadlineMs = (options.deadlineHours ?? 48) * 3_600_000;\n const backoffMs = options.backoffMs ?? 2000;\n const screenOn = options.screen ?? true;\n const signal = options.signal;\n const today = utcDate(now());\n const listThrough = addDays(today, options.listAheadDays ?? 1);\n const dates = options.dates ?? Array.from({ length: (options.daysAhead ?? 1) + 1 }, (_, i) => addDays(today, i));\n const generators = options.generators\n .filter((g) => options.games === undefined || options.games.includes(g.gameId))\n .map((generator) => withDailyBank(store, generator));\n const runId = store.startRun(options.trigger ?? 'manual', now());\n log.info('run_started', {\n runId,\n today,\n dates,\n games: generators.map((g) => g.gameId),\n model: options.model?.model ?? null,\n dryRun: publisher === null,\n });\n\n let consecutiveEndpointErrors = 0;\n let breakerOpen = false;\n let aborted = false;\n const slots: SlotReport[] = [];\n const published: PublishReport[] = [];\n\n const slotReport = (\n row: ItemRow | null,\n base: { gameId: string; date: string; puzzleId: string },\n action: SlotAction,\n attempts: AttemptReport[],\n ): SlotReport => ({\n ...base,\n action,\n status: row?.status ?? 'none',\n source: row?.source ?? null,\n answer: row?.answer ?? null,\n inputs: row?.inputs ?? null,\n attempts,\n contentHash: row?.contentHash ?? null,\n config: row?.config ?? null,\n });\n\n const useBank = async (\n generator: DailyGenerator,\n row: ItemRow,\n base: { gameId: string; date: string; puzzleId: string },\n ): Promise<ItemRow | null> => {\n const used = usedNear(store, generator.gameId, base.date, base.puzzleId, generator.historyDays);\n const usedSet = new Set(used.map((u) => u.answer));\n const size = generator.bank.size;\n const start = stableIndex(base.puzzleId, size);\n for (let k = 0; k < size; k++) {\n const index = (start + k) % size;\n const candidate = generator.bank.config(index, base.puzzleId);\n const answer = generator.answerOf(candidate);\n if (answer !== null && usedSet.has(answer)) continue;\n const verdict = await checkDailyConfig(generator, candidate, {\n ...base,\n inputs: null,\n used,\n filter,\n recentKeys: await recentContentKeys(store, generator, base.date),\n });\n if (verdict.outcome !== 'validated' || verdict.config === null) {\n log.warn('bank_item_rejected', {\n puzzleId: base.puzzleId,\n index,\n outcome: verdict.outcome,\n issues: verdict.issues,\n });\n continue;\n }\n log.warn('bank_used', { puzzleId: base.puzzleId, index, answer });\n return store.updateItem(\n row.id,\n {\n status: 'validated',\n source: 'bank',\n answer,\n config: verdict.config,\n contentHash: contentHash(verdict.config),\n model: null,\n promptVersion: `bank:${String(index)}`,\n },\n now(),\n { kind: 'bank', detail: { index } },\n );\n }\n log.error('bank_exhausted', { puzzleId: base.puzzleId, size });\n return null;\n };\n\n const fillSlot = async (generator: DailyGenerator, date: string): Promise<SlotReport> => {\n const puzzleId = makePuzzleId(generator.gameId, date);\n const base = { gameId: generator.gameId, date, puzzleId };\n let row = store.current(puzzleId);\n if (row !== null && (row.status === 'validated' || row.status === 'published'))\n return slotReport(row, base, 'existing', []);\n\n if (row === null) {\n const scheduled = generator.schedule({\n date,\n puzzleId,\n used: usedNear(store, generator.gameId, date, puzzleId, generator.historyDays),\n });\n row = store.createItem({\n gameId: generator.gameId,\n puzzleId,\n date,\n inputs: scheduled.inputs,\n seed: scheduled.seed,\n answer: scheduled.answer,\n now: now(),\n });\n }\n const attempts: AttemptReport[] = [];\n const pastDeadline = now() >= releaseInstant(date) - deadlineMs;\n const model: ModelClient | null =\n generator.modelFree === true\n ? {\n model: 'deterministic',\n complete: async () =>\n err({\n kind: 'network',\n message: 'A model-free generator must not call a model.',\n call: {\n name: 'forbidden',\n latencyMs: 0,\n requests: 0,\n mode: 'json_schema',\n finishReason: null,\n usage: null,\n },\n }),\n }\n : options.model;\n\n if (model !== null && (!breakerOpen || generator.modelFree === true)) {\n row = store.updateItem(\n row.id,\n { status: 'generating', model: model.model, promptVersion: generator.promptVersion },\n now(),\n );\n let feedback: string[] = [];\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n if (signal?.aborted === true) {\n aborted = true;\n break;\n }\n if (attempt > 1) await sleep(backoffMs * 2 ** (attempt - 2), signal);\n const calls: CallReport[] = [];\n const recorded = recording(model, calls);\n const checks: Record<string, Json> = {};\n const finish = (report: Omit<AttemptReport, 'attempt' | 'calls' | 'checks'>): AttemptReport => {\n const full: AttemptReport = {\n attempt,\n calls,\n ...report,\n ...(Object.keys(checks).length > 0 ? { checks } : {}),\n };\n attempts.push(full);\n log.info('attempt', {\n puzzleId,\n attempt,\n outcome: full.outcome,\n issues: full.issues.slice(0, 8),\n calls: calls.map((c) => ({ name: c.name, ok: c.ok, ms: c.latencyMs, tokens: c.totalTokens })),\n });\n return full;\n };\n const generated = await generator.generate({\n date,\n puzzleId,\n inputs: row.inputs ?? {},\n seed: row.seed ?? puzzleId,\n attempt,\n feedback,\n model: recorded,\n recentKeys: await recentContentKeys(store, generator, date),\n catalogs:\n generator.reusesCatalogs === true\n ? store\n .versions(generator.gameId, '0000-01-01', '9999-12-31')\n .filter(\n (item) =>\n item.status === 'published' &&\n item.puzzleId.startsWith(`${generator.gameId}:pack:`) &&\n item.config !== null,\n )\n .map((item) => item.config as Json)\n : [],\n record: (name, detail) => {\n checks[name] = detail;\n },\n ...(signal === undefined ? {} : { signal }),\n });\n if (!generated.ok) {\n const failure = generated.error;\n const outcome = failure.kind === 'model' ? outcomeOf(failure.error) : 'rejected';\n const issues = failure.kind === 'model' ? issuesOf(failure.error) : [...failure.issues];\n finish({ outcome, issues, filter: null, screen: null });\n if (outcome === 'aborted') {\n aborted = true;\n break;\n }\n if (outcome === 'endpoint') {\n consecutiveEndpointErrors++;\n if (consecutiveEndpointErrors >= breakerThreshold) {\n breakerOpen = true;\n log.error('circuit_breaker_open', { consecutiveEndpointErrors });\n break;\n }\n } else consecutiveEndpointErrors = 0;\n feedback = outcome === 'endpoint' ? feedback : issues;\n continue;\n }\n consecutiveEndpointErrors = 0;\n const used = usedNear(store, generator.gameId, date, puzzleId, generator.historyDays);\n const verdict = await checkDailyConfig(generator, generated.value, {\n date,\n puzzleId,\n inputs: row.inputs,\n used,\n filter,\n recentKeys: await recentContentKeys(store, generator, date),\n });\n if (verdict.outcome !== 'validated' || verdict.config === null) {\n finish({ outcome: verdict.outcome, issues: verdict.issues, filter: verdict.filter, screen: null });\n feedback = verdict.issues;\n continue;\n }\n let screen: AttemptReport['screen'] = null;\n if (screenOn && generator.texts(verdict.config).length > 0) {\n const screened = await modelScreen(recorded, generator.texts(verdict.config), signal);\n if (!screened.ok) {\n const outcome = outcomeOf(screened.error);\n finish({ outcome, issues: issuesOf(screened.error), filter: verdict.filter, screen: null });\n if (outcome === 'aborted') {\n aborted = true;\n break;\n }\n if (outcome === 'endpoint' && ++consecutiveEndpointErrors >= breakerThreshold) {\n breakerOpen = true;\n log.error('circuit_breaker_open', { consecutiveEndpointErrors });\n break;\n }\n continue;\n }\n screen = { safe: screened.value.verdict.safe, reasons: screened.value.verdict.reasons };\n if (!screen.safe) {\n const reasons = screen.reasons.length > 0 ? screen.reasons : ['The safety screen refused the text.'];\n finish({\n outcome: 'screened',\n issues: reasons.map((reason) => `Safety screen: ${reason}`),\n filter: verdict.filter,\n screen,\n });\n feedback = reasons.map((reason) => `A safety reviewer objected: ${reason}`);\n continue;\n }\n }\n const report = finish({ outcome: 'validated', issues: [], filter: verdict.filter, screen });\n row = store.updateItem(\n row.id,\n {\n status: 'validated',\n source: 'generated',\n answer: generator.answerOf(verdict.config),\n config: verdict.config,\n contentHash: contentHash(verdict.config),\n attempts: row.attempts + attempts.length,\n report: [...((row.report as Json[] | null) ?? []), ...attempts.map((a) => a as unknown as Json)],\n },\n now(),\n {\n kind: 'validated',\n detail: { attempt: report.attempt, screen: screen === null ? null : SCREEN_PROMPT_VERSION },\n },\n );\n return slotReport(row, base, 'generated', attempts);\n }\n }\n\n const history = [...((row.report as Json[] | null) ?? []), ...attempts.map((a) => a as unknown as Json)];\n if (aborted) {\n row = store.updateItem(\n row.id,\n { status: 'scheduled', attempts: row.attempts + attempts.length, report: history },\n now(),\n { kind: 'interrupted' },\n );\n return slotReport(row, base, 'aborted', attempts);\n }\n row = store.updateItem(\n row.id,\n { status: 'failed', attempts: row.attempts + attempts.length, report: history },\n now(),\n attempts.length > 0 ? { kind: 'failed', detail: { attempts: attempts.length } } : undefined,\n );\n if (!pastDeadline || options.bankFallback === false) {\n log.warn('slot_deferred', { puzzleId, attempts: attempts.length, breakerOpen });\n return slotReport(row, base, attempts.length > 0 ? 'failed' : 'deferred', attempts);\n }\n const banked = await useBank(generator, row, base);\n if (banked === null) return slotReport(row, base, 'failed', attempts);\n return slotReport(banked, base, 'bank', attempts);\n };\n\n for (const generator of generators) {\n for (const date of [...dates].sort()) {\n if (signal?.aborted === true) {\n aborted = true;\n break;\n }\n const report = await fillSlot(generator, date);\n slots.push(report);\n log.info('slot', {\n puzzleId: report.puzzleId,\n action: report.action,\n status: report.status,\n source: report.source,\n attempts: report.attempts.length,\n });\n if (aborted) break;\n }\n if (aborted) break;\n }\n\n // Publish what is due (even after an interruption: those items are already validated).\n if (publisher !== null) {\n for (const generator of generators) {\n for (const row of store.validatedThrough(generator.gameId, listThrough)) {\n if (row.config === null) continue;\n const file = publisher.writePuzzle(generator.gameId, row.date, row.config);\n store.updateItem(\n row.id,\n { status: 'published', contentHash: file.hash, size: file.size, publishedAt: new Date(now()).toISOString() },\n now(),\n {\n kind: 'published',\n detail: { file: file.file },\n },\n );\n published.push({ gameId: generator.gameId, date: row.date, file: file.file, hash: file.hash, size: file.size });\n log.info('published', { puzzleId: row.puzzleId, file: file.file, size: file.size });\n }\n rebuildIndex(store, publisher, generator.gameId, today, {\n listAheadDays: options.listAheadDays ?? 1,\n indexBackDays: options.indexBackDays ?? 14,\n });\n }\n }\n\n const unfinished = slots.filter((slot) => slot.status !== 'validated' && slot.status !== 'published');\n const status: RunReport['status'] = aborted\n ? 'aborted'\n : unfinished.some((slot) => slot.date <= listThrough)\n ? unfinished.length === slots.length\n ? 'failed'\n : 'partial'\n : 'ok';\n const summary = {\n today,\n status,\n breakerOpen,\n slots: slots.map((slot) => ({\n puzzleId: slot.puzzleId,\n action: slot.action,\n status: slot.status,\n source: slot.source,\n attempts: slot.attempts.length,\n })),\n published: published.map((p) => p.file),\n };\n store.finishRun(runId, status, summary, now());\n log.info('run_finished', { runId, ...summary });\n return { runId, status, today, slots, published, breakerOpen };\n};\n\n/** Rewrites a game's index from the store: published items from `indexBackDays` ago through today + `listAheadDays`. */\nexport const rebuildIndex = (\n store: ContentStore,\n publisher: Publisher,\n gameId: string,\n today: string,\n { listAheadDays = 1, indexBackDays = 14 }: { listAheadDays?: number; indexBackDays?: number } = {},\n): void => {\n const rows = store.published(gameId, addDays(today, -indexBackDays), addDays(today, listAheadDays));\n publisher.writeIndex(\n gameId,\n rows\n .filter((row) => row.contentHash !== null && row.size !== null)\n .map((row) => ({ date: row.date, hash: row.contentHash as string, size: row.size as number })),\n );\n};\n\n/**\n * Takes an item down (content-pipeline.md, \"Corrections\"): the item is withdrawn with a\n * reason code and actor, the game's index drops it at once, and the next run fills the slot\n * again, from the model or the bank. The withdrawn answer counts as used, so the replacement\n * gets a different one. Returns the withdrawn item, or null if no item held the puzzle.\n */\nexport const withdrawItem = (options: {\n readonly store: ContentStore;\n readonly publisher: Publisher | null;\n readonly gameId: string;\n readonly date: string;\n readonly reason: WithdrawReason;\n readonly actor: string;\n readonly note?: string;\n readonly now?: number;\n readonly listAheadDays?: number;\n}): ItemRow | null => {\n const at = options.now ?? Date.now();\n const row = options.store.withdraw(\n makePuzzleId(options.gameId, options.date),\n options.reason,\n options.actor,\n at,\n options.note,\n );\n if (row !== null && options.publisher !== null) {\n rebuildIndex(options.store, options.publisher, options.gameId, utcDate(at), {\n listAheadDays: options.listAheadDays ?? 1,\n });\n }\n return row;\n};\nimport { withDailyBank } from './banks.ts';\n"],"mappings":";;;;;;;;;AAwNA,IAAa,gBAAgB,IAAY,WACvC,IAAI,SAAS,YAAY;CACvB,IAAI,MAAM,KAAK,QAAQ,YAAY,MAAM,OAAO,QAAQ;CACxD,MAAM,QAAQ,WAAW,MAAM,EAAE;CACjC,SAAS,OAAO;EACd,aAAa,KAAK;EAClB,QAAQ,oBAAoB,SAAS,IAAI;EACzC,QAAQ;CACV;CACA,QAAQ,iBAAiB,SAAS,MAAM,EAAE,MAAM,KAAK,CAAC;AACxD,CAAC;;AAGH,IAAa,cAAc,MAAgB,WAAiD;CAC1F,MAAM,KAAK;CACX,IAAI,UAAU;CACd,OAAO,UAAU,OAAO,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM;CACvD,WAAW,KAAK;CAChB,UAAU,KAAK;CACf,MAAM,KAAK;CACX,cAAc,KAAK,OAAO,gBAAgB;CAC1C,kBAAkB,KAAK,OAAO,oBAAoB;CAClD,aAAa,KAAK,OAAO,eAAe;AAC1C;;;AAIA,IAAa,aAAa,OAAoB,WAAsC;CAClF,OAAO,MAAM;CACb,UAAU,OAAO,YAAY;EAC3B,MAAM,SAAS,MAAM,MAAM,SAAS,OAAO;EAC3C,MAAM,KAAK,OAAO,KAAK,WAAW,OAAO,MAAM,MAAM,IAAI,IAAI,WAAW,OAAO,MAAM,MAAM,OAAO,KAAK,CAAC;EACxG,OAAO;CACT;AACF;;AAGA,IAAa,aAAa,UACxB,MAAM,SAAS,YACX,YACA,gBAAgB,IAAI,MAAM,IAAI,IAC5B,aACA,MAAM,SAAS,WACb,WACA;;AAGV,IAAa,YAAY,UAAuC,CAAC,MAAM,SAAS,GAAI,MAAM,UAAU,CAAC,CAAE;;AAGvG,IAAa,eAAe,OAC1B,QACA,UACsC;CACtC,MAAM,SAAS,MAAM,OAAO,YAAY,CAAC,SAAS,KAAK;CACvD,IAAI,OAAO,WAAW,KAAA,GAAW,OAAO,GAAG,OAAO,KAAK;CACvD,OAAO,IACL,OAAO,OAAO,KAAK,UAAU;EAC3B,MAAM,QAAQ,MAAM,QAAQ,CAAC,EAAA,CAC1B,KAAK,YAAa,OAAO,YAAY,WAAW,OAAO,QAAQ,GAAG,IAAI,OAAO,OAAO,CAAE,CAAC,CACvF,KAAK,GAAG;EACX,OAAO,SAAS,KAAK,MAAM,UAAU,GAAG,KAAK,IAAI,MAAM;CACzD,CAAC,CACH;AACF;;AAGA,IAAM,YAAY,OAAqB,QAAgB,MAAc,YAAkB,OAAO,QAC5F,MACG,YAAY,QAAQ,QAAQ,MAAM,CAAC,IAAI,GAAG,QAAQ,MAAM,IAAI,CAAC,CAAC,CAC9D,QAAQ,QAAQA,SAAa,QAAQ,IAAI,IAAI,MAAMC,cAAY,IAAI,WAAW,WAAW;;AAG9F,IAAa,oBAAoB,OAC/B,OACA,WACA,SAC6C;CAC7C,IAAI,UAAU,eAAe,KAAA,KAAa,UAAU,qBAAqB,KAAA,GAAW,OAAO,CAAC;CAC5F,MAAM,MAAuC,CAAC;CAC9C,KAAK,MAAM,OAAO,MAAM,SACtB,UAAU,QACV,QAAQ,MAAM,CAAC,UAAU,gBAAgB,GACzC,QAAQ,MAAM,UAAU,gBAAgB,CAC1C,GAAG;EACD,IAAI,IAAI,SAAS,QAAQ,IAAI,WAAW,MAAM;EAC9C,MAAM,SAAS,MAAM,aAAa,UAAU,cAAc,IAAI,MAAM;EACpE,IAAI,CAAC,OAAO,IACV,MAAM,IAAI,MAAM,uCAAuC,IAAI,SAAS,WAAW,OAAO,IAAI,EAAE,EAAE,eAAe;EAC/G,KAAK,MAAM,OAAO,UAAU,WAAW,OAAO,KAAK,GAAG,IAAI,KAAK;GAAE,MAAM,IAAI;GAAM;EAAI,CAAC;CACxF;CACA,OAAO;AACT;;;;;AAMA,IAAa,mBAAmB,OAC9B,WACA,OACA,YAQiH;CACjH,MAAM,UAAU,MAAM,aAAa,UAAU,cAAc,KAAK;CAChE,IAAI,CAAC,QAAQ,IAAI,OAAO;EAAE,SAAS;EAAU,QAAQ,QAAQ;EAAO,QAAQ;EAAM,QAAQ;CAAK;CAC/F,MAAM,SAAS,QAAQ;CACvB,IAAI,UAAU,cAAc,QAAQ,UAAU,MAAM,MAAM,CAAC,CAAC,SAAS,GACnE,OAAO;EACL,SAAS;EACT,QAAQ,CAAC,uEAAuE;EAChF;EACA,QAAQ;CACV;CACF,MAAM,SAAS,CACb,GAAG,UAAU,SAAS,QAAQ;EAAE,MAAM,QAAQ;EAAM,UAAU,QAAQ;EAAU,QAAQ,QAAQ;CAAO,CAAC,CAC1G;CACA,IAAI,OAAO,SAAS,GAAG,OAAO;EAAE,SAAS;EAAW;EAAQ;EAAQ,QAAQ;CAAK;CACjF,MAAM,SAAS,UAAU,SAAS,MAAM;CACxC,MAAM,QAAQ,WAAW,OAAO,KAAA,IAAY,QAAQ,KAAK,MAAM,QAAQ,IAAI,WAAW,MAAM;CAC5F,IAAI,UAAU,KAAA,GACZ,OAAO;EAAE,SAAS;EAAa,QAAQ,CAAC,kCAAkC,MAAM,KAAK,EAAE;EAAG;EAAQ,QAAQ;CAAK;CACjH,MAAM,SAAS,UACZ,aAAa,MAAM,CAAC,CACpB,KAAK,QAAQ,QAAQ,YAAY,MAAM,QAAQ,IAAI,QAAQ,GAAG,CAAC,CAAC,CAChE,MAAM,QAAQ,QAAQ,KAAA,CAAS;CAClC,IAAI,WAAW,KAAA,GACb,OAAO;EACL,SAAS;EACT,QAAQ,CAAC,mCAAmC,OAAO,KAAK,KAAK,OAAO,IAAI,GAAG;EAC3E;EACA,QAAQ;CACV;CACF,MAAM,WAAW,QAAQ,OAAO,MAAM,UAAU,MAAM,MAAM,CAAC;CAC7D,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,QAAQ,CAAC,GAAG,IAAI,IAAI,SAAS,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;EAC1E,OAAO;GACL,SAAS;GACT,QAAQ,CACN,WAAW,CAAC,GAAG,IAAI,IAAI,SAAS,KAAK,KAAK,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,EAAE,+CAA+C,MAAM,GAChI;GACA;GACA,QAAQ;EACV;CACF;CACA,OAAO;EAAE,SAAS;EAAa,QAAQ,CAAC;EAAG;EAAQ,QAAQ;CAAS;AACtE;AAEA,IAAa,cAAc,OAAO,YAAiD;CACjF,MAAM,EAAE,OAAO,QAAQ,cAAc;CACrC,MAAM,MAAM,QAAQ,OAAO,KAAK;CAChC,MAAM,MAAM,QAAQ,OAAO;CAC3B,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,cAAc,QAAQ,eAAe;CAC3C,MAAM,mBAAmB,QAAQ,oBAAoB;CACrD,MAAM,cAAc,QAAQ,iBAAiB,MAAM;CACnD,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,WAAW,QAAQ,UAAU;CACnC,MAAM,SAAS,QAAQ;CACvB,MAAM,QAAQ,QAAQ,IAAI,CAAC;CAC3B,MAAM,cAAc,QAAQ,OAAO,QAAQ,iBAAiB,CAAC;CAC7D,MAAM,QAAQ,QAAQ,SAAS,MAAM,KAAK,EAAE,SAAS,QAAQ,aAAa,KAAK,EAAE,IAAI,GAAG,MAAM,QAAQ,OAAO,CAAC,CAAC;CAC/G,MAAM,aAAa,QAAQ,WACxB,QAAQ,MAAM,QAAQ,UAAU,KAAA,KAAa,QAAQ,MAAM,SAAS,EAAE,MAAM,CAAC,CAAC,CAC9E,KAAK,cAAc,cAAc,OAAO,SAAS,CAAC;CACrD,MAAM,QAAQ,MAAM,SAAS,QAAQ,WAAW,UAAU,IAAI,CAAC;CAC/D,IAAI,KAAK,eAAe;EACtB;EACA;EACA;EACA,OAAO,WAAW,KAAK,MAAM,EAAE,MAAM;EACrC,OAAO,QAAQ,OAAO,SAAS;EAC/B,QAAQ,cAAc;CACxB,CAAC;CAED,IAAI,4BAA4B;CAChC,IAAI,cAAc;CAClB,IAAI,UAAU;CACd,MAAM,QAAsB,CAAC;CAC7B,MAAM,YAA6B,CAAC;CAEpC,MAAM,cACJ,KACA,MACA,QACA,cACgB;EAChB,GAAG;EACH;EACA,QAAQ,KAAK,UAAU;EACvB,QAAQ,KAAK,UAAU;EACvB,QAAQ,KAAK,UAAU;EACvB,QAAQ,KAAK,UAAU;EACvB;EACA,aAAa,KAAK,eAAe;EACjC,QAAQ,KAAK,UAAU;CACzB;CAEA,MAAM,UAAU,OACd,WACA,KACA,SAC4B;EAC5B,MAAM,OAAO,SAAS,OAAO,UAAU,QAAQ,KAAK,MAAM,KAAK,UAAU,UAAU,WAAW;EAC9F,MAAM,UAAU,IAAI,IAAI,KAAK,KAAK,MAAM,EAAE,MAAM,CAAC;EACjD,MAAM,OAAO,UAAU,KAAK;EAC5B,MAAM,QAAQ,YAAY,KAAK,UAAU,IAAI;EAC7C,KAAK,IAAI,IAAI,GAAG,IAAI,MAAM,KAAK;GAC7B,MAAM,SAAS,QAAQ,KAAK;GAC5B,MAAM,YAAY,UAAU,KAAK,OAAO,OAAO,KAAK,QAAQ;GAC5D,MAAM,SAAS,UAAU,SAAS,SAAS;GAC3C,IAAI,WAAW,QAAQ,QAAQ,IAAI,MAAM,GAAG;GAC5C,MAAM,UAAU,MAAM,iBAAiB,WAAW,WAAW;IAC3D,GAAG;IACH,QAAQ;IACR;IACA;IACA,YAAY,MAAM,kBAAkB,OAAO,WAAW,KAAK,IAAI;GACjE,CAAC;GACD,IAAI,QAAQ,YAAY,eAAe,QAAQ,WAAW,MAAM;IAC9D,IAAI,KAAK,sBAAsB;KAC7B,UAAU,KAAK;KACf;KACA,SAAS,QAAQ;KACjB,QAAQ,QAAQ;IAClB,CAAC;IACD;GACF;GACA,IAAI,KAAK,aAAa;IAAE,UAAU,KAAK;IAAU;IAAO;GAAO,CAAC;GAChE,OAAO,MAAM,WACX,IAAI,IACJ;IACE,QAAQ;IACR,QAAQ;IACR;IACA,QAAQ,QAAQ;IAChB,aAAa,YAAY,QAAQ,MAAM;IACvC,OAAO;IACP,eAAe,QAAQ,OAAO,KAAK;GACrC,GACA,IAAI,GACJ;IAAE,MAAM;IAAQ,QAAQ,EAAE,MAAM;GAAE,CACpC;EACF;EACA,IAAI,MAAM,kBAAkB;GAAE,UAAU,KAAK;GAAU;EAAK,CAAC;EAC7D,OAAO;CACT;CAEA,MAAM,WAAW,OAAO,WAA2B,SAAsC;EACvF,MAAMA,aAAWD,SAAa,UAAU,QAAQ,IAAI;EACpD,MAAM,OAAO;GAAE,QAAQ,UAAU;GAAQ;GAAM,UAAA;EAAS;EACxD,IAAI,MAAM,MAAM,QAAQC,UAAQ;EAChC,IAAI,QAAQ,SAAS,IAAI,WAAW,eAAe,IAAI,WAAW,cAChE,OAAO,WAAW,KAAK,MAAM,YAAY,CAAC,CAAC;EAE7C,IAAI,QAAQ,MAAM;GAChB,MAAM,YAAY,UAAU,SAAS;IACnC;IACA,UAAA;IACA,MAAM,SAAS,OAAO,UAAU,QAAQ,MAAMA,YAAU,UAAU,WAAW;GAC/E,CAAC;GACD,MAAM,MAAM,WAAW;IACrB,QAAQ,UAAU;IAClB,UAAA;IACA;IACA,QAAQ,UAAU;IAClB,MAAM,UAAU;IAChB,QAAQ,UAAU;IAClB,KAAK,IAAI;GACX,CAAC;EACH;EACA,MAAM,WAA4B,CAAC;EACnC,MAAM,eAAe,IAAI,KAAK,eAAe,IAAI,IAAI;EACrD,MAAM,QACJ,UAAU,cAAc,OACpB;GACE,OAAO;GACP,UAAU,YACR,IAAI;IACF,MAAM;IACN,SAAS;IACT,MAAM;KACJ,MAAM;KACN,WAAW;KACX,UAAU;KACV,MAAM;KACN,cAAc;KACd,OAAO;IACT;GACF,CAAC;EACL,IACA,QAAQ;EAEd,IAAI,UAAU,SAAS,CAAC,eAAe,UAAU,cAAc,OAAO;GACpE,MAAM,MAAM,WACV,IAAI,IACJ;IAAE,QAAQ;IAAc,OAAO,MAAM;IAAO,eAAe,UAAU;GAAc,GACnF,IAAI,CACN;GACA,IAAI,WAAqB,CAAC;GAC1B,KAAK,IAAI,UAAU,GAAG,WAAW,aAAa,WAAW;IACvD,IAAI,QAAQ,YAAY,MAAM;KAC5B,UAAU;KACV;IACF;IACA,IAAI,UAAU,GAAG,MAAM,MAAM,YAAY,MAAM,UAAU,IAAI,MAAM;IACnE,MAAM,QAAsB,CAAC;IAC7B,MAAM,WAAW,UAAU,OAAO,KAAK;IACvC,MAAM,SAA+B,CAAC;IACtC,MAAM,UAAU,WAA+E;KAC7F,MAAM,OAAsB;MAC1B;MACA;MACA,GAAG;MACH,GAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,EAAE,OAAO,IAAI,CAAC;KACrD;KACA,SAAS,KAAK,IAAI;KAClB,IAAI,KAAK,WAAW;MAClB,UAAA;MACA;MACA,SAAS,KAAK;MACd,QAAQ,KAAK,OAAO,MAAM,GAAG,CAAC;MAC9B,OAAO,MAAM,KAAK,OAAO;OAAE,MAAM,EAAE;OAAM,IAAI,EAAE;OAAI,IAAI,EAAE;OAAW,QAAQ,EAAE;MAAY,EAAE;KAC9F,CAAC;KACD,OAAO;IACT;IACA,MAAM,YAAY,MAAM,UAAU,SAAS;KACzC;KACA,UAAA;KACA,QAAQ,IAAI,UAAU,CAAC;KACvB,MAAM,IAAI,QAAQA;KAClB;KACA;KACA,OAAO;KACP,YAAY,MAAM,kBAAkB,OAAO,WAAW,IAAI;KAC1D,UACE,UAAU,mBAAmB,OACzB,MACG,SAAS,UAAU,QAAQ,cAAc,YAAY,CAAC,CACtD,QACE,SACC,KAAK,WAAW,eAChB,KAAK,SAAS,WAAW,GAAG,UAAU,OAAO,OAAO,KACpD,KAAK,WAAW,IACpB,CAAC,CACA,KAAK,SAAS,KAAK,MAAc,IACpC,CAAC;KACP,SAAS,MAAM,WAAW;MACxB,OAAO,QAAQ;KACjB;KACA,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;IAC3C,CAAC;IACD,IAAI,CAAC,UAAU,IAAI;KACjB,MAAM,UAAU,UAAU;KAC1B,MAAM,UAAU,QAAQ,SAAS,UAAU,UAAU,QAAQ,KAAK,IAAI;KACtE,MAAM,SAAS,QAAQ,SAAS,UAAU,SAAS,QAAQ,KAAK,IAAI,CAAC,GAAG,QAAQ,MAAM;KACtF,OAAO;MAAE;MAAS;MAAQ,QAAQ;MAAM,QAAQ;KAAK,CAAC;KACtD,IAAI,YAAY,WAAW;MACzB,UAAU;MACV;KACF;KACA,IAAI,YAAY,YAAY;MAC1B;MACA,IAAI,6BAA6B,kBAAkB;OACjD,cAAc;OACd,IAAI,MAAM,wBAAwB,EAAE,0BAA0B,CAAC;OAC/D;MACF;KACF,OAAO,4BAA4B;KACnC,WAAW,YAAY,aAAa,WAAW;KAC/C;IACF;IACA,4BAA4B;IAC5B,MAAM,OAAO,SAAS,OAAO,UAAU,QAAQ,MAAMA,YAAU,UAAU,WAAW;IACpF,MAAM,UAAU,MAAM,iBAAiB,WAAW,UAAU,OAAO;KACjE;KACA,UAAA;KACA,QAAQ,IAAI;KACZ;KACA;KACA,YAAY,MAAM,kBAAkB,OAAO,WAAW,IAAI;IAC5D,CAAC;IACD,IAAI,QAAQ,YAAY,eAAe,QAAQ,WAAW,MAAM;KAC9D,OAAO;MAAE,SAAS,QAAQ;MAAS,QAAQ,QAAQ;MAAQ,QAAQ,QAAQ;MAAQ,QAAQ;KAAK,CAAC;KACjG,WAAW,QAAQ;KACnB;IACF;IACA,IAAI,SAAkC;IACtC,IAAI,YAAY,UAAU,MAAM,QAAQ,MAAM,CAAC,CAAC,SAAS,GAAG;KAC1D,MAAM,WAAW,MAAM,YAAY,UAAU,UAAU,MAAM,QAAQ,MAAM,GAAG,MAAM;KACpF,IAAI,CAAC,SAAS,IAAI;MAChB,MAAM,UAAU,UAAU,SAAS,KAAK;MACxC,OAAO;OAAE;OAAS,QAAQ,SAAS,SAAS,KAAK;OAAG,QAAQ,QAAQ;OAAQ,QAAQ;MAAK,CAAC;MAC1F,IAAI,YAAY,WAAW;OACzB,UAAU;OACV;MACF;MACA,IAAI,YAAY,cAAc,EAAE,6BAA6B,kBAAkB;OAC7E,cAAc;OACd,IAAI,MAAM,wBAAwB,EAAE,0BAA0B,CAAC;OAC/D;MACF;MACA;KACF;KACA,SAAS;MAAE,MAAM,SAAS,MAAM,QAAQ;MAAM,SAAS,SAAS,MAAM,QAAQ;KAAQ;KACtF,IAAI,CAAC,OAAO,MAAM;MAChB,MAAM,UAAU,OAAO,QAAQ,SAAS,IAAI,OAAO,UAAU,CAAC,qCAAqC;MACnG,OAAO;OACL,SAAS;OACT,QAAQ,QAAQ,KAAK,WAAW,kBAAkB,QAAQ;OAC1D,QAAQ,QAAQ;OAChB;MACF,CAAC;MACD,WAAW,QAAQ,KAAK,WAAW,+BAA+B,QAAQ;MAC1E;KACF;IACF;IACA,MAAM,SAAS,OAAO;KAAE,SAAS;KAAa,QAAQ,CAAC;KAAG,QAAQ,QAAQ;KAAQ;IAAO,CAAC;IAC1F,MAAM,MAAM,WACV,IAAI,IACJ;KACE,QAAQ;KACR,QAAQ;KACR,QAAQ,UAAU,SAAS,QAAQ,MAAM;KACzC,QAAQ,QAAQ;KAChB,aAAa,YAAY,QAAQ,MAAM;KACvC,UAAU,IAAI,WAAW,SAAS;KAClC,QAAQ,CAAC,GAAK,IAAI,UAA4B,CAAC,GAAI,GAAG,SAAS,KAAK,MAAM,CAAoB,CAAC;IACjG,GACA,IAAI,GACJ;KACE,MAAM;KACN,QAAQ;MAAE,SAAS,OAAO;MAAS,QAAQ,WAAW,OAAO,OAAO;KAAsB;IAC5F,CACF;IACA,OAAO,WAAW,KAAK,MAAM,aAAa,QAAQ;GACpD;EACF;EAEA,MAAM,UAAU,CAAC,GAAK,IAAI,UAA4B,CAAC,GAAI,GAAG,SAAS,KAAK,MAAM,CAAoB,CAAC;EACvG,IAAI,SAAS;GACX,MAAM,MAAM,WACV,IAAI,IACJ;IAAE,QAAQ;IAAa,UAAU,IAAI,WAAW,SAAS;IAAQ,QAAQ;GAAQ,GACjF,IAAI,GACJ,EAAE,MAAM,cAAc,CACxB;GACA,OAAO,WAAW,KAAK,MAAM,WAAW,QAAQ;EAClD;EACA,MAAM,MAAM,WACV,IAAI,IACJ;GAAE,QAAQ;GAAU,UAAU,IAAI,WAAW,SAAS;GAAQ,QAAQ;EAAQ,GAC9E,IAAI,GACJ,SAAS,SAAS,IAAI;GAAE,MAAM;GAAU,QAAQ,EAAE,UAAU,SAAS,OAAO;EAAE,IAAI,KAAA,CACpF;EACA,IAAI,CAAC,gBAAgB,QAAQ,iBAAiB,OAAO;GACnD,IAAI,KAAK,iBAAiB;IAAE,UAAA;IAAU,UAAU,SAAS;IAAQ;GAAY,CAAC;GAC9E,OAAO,WAAW,KAAK,MAAM,SAAS,SAAS,IAAI,WAAW,YAAY,QAAQ;EACpF;EACA,MAAM,SAAS,MAAM,QAAQ,WAAW,KAAK,IAAI;EACjD,IAAI,WAAW,MAAM,OAAO,WAAW,KAAK,MAAM,UAAU,QAAQ;EACpE,OAAO,WAAW,QAAQ,MAAM,QAAQ,QAAQ;CAClD;CAEA,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,QAAQ,CAAC,GAAG,KAAK,CAAC,CAAC,KAAK,GAAG;GACpC,IAAI,QAAQ,YAAY,MAAM;IAC5B,UAAU;IACV;GACF;GACA,MAAM,SAAS,MAAM,SAAS,WAAW,IAAI;GAC7C,MAAM,KAAK,MAAM;GACjB,IAAI,KAAK,QAAQ;IACf,UAAU,OAAO;IACjB,QAAQ,OAAO;IACf,QAAQ,OAAO;IACf,QAAQ,OAAO;IACf,UAAU,OAAO,SAAS;GAC5B,CAAC;GACD,IAAI,SAAS;EACf;EACA,IAAI,SAAS;CACf;CAGA,IAAI,cAAc,MAChB,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,OAAO,MAAM,iBAAiB,UAAU,QAAQ,WAAW,GAAG;GACvE,IAAI,IAAI,WAAW,MAAM;GACzB,MAAM,OAAO,UAAU,YAAY,UAAU,QAAQ,IAAI,MAAM,IAAI,MAAM;GACzE,MAAM,WACJ,IAAI,IACJ;IAAE,QAAQ;IAAa,aAAa,KAAK;IAAM,MAAM,KAAK;IAAM,aAAa,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY;GAAE,GAC3G,IAAI,GACJ;IACE,MAAM;IACN,QAAQ,EAAE,MAAM,KAAK,KAAK;GAC5B,CACF;GACA,UAAU,KAAK;IAAE,QAAQ,UAAU;IAAQ,MAAM,IAAI;IAAM,MAAM,KAAK;IAAM,MAAM,KAAK;IAAM,MAAM,KAAK;GAAK,CAAC;GAC9G,IAAI,KAAK,aAAa;IAAE,UAAU,IAAI;IAAU,MAAM,KAAK;IAAM,MAAM,KAAK;GAAK,CAAC;EACpF;EACA,aAAa,OAAO,WAAW,UAAU,QAAQ,OAAO;GACtD,eAAe,QAAQ,iBAAiB;GACxC,eAAe,QAAQ,iBAAiB;EAC1C,CAAC;CACH;CAGF,MAAM,aAAa,MAAM,QAAQ,SAAS,KAAK,WAAW,eAAe,KAAK,WAAW,WAAW;CACpG,MAAM,SAA8B,UAChC,YACA,WAAW,MAAM,SAAS,KAAK,QAAQ,WAAW,IAChD,WAAW,WAAW,MAAM,SAC1B,WACA,YACF;CACN,MAAM,UAAU;EACd;EACA;EACA;EACA,OAAO,MAAM,KAAK,UAAU;GAC1B,UAAU,KAAK;GACf,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,QAAQ,KAAK;GACb,UAAU,KAAK,SAAS;EAC1B,EAAE;EACF,WAAW,UAAU,KAAK,MAAM,EAAE,IAAI;CACxC;CACA,MAAM,UAAU,OAAO,QAAQ,SAAS,IAAI,CAAC;CAC7C,IAAI,KAAK,gBAAgB;EAAE;EAAO,GAAG;CAAQ,CAAC;CAC9C,OAAO;EAAE;EAAO;EAAQ;EAAO;EAAO;EAAW;CAAY;AAC/D;;AAGA,IAAa,gBACX,OACA,WACA,QACA,OACA,EAAE,gBAAgB,GAAG,gBAAgB,OAA2D,CAAC,MACxF;CACT,MAAM,OAAO,MAAM,UAAU,QAAQ,QAAQ,OAAO,CAAC,aAAa,GAAG,QAAQ,OAAO,aAAa,CAAC;CAClG,UAAU,WACR,QACA,KACG,QAAQ,QAAQ,IAAI,gBAAgB,QAAQ,IAAI,SAAS,IAAI,CAAC,CAC9D,KAAK,SAAS;EAAE,MAAM,IAAI;EAAM,MAAM,IAAI;EAAuB,MAAM,IAAI;CAAe,EAAE,CACjG;AACF;;;;;;;AAQA,IAAa,gBAAgB,YAUP;CACpB,MAAM,KAAK,QAAQ,OAAO,KAAK,IAAI;CACnC,MAAM,MAAM,QAAQ,MAAM,SACxBD,SAAa,QAAQ,QAAQ,QAAQ,IAAI,GACzC,QAAQ,QACR,QAAQ,OACR,IACA,QAAQ,IACV;CACA,IAAI,QAAQ,QAAQ,QAAQ,cAAc,MACxC,aAAa,QAAQ,OAAO,QAAQ,WAAW,QAAQ,QAAQ,QAAQ,EAAE,GAAG,EAC1E,eAAe,QAAQ,iBAAiB,EAC1C,CAAC;CAEH,OAAO;AACT"}
|