@sudobility/screenwriter_types 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/keys.js ADDED
@@ -0,0 +1,29 @@
1
+ import { z } from "zod";
2
+ const API_KEY_SCOPES = ["read", "read_write"];
3
+ const apiKeyScopeSchema = z.enum(API_KEY_SCOPES);
4
+ const API_KEY_MAX_ACTIVE = 25;
5
+ const API_KEY_FORMAT_RE = /^fwk_([a-z0-9]{8})_([A-Za-z0-9_-]{43})$/;
6
+ const apiKeyCreateSchema = z.object({
7
+ name: z.string().trim().min(1).max(60),
8
+ workspaceId: z.string().min(1),
9
+ scope: apiKeyScopeSchema,
10
+ ai: z.boolean().default(false),
11
+ expiresInDays: z.number().int().min(1).max(365).optional()
12
+ });
13
+ const apiKeyUpdateSchema = z.object({
14
+ name: z.string().trim().min(1).max(60).optional(),
15
+ ai: z.boolean().optional()
16
+ });
17
+ const CLIENT_HEADER = "X-Client";
18
+ const MCP_CLIENT_NAME = "fadewright-mcp";
19
+ export {
20
+ API_KEY_FORMAT_RE,
21
+ API_KEY_MAX_ACTIVE,
22
+ API_KEY_SCOPES,
23
+ CLIENT_HEADER,
24
+ MCP_CLIENT_NAME,
25
+ apiKeyCreateSchema,
26
+ apiKeyScopeSchema,
27
+ apiKeyUpdateSchema
28
+ };
29
+ //# sourceMappingURL=keys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keys.js","sources":["../src/keys/index.ts"],"sourcesContent":["import { z } from 'zod';\n\n/** Personal API keys (`fwk_`), spec 05 4.2 and 6.5. */\nexport const API_KEY_SCOPES = ['read', 'read_write'] as const;\nexport type ApiKeyScope = (typeof API_KEY_SCOPES)[number];\nexport const apiKeyScopeSchema = z.enum(API_KEY_SCOPES);\n\n/** At most this many active (unrevoked) keys per user. */\nexport const API_KEY_MAX_ACTIVE = 25;\n\n/** `fwk_` + 8-char public prefix + `_` + 32 bytes base64url secret. */\nexport const API_KEY_FORMAT_RE = /^fwk_([a-z0-9]{8})_([A-Za-z0-9_-]{43})$/;\n\n/** `GET /api-keys` item. Never contains the secret. */\nexport interface ApiKeySummary {\n id: string;\n name: string;\n /** The 8-char public prefix. */\n prefix: string;\n workspaceId: string;\n scope: ApiKeyScope;\n ai: boolean;\n createdAt: string;\n lastUsedAt: string | null;\n expiresAt: string | null;\n revokedAt: string | null;\n}\n\nexport const apiKeyCreateSchema = z.object({\n name: z.string().trim().min(1).max(60),\n workspaceId: z.string().min(1),\n scope: apiKeyScopeSchema,\n ai: z.boolean().default(false),\n expiresInDays: z.number().int().min(1).max(365).optional(),\n});\nexport type ApiKeyCreateRequest = z.input<typeof apiKeyCreateSchema>;\n\n/** `POST /api-keys` response: the only time `key` is ever returned. */\nexport type ApiKeyCreated = ApiKeySummary & { key: string };\n\nexport const apiKeyUpdateSchema = z.object({\n name: z.string().trim().min(1).max(60).optional(),\n ai: z.boolean().optional(),\n});\nexport type ApiKeyUpdateRequest = z.infer<typeof apiKeyUpdateSchema>;\n\n/** `DELETE /api-keys/:kid` response. */\nexport interface ApiKeyRevokeResponse {\n revokedAt: string;\n}\n\n/** Sent by API clients so command transactions record the right origin (`fadewright-mcp/<version>`). */\nexport const CLIENT_HEADER = 'X-Client';\nexport const MCP_CLIENT_NAME = 'fadewright-mcp';\n"],"names":[],"mappings":";AAGO,MAAM,iBAAiB,CAAC,QAAQ,YAAY;AAE5C,MAAM,oBAAoB,EAAE,KAAK,cAAc;AAG/C,MAAM,qBAAqB;AAG3B,MAAM,oBAAoB;AAiB1B,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,OAAA,EAAS,KAAA,EAAO,IAAI,CAAC,EAAE,IAAI,EAAE;AAAA,EACrC,aAAa,EAAE,SAAS,IAAI,CAAC;AAAA,EAC7B,OAAO;AAAA,EACP,IAAI,EAAE,UAAU,QAAQ,KAAK;AAAA,EAC7B,eAAe,EAAE,OAAA,EAAS,IAAA,EAAM,IAAI,CAAC,EAAE,IAAI,GAAG,EAAE,SAAA;AAClD,CAAC;AAMM,MAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,OAAA,EAAS,KAAA,EAAO,IAAI,CAAC,EAAE,IAAI,EAAE,EAAE,SAAA;AAAA,EACvC,IAAI,EAAE,QAAA,EAAU,SAAA;AAClB,CAAC;AASM,MAAM,gBAAgB;AACtB,MAAM,kBAAkB;"}
package/dist/test.js ADDED
@@ -0,0 +1,95 @@
1
+ const T = "2026-01-01T00:00:00.000Z";
2
+ function makeWorkspace(over = {}) {
3
+ return {
4
+ id: "ws_test",
5
+ kind: "personal",
6
+ name: "Personal",
7
+ avatarAssetId: null,
8
+ defaultTemplateId: null,
9
+ defaultLanguage: "en",
10
+ aiEnabled: true,
11
+ allowPublicLinks: true,
12
+ createdAt: T,
13
+ updatedAt: T,
14
+ ...over
15
+ };
16
+ }
17
+ function makeProject(over = {}) {
18
+ return {
19
+ id: "prj_test",
20
+ workspaceId: "ws_test",
21
+ name: "Untitled Project",
22
+ description: null,
23
+ color: null,
24
+ coverAssetId: null,
25
+ kind: "feature",
26
+ logline: null,
27
+ documentCount: 0,
28
+ createdAt: T,
29
+ updatedAt: T,
30
+ trashedAt: null,
31
+ role: "owner",
32
+ folders: [],
33
+ documents: [],
34
+ ...over
35
+ };
36
+ }
37
+ function makeDocumentMeta(over = {}) {
38
+ return {
39
+ id: "doc_test",
40
+ projectId: "prj_test",
41
+ workspaceId: "ws_test",
42
+ folderId: null,
43
+ kind: "script",
44
+ title: "Untitled Screenplay",
45
+ logline: null,
46
+ color: null,
47
+ labels: [],
48
+ season: null,
49
+ episode: null,
50
+ position: 0,
51
+ language: "en",
52
+ templateId: null,
53
+ templateVersion: null,
54
+ epoch: 0,
55
+ excludeFromAi: false,
56
+ locked: false,
57
+ pageCount: 0,
58
+ sceneCount: 0,
59
+ wordCount: 0,
60
+ parentSnapshotId: null,
61
+ createdBy: null,
62
+ createdAt: T,
63
+ updatedAt: T,
64
+ lastEditedAt: null,
65
+ trashedAt: null,
66
+ ...over
67
+ };
68
+ }
69
+ function makeSnapshotSummary(over = {}) {
70
+ return {
71
+ id: "snap_test",
72
+ documentId: "doc_test",
73
+ parentId: null,
74
+ name: "First draft",
75
+ note: null,
76
+ kind: "manual",
77
+ autoReason: null,
78
+ stats: { pages: 0, scenes: 0, words: 0 },
79
+ createdBy: null,
80
+ createdOnDevice: null,
81
+ createdAt: T,
82
+ receivedAt: T,
83
+ forkedDocumentIds: [],
84
+ hiddenForMe: false,
85
+ noteCount: 0,
86
+ ...over
87
+ };
88
+ }
89
+ export {
90
+ makeDocumentMeta,
91
+ makeProject,
92
+ makeSnapshotSummary,
93
+ makeWorkspace
94
+ };
95
+ //# sourceMappingURL=test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"test.js","sources":["../src/test/index.ts"],"sourcesContent":["/** Fixture factories for consumers' tests (`@sudobility/screenwriter_types/test`). */\nimport type { Workspace } from '../tenancy/index.js';\nimport type { Project, DocumentMeta } from '../projects/index.js';\nimport type { SnapshotSummary } from '../versions/index.js';\n\nconst T = '2026-01-01T00:00:00.000Z';\n\nexport function makeWorkspace(over: Partial<Workspace> = {}): Workspace {\n return {\n id: 'ws_test',\n kind: 'personal',\n name: 'Personal',\n avatarAssetId: null,\n defaultTemplateId: null,\n defaultLanguage: 'en',\n aiEnabled: true,\n allowPublicLinks: true,\n createdAt: T,\n updatedAt: T,\n ...over,\n };\n}\n\nexport function makeProject(over: Partial<Project> = {}): Project {\n return {\n id: 'prj_test',\n workspaceId: 'ws_test',\n name: 'Untitled Project',\n description: null,\n color: null,\n coverAssetId: null,\n kind: 'feature',\n logline: null,\n documentCount: 0,\n createdAt: T,\n updatedAt: T,\n trashedAt: null,\n role: 'owner',\n folders: [],\n documents: [],\n ...over,\n };\n}\n\nexport function makeDocumentMeta(\n over: Partial<DocumentMeta> = {}\n): DocumentMeta {\n return {\n id: 'doc_test',\n projectId: 'prj_test',\n workspaceId: 'ws_test',\n folderId: null,\n kind: 'script',\n title: 'Untitled Screenplay',\n logline: null,\n color: null,\n labels: [],\n season: null,\n episode: null,\n position: 0,\n language: 'en',\n templateId: null,\n templateVersion: null,\n epoch: 0,\n excludeFromAi: false,\n locked: false,\n pageCount: 0,\n sceneCount: 0,\n wordCount: 0,\n parentSnapshotId: null,\n createdBy: null,\n createdAt: T,\n updatedAt: T,\n lastEditedAt: null,\n trashedAt: null,\n ...over,\n };\n}\n\nexport function makeSnapshotSummary(\n over: Partial<SnapshotSummary> = {}\n): SnapshotSummary {\n return {\n id: 'snap_test',\n documentId: 'doc_test',\n parentId: null,\n name: 'First draft',\n note: null,\n kind: 'manual',\n autoReason: null,\n stats: { pages: 0, scenes: 0, words: 0 },\n createdBy: null,\n createdOnDevice: null,\n createdAt: T,\n receivedAt: T,\n forkedDocumentIds: [],\n hiddenForMe: false,\n noteCount: 0,\n ...over,\n };\n}\n"],"names":[],"mappings":"AAKA,MAAM,IAAI;AAEH,SAAS,cAAc,OAA2B,IAAe;AACtE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,MAAM;AAAA,IACN,eAAe;AAAA,IACf,mBAAmB;AAAA,IACnB,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,WAAW;AAAA,IACX,WAAW;AAAA,IACX,GAAG;AAAA,EAAA;AAEP;AAEO,SAAS,YAAY,OAAyB,IAAa;AAChE,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,aAAa;AAAA,IACb,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,cAAc;AAAA,IACd,MAAM;AAAA,IACN,SAAS;AAAA,IACT,eAAe;AAAA,IACf,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,MAAM;AAAA,IACN,SAAS,CAAA;AAAA,IACT,WAAW,CAAA;AAAA,IACX,GAAG;AAAA,EAAA;AAEP;AAEO,SAAS,iBACd,OAA8B,IAChB;AACd,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,WAAW;AAAA,IACX,aAAa;AAAA,IACb,UAAU;AAAA,IACV,MAAM;AAAA,IACN,OAAO;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,IACP,QAAQ,CAAA;AAAA,IACR,QAAQ;AAAA,IACR,SAAS;AAAA,IACT,UAAU;AAAA,IACV,UAAU;AAAA,IACV,YAAY;AAAA,IACZ,iBAAiB;AAAA,IACjB,OAAO;AAAA,IACP,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,cAAc;AAAA,IACd,WAAW;AAAA,IACX,GAAG;AAAA,EAAA;AAEP;AAEO,SAAS,oBACd,OAAiC,IAChB;AACjB,SAAO;AAAA,IACL,IAAI;AAAA,IACJ,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,MAAM;AAAA,IACN,MAAM;AAAA,IACN,MAAM;AAAA,IACN,YAAY;AAAA,IACZ,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,OAAO,EAAA;AAAA,IACrC,WAAW;AAAA,IACX,iBAAiB;AAAA,IACjB,WAAW;AAAA,IACX,YAAY;AAAA,IACZ,mBAAmB,CAAA;AAAA,IACnB,aAAa;AAAA,IACb,WAAW;AAAA,IACX,GAAG;AAAA,EAAA;AAEP;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/screenwriter_types",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Shared TypeScript types and Zod schemas for the Fadewright frontend and backend",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,15 +12,15 @@
12
12
  },
13
13
  "./test": {
14
14
  "types": "./dist/test/index.d.ts",
15
- "import": "./dist/test/index.js"
15
+ "import": "./dist/test.js"
16
16
  },
17
17
  "./keys": {
18
18
  "types": "./dist/keys/index.d.ts",
19
- "import": "./dist/keys/index.js"
19
+ "import": "./dist/keys.js"
20
20
  }
21
21
  },
22
22
  "scripts": {
23
- "build": "tsc -p tsconfig.esm.json",
23
+ "build": "vite build && tsc -p tsconfig.build.json",
24
24
  "clean": "rimraf dist",
25
25
  "test:unit": "vitest run",
26
26
  "test:watch": "vitest",
@@ -50,6 +50,7 @@
50
50
  "prettier": "^3.6.2",
51
51
  "rimraf": "^6.0.1",
52
52
  "typescript": "^5.9.3",
53
+ "vite": "^7.1.12",
53
54
  "vitest": "^4.0.15"
54
55
  },
55
56
  "publishConfig": {
@@ -1,274 +0,0 @@
1
- import { z } from 'zod';
2
- /**
3
- * Account and per-user data (slice B13; spec 05 §6.1, §6.1.1, §6.26, §8.1, §17).
4
- * Everything here belongs to one user: preferences, spelling dictionary, macros, writing sessions, stats, goals, the
5
- * per-document view state (`my-state`) and stars. All are user-only routes (an API key gets 403 `API_KEY_FORBIDDEN`),
6
- * except the macro list, which a key may read.
7
- */
8
- // ─── user preferences (§6.1.1) ────────────────────────────────────────────────
9
- export const USER_PREFERENCES_VERSION = 1;
10
- /** Serialised size cap of `PUT /me/preferences` (`LIMIT_EXCEEDED`). */
11
- export const USER_PREFERENCES_MAX_BYTES = 256 * 1024;
12
- export const AUTOCORRECT_MAX_PAIRS = 5000;
13
- export const RECENT_SEARCHES_MAX = 20;
14
- export const DEVICE_CLASSES = ['phone', 'tablet', 'desktop'];
15
- /** A section is an open object: its keys are owned by spec 09/10 features and grow without a types release. */
16
- const section = z.looseObject({});
17
- const appearanceSchema = z.looseObject({
18
- theme: z.enum(['system', 'light', 'dark']).optional(),
19
- pageTheme: z
20
- .enum(['paper', 'night', 'midnight', 'sepia', 'custom'])
21
- .optional(),
22
- custom: section.optional(),
23
- density: z.string().max(40).optional(),
24
- largeIcons: z.boolean().optional(),
25
- reducedMotion: z.boolean().optional(),
26
- });
27
- const keymapSchema = z.looseObject({
28
- preset: z.enum(['fadewright', 'finalDraft', 'fadeIn']).optional(),
29
- overrides: z.record(z.string(), z.string().nullable()).optional(),
30
- byPlatform: section.optional(),
31
- });
32
- const autocorrectSchema = z.looseObject({
33
- options: section.optional(),
34
- pairs: z
35
- .array(z.object({ from: z.string().min(1).max(200), to: z.string().max(200) }))
36
- .max(AUTOCORRECT_MAX_PAIRS)
37
- .optional(),
38
- });
39
- const languageSchema = z.looseObject({
40
- scriptDefault: z.string().max(35).optional(),
41
- installedDictionaries: z.array(z.string().max(35)).max(50).optional(),
42
- ignoreLists: section.optional(),
43
- lookups: z
44
- .array(z.object({ label: z.string().max(80), url: z.string().max(500) }))
45
- .max(50)
46
- .optional(),
47
- });
48
- const tableReadSchema = z.looseObject({
49
- narratorVoiceByLanguage: z.record(z.string(), section).optional(),
50
- rate: z.number().optional(),
51
- actors: z
52
- .array(z.looseObject({ id: z.string(), name: z.string() }))
53
- .max(200)
54
- .optional(),
55
- });
56
- /**
57
- * `UserPreferences`: one JSON document per user, closed at the top level (an unknown top-level key is `VALIDATION`),
58
- * open inside each section. `version` is `USER_PREFERENCES_VERSION`; a higher version from a newer client is stored as is.
59
- */
60
- export const userPreferencesSchema = z.strictObject({
61
- version: z.number().int().min(1).max(1000).default(USER_PREFERENCES_VERSION),
62
- appearance: appearanceSchema.optional(),
63
- editor: section.optional(),
64
- deviceClasses: z
65
- .strictObject({
66
- phone: section.optional(),
67
- tablet: section.optional(),
68
- desktop: section.optional(),
69
- })
70
- .optional(),
71
- keymap: keymapSchema.optional(),
72
- autocorrect: autocorrectSchema.optional(),
73
- language: languageSchema.optional(),
74
- tableRead: tableReadSchema.optional(),
75
- toolbar: section.optional(),
76
- recentSearches: z
77
- .array(z.string().max(500))
78
- .max(RECENT_SEARCHES_MAX)
79
- .optional(),
80
- revisionColourSets: z.array(section).max(100).optional(),
81
- offline: section.optional(),
82
- });
83
- export const USER_PREFERENCES_KEYS = [
84
- 'version',
85
- 'appearance',
86
- 'editor',
87
- 'deviceClasses',
88
- 'keymap',
89
- 'autocorrect',
90
- 'language',
91
- 'tableRead',
92
- 'toolbar',
93
- 'recentSearches',
94
- 'revisionColourSets',
95
- 'offline',
96
- ];
97
- /** `PUT /me/preferences`: a whole-document replace; `baseUpdatedAt` is the `updatedAt` of the last GET/PUT. */
98
- export const userPreferencesPutSchema = userPreferencesSchema.extend({
99
- baseUpdatedAt: z.string().min(1).max(40),
100
- });
101
- // ─── dictionary ───────────────────────────────────────────────────────────────
102
- export const DICTIONARY_MAX_WORDS = 50_000;
103
- const dictionaryWord = z.string().trim().min(1).max(100);
104
- export const dictionaryUpdateSchema = z.object({
105
- add: z.array(dictionaryWord).max(5000).optional(),
106
- remove: z.array(dictionaryWord).max(5000).optional(),
107
- });
108
- // ─── macros ───────────────────────────────────────────────────────────────────
109
- export const USER_MACRO_MAX = 500;
110
- export const MACRO_TRIGGER_KINDS = ['shortcut', 'alias'];
111
- const macroOptionFields = {
112
- smartReplace: z.boolean(),
113
- confirm: z.boolean(),
114
- wordOnly: z.boolean(),
115
- matchCase: z.boolean(),
116
- activeInStyles: z.array(z.string().max(80)).max(100),
117
- };
118
- /** Create: every option defaulted. */
119
- const macroOptionsSchema = z.object({
120
- smartReplace: macroOptionFields.smartReplace.default(false),
121
- confirm: macroOptionFields.confirm.default(false),
122
- wordOnly: macroOptionFields.wordOnly.default(true),
123
- matchCase: macroOptionFields.matchCase.default(false),
124
- activeInStyles: macroOptionFields.activeInStyles.default([]),
125
- });
126
- /** Patch: no defaults (a default would overwrite the stored value of an option the request did not mention). */
127
- const macroOptionsPatchSchema = z.object(macroOptionFields).partial();
128
- export const userMacroSchema = z.object({
129
- name: z.string().trim().min(1).max(120),
130
- trigger: z.object({
131
- kind: z.enum(MACRO_TRIGGER_KINDS),
132
- value: z.string().trim().min(1).max(80),
133
- }),
134
- insertText: z.string().max(10_000),
135
- setElementStyle: z.string().max(80).nullable().optional(),
136
- nextElementStyle: z.string().max(80).nullable().optional(),
137
- options: macroOptionsSchema.prefault({}),
138
- });
139
- /** `PATCH /me/macros/:mid`: any subset; `options` and `trigger` merge into the stored ones. */
140
- export const userMacroPatchSchema = z.object({
141
- name: userMacroSchema.shape.name.optional(),
142
- trigger: z
143
- .object({
144
- kind: z.enum(MACRO_TRIGGER_KINDS).optional(),
145
- value: userMacroSchema.shape.trigger.shape.value.optional(),
146
- })
147
- .optional(),
148
- insertText: userMacroSchema.shape.insertText.optional(),
149
- setElementStyle: userMacroSchema.shape.setElementStyle,
150
- nextElementStyle: userMacroSchema.shape.nextElementStyle,
151
- options: macroOptionsPatchSchema.optional(),
152
- });
153
- // ─── writing sessions, stats, goals ───────────────────────────────────────────
154
- export const WRITING_GOAL_KINDS = [
155
- 'wordsPerDay',
156
- 'pagesPerDay',
157
- 'minutesPerDay',
158
- 'wordsPerWeek',
159
- 'documentTarget',
160
- ];
161
- export const WRITING_GOALS_MAX = 50;
162
- export const WRITING_STATS_GRANULARITIES = [
163
- 'day',
164
- 'week',
165
- 'month',
166
- 'year',
167
- ];
168
- const isoInstant = z.iso.datetime({ offset: true });
169
- /**
170
- * `POST /me/writing-sessions`. `clientSessionId` is the row id (`wss_...`, made by the client) and makes the upload
171
- * idempotent: a repeat is accepted and changes nothing, so a device may retry and may upload offline sessions later.
172
- */
173
- export const writingSessionSchema = z
174
- .object({
175
- clientSessionId: z
176
- .string()
177
- .min(8)
178
- .max(64)
179
- .regex(/^[A-Za-z0-9_-]+$/),
180
- documentId: z.string().min(1).max(100).nullable().optional(),
181
- startedAt: isoInstant,
182
- endedAt: isoInstant,
183
- activeSeconds: z.number().int().min(0).max(86_400),
184
- wordsAdded: z.number().int().min(0).max(1_000_000),
185
- wordsRemoved: z.number().int().min(0).max(1_000_000),
186
- netPagesEighths: z.number().int().min(-100_000).max(100_000),
187
- sprint: z
188
- .object({
189
- targetWords: z.number().int().min(1).max(1_000_000).optional(),
190
- targetMinutes: z.number().int().min(1).max(1440).optional(),
191
- })
192
- .optional(),
193
- })
194
- .refine((s) => Date.parse(s.endedAt) >= Date.parse(s.startedAt), {
195
- message: 'endedAt is before startedAt',
196
- path: ['endedAt'],
197
- });
198
- const localDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/);
199
- /** `GET /me/writing-stats`: dates are calendar days in the user's time zone (`users.time_zone`). Defaults: the last 30 days by day. */
200
- export const writingStatsQuerySchema = z.object({
201
- from: localDate.optional(),
202
- to: localDate.optional(),
203
- granularity: z.enum(WRITING_STATS_GRANULARITIES).default('day'),
204
- documentId: z.string().min(1).max(100).optional(),
205
- });
206
- export const writingGoalInputSchema = z
207
- .object({
208
- id: z
209
- .string()
210
- .min(4)
211
- .max(64)
212
- .regex(/^[A-Za-z0-9_-]+$/)
213
- .optional(),
214
- kind: z.enum(WRITING_GOAL_KINDS),
215
- documentId: z.string().min(1).max(100).nullable().optional(),
216
- target: z.number().int().min(1).max(10_000_000),
217
- daysOfWeek: z.array(z.number().int().min(0).max(6)).max(7).optional(),
218
- idleTimeoutSec: z.number().int().min(5).max(3600).optional(),
219
- })
220
- .refine((g) => g.kind !== 'documentTarget' || !!g.documentId, {
221
- message: 'documentTarget needs documentId',
222
- path: ['documentId'],
223
- });
224
- /** `PUT /me/writing-goals`: the whole list. A goal missing from it is deleted; one without `id` is created. `daysOfWeek`: 0 = Sunday. */
225
- /** No `.max` here on purpose: more than `WRITING_GOALS_MAX` is 409 `LIMIT_EXCEEDED` from the server, not `VALIDATION`. */
226
- export const writingGoalsPutSchema = z.object({
227
- goals: z.array(writingGoalInputSchema),
228
- });
229
- // ─── export, delete, restore ──────────────────────────────────────────────────
230
- export const ACCOUNT_DELETE_CONFIRM = 'DELETE';
231
- export const accountDeleteSchema = z.object({
232
- confirm: z.literal(ACCOUNT_DELETE_CONFIRM),
233
- });
234
- /** `account.export` job input: none. The output is `fadewright-export.zip`. */
235
- export const accountExportInputSchema = z.object({}).default({});
236
- export const ACCOUNT_EXPORT_FILENAME = 'fadewright-export.zip';
237
- // ─── per-user document state and stars (§6.26) ────────────────────────────────
238
- export const DOCUMENT_VIEW_STATE_MAX_BYTES = 64 * 1024;
239
- const deviceViewSchema = z.looseObject({
240
- view: z.string().max(40).optional(),
241
- zoom: z.number().optional(),
242
- invisibles: z.boolean().optional(),
243
- ruler: z.boolean().optional(),
244
- });
245
- /**
246
- * Cross-device view state of one document for one user: Navigator tabs and columns, split layout, view mode and zoom per
247
- * device class. Caret, selection, undo history and the Beat Board viewport stay device-local. Every key optional: the
248
- * client merges per top-level key.
249
- */
250
- export const documentViewStateSchema = z.strictObject({
251
- navigator: z
252
- .looseObject({
253
- tabs: z.array(z.looseObject({})).max(50).optional(),
254
- activeTab: z.string().max(80).nullable().optional(),
255
- })
256
- .optional(),
257
- layout: z
258
- .looseObject({
259
- split: z.union([z.string().max(40), z.number(), z.null()]).optional(),
260
- panes: z.array(z.unknown()).max(20).optional(),
261
- })
262
- .optional(),
263
- views: z
264
- .strictObject({
265
- phone: deviceViewSchema.optional(),
266
- tablet: deviceViewSchema.optional(),
267
- desktop: deviceViewSchema.optional(),
268
- })
269
- .optional(),
270
- });
271
- /** `PUT`: the whole state; `baseUpdatedAt` is the `updatedAt` last read (null or absent when the GET returned `{}`). */
272
- export const documentViewStatePutSchema = documentViewStateSchema.extend({
273
- baseUpdatedAt: z.string().min(1).max(40).nullable().optional(),
274
- });
@@ -1,20 +0,0 @@
1
- import { z } from 'zod';
2
- import { cursorQuerySchema } from '../api/pagination';
3
- import { JOB_STATUSES } from '../jobs/index';
4
- export const adminJobListQuerySchema = cursorQuerySchema.extend({
5
- status: z.enum(JOB_STATUSES).optional(),
6
- kind: z.string().optional(),
7
- });
8
- /** `PATCH /admin/job-kinds/:kind`: a sparse patch onto `job_kind_config`. */
9
- export const adminJobKindPatchSchema = z.object({
10
- enabled: z.boolean().optional(),
11
- maxAttempts: z.number().int().min(1).max(10).optional(),
12
- maxRetries: z.number().int().min(0).max(10).optional(),
13
- maxConcurrentPerUser: z.number().int().min(1).nullable().optional(),
14
- maxConcurrentGlobal: z.number().int().min(1).nullable().optional(),
15
- timeoutMs: z.number().int().min(1000).optional(),
16
- });
17
- /** `POST /admin/jobs/:id/refund`: an optional partial amount; defaults to the job's full `chargedCredits`. */
18
- export const adminJobRefundSchema = z.object({
19
- credits: z.number().int().positive().optional(),
20
- });
package/dist/ai/index.js DELETED
@@ -1,179 +0,0 @@
1
- import { z } from 'zod';
2
- /**
3
- * AI features (spec 06): script review and polish through ShapeShyft.
4
- *
5
- * Only the tasks implemented so far are in `AI_TASKS`; adding one means a registry row in
6
- * `screenwriter_api/src/services/ai/tasks.ts`, a prompt builder, an output schema here and a fixture.
7
- * Output schemas are written for structured output: every field is required (nullable, never optional)
8
- * and objects are closed, so `z.toJSONSchema` gives the exact schema to store on the ShapeShyft endpoint.
9
- */
10
- export const AI_TASKS = ['coverage', 'polish.dialogue'];
11
- export const aiTaskSchema = z.enum(AI_TASKS);
12
- /** ShapeShyft endpoint names (spec 06 2.1). `summarize-chunk` is stage 1 of a long coverage run. */
13
- export const AI_ENDPOINTS = [
14
- 'coverage-review',
15
- 'polish',
16
- 'summarize-chunk',
17
- ];
18
- export const AI_JOB_STATUSES = [
19
- 'queued',
20
- 'running',
21
- 'succeeded',
22
- 'failed',
23
- 'cancelled',
24
- ];
25
- /** Counted limits, declared once: schemas, validators and prompt text all read these. */
26
- export const AI_LIMITS = {
27
- noteTitleMax: 80,
28
- noteBodyMax: 1200,
29
- noteAnchorsMax: 8,
30
- strengthsMax: 8,
31
- weaknessesMax: 12,
32
- sceneNotesMax: 20,
33
- loglineMax: 300,
34
- summaryMax: 4000,
35
- genreMax: 3,
36
- verdictRationaleMax: 800,
37
- polishSuggestionsMax: 200,
38
- polishTextMax: 4000,
39
- rationaleMax: 400,
40
- chunkSummaryMax: 1200,
41
- sceneSummaryMax: 280,
42
- polishMaxScenes: 8,
43
- instructionsMax: 500,
44
- };
45
- export const REVIEW_CATEGORIES = [
46
- 'premise',
47
- 'structure',
48
- 'character',
49
- 'dialogue',
50
- 'pacing',
51
- 'tone',
52
- 'theme',
53
- 'worldbuilding',
54
- 'format',
55
- 'marketability',
56
- 'clarity',
57
- 'continuity',
58
- ];
59
- export const SEVERITIES = ['praise', 'minor', 'moderate', 'major'];
60
- export const COVERAGE_VERDICTS = ['recommend', 'consider', 'pass'];
61
- // ---- Model output (untrusted until validated; what the ShapeShyft endpoint returns) ----
62
- /** Where a note points. `elementId: null` means the whole scene. The server adds the content hash. */
63
- export const anchorRefSchema = z.object({
64
- sceneId: z.string(),
65
- elementId: z.string().nullable(),
66
- });
67
- export const noteSchema = z.object({
68
- category: z.enum(REVIEW_CATEGORIES),
69
- severity: z.enum(SEVERITIES),
70
- title: z.string().min(1).max(AI_LIMITS.noteTitleMax),
71
- body: z.string().min(1).max(AI_LIMITS.noteBodyMax),
72
- anchors: z.array(anchorRefSchema).max(AI_LIMITS.noteAnchorsMax),
73
- });
74
- /** `coverage-review` output. */
75
- export const coverageResponseSchema = z.object({
76
- logline: z.string().max(AI_LIMITS.loglineMax),
77
- summary: z.string().max(AI_LIMITS.summaryMax),
78
- genre: z.array(z.string()).max(AI_LIMITS.genreMax),
79
- strengths: z.array(noteSchema).max(AI_LIMITS.strengthsMax),
80
- weaknesses: z.array(noteSchema).max(AI_LIMITS.weaknessesMax),
81
- /** Notes about specific scenes: each must anchor at least one scene. */
82
- sceneNotes: z.array(noteSchema).max(AI_LIMITS.sceneNotesMax),
83
- verdict: z.enum(COVERAGE_VERDICTS),
84
- verdictRationale: z.string().max(AI_LIMITS.verdictRationaleMax),
85
- });
86
- /** `polish` output: one proposed replacement per element. */
87
- export const polishResponseSchema = z.object({
88
- suggestions: z
89
- .array(z.object({
90
- elementId: z.string(),
91
- after: z.string().max(AI_LIMITS.polishTextMax),
92
- rationale: z.string().max(AI_LIMITS.rationaleMax),
93
- }))
94
- .max(AI_LIMITS.polishSuggestionsMax),
95
- });
96
- /** `summarize-chunk` output (stage 1 of a long-script coverage run). */
97
- export const chunkSummaryResponseSchema = z.object({
98
- scenes: z.array(z.object({
99
- sceneId: z.string(),
100
- summary: z.string().max(AI_LIMITS.sceneSummaryMax),
101
- intensity: z.number().int().min(1).max(5),
102
- })),
103
- chunkSummary: z.string().max(AI_LIMITS.chunkSummaryMax),
104
- });
105
- /** The five input fields every endpoint takes (spec 06 2.1). */
106
- export const aiEndpointInputSchema = z.object({
107
- context: z.string(),
108
- brief: z.string(),
109
- max_output_tokens: z.number().int().positive(),
110
- locale: z.string(),
111
- script_language: z.string(),
112
- });
113
- /** Endpoint -> output schema. `bun run ai:schemas` in screenwriter_api prints these as JSON Schema. */
114
- export const AI_ENDPOINT_OUTPUT_SCHEMAS = {
115
- 'coverage-review': coverageResponseSchema,
116
- polish: polishResponseSchema,
117
- 'summarize-chunk': chunkSummaryResponseSchema,
118
- };
119
- // ---- Jobs ----
120
- export const aiScopeSchema = z.object({
121
- sceneIds: z.array(z.string().min(1)).max(200).optional(),
122
- elementIds: z.array(z.string().min(1)).max(2000).optional(),
123
- });
124
- export const aiJobOptionsSchema = z.object({
125
- /** Writer's own request, fenced separately from the script. Fixture failure markers only work in test mode. */
126
- instructions: z.string().max(AI_LIMITS.instructionsMax).optional(),
127
- intensity: z.enum(['light', 'medium', 'bold']).optional(),
128
- locale: z.string().min(2).max(20).optional(),
129
- });
130
- /** `POST /documents/:did/ai/jobs` body. `polish.dialogue` needs a scope; `coverage` defaults to the whole script. */
131
- export const aiJobCreateSchema = z.object({
132
- task: aiTaskSchema,
133
- scope: aiScopeSchema.optional(),
134
- options: aiJobOptionsSchema.optional(),
135
- });
136
- // ---- Suggestion sets (spec 06 7) ----
137
- export const SUGGESTION_STATUSES = [
138
- 'pending',
139
- 'accepted',
140
- 'rejected',
141
- 'stale',
142
- ];
143
- export const SUGGESTION_SET_STATUSES = [
144
- 'pending',
145
- 'partiallyAccepted',
146
- 'accepted',
147
- 'rejected',
148
- 'stale',
149
- 'expired',
150
- ];
151
- export const suggestionAcceptSchema = z.object({
152
- suggestionIds: z.array(z.string().min(1)).min(1).max(200),
153
- });
154
- /** `reject` with no ids rejects every pending suggestion in the set. */
155
- export const suggestionRejectSchema = z.object({
156
- suggestionIds: z.array(z.string().min(1)).min(1).max(200).optional(),
157
- });
158
- // ---- Consent (spec 06 §9.3: gate 3) ----
159
- /** Bumping this requires every user to re-accept before their next AI job. */
160
- export const AI_CONSENT_VERSION = 1;
161
- /** `POST /me/ai-consents`: `version` must equal `AI_CONSENT_VERSION`, else `VALIDATION`. */
162
- export const aiConsentAcceptSchema = z.object({ version: z.number().int() });
163
- export const aiEstimateRequestSchema = z.object({
164
- task: aiTaskSchema,
165
- scope: z
166
- .object({
167
- sceneIds: z.array(z.string()).optional(),
168
- elementIds: z.array(z.string()).optional(),
169
- })
170
- .optional(),
171
- options: z.record(z.string(), z.unknown()).optional(),
172
- });
173
- // ---- Suggestion decide (spec 06 §7.4): a combined, server-side accept/reject for API/MCP callers ----
174
- export const suggestionDecideSchema = z.object({
175
- accept: z.union([z.array(z.string().min(1)), z.literal('allPending')]),
176
- reject: z.union([z.array(z.string().min(1)), z.literal('allPending')]),
177
- expectedHashes: z.record(z.string(), z.string()).optional(),
178
- dryRun: z.boolean().optional(),
179
- });