@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +36 -0
  4. package/README.md +113 -2
  5. package/dist/banks.d.ts +14 -0
  6. package/dist/banks.js +55 -0
  7. package/dist/banks.js.map +1 -0
  8. package/dist/extract-json.d.ts +12 -0
  9. package/dist/extract-json.js +59 -0
  10. package/dist/extract-json.js.map +1 -0
  11. package/dist/filter.d.ts +56 -0
  12. package/dist/filter.js +127 -0
  13. package/dist/filter.js.map +1 -0
  14. package/dist/games.d.ts +9 -0
  15. package/dist/games.js +40 -0
  16. package/dist/games.js.map +1 -0
  17. package/dist/hash.d.ts +4 -0
  18. package/dist/hash.js +17 -0
  19. package/dist/hash.js.map +1 -0
  20. package/dist/index.d.ts +17 -0
  21. package/dist/index.js +16 -0
  22. package/dist/log.d.ts +26 -0
  23. package/dist/log.js +58 -0
  24. package/dist/log.js.map +1 -0
  25. package/dist/model.d.ts +77 -0
  26. package/dist/model.js +268 -0
  27. package/dist/model.js.map +1 -0
  28. package/dist/packs.d.ts +144 -0
  29. package/dist/packs.js +466 -0
  30. package/dist/packs.js.map +1 -0
  31. package/dist/pipeline.d.ts +252 -0
  32. package/dist/pipeline.js +558 -0
  33. package/dist/pipeline.js.map +1 -0
  34. package/dist/publish.d.ts +21 -0
  35. package/dist/publish.js +56 -0
  36. package/dist/publish.js.map +1 -0
  37. package/dist/ranked.d.ts +12 -0
  38. package/dist/ranked.js +40 -0
  39. package/dist/ranked.js.map +1 -0
  40. package/dist/screen.d.ts +25 -0
  41. package/dist/screen.js +46 -0
  42. package/dist/screen.js.map +1 -0
  43. package/dist/store.d.ts +136 -0
  44. package/dist/store.js +300 -0
  45. package/dist/store.js.map +1 -0
  46. package/dist/text.d.ts +31 -0
  47. package/dist/text.js +111 -0
  48. package/dist/text.js.map +1 -0
  49. package/dist/vocabulary.d.ts +16 -0
  50. package/dist/vocabulary.js +44 -0
  51. package/dist/vocabulary.js.map +1 -0
  52. package/lists/SCOWL-COPYRIGHT +252 -0
  53. package/lists/blocklist.txt +214 -0
  54. package/lists/common-extra.txt +13 -0
  55. package/lists/scowl-common.txt +70763 -0
  56. package/lists/scowl-ranked.txt +72076 -0
  57. package/lists/scowl-vocabulary.txt +55636 -0
  58. package/lists/sensitive.txt +280 -0
  59. package/package.json +53 -3
@@ -0,0 +1,21 @@
1
+ import type { Json } from '@joystyk/contracts';
2
+ import { type DailyIndexEntry } from '@joystyk/daily';
3
+ export interface PublishedFile {
4
+ readonly hash: string;
5
+ readonly size: number;
6
+ /** Relative to the publish directory: "<game>/<date>.<hash>.json". */
7
+ readonly file: string;
8
+ }
9
+ export interface Publisher {
10
+ readonly dir: string;
11
+ /** Writes one day's puzzle file (if it isn't there already) and returns its hash and size. */
12
+ writePuzzle(gameId: string, date: string, config: Json): PublishedFile;
13
+ /** Replaces a game's index. */
14
+ writeIndex(gameId: string, dates: readonly DailyIndexEntry[]): void;
15
+ }
16
+ /** The puzzle file body for a config: the envelope, as compact JSON. */
17
+ export declare const puzzleFileBody: (gameId: string, date: string, config: Json) => {
18
+ body: string;
19
+ hash: string;
20
+ };
21
+ export declare const createPublisher: (dir: string) => Publisher;
@@ -0,0 +1,56 @@
1
+ import { contentHash } from "./hash.js";
2
+ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
3
+ import { DAILY_SCHEMA_VERSION, puzzleId } from "@joystyk/daily";
4
+ import { join } from "node:path";
5
+ //#region src/publish.ts
6
+ /** The puzzle file body for a config: the envelope, as compact JSON. */
7
+ var puzzleFileBody = (gameId, date, config) => {
8
+ const hash = contentHash(config);
9
+ const file = {
10
+ schemaVersion: DAILY_SCHEMA_VERSION,
11
+ puzzleId: puzzleId(gameId, date),
12
+ date,
13
+ contentHash: hash,
14
+ config
15
+ };
16
+ return {
17
+ body: JSON.stringify(file),
18
+ hash
19
+ };
20
+ };
21
+ var counter = 0;
22
+ var writeAtomic = (path, body) => {
23
+ const temporary = `${path}.${String(process.pid)}.${String(++counter)}.tmp`;
24
+ writeFileSync(temporary, body);
25
+ renameSync(temporary, path);
26
+ };
27
+ var createPublisher = (dir) => ({
28
+ dir,
29
+ writePuzzle: (gameId, date, config) => {
30
+ const { body, hash } = puzzleFileBody(gameId, date, config);
31
+ const folder = join(dir, gameId);
32
+ mkdirSync(folder, { recursive: true });
33
+ const name = `${date}.${hash}.json`;
34
+ const path = join(folder, name);
35
+ if (!existsSync(path)) writeAtomic(path, body);
36
+ return {
37
+ hash,
38
+ size: Buffer.byteLength(body),
39
+ file: `${gameId}/${name}`
40
+ };
41
+ },
42
+ writeIndex: (gameId, dates) => {
43
+ const folder = join(dir, gameId);
44
+ mkdirSync(folder, { recursive: true });
45
+ const index = {
46
+ schemaVersion: DAILY_SCHEMA_VERSION,
47
+ gameId,
48
+ dates: [...dates].sort((a, b) => a.date < b.date ? -1 : 1)
49
+ };
50
+ writeAtomic(join(folder, "index.json"), `${JSON.stringify(index)}\n`);
51
+ }
52
+ });
53
+ //#endregion
54
+ export { createPublisher, puzzleFileBody };
55
+
56
+ //# sourceMappingURL=publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish.js","names":[],"sources":["../src/publish.ts"],"sourcesContent":["// Publishing: the daily files and each game's index, in exactly the format @joystyk/daily\n// decodes (content-pipeline.md, \"Delivery\"):\n//\n// <dir>/<game>/index.json { schemaVersion: 1, gameId, dates: [{ date, hash, size }] }\n// <dir>/<game>/<date>.<hash>.json { schemaVersion: 1, puzzleId, date, contentHash, config }\n//\n// The site serves the folder: the index `no-cache`, puzzle files immutable. A puzzle file is\n// never rewritten (its name is its hash); every write goes to a temporary file first and is\n// renamed into place, so the site never serves half a file.\nimport { existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';\nimport { join } from 'node:path';\nimport type { Json } from '@joystyk/contracts';\nimport { DAILY_SCHEMA_VERSION, puzzleId, type DailyFile, type DailyIndex, type DailyIndexEntry } from '@joystyk/daily';\nimport { contentHash } from './hash.ts';\n\nexport interface PublishedFile {\n readonly hash: string;\n readonly size: number;\n /** Relative to the publish directory: \"<game>/<date>.<hash>.json\". */\n readonly file: string;\n}\n\nexport interface Publisher {\n readonly dir: string;\n /** Writes one day's puzzle file (if it isn't there already) and returns its hash and size. */\n writePuzzle(gameId: string, date: string, config: Json): PublishedFile;\n /** Replaces a game's index. */\n writeIndex(gameId: string, dates: readonly DailyIndexEntry[]): void;\n}\n\n/** The puzzle file body for a config: the envelope, as compact JSON. */\nexport const puzzleFileBody = (gameId: string, date: string, config: Json): { body: string; hash: string } => {\n const hash = contentHash(config);\n const file: DailyFile = {\n schemaVersion: DAILY_SCHEMA_VERSION,\n puzzleId: puzzleId(gameId, date),\n date,\n contentHash: hash,\n config,\n };\n return { body: JSON.stringify(file), hash };\n};\n\nlet counter = 0;\nconst writeAtomic = (path: string, body: string): void => {\n const temporary = `${path}.${String(process.pid)}.${String(++counter)}.tmp`;\n writeFileSync(temporary, body);\n renameSync(temporary, path);\n};\n\nexport const createPublisher = (dir: string): Publisher => ({\n dir,\n writePuzzle: (gameId, date, config) => {\n const { body, hash } = puzzleFileBody(gameId, date, config);\n const folder = join(dir, gameId);\n mkdirSync(folder, { recursive: true });\n const name = `${date}.${hash}.json`;\n const path = join(folder, name);\n if (!existsSync(path)) writeAtomic(path, body);\n return { hash, size: Buffer.byteLength(body), file: `${gameId}/${name}` };\n },\n writeIndex: (gameId, dates) => {\n const folder = join(dir, gameId);\n mkdirSync(folder, { recursive: true });\n const index: DailyIndex = {\n schemaVersion: DAILY_SCHEMA_VERSION,\n gameId,\n dates: [...dates].sort((a, b) => (a.date < b.date ? -1 : 1)),\n };\n writeAtomic(join(folder, 'index.json'), `${JSON.stringify(index)}\\n`);\n },\n});\n"],"mappings":";;;;;;AA+BA,IAAa,kBAAkB,QAAgB,MAAc,WAAiD;CAC5G,MAAM,OAAO,YAAY,MAAM;CAC/B,MAAM,OAAkB;EACtB,eAAe;EACf,UAAU,SAAS,QAAQ,IAAI;EAC/B;EACA,aAAa;EACb;CACF;CACA,OAAO;EAAE,MAAM,KAAK,UAAU,IAAI;EAAG;CAAK;AAC5C;AAEA,IAAI,UAAU;AACd,IAAM,eAAe,MAAc,SAAuB;CACxD,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,QAAQ,GAAG,EAAE,GAAG,OAAO,EAAE,OAAO,EAAE;CACtE,cAAc,WAAW,IAAI;CAC7B,WAAW,WAAW,IAAI;AAC5B;AAEA,IAAa,mBAAmB,SAA4B;CAC1D;CACA,cAAc,QAAQ,MAAM,WAAW;EACrC,MAAM,EAAE,MAAM,SAAS,eAAe,QAAQ,MAAM,MAAM;EAC1D,MAAM,SAAS,KAAK,KAAK,MAAM;EAC/B,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;EACrC,MAAM,OAAO,GAAG,KAAK,GAAG,KAAK;EAC7B,MAAM,OAAO,KAAK,QAAQ,IAAI;EAC9B,IAAI,CAAC,WAAW,IAAI,GAAG,YAAY,MAAM,IAAI;EAC7C,OAAO;GAAE;GAAM,MAAM,OAAO,WAAW,IAAI;GAAG,MAAM,GAAG,OAAO,GAAG;EAAO;CAC1E;CACA,aAAa,QAAQ,UAAU;EAC7B,MAAM,SAAS,KAAK,KAAK,MAAM;EAC/B,UAAU,QAAQ,EAAE,WAAW,KAAK,CAAC;EACrC,MAAM,QAAoB;GACxB,eAAe;GACf;GACA,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAO,EAAE,OAAO,EAAE,OAAO,KAAK,CAAE;EAC7D;EACA,YAAY,KAAK,QAAQ,YAAY,GAAG,GAAG,KAAK,UAAU,KAAK,EAAE,GAAG;CACtE;AACF"}
@@ -0,0 +1,12 @@
1
+ /** Word → SCOWL size (10, 20, 35, 40, 50, 55 or 60). Lower is more common. */
2
+ export type RankedWords = ReadonlyMap<string, number>;
3
+ /** Parses "<word> <size>" lines; blank lines and "#" comments are skipped. */
4
+ export declare const parseRanked: (text: string) => Map<string, number>;
5
+ /** The ranked list from lists/scowl-ranked.txt. */
6
+ export declare const rankedWords: () => RankedWords;
7
+ /**
8
+ * A word's familiarity score from 1 to 100, from its SCOWL size (crossword.md, "The crossword
9
+ * word list"): size 10 scores 100 and each level down costs ten points (size 50 scores 60,
10
+ * size 60 scores 40). Staff adjustments, when they exist, override it.
11
+ */
12
+ export declare const scoreOfSize: (size: number) => number;
package/dist/ranked.js ADDED
@@ -0,0 +1,40 @@
1
+ import { readFileSync } from "node:fs";
2
+ //#region src/ranked.ts
3
+ /** Parses "<word> <size>" lines; blank lines and "#" comments are skipped. */
4
+ var parseRanked = (text) => {
5
+ const words = /* @__PURE__ */ new Map();
6
+ for (const line of text.split("\n")) {
7
+ const trimmed = line.trim();
8
+ if (trimmed === "" || trimmed.startsWith("#")) continue;
9
+ const [word, size] = trimmed.split(/\s+/);
10
+ const level = Number(size);
11
+ if (word !== void 0 && /^[a-z]+$/.test(word) && Number.isInteger(level)) words.set(word, level);
12
+ }
13
+ return words;
14
+ };
15
+ var cached = null;
16
+ /** The ranked list from lists/scowl-ranked.txt. */
17
+ var rankedWords = () => {
18
+ cached ??= parseRanked(readFileSync(new URL("../lists/scowl-ranked.txt", import.meta.url), "utf8"));
19
+ return cached;
20
+ };
21
+ /**
22
+ * A word's familiarity score from 1 to 100, from its SCOWL size (crossword.md, "The crossword
23
+ * word list"): size 10 scores 100 and each level down costs ten points (size 50 scores 60,
24
+ * size 60 scores 40). Staff adjustments, when they exist, override it.
25
+ */
26
+ var scoreOfSize = (size) => {
27
+ return {
28
+ 10: 100,
29
+ 20: 90,
30
+ 35: 75,
31
+ 40: 70,
32
+ 50: 60,
33
+ 55: 50,
34
+ 60: 40
35
+ }[size] ?? Math.max(1, Math.min(100, 110 - size));
36
+ };
37
+ //#endregion
38
+ export { parseRanked, rankedWords, scoreOfSize };
39
+
40
+ //# sourceMappingURL=ranked.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ranked.js","names":[],"sources":["../src/ranked.ts"],"sourcesContent":["// The ranked word list (lists/build-ranked.ts): lowercase SCOWL words of 3 to 12 letters with\n// the smallest SCOWL size level that lists each one. Grid games build their answers from it:\n// Crossword scores fill by size, and Word Flux limits each puzzle size to a size level.\n// Loaded once, lazily, from the package's lists/ folder.\nimport { readFileSync } from 'node:fs';\n\n/** Word → SCOWL size (10, 20, 35, 40, 50, 55 or 60). Lower is more common. */\nexport type RankedWords = ReadonlyMap<string, number>;\n\n/** Parses \"<word> <size>\" lines; blank lines and \"#\" comments are skipped. */\nexport const parseRanked = (text: string): Map<string, number> => {\n const words = new Map<string, number>();\n for (const line of text.split('\\n')) {\n const trimmed = line.trim();\n if (trimmed === '' || trimmed.startsWith('#')) continue;\n const [word, size] = trimmed.split(/\\s+/);\n const level = Number(size);\n if (word !== undefined && /^[a-z]+$/.test(word) && Number.isInteger(level)) words.set(word, level);\n }\n return words;\n};\n\nlet cached: RankedWords | null = null;\n\n/** The ranked list from lists/scowl-ranked.txt. */\nexport const rankedWords = (): RankedWords => {\n cached ??= parseRanked(readFileSync(new URL('../lists/scowl-ranked.txt', import.meta.url), 'utf8'));\n return cached;\n};\n\n/**\n * A word's familiarity score from 1 to 100, from its SCOWL size (crossword.md, \"The crossword\n * word list\"): size 10 scores 100 and each level down costs ten points (size 50 scores 60,\n * size 60 scores 40). Staff adjustments, when they exist, override it.\n */\nexport const scoreOfSize = (size: number): number => {\n const scores: Readonly<Record<number, number>> = { 10: 100, 20: 90, 35: 75, 40: 70, 50: 60, 55: 50, 60: 40 };\n return scores[size] ?? Math.max(1, Math.min(100, 110 - size));\n};\n"],"mappings":";;;AAUA,IAAa,eAAe,SAAsC;CAChE,MAAM,wBAAQ,IAAI,IAAoB;CACtC,KAAK,MAAM,QAAQ,KAAK,MAAM,IAAI,GAAG;EACnC,MAAM,UAAU,KAAK,KAAK;EAC1B,IAAI,YAAY,MAAM,QAAQ,WAAW,GAAG,GAAG;EAC/C,MAAM,CAAC,MAAM,QAAQ,QAAQ,MAAM,KAAK;EACxC,MAAM,QAAQ,OAAO,IAAI;EACzB,IAAI,SAAS,KAAA,KAAa,WAAW,KAAK,IAAI,KAAK,OAAO,UAAU,KAAK,GAAG,MAAM,IAAI,MAAM,KAAK;CACnG;CACA,OAAO;AACT;AAEA,IAAI,SAA6B;;AAGjC,IAAa,oBAAiC;CAC5C,WAAW,YAAY,aAAa,IAAI,IAAI,6BAA6B,YAAY,GAAG,GAAG,MAAM,CAAC;CAClG,OAAO;AACT;;;;;;AAOA,IAAa,eAAe,SAAyB;CAEnD,OAAO;EAD4C,IAAI;EAAK,IAAI;EAAI,IAAI;EAAI,IAAI;EAAI,IAAI;EAAI,IAAI;EAAI,IAAI;CACjG,EAAO,SAAS,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,CAAC;AAC9D"}
@@ -0,0 +1,25 @@
1
+ import { type Result } from '@joystyk/contracts';
2
+ import * as z from 'zod';
3
+ import type { FieldText } from './filter.js';
4
+ import type { CompletionFailure, ModelClient, CallInfo } from './model.js';
5
+ export declare const SCREEN_PROMPT_VERSION = "screen-v1";
6
+ export declare const screenVerdict: z.ZodObject<{
7
+ safe: z.ZodBoolean;
8
+ reasons: z.ZodArray<z.ZodString>;
9
+ }, z.core.$strip>;
10
+ export type ScreenVerdict = z.infer<typeof screenVerdict>;
11
+ export declare const SCREEN_SYSTEM: string;
12
+ export declare const screenMessages: (fields: readonly FieldText[]) => readonly [{
13
+ readonly role: "system";
14
+ readonly content: string;
15
+ }, {
16
+ readonly role: "user";
17
+ readonly content: `Check these fields:
18
+ ${string}`;
19
+ }];
20
+ export type ScreenResult = Result<{
21
+ verdict: ScreenVerdict;
22
+ call: CallInfo;
23
+ }, CompletionFailure>;
24
+ /** Runs the model screen over the fields. */
25
+ export declare const modelScreen: (model: ModelClient, fields: readonly FieldText[], signal?: AbortSignal) => Promise<ScreenResult>;
package/dist/screen.js ADDED
@@ -0,0 +1,46 @@
1
+ import { err, ok } from "@joystyk/contracts";
2
+ import * as z from "zod";
3
+ //#region src/screen.ts
4
+ var SCREEN_PROMPT_VERSION = "screen-v1";
5
+ var screenVerdict = z.object({
6
+ safe: z.boolean(),
7
+ reasons: z.array(z.string().max(200)).max(10)
8
+ });
9
+ var SCREEN_SYSTEM = [
10
+ "You check text for a puzzle website that children play. Be strict.",
11
+ "Mark the text unsafe if any field contains or implies any of these:",
12
+ "- profanity, insults, slurs, or anything sexual or romantic;",
13
+ "- violence, weapons, war, death, injury, illness, or disasters;",
14
+ "- crime, drugs, alcohol, tobacco, or gambling;",
15
+ "- religion, politics, or current news;",
16
+ "- a real living person, a brand, a product, or a company;",
17
+ "- anything scary, cruel, mean, or sad enough to upset a young child.",
18
+ "Judge safety only. Puzzles include wrong answer choices on purpose, so never mark text unsafe for being false, nonsensical or ungrammatical; other checks handle accuracy.",
19
+ "Everyday things are fine: mild feelings (bored, grumpy, tired), harmless tricks and games, the ordinary words \"news\" or \"newspaper\" without naming real events, school, family and pets. Words for ordinary unkind behavior, such as scolding, teasing or grumbling, are fine when the text itself is gentle.",
20
+ "Reply with JSON: {\"safe\": boolean, \"reasons\": [short reasons, empty when safe]}."
21
+ ].join("\n");
22
+ var screenMessages = (fields) => [{
23
+ role: "system",
24
+ content: SCREEN_SYSTEM
25
+ }, {
26
+ role: "user",
27
+ content: `Check these fields:\n${fields.map((field) => `${field.path}: ${field.text}`).join("\n")}`
28
+ }];
29
+ /** Runs the model screen over the fields. */
30
+ var modelScreen = async (model, fields, signal) => {
31
+ const result = await model.complete({
32
+ name: "safety_screen",
33
+ schema: screenVerdict,
34
+ messages: screenMessages(fields),
35
+ temperature: 0,
36
+ ...signal === void 0 ? {} : { signal }
37
+ });
38
+ return result.ok ? ok({
39
+ verdict: result.value.value,
40
+ call: result.value.call
41
+ }) : err(result.error);
42
+ };
43
+ //#endregion
44
+ export { SCREEN_PROMPT_VERSION, SCREEN_SYSTEM, modelScreen, screenMessages, screenVerdict };
45
+
46
+ //# sourceMappingURL=screen.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen.js","names":[],"sources":["../src/screen.ts"],"sourcesContent":["// Layer 3 of the all-ages filter: a second model call screens every text field against the\n// same rules as the word lists (content-pipeline.md, \"The all-ages filter\"). It can only add\n// a rejection: a \"safe\" verdict never overrides the blocklist or the sensitive list, and a\n// failed screening call fails the attempt, because nothing publishes unscreened.\nimport { err, ok, type Result } from '@joystyk/contracts';\nimport * as z from 'zod';\nimport type { FieldText } from './filter.ts';\nimport type { CompletionFailure, ModelClient, CallInfo } from './model.ts';\n\nexport const SCREEN_PROMPT_VERSION = 'screen-v1';\n\nexport const screenVerdict = z.object({\n safe: z.boolean(),\n reasons: z.array(z.string().max(200)).max(10),\n});\nexport type ScreenVerdict = z.infer<typeof screenVerdict>;\n\nexport const SCREEN_SYSTEM = [\n 'You check text for a puzzle website that children play. Be strict.',\n 'Mark the text unsafe if any field contains or implies any of these:',\n '- profanity, insults, slurs, or anything sexual or romantic;',\n '- violence, weapons, war, death, injury, illness, or disasters;',\n '- crime, drugs, alcohol, tobacco, or gambling;',\n '- religion, politics, or current news;',\n '- a real living person, a brand, a product, or a company;',\n '- anything scary, cruel, mean, or sad enough to upset a young child.',\n 'Judge safety only. Puzzles include wrong answer choices on purpose, so never mark text unsafe for being false, nonsensical or ungrammatical; other checks handle accuracy.',\n 'Everyday things are fine: mild feelings (bored, grumpy, tired), harmless tricks and games, the ordinary words \"news\" or \"newspaper\" without naming real events, school, family and pets. Words for ordinary unkind behavior, such as scolding, teasing or grumbling, are fine when the text itself is gentle.',\n 'Reply with JSON: {\"safe\": boolean, \"reasons\": [short reasons, empty when safe]}.',\n].join('\\n');\n\nexport const screenMessages = (fields: readonly FieldText[]) =>\n [\n { role: 'system', content: SCREEN_SYSTEM },\n {\n role: 'user',\n content: `Check these fields:\\n${fields.map((field) => `${field.path}: ${field.text}`).join('\\n')}`,\n },\n ] as const;\n\nexport type ScreenResult = Result<{ verdict: ScreenVerdict; call: CallInfo }, CompletionFailure>;\n\n/** Runs the model screen over the fields. */\nexport const modelScreen = async (\n model: ModelClient,\n fields: readonly FieldText[],\n signal?: AbortSignal,\n): Promise<ScreenResult> => {\n const result = await model.complete({\n name: 'safety_screen',\n schema: screenVerdict,\n messages: screenMessages(fields),\n temperature: 0,\n ...(signal === undefined ? {} : { signal }),\n });\n return result.ok ? ok({ verdict: result.value.value, call: result.value.call }) : err(result.error);\n};\n"],"mappings":";;;AASA,IAAa,wBAAwB;AAErC,IAAa,gBAAgB,EAAE,OAAO;CACpC,MAAM,EAAE,QAAQ;CAChB,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE;AAC9C,CAAC;AAGD,IAAa,gBAAgB;CAC3B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC,CAAC,KAAK,IAAI;AAEX,IAAa,kBAAkB,WAC7B,CACE;CAAE,MAAM;CAAU,SAAS;AAAc,GACzC;CACE,MAAM;CACN,SAAS,wBAAwB,OAAO,KAAK,UAAU,GAAG,MAAM,KAAK,IAAI,MAAM,MAAM,CAAC,CAAC,KAAK,IAAI;AAClG,CACF;;AAKF,IAAa,cAAc,OACzB,OACA,QACA,WAC0B;CAC1B,MAAM,SAAS,MAAM,MAAM,SAAS;EAClC,MAAM;EACN,QAAQ;EACR,UAAU,eAAe,MAAM;EAC/B,aAAa;EACb,GAAI,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,OAAO;CAC3C,CAAC;CACD,OAAO,OAAO,KAAK,GAAG;EAAE,SAAS,OAAO,MAAM;EAAO,MAAM,OAAO,MAAM;CAAK,CAAC,IAAI,IAAI,OAAO,KAAK;AACpG"}
@@ -0,0 +1,136 @@
1
+ import { type Json } from '@joystyk/contracts';
2
+ export type ItemStatus = 'scheduled' | 'generating' | 'failed' | 'validated' | 'published' | 'withdrawn';
3
+ export type ItemSource = 'generated' | 'bank' | 'staff';
4
+ /** Reason codes for a takedown (content-pipeline.md, "Moderation in /admin"). */
5
+ export declare const WITHDRAW_REASONS: readonly ["inaccurate", "unfair", "unsafe", "dull", "other"];
6
+ export type WithdrawReason = (typeof WITHDRAW_REASONS)[number];
7
+ export interface ItemRow {
8
+ readonly id: number;
9
+ readonly gameId: string;
10
+ readonly puzzleId: string;
11
+ readonly date: string;
12
+ readonly status: ItemStatus;
13
+ readonly source: ItemSource | null;
14
+ readonly inputs: Json | null;
15
+ readonly seed: string | null;
16
+ readonly model: string | null;
17
+ readonly promptVersion: string | null;
18
+ /** The answer key used for duplicate checks, e.g. the day's word. */
19
+ readonly answer: string | null;
20
+ readonly config: Json | null;
21
+ readonly contentHash: string | null;
22
+ readonly size: number | null;
23
+ readonly attempts: number;
24
+ /** Every attempt's report: calls, validator issues, filter and screen outcomes. */
25
+ readonly report: Json | null;
26
+ readonly createdAt: string;
27
+ readonly updatedAt: string;
28
+ readonly publishedAt: string | null;
29
+ readonly withdrawnAt: string | null;
30
+ readonly withdrawnReason: string | null;
31
+ readonly withdrawnBy: string | null;
32
+ }
33
+ export type ItemPatch = Partial<Pick<ItemRow, 'status' | 'source' | 'inputs' | 'seed' | 'model' | 'promptVersion' | 'answer' | 'config' | 'contentHash' | 'size' | 'attempts' | 'report' | 'publishedAt'>>;
34
+ export interface BankChange {
35
+ readonly gameId: string;
36
+ readonly key: string;
37
+ readonly config: Json | null;
38
+ readonly retired: boolean;
39
+ readonly actor: string;
40
+ readonly changedAt: string;
41
+ }
42
+ export interface RunRow {
43
+ readonly id: number;
44
+ readonly startedAt: string;
45
+ readonly finishedAt: string | null;
46
+ readonly trigger: string;
47
+ readonly status: 'running' | 'ok' | 'partial' | 'failed' | 'aborted';
48
+ readonly summary: Json | null;
49
+ }
50
+ export interface ContentStore {
51
+ readonly path: string;
52
+ bankChanges(gameId: string): readonly BankChange[];
53
+ changeBank(input: {
54
+ readonly gameId: string;
55
+ readonly key: string;
56
+ readonly config: Json | null;
57
+ readonly retired: boolean;
58
+ readonly actor: string;
59
+ readonly now: number;
60
+ }): void;
61
+ /** Atomically archives the prior version and creates a validated staff replacement. */
62
+ replaceItem(input: {
63
+ readonly expectedId: number;
64
+ readonly actor: string;
65
+ readonly note: string;
66
+ readonly now: number;
67
+ readonly source: 'staff' | 'bank';
68
+ readonly inputs?: Json | null;
69
+ readonly status: 'validated' | 'published';
70
+ readonly config: Json;
71
+ readonly answer: string | null;
72
+ readonly hash: string;
73
+ readonly size: number;
74
+ }): ItemRow | null;
75
+ /** The item that currently holds a puzzle id (any status but withdrawn), or null. */
76
+ current(puzzleId: string): ItemRow | null;
77
+ item(id: number): ItemRow | null;
78
+ /** Every item for a puzzle id, oldest first, withdrawn ones included. */
79
+ history(puzzleId: string): ItemRow[];
80
+ createItem(input: {
81
+ gameId: string;
82
+ puzzleId: string;
83
+ date: string;
84
+ inputs: Json;
85
+ seed: string;
86
+ answer: string | null;
87
+ now: number;
88
+ }): ItemRow;
89
+ updateItem(id: number, patch: ItemPatch, now: number, event?: {
90
+ kind: string;
91
+ actor?: string;
92
+ detail?: Json;
93
+ }): ItemRow;
94
+ /**
95
+ * Answers used by a game's items dated in [from, to], withdrawn ones included (they count as
96
+ * used), and the answers their schedules reserved, even where a bank item took the slot: a
97
+ * word that failed on one day isn't tried again the next.
98
+ */
99
+ usedAnswers(gameId: string, from: string, to: string): {
100
+ date: string;
101
+ answer: string;
102
+ status: ItemStatus;
103
+ }[];
104
+ /** Published (not withdrawn) items of a game dated in [from, to], by date. */
105
+ published(gameId: string, from: string, to: string): ItemRow[];
106
+ /** Validated items waiting to publish, dated up to `through`. */
107
+ validatedThrough(gameId: string, through: string): ItemRow[];
108
+ /** Items of a game in [from, to] with any status, current ones only, by date. */
109
+ items(gameId: string, from: string, to: string): ItemRow[];
110
+ /** All versions in the date range, including withdrawn content, for repeat checks. */
111
+ versions(gameId: string, from: string, to: string): ItemRow[];
112
+ /** Takes an item down (content-pipeline.md, "Corrections"). Returns the withdrawn row, or null if none held the id. */
113
+ withdraw(puzzleId: string, reason: WithdrawReason, actor: string, now: number, note?: string): ItemRow | null;
114
+ addEvent(itemId: number | null, kind: string, actor: string, now: number, detail?: Json): void;
115
+ events(itemId: number): {
116
+ at: string;
117
+ kind: string;
118
+ actor: string;
119
+ detail: Json | null;
120
+ }[];
121
+ startRun(trigger: string, now: number): number;
122
+ finishRun(id: number, status: RunRow['status'], summary: Json, now: number): void;
123
+ recentRuns(limit: number): RunRow[];
124
+ /**
125
+ * Takes the named lock for `owner` until `now + ttlMs`, or refreshes it if `owner` holds it.
126
+ * False when someone else holds an unexpired lock. A crashed holder's lock expires.
127
+ */
128
+ tryLock(name: string, owner: string, now: number, ttlMs: number): boolean;
129
+ releaseLock(name: string, owner: string): void;
130
+ lockHolder(name: string, now: number): {
131
+ owner: string;
132
+ acquiredAt: string;
133
+ } | null;
134
+ close(): void;
135
+ }
136
+ export declare const openStore: (path: string) => ContentStore;
package/dist/store.js ADDED
@@ -0,0 +1,300 @@
1
+ import { isJson } from "@joystyk/contracts";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import * as z from "zod/mini";
4
+ //#region src/store.ts
5
+ /** Reason codes for a takedown (content-pipeline.md, "Moderation in /admin"). */
6
+ var WITHDRAW_REASONS = [
7
+ "inaccurate",
8
+ "unfair",
9
+ "unsafe",
10
+ "dull",
11
+ "other"
12
+ ];
13
+ /** The schema, one migration per `user_version`. Append; never edit a shipped entry. */
14
+ var MIGRATIONS = [`CREATE TABLE items (
15
+ id INTEGER PRIMARY KEY,
16
+ game_id TEXT NOT NULL,
17
+ puzzle_id TEXT NOT NULL,
18
+ date TEXT NOT NULL,
19
+ status TEXT NOT NULL,
20
+ source TEXT,
21
+ inputs TEXT,
22
+ seed TEXT,
23
+ model TEXT,
24
+ prompt_version TEXT,
25
+ answer TEXT,
26
+ reserved TEXT,
27
+ config TEXT,
28
+ content_hash TEXT,
29
+ size INTEGER,
30
+ attempts INTEGER NOT NULL DEFAULT 0,
31
+ report TEXT,
32
+ created_at TEXT NOT NULL,
33
+ updated_at TEXT NOT NULL,
34
+ published_at TEXT,
35
+ withdrawn_at TEXT,
36
+ withdrawn_reason TEXT,
37
+ withdrawn_by TEXT
38
+ );
39
+ CREATE UNIQUE INDEX items_current ON items (puzzle_id) WHERE status <> 'withdrawn';
40
+ CREATE INDEX items_game_date ON items (game_id, date);
41
+ CREATE TABLE runs (
42
+ id INTEGER PRIMARY KEY,
43
+ started_at TEXT NOT NULL,
44
+ finished_at TEXT,
45
+ trigger TEXT NOT NULL,
46
+ status TEXT NOT NULL,
47
+ summary TEXT
48
+ );
49
+ CREATE TABLE events (
50
+ id INTEGER PRIMARY KEY,
51
+ item_id INTEGER REFERENCES items (id),
52
+ at TEXT NOT NULL,
53
+ kind TEXT NOT NULL,
54
+ actor TEXT NOT NULL,
55
+ detail TEXT
56
+ );
57
+ CREATE INDEX events_item ON events (item_id);
58
+ CREATE TABLE locks (
59
+ name TEXT PRIMARY KEY,
60
+ owner TEXT NOT NULL,
61
+ acquired_at TEXT NOT NULL,
62
+ expires_at INTEGER NOT NULL
63
+ );`, `CREATE TABLE bank_changes (
64
+ game_id TEXT NOT NULL,
65
+ bank_key TEXT NOT NULL,
66
+ config TEXT,
67
+ retired INTEGER NOT NULL CHECK (retired IN (0, 1)),
68
+ actor TEXT NOT NULL,
69
+ changed_at TEXT NOT NULL,
70
+ PRIMARY KEY (game_id, bank_key)
71
+ );`];
72
+ var iso = (ms) => new Date(ms).toISOString();
73
+ var json = (value) => value === null || value === void 0 ? null : JSON.stringify(value);
74
+ var storedJson = z.custom(isJson);
75
+ var parse = (text) => typeof text === "string" ? storedJson.parse(JSON.parse(text)) : null;
76
+ var str = (value) => typeof value === "string" ? value : null;
77
+ var toItem = (row) => ({
78
+ id: Number(row["id"]),
79
+ gameId: String(row["game_id"]),
80
+ puzzleId: String(row["puzzle_id"]),
81
+ date: String(row["date"]),
82
+ status: String(row["status"]),
83
+ source: str(row["source"]),
84
+ inputs: parse(row["inputs"]),
85
+ seed: str(row["seed"]),
86
+ model: str(row["model"]),
87
+ promptVersion: str(row["prompt_version"]),
88
+ answer: str(row["answer"]),
89
+ config: parse(row["config"]),
90
+ contentHash: str(row["content_hash"]),
91
+ size: row["size"] === null || row["size"] === void 0 ? null : Number(row["size"]),
92
+ attempts: Number(row["attempts"]),
93
+ report: parse(row["report"]),
94
+ createdAt: String(row["created_at"]),
95
+ updatedAt: String(row["updated_at"]),
96
+ publishedAt: str(row["published_at"]),
97
+ withdrawnAt: str(row["withdrawn_at"]),
98
+ withdrawnReason: str(row["withdrawn_reason"]),
99
+ withdrawnBy: str(row["withdrawn_by"])
100
+ });
101
+ var COLUMNS = {
102
+ status: "status",
103
+ source: "source",
104
+ inputs: "inputs",
105
+ seed: "seed",
106
+ model: "model",
107
+ promptVersion: "prompt_version",
108
+ answer: "answer",
109
+ config: "config",
110
+ contentHash: "content_hash",
111
+ size: "size",
112
+ attempts: "attempts",
113
+ report: "report",
114
+ publishedAt: "published_at"
115
+ };
116
+ var JSON_FIELDS = /* @__PURE__ */ new Set([
117
+ "inputs",
118
+ "config",
119
+ "report"
120
+ ]);
121
+ var openStore = (path) => {
122
+ const db = new DatabaseSync(path);
123
+ db.exec("PRAGMA busy_timeout = 5000");
124
+ if (path !== ":memory:") db.exec("PRAGMA journal_mode = WAL");
125
+ db.exec("PRAGMA foreign_keys = ON");
126
+ const version = Number(db.prepare("PRAGMA user_version").get().user_version);
127
+ if (version > MIGRATIONS.length) {
128
+ db.close();
129
+ throw new Error(`content.db has schema version ${String(version)}, newer than this build (${String(MIGRATIONS.length)}). Roll forward, or restore an older backup.`);
130
+ }
131
+ for (let v = version; v < MIGRATIONS.length; v++) {
132
+ db.exec("BEGIN IMMEDIATE");
133
+ try {
134
+ db.exec(MIGRATIONS[v] ?? "");
135
+ db.exec(`PRAGMA user_version = ${String(v + 1)}`);
136
+ db.exec("COMMIT");
137
+ } catch (error) {
138
+ db.exec("ROLLBACK");
139
+ throw error;
140
+ }
141
+ }
142
+ const all = (sql, ...params) => db.prepare(sql).all(...params);
143
+ const one = (sql, ...params) => db.prepare(sql).get(...params);
144
+ const run = (sql, ...params) => db.prepare(sql).run(...params);
145
+ const transaction = (fn) => {
146
+ db.exec("BEGIN IMMEDIATE");
147
+ try {
148
+ const value = fn();
149
+ db.exec("COMMIT");
150
+ return value;
151
+ } catch (error) {
152
+ db.exec("ROLLBACK");
153
+ throw error;
154
+ }
155
+ };
156
+ const addEvent = (itemId, kind, actor, now, detail) => {
157
+ run("INSERT INTO events (item_id, at, kind, actor, detail) VALUES (?, ?, ?, ?, ?)", itemId, iso(now), kind, actor, json(detail));
158
+ };
159
+ const item = (id) => {
160
+ const row = one("SELECT * FROM items WHERE id = ?", id);
161
+ return row === void 0 ? null : toItem(row);
162
+ };
163
+ return {
164
+ path,
165
+ current: (puzzleId) => {
166
+ const row = one("SELECT * FROM items WHERE puzzle_id = ? AND status <> 'withdrawn'", puzzleId);
167
+ return row === void 0 ? null : toItem(row);
168
+ },
169
+ item,
170
+ history: (puzzleId) => all("SELECT * FROM items WHERE puzzle_id = ? ORDER BY id", puzzleId).map(toItem),
171
+ bankChanges: (gameId) => all("SELECT * FROM bank_changes WHERE game_id = ? ORDER BY bank_key", gameId).map((row) => ({
172
+ gameId: String(row["game_id"]),
173
+ key: String(row["bank_key"]),
174
+ config: parse(row["config"]),
175
+ retired: row["retired"] === 1,
176
+ actor: String(row["actor"]),
177
+ changedAt: String(row["changed_at"])
178
+ })),
179
+ changeBank: ({ gameId, key, config, retired, actor, now }) => transaction(() => {
180
+ run("INSERT INTO bank_changes (game_id, bank_key, config, retired, actor, changed_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (game_id, bank_key) DO UPDATE SET config = excluded.config, retired = excluded.retired, actor = excluded.actor, changed_at = excluded.changed_at", gameId, key, json(config), retired ? 1 : 0, actor, iso(now));
181
+ addEvent(null, "bank.changed", actor, now, {
182
+ gameId,
183
+ key,
184
+ retired
185
+ });
186
+ }),
187
+ replaceItem: (input) => transaction(() => {
188
+ const previous = item(input.expectedId);
189
+ if (previous === null || previous.status === "withdrawn") return null;
190
+ const at = iso(input.now);
191
+ run("UPDATE items SET status = 'withdrawn', withdrawn_at = ?, withdrawn_reason = 'other', withdrawn_by = ?, updated_at = ? WHERE id = ?", at, input.actor, at, previous.id);
192
+ addEvent(previous.id, "withdrawn", input.actor, input.now, {
193
+ reason: "other",
194
+ from: previous.status,
195
+ note: input.note || "Staff correction."
196
+ });
197
+ const report = [{ checks: { staff: {
198
+ schema: true,
199
+ gameValidators: true,
200
+ allAgesFilter: true,
201
+ modelChecks: "not rerun; human correction",
202
+ previousItem: previous.id
203
+ } } }];
204
+ const result = run(`INSERT INTO items (game_id, puzzle_id, date, status, source, inputs, seed, answer, reserved, config, content_hash, size, prompt_version, report, created_at, updated_at, published_at)
205
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, previous.gameId, previous.puzzleId, previous.date, input.status, input.source, json(input.inputs === void 0 ? previous.inputs : input.inputs), previous.seed, input.answer, input.answer, json(input.config), input.hash, input.size, "staff-validated-v1", json(report), at, at, input.status === "published" ? at : null);
206
+ const id = Number(result.lastInsertRowid);
207
+ addEvent(id, input.source === "bank" ? "bank.selected" : "staff.edited", input.actor, input.now, {
208
+ previousItem: previous.id,
209
+ note: input.note,
210
+ hash: input.hash
211
+ });
212
+ return item(id);
213
+ }),
214
+ createItem: ({ gameId, puzzleId, date, inputs, seed, answer, now }) => transaction(() => {
215
+ const result = run(`INSERT INTO items (game_id, puzzle_id, date, status, inputs, seed, answer, reserved, created_at, updated_at)
216
+ VALUES (?, ?, ?, 'scheduled', ?, ?, ?, ?, ?, ?)`, gameId, puzzleId, date, json(inputs), seed, answer, answer, iso(now), iso(now));
217
+ const id = Number(result.lastInsertRowid);
218
+ addEvent(id, "scheduled", "content", now, { inputs });
219
+ return item(id);
220
+ }),
221
+ updateItem: (id, patch, now, event) => transaction(() => {
222
+ const sets = ["updated_at = ?"];
223
+ const values = [iso(now)];
224
+ for (const [key, value] of Object.entries(patch)) {
225
+ if (value === void 0) continue;
226
+ sets.push(`${COLUMNS[key]} = ?`);
227
+ values.push(JSON_FIELDS.has(key) ? json(value) : value);
228
+ }
229
+ if (run(`UPDATE items SET ${sets.join(", ")} WHERE id = ? AND status <> 'withdrawn'`, ...values, id).changes !== 0 && event !== void 0) addEvent(id, event.kind, event.actor ?? "content", now, event.detail);
230
+ return item(id);
231
+ }),
232
+ usedAnswers: (gameId, from, to) => all(`SELECT date, answer, status FROM items WHERE game_id = ? AND date >= ? AND date <= ? AND answer IS NOT NULL
233
+ UNION SELECT date, reserved, status FROM items WHERE game_id = ? AND date >= ? AND date <= ? AND reserved IS NOT NULL
234
+ ORDER BY date`, gameId, from, to, gameId, from, to).map((row) => ({
235
+ date: String(row["date"]),
236
+ answer: String(row["answer"]),
237
+ status: String(row["status"])
238
+ })),
239
+ published: (gameId, from, to) => all("SELECT * FROM items WHERE game_id = ? AND status = 'published' AND date >= ? AND date <= ? ORDER BY date", gameId, from, to).map(toItem),
240
+ validatedThrough: (gameId, through) => all("SELECT * FROM items WHERE game_id = ? AND status = 'validated' AND date <= ? ORDER BY date", gameId, through).map(toItem),
241
+ items: (gameId, from, to) => all("SELECT * FROM items WHERE game_id = ? AND status <> 'withdrawn' AND date >= ? AND date <= ? ORDER BY date", gameId, from, to).map(toItem),
242
+ versions: (gameId, from, to) => all("SELECT * FROM items WHERE game_id = ? AND date >= ? AND date <= ? ORDER BY date, id", gameId, from, to).map(toItem),
243
+ withdraw: (puzzleId, reason, actor, now, note) => transaction(() => {
244
+ const row = one("SELECT * FROM items WHERE puzzle_id = ? AND status <> 'withdrawn'", puzzleId);
245
+ if (row === void 0) return null;
246
+ const before = toItem(row);
247
+ run("UPDATE items SET status = 'withdrawn', withdrawn_at = ?, withdrawn_reason = ?, withdrawn_by = ?, updated_at = ? WHERE id = ?", iso(now), reason, actor, iso(now), before.id);
248
+ addEvent(before.id, "withdrawn", actor, now, {
249
+ reason,
250
+ from: before.status,
251
+ ...note === void 0 ? {} : { note }
252
+ });
253
+ return item(before.id);
254
+ }),
255
+ addEvent,
256
+ events: (itemId) => all("SELECT at, kind, actor, detail FROM events WHERE item_id = ? ORDER BY id", itemId).map((row) => ({
257
+ at: String(row["at"]),
258
+ kind: String(row["kind"]),
259
+ actor: String(row["actor"]),
260
+ detail: parse(row["detail"])
261
+ })),
262
+ startRun: (trigger, now) => Number(run("INSERT INTO runs (started_at, trigger, status) VALUES (?, ?, 'running')", iso(now), trigger).lastInsertRowid),
263
+ finishRun: (id, status, summary, now) => {
264
+ run("UPDATE runs SET finished_at = ?, status = ?, summary = ? WHERE id = ?", iso(now), status, json(summary), id);
265
+ },
266
+ recentRuns: (limit) => all("SELECT * FROM runs ORDER BY id DESC LIMIT ?", limit).map((row) => ({
267
+ id: Number(row["id"]),
268
+ startedAt: String(row["started_at"]),
269
+ finishedAt: str(row["finished_at"]),
270
+ trigger: String(row["trigger"]),
271
+ status: String(row["status"]),
272
+ summary: parse(row["summary"])
273
+ })),
274
+ tryLock: (name, owner, now, ttlMs) => transaction(() => {
275
+ const row = one("SELECT owner, expires_at FROM locks WHERE name = ?", name);
276
+ if (row !== void 0 && row["owner"] !== owner && Number(row["expires_at"]) > now) return false;
277
+ run(`INSERT INTO locks (name, owner, acquired_at, expires_at) VALUES (?, ?, ?, ?)
278
+ ON CONFLICT (name) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at,
279
+ acquired_at = CASE WHEN locks.owner = excluded.owner THEN locks.acquired_at ELSE excluded.acquired_at END`, name, owner, iso(now), now + ttlMs);
280
+ return true;
281
+ }),
282
+ releaseLock: (name, owner) => {
283
+ run("DELETE FROM locks WHERE name = ? AND owner = ?", name, owner);
284
+ },
285
+ lockHolder: (name, now) => {
286
+ const row = one("SELECT owner, acquired_at, expires_at FROM locks WHERE name = ?", name);
287
+ return row === void 0 || Number(row["expires_at"]) <= now ? null : {
288
+ owner: String(row["owner"]),
289
+ acquiredAt: String(row["acquired_at"])
290
+ };
291
+ },
292
+ close: () => {
293
+ if (db.isOpen) db.close();
294
+ }
295
+ };
296
+ };
297
+ //#endregion
298
+ export { WITHDRAW_REASONS, openStore };
299
+
300
+ //# sourceMappingURL=store.js.map