@clipwright/core 0.1.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.
@@ -0,0 +1,358 @@
1
+ import { z } from "zod";
2
+ import { makeUgcInputShape } from "./skills.js";
3
+ /**
4
+ * РЕЕСТР ДИСПОЗИЦИЙ ПОЛЕЙ ВХОДА `make_ugc`.
5
+ *
6
+ * ЗАЧЕМ ОН ЕСТЬ. Аудит 2026-07-21 показал: из четырнадцати полей публичного
7
+ * контракта на выход влияли четыре, а десять принимались и МОЛЧА игнорировались
8
+ * — включая все три поля идентичности и `webhook_url` (клиент подписывался на
9
+ * уведомление и не получал ничего). Это прямое нарушение правила проекта №3.
10
+ * Точечная починка вернула бы тот же класс дефекта при следующей фиче, поэтому
11
+ * лечится не список полей, а МЕХАНИЗМ: у каждого поля обязано быть записанное
12
+ * решение, и отсутствие решения ловится компилятором.
13
+ *
14
+ * ЧТО ЭТОТ ФАЙЛ НЕ ДЕЛАЕТ — И ЭТО ГЛАВНОЕ. Тип `Record<keyof …, Disposition>`
15
+ * — страж ПОЛНОТЫ, а не ИСТИННОСТИ: он ловит отсутствие строки, но не её
16
+ * неверность. Именно так `resolution` числился бы «работающим», хотя адаптер
17
+ * шлёт вендору литерал. Истинность строки `implemented` доказывает EGRESS-ТЕСТ
18
+ * (`contract-dispositions.egress.test.ts`): изменение значения поля обязано
19
+ * давать наблюдаемое различие в захваченном теле исходящего запроса вендору.
20
+ * Запись без egress-теста считается недоказанной — за этим следит мета-тест.
21
+ */
22
+ /**
23
+ * Версия публичного контракта. Отдаётся заголовком `X-Clipwright-Contract` и
24
+ * полем `contract_version` в ответе quote. Любое следующее ломающее изменение
25
+ * обязано её двигать; до этого пересмотра версии не было вообще, поэтому первое
26
+ * её значение совпадает с месяцем самого пересмотра.
27
+ */
28
+ export const CONTRACT_VERSION = "2026-08";
29
+ export const CONTRACT_VERSION_HEADER = "X-Clipwright-Contract";
30
+ /**
31
+ * Таблица решений. `as const satisfies Record<…>` даёт СРАЗУ ДВА свойства:
32
+ * `satisfies` — страж полноты (поле входа без строки здесь не компилируется),
33
+ * `as const` — литеральные типы, из которых ниже выводится множество
34
+ * отклонённых полей. Обычная аннотация `: Record<…>` дала бы только первое, и
35
+ * `offeredUgcInputShape` пришлось бы перечислять руками — то есть завести
36
+ * второй источник правды ровно там, где мы чиним последствия первого.
37
+ */
38
+ export const FIELD_DISPOSITIONS = {
39
+ script: { kind: "implemented", provenBy: "egress" },
40
+ voice: { kind: "implemented", provenBy: "egress" },
41
+ voice_id: { kind: "implemented", provenBy: "egress" },
42
+ // Поля, которые не доезжают до рендера и объявляются несоблюдёнными.
43
+ person: {
44
+ kind: "warned",
45
+ warning: "person is not honored yet: every clip is rendered with the default actor",
46
+ triggersWhen: "present",
47
+ },
48
+ name: {
49
+ kind: "warned",
50
+ warning: "name is not honored yet: it does not reach the renderer",
51
+ triggersWhen: "present",
52
+ },
53
+ look: {
54
+ kind: "warned",
55
+ warning: "look is not honored yet: it does not reach the renderer",
56
+ triggersWhen: "differs-from-default",
57
+ },
58
+ captions: {
59
+ kind: "warned",
60
+ warning: "captions requested but not rendered in this prototype (stage-B)",
61
+ triggersWhen: "differs-from-default",
62
+ },
63
+ caption_style: {
64
+ kind: "warned",
65
+ warning: "caption_style is not honored: captions are not rendered in this prototype (stage-B)",
66
+ triggersWhen: "differs-from-default",
67
+ },
68
+ // US-522 перевёл обе строки из `warned` в `implemented`: адаптер больше не
69
+ // шлёт вендору литералы `1080p`/`9:16`, а берёт значения из запроса. Именно
70
+ // `resolution` был самым опасным полем аудита — он ЧИТАЛСЯ в таске и потому
71
+ // выглядел рабочим, а до вендора не доезжал. Поэтому обе строки прикрыты
72
+ // egress-тестами: три разрешения обязаны дать три РАЗНЫХ тела `/v3/videos`.
73
+ resolution: { kind: "implemented", provenBy: "egress" },
74
+ aspect_ratio: { kind: "implemented", provenBy: "egress" },
75
+ // US-524 довёл `image` до вендора. Диспозиция ОБУСЛОВЛЕНА килсвитчем
76
+ // `CLIPWRIGHT_REMOTE_FETCH`: базовая строка здесь — `implemented`, а при
77
+ // выключенном флаге `resolveDispositions` переводит её в `rejected` (см. ниже).
78
+ image: { kind: "implemented", provenBy: "egress" },
79
+ // Отклоняются: принять поле, зная, что оно не работает, план запрещает сам
80
+ // себе.
81
+ //
82
+ // `segments` заведён СХЕМОЙ в Slice 1 и отклоняется до Slice 2. Именно так
83
+ // страж и должен был сработать: поле добавили — компиляция упала, пока
84
+ // решение не записано. Новое поле без решения не проезжает, кем бы оно ни
85
+ // было добавлено, включая сам план.
86
+ //
87
+ // ПОЧЕМУ `rejected` ПЕРЕЖИЛ US-526 (решение владельца 2026-07-22). План
88
+ // переключал строку на `implemented` с `provenBy: "composition"` уже в
89
+ // US-526, но композиция приезжает только в US-527: без неё N клипов не
90
+ // сводятся в один файл и `final_output` предъявить нечем. `implemented`
91
+ // означает здесь «доказано тестом», а доказывать было бы нечем — то есть
92
+ // строка обещала бы работу, которой нет. US-526 построил механику
93
+ // (раскладку ключей, посегментную озвучку, вендорские области
94
+ // идемпотентности); переключение — одна строка в US-527, когда появится
95
+ // `composed.mp4`, который её докажет.
96
+ segments: {
97
+ kind: "rejected",
98
+ message: "segmented clips are not available yet: pass `script` with the full spoken text instead",
99
+ },
100
+ character: {
101
+ kind: "rejected",
102
+ message: "character is not supported: saved characters do not exist yet. Describe the actor with `person` instead, or omit it to use the default actor",
103
+ },
104
+ broll_url: {
105
+ kind: "rejected",
106
+ message: "broll_url is not supported yet: b-roll requires segmented composition. Omit it",
107
+ },
108
+ // Заведено US-528 вместе с самой маркировкой; US-539 перевёл строку из
109
+ // `rejected` в `implemented`. Плашка требует выжигания в кадр, то есть
110
+ // композиции, — она появилась в US-527 и доказана живым `composed.mp4`.
111
+ // Диспозиция ОБУСЛОВЛЕНА килсвитчем `CLIPWRIGHT_COMPOSE` тем же приёмом, что
112
+ // и `background`: при выключенной стадии выжечь плашку некому, и поле снова
113
+ // отклоняется, а не принимается и тихо игнорируется.
114
+ disclosure_overlay: { kind: "implemented", provenBy: "composition" },
115
+ // Заведено US-527 вместе с самой укладкой. Поле принимается только тогда,
116
+ // когда композиция действительно кладёт клип в кадр — до этого оно обещало
117
+ // бы выбор фона, которого некому исполнить. Переключение доказывается
118
+ // рендером, а не готовностью схемы.
119
+ // US-527 перевёл строку из `rejected` в `implemented`: композиция реально
120
+ // кладёт клип в кадр и объявляет долю полей в `warnings[]`. Диспозиция
121
+ // ОБУСЛОВЛЕНА килсвитчем `CLIPWRIGHT_COMPOSE` — как `image` обусловлен
122
+ // `CLIPWRIGHT_REMOTE_FETCH`: при выключенной стадии поле снова отклоняется,
123
+ // потому что исполнять выбор фона в этот момент некому.
124
+ background: { kind: "implemented", provenBy: "composition" },
125
+ // ЕДИНСТВЕННАЯ форма раскрытия, которая доходит. Проигнорированный `look`
126
+ // портит кадр — пользователь видит результат. Проигнорированный
127
+ // `webhook_url` ломает интеграцию: агент подписался и ПЕРЕСТАЛ поллить, а
128
+ // `warnings[]` доставляются через `get_run` — по тому самому каналу, от
129
+ // которого клиент отказался. Предупреждение на канале, который никто не
130
+ // слушает, раскрытием не является; поэтому здесь только синхронный отказ.
131
+ webhook_url: {
132
+ kind: "rejected",
133
+ message: "webhook_url is not supported: no webhook is ever delivered. Poll get_run with the run_id instead",
134
+ },
135
+ };
136
+ /** Поля входа в порядке объявления shape — детерминированный обход реестра. */
137
+ const CONTRACT_FIELDS = Object.keys(makeUgcInputShape);
138
+ /**
139
+ * Значение флагов из окружения. Читается один раз на процесс: килсвитч ставится
140
+ * на деплое, а не меняется от запроса к запросу, и `tools/list` не должен
141
+ * дрожать между двумя соседними вызовами.
142
+ */
143
+ export function contractFlagsFromEnv(env = process.env) {
144
+ return {
145
+ remoteFetch: env.CLIPWRIGHT_REMOTE_FETCH === "1",
146
+ compose: env.CLIPWRIGHT_COMPOSE === "1",
147
+ };
148
+ }
149
+ const REMOTE_FETCH_DISABLED = {
150
+ kind: "rejected",
151
+ message: "image is temporarily disabled (remote media fetch is off); omit it to render with the default actor",
152
+ };
153
+ /**
154
+ * Таблица диспозиций С УЧЁТОМ флагов. Базовая `FIELD_DISPOSITIONS` описывает,
155
+ * что код умеет; эта функция — что он предлагает ПРЯМО СЕЙЧАС.
156
+ */
157
+ /** Отказ по `background`, когда стадия композиции выключена. */
158
+ const COMPOSE_DISABLED = {
159
+ kind: "rejected",
160
+ message: "background is not available: composition is disabled on this deployment. Request an aspect_ratio close to your image instead",
161
+ };
162
+ /** Отказ по `disclosure_overlay`, когда стадия композиции выключена. */
163
+ const DISCLOSURE_OVERLAY_DISABLED = {
164
+ kind: "rejected",
165
+ message: "disclosure_overlay is not available: composition is disabled on this deployment. The output is still marked in the run contract and in the file metadata",
166
+ };
167
+ export function resolveDispositions(flags) {
168
+ return {
169
+ ...FIELD_DISPOSITIONS,
170
+ image: flags.remoteFetch ? FIELD_DISPOSITIONS.image : REMOTE_FETCH_DISABLED,
171
+ // Тот же приём, что у `image` (US-524): базовая строка объявляет, что поле
172
+ // РАБОТАЕТ, а выключенный флаг переводит её в отказ. Обещать выбор фона,
173
+ // когда стадия композиции выключена, значило бы принять поле, которое
174
+ // некому исполнить.
175
+ background: flags.compose ? FIELD_DISPOSITIONS.background : COMPOSE_DISABLED,
176
+ // Та же обусловленность, что у `background`: видимая плашка выжигается
177
+ // стадией композиции, и при выключенном флаге исполнять её некому.
178
+ disclosure_overlay: flags.compose
179
+ ? FIELD_DISPOSITIONS.disclosure_overlay
180
+ : DISCLOSURE_OVERLAY_DISABLED,
181
+ };
182
+ }
183
+ /**
184
+ * ДЕЙСТВУЮЩАЯ таблица процесса — то, из чего строятся все поверхности.
185
+ *
186
+ * Отличается от `FIELD_DISPOSITIONS` ровно на применённые флаги: базовая
187
+ * таблица говорит, что код УМЕЕТ, эта — что он предлагает прямо сейчас.
188
+ */
189
+ export const EFFECTIVE_DISPOSITIONS = resolveDispositions(contractFlagsFromEnv());
190
+ /**
191
+ * Дефолт поля — то, что схема подставит на `undefined`. Берём у самой схемы, а
192
+ * не переписываем значения рядом: второй список дефолтов разошёлся бы с первым
193
+ * тем же способом, которым разошлись схема и рендер.
194
+ */
195
+ const FIELD_DEFAULTS = Object.fromEntries(CONTRACT_FIELDS.map((field) => {
196
+ const parsed = makeUgcInputShape[field].safeParse(undefined);
197
+ return [field, parsed.success ? parsed.data : undefined];
198
+ }));
199
+ /**
200
+ * Текст диспозиции для JSON-Schema `description` (блокер B9).
201
+ *
202
+ * ЗАЧЕМ: `offeredUgcInputShape` убирает отклонённое поле из схемы, но там, где
203
+ * поле почему-либо остаётся видимым, вызыватель обязан прочитать ПРИЧИНУ прямо
204
+ * в описании. Тогда самоописательность — свойство схемы по построению, а не
205
+ * разовая правка двух файлов, которую следующее поле воспроизведёт заново.
206
+ */
207
+ /**
208
+ * Примечание для JSON-Schema `description` — по БАЗОВОЙ таблице.
209
+ *
210
+ * Варианта «по действующей таблице» здесь нет НАМЕРЕННО, и это следствие
211
+ * топологии: схему видит клиент, а флаги принадлежат серверу. Причину
212
+ * аварийного отказа вызыватель узнаёт из текста 400, а не из описания поля,
213
+ * которое у него в схеме и так осталось.
214
+ */
215
+ export function dispositionNote(field) {
216
+ return noteFor(FIELD_DISPOSITIONS[field]);
217
+ }
218
+ function noteFor(disposition) {
219
+ if (disposition.kind === "rejected") {
220
+ return `REJECTED: ${disposition.message}`;
221
+ }
222
+ if (disposition.kind === "warned") {
223
+ return `NOT HONORED YET: ${disposition.warning}`;
224
+ }
225
+ return null;
226
+ }
227
+ function annotate(field) {
228
+ const note = dispositionNote(field);
229
+ const schema = makeUgcInputShape[field];
230
+ return (note ? schema.describe(note) : schema);
231
+ }
232
+ /**
233
+ * ПРИМЕЧАНИЕ О ТОПОЛОГИИ, БЕЗ КОТОРОГО ВЫБОР НИЖЕ ВЫГЛЯДИТ ПРОИЗВОЛЬНЫМ.
234
+ *
235
+ * `offeredUgcInputShape` порождает `tools/list` MCP-сервера, а MCP-сервер —
236
+ * stdio-процесс НА МАШИНЕ ПОЛЬЗОВАТЕЛЯ, говорящий с нашим API по HTTP. Значит
237
+ * его `process.env` — окружение КЛИЕНТА, а не сервера. Строй мы предлагаемую
238
+ * поверхность по флагам, аварийный рубильник сервера начал бы управлять
239
+ * клиентской схемой: у пользователя без `CLIPWRIGHT_REMOTE_FETCH` поле `image`
240
+ * пропадало бы из `tools/list`, даже когда на сервере оно работает, — и
241
+ * наоборот, выставив переменную у себя, клиент вернул бы поле в схему, а
242
+ * сервер ответил бы 400.
243
+ *
244
+ * Поэтому граница проведена так:
245
+ * - ЧТО ПРЕДЛАГАЕТСЯ (схема, типы SDK, `--help`) — из БАЗОВОЙ таблицы: это
246
+ * форма контракта, одинаковая у всех;
247
+ * - ЧТО ПРИНИМАЕТСЯ ПРЯМО СЕЙЧАС (`rejectedFields`, `dispositionWarnings`) —
248
+ * из ДЕЙСТВУЮЩЕЙ: это решение сервера, и принимает его сервер.
249
+ * Килсвитч остаётся аварийным отказом на входе API, а не правкой чужой схемы.
250
+ */
251
+ /**
252
+ * Полный shape входа, где каждое несоблюдённое поле несёт причину в описании.
253
+ * Нужен там, где поверхность обязана показать ВСЕ поля (например, таблица
254
+ * диспозиций в доках); предлагать поля вызывателю следует не отсюда, а из
255
+ * `offeredUgcInputShape`.
256
+ */
257
+ export const annotatedUgcInputShape = Object.fromEntries(CONTRACT_FIELDS.map((field) => [field, annotate(field)]));
258
+ /**
259
+ * ПРЕДЛАГАЕМАЯ поверхность входа = полный shape МИНУС отклонённые поля
260
+ * (блокер B4).
261
+ *
262
+ * Отбивать отклонённое поле четырёхсотым, продолжая объявлять его в
263
+ * `tools/list`, в `--help` и в типах SDK, — это «предлагаем и наказываем»:
264
+ * агент читает схему, видит `character`, вызывает и получает 400. Множество
265
+ * выводится ИЗ РЕЕСТРА, поэтому смена диспозиции переносит поле между
266
+ * поверхностями одной строкой, а не правкой каждого потребителя.
267
+ */
268
+ export const offeredUgcInputShape = Object.fromEntries(CONTRACT_FIELDS.filter((field) => FIELD_DISPOSITIONS[field].kind !== "rejected").map((field) => [field, annotate(field)]));
269
+ /**
270
+ * Тип ВХОДА для SDK/CLI/MCP — выводится из ПРЕДЛАГАЕМОГО shape, а не из
271
+ * полного (блокер B4 на поверхности типов). Раньше он строился из полной схемы,
272
+ * и TypeScript-пользователь получал автокомплит на `character`/`webhook_url`,
273
+ * вызывал и получал 400 — тот же дефект «предлагаем и наказываем», только не в
274
+ * MCP. По правилу проекта №2 типы SDK — контрактная поверхность наравне с
275
+ * остальными.
276
+ *
277
+ * ЗАЧЕМ отдельно от `MakeUgcInput` (= выход `z.infer`, где дефолты уже
278
+ * обязательны): без него SDK ждал бы полностью заполненный объект.
279
+ */
280
+ export const offeredUgcInput = z.object(offeredUgcInputShape);
281
+ /**
282
+ * Отклонённые поля, ПРИСУТСТВУЮЩИЕ во входе. Один вызов — три поверхности:
283
+ * зовётся на входе API (400 до создания рана и до резервирования кредитов), в
284
+ * MCP и в SDK.
285
+ */
286
+ export function rejectedFields(input) {
287
+ const hits = [];
288
+ for (const field of CONTRACT_FIELDS) {
289
+ const disposition = EFFECTIVE_DISPOSITIONS[field];
290
+ if (disposition.kind !== "rejected")
291
+ continue;
292
+ if (input[field] === undefined)
293
+ continue;
294
+ hits.push({ field, message: disposition.message });
295
+ }
296
+ return hits;
297
+ }
298
+ /**
299
+ * Warnings реестра для входа рана (правило проекта №3).
300
+ *
301
+ * Вход ожидается УЖЕ РАСПАРСЕННЫМ: дефолты подставлены, поэтому сравнение с
302
+ * дефолтом различает «клиент промолчал» и «клиент явно попросил другое».
303
+ * Сравнение через `===` достаточно: все дефолтные значения контракта —
304
+ * примитивы (строка или булев).
305
+ *
306
+ * Это статические тексты диспозиций. ДИНАМИЧЕСКИЕ warnings (подмена
307
+ * разрешения/формата, голосовые) порождаются именованными генераторами
308
+ * (`negotiateCapabilities`, `buildVoiceWarnings`,
309
+ * `RAW_VOICE_ID_VALIDATION_WARNING`) — каждый со своим тестом; реестр их не
310
+ * поглощает и на них не претендует.
311
+ */
312
+ export function dispositionWarnings(input) {
313
+ const warnings = [];
314
+ for (const field of CONTRACT_FIELDS) {
315
+ const disposition = EFFECTIVE_DISPOSITIONS[field];
316
+ if (disposition.kind !== "warned")
317
+ continue;
318
+ const value = input[field];
319
+ if (value === undefined)
320
+ continue;
321
+ if (disposition.triggersWhen === "differs-from-default" &&
322
+ value === FIELD_DEFAULTS[field]) {
323
+ continue;
324
+ }
325
+ warnings.push(disposition.warning);
326
+ }
327
+ return warnings;
328
+ }
329
+ /**
330
+ * Поля с диспозицией `implemented` — вход мета-тестов покрытия.
331
+ *
332
+ * Считается по БАЗОВОЙ таблице, а не по действующей: доказательство относится к
333
+ * КОДУ, и выключенный операционный килсвитч его не отменяет.
334
+ */
335
+ export const IMPLEMENTED_FIELDS = CONTRACT_FIELDS.filter((field) => FIELD_DISPOSITIONS[field].kind === "implemented");
336
+ /**
337
+ * ДВА РОДА ДОКАЗАТЕЛЬСТВА, И ОНИ НЕ ВЗАИМОЗАМЕНЯЕМЫ (US-527).
338
+ *
339
+ * До этой стори `implemented` означало ровно одно: «поле доезжает до вендора»,
340
+ * и мета-тест сверял весь список с egress-покрытием. Композиция ввела второй
341
+ * род: `background` до вендора не доезжает НИКОГДА — он применяется у нас, уже
342
+ * после того как вендор отдал клип. Сверять его egress-тестом бессмысленно,
343
+ * а записать в тот же список — значит либо получить вечно красный мета-тест,
344
+ * либо ослабить его до бесполезности.
345
+ *
346
+ * Поэтому список разделён по `provenBy`, и у каждого рода — свой мета-тест:
347
+ * egress-поля сверяются с захватом исходящих запросов, композиционные — с
348
+ * тестами стадии композиции.
349
+ */
350
+ export const EGRESS_PROVEN_FIELDS = CONTRACT_FIELDS.filter((field) => {
351
+ const disposition = FIELD_DISPOSITIONS[field];
352
+ return disposition.kind === "implemented" && disposition.provenBy === "egress";
353
+ });
354
+ export const COMPOSITION_PROVEN_FIELDS = CONTRACT_FIELDS.filter((field) => {
355
+ const disposition = FIELD_DISPOSITIONS[field];
356
+ return disposition.kind === "implemented" && disposition.provenBy === "composition";
357
+ });
358
+ //# sourceMappingURL=contract-dispositions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"contract-dispositions.js","sourceRoot":"","sources":["../src/contract-dispositions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Отпечаток запроса под ключом идемпотентности + ключ идемпотентности per-client.
3
+ *
4
+ * ЗАЧЕМ ЭТОТ МОДУЛЬ ЖИВЁТ ПОДПУТЁМ `@clipwright/core/idempotency`, А НЕ В barrel:
5
+ * он тянет `node:crypto`. Barrel `@clipwright/core` импортируют И `apps/web`
6
+ * (через `transpilePackages` в `next.config.ts`), и клиентские компоненты Next.
7
+ * Стоит `node:crypto` протечь в barrel — `next build` падает на клиентском
8
+ * бандле. Поэтому barrel НЕ реэкспортирует этот файл; те, кому нужен ключ (SDK,
9
+ * apps/api), импортируют строго подпуть.
10
+ */
11
+ /**
12
+ * Canonical JSON: ключи объектов отсортированы, порядок элементов массива
13
+ * сохранён (в массиве порядок — часть значения, сортировать его нельзя).
14
+ *
15
+ * Нужен потому, что JSON.stringify сохраняет порядок вставки ключей, а он
16
+ * зависит от того, как агент собрал тело запроса. Два побайтово разных, но
17
+ * семантически одинаковых запроса обязаны дать один хеш, иначе штатный ретрай
18
+ * прилетит в 409 и агент будет считать это конфликтом.
19
+ *
20
+ * `undefined` внутри объектов отбрасывается — как и в JSON.stringify, чтобы
21
+ * «ключ отсутствует» и «ключ = undefined» не расходились в хеше.
22
+ */
23
+ export declare function canonicalize(value: unknown): unknown;
24
+ /**
25
+ * Хешируется вход ПОСЛЕ zod-парсинга, то есть с уже подставленными дефолтами.
26
+ * Это осознанная нормализация: «captions не передан» и «captions: false» —
27
+ * один и тот же запрос и обязаны совпасть по хешу. Хешировать сырое тело
28
+ * нельзя ещё и потому, что тогда лишний пробел в JSON давал бы 409.
29
+ */
30
+ export declare function requestHash(input: unknown): string;
31
+ /**
32
+ * Ключ идемпотентности, детерминированный от входа И идентичности клиента.
33
+ *
34
+ * ЗАЧЕМ ПАРСИТЬ ВНУТРИ, А НЕ ПРИНИМАТЬ ГОТОВЫЙ ХЕШ: единственный корректный
35
+ * путь построить ключ — прогнать сырой вход через `makeUgcInput.parse`, чтобы
36
+ * подставились дефолты, и только потом канонизировать. Прямой `requestHash`
37
+ * по сырому телу (без parse) даёт РАЗНЫЕ ключи для «captions не передан» и
38
+ * «captions: false» — это дефект 3.1 (штатный ретрай агента → ложный новый ран).
39
+ *
40
+ * ЗАЧЕМ ПРИМЕШИВАТЬ `clientId`: два разных клиента могут прислать побайтово
41
+ * один вход; без разделения по клиенту второй получил бы ран первого. `clientId`
42
+ * идёт ПРЕФИКСОМ хеша — разные клиенты на одном входе дают разные ключи
43
+ * (дефект 3.2), один клиент на одном входе — стабильный ключ (дедуп денег).
44
+ */
45
+ export declare function idempotencyKeyFor(input: unknown, clientId: string): string;
@@ -0,0 +1,72 @@
1
+ import { createHash } from "node:crypto";
2
+ import { makeUgcInput } from "./skills.js";
3
+ /**
4
+ * Отпечаток запроса под ключом идемпотентности + ключ идемпотентности per-client.
5
+ *
6
+ * ЗАЧЕМ ЭТОТ МОДУЛЬ ЖИВЁТ ПОДПУТЁМ `@clipwright/core/idempotency`, А НЕ В barrel:
7
+ * он тянет `node:crypto`. Barrel `@clipwright/core` импортируют И `apps/web`
8
+ * (через `transpilePackages` в `next.config.ts`), и клиентские компоненты Next.
9
+ * Стоит `node:crypto` протечь в barrel — `next build` падает на клиентском
10
+ * бандле. Поэтому barrel НЕ реэкспортирует этот файл; те, кому нужен ключ (SDK,
11
+ * apps/api), импортируют строго подпуть.
12
+ */
13
+ /**
14
+ * Canonical JSON: ключи объектов отсортированы, порядок элементов массива
15
+ * сохранён (в массиве порядок — часть значения, сортировать его нельзя).
16
+ *
17
+ * Нужен потому, что JSON.stringify сохраняет порядок вставки ключей, а он
18
+ * зависит от того, как агент собрал тело запроса. Два побайтово разных, но
19
+ * семантически одинаковых запроса обязаны дать один хеш, иначе штатный ретрай
20
+ * прилетит в 409 и агент будет считать это конфликтом.
21
+ *
22
+ * `undefined` внутри объектов отбрасывается — как и в JSON.stringify, чтобы
23
+ * «ключ отсутствует» и «ключ = undefined» не расходились в хеше.
24
+ */
25
+ export function canonicalize(value) {
26
+ if (Array.isArray(value)) {
27
+ return value.map(canonicalize);
28
+ }
29
+ if (value !== null && typeof value === "object") {
30
+ const source = value;
31
+ const result = {};
32
+ for (const key of Object.keys(source).sort()) {
33
+ if (source[key] !== undefined) {
34
+ result[key] = canonicalize(source[key]);
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ return value;
40
+ }
41
+ /**
42
+ * Хешируется вход ПОСЛЕ zod-парсинга, то есть с уже подставленными дефолтами.
43
+ * Это осознанная нормализация: «captions не передан» и «captions: false» —
44
+ * один и тот же запрос и обязаны совпасть по хешу. Хешировать сырое тело
45
+ * нельзя ещё и потому, что тогда лишний пробел в JSON давал бы 409.
46
+ */
47
+ export function requestHash(input) {
48
+ return createHash("sha256")
49
+ .update(JSON.stringify(canonicalize(input)))
50
+ .digest("hex");
51
+ }
52
+ /**
53
+ * Ключ идемпотентности, детерминированный от входа И идентичности клиента.
54
+ *
55
+ * ЗАЧЕМ ПАРСИТЬ ВНУТРИ, А НЕ ПРИНИМАТЬ ГОТОВЫЙ ХЕШ: единственный корректный
56
+ * путь построить ключ — прогнать сырой вход через `makeUgcInput.parse`, чтобы
57
+ * подставились дефолты, и только потом канонизировать. Прямой `requestHash`
58
+ * по сырому телу (без parse) даёт РАЗНЫЕ ключи для «captions не передан» и
59
+ * «captions: false» — это дефект 3.1 (штатный ретрай агента → ложный новый ран).
60
+ *
61
+ * ЗАЧЕМ ПРИМЕШИВАТЬ `clientId`: два разных клиента могут прислать побайтово
62
+ * один вход; без разделения по клиенту второй получил бы ран первого. `clientId`
63
+ * идёт ПРЕФИКСОМ хеша — разные клиенты на одном входе дают разные ключи
64
+ * (дефект 3.2), один клиент на одном входе — стабильный ключ (дедуп денег).
65
+ */
66
+ export function idempotencyKeyFor(input, clientId) {
67
+ const parsed = makeUgcInput.parse(input);
68
+ return createHash("sha256")
69
+ .update(clientId + JSON.stringify(canonicalize(parsed)))
70
+ .digest("hex");
71
+ }
72
+ //# sourceMappingURL=idempotency.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.js","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C;;;;;;;;;GASG;AAEH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACjC,CAAC;IAED,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAChD,MAAM,MAAM,GAAG,KAAgC,CAAC;QAChD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC7C,IAAI,MAAM,CAAC,GAAG,CAAC,KAAK,SAAS,EAAE,CAAC;gBAC9B,MAAM,CAAC,GAAG,CAAC,GAAG,YAAY,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC;SAC3C,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAc,EAAE,QAAgB;IAChE,MAAM,MAAM,GAAG,YAAY,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACzC,OAAO,UAAU,CAAC,QAAQ,CAAC;SACxB,MAAM,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;SACvD,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC"}
@@ -0,0 +1,8 @@
1
+ export * from "./skills.js";
2
+ export * from "./contract-dispositions.js";
3
+ export * from "./aspect.js";
4
+ export * from "./voices.js";
5
+ export * from "./runs.js";
6
+ export * from "./pacing.js";
7
+ export * from "./render-backend.js";
8
+ export * from "./tts-backend.js";
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ export * from "./skills.js";
2
+ // Реестр диспозиций импортирует shape из skills.js — направление одностороннее,
3
+ // цикла нет. Отсюда же приходит MakeUgcInputArgs (выводится из предлагаемого
4
+ // shape, а не из полного).
5
+ export * from "./contract-dispositions.js";
6
+ // Резолвер формата: чистые функции, ноль сети. Импортирует ASPECT_RATIOS из
7
+ // skills.js — направление то же одностороннее, что и у реестра.
8
+ export * from "./aspect.js";
9
+ export * from "./voices.js";
10
+ export * from "./runs.js";
11
+ export * from "./pacing.js";
12
+ // Вендор-нейтральные контракты: всё вендорское живёт в адаптерах, не здесь.
13
+ export * from "./render-backend.js";
14
+ export * from "./tts-backend.js";
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,gFAAgF;AAChF,6EAA6E;AAC7E,2BAA2B;AAC3B,cAAc,4BAA4B,CAAC;AAC3C,4EAA4E;AAC5E,gEAAgE;AAChE,cAAc,aAAa,CAAC;AAC5B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,aAAa,CAAC;AAC5B,4EAA4E;AAC5E,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** Пейсинг речи (docs/09 §5): 2–4 слова в секунду. */
2
+ export declare const WORDS_PER_SECOND_MIN = 2;
3
+ export declare const WORDS_PER_SECOND_MAX = 4;
4
+ export declare function countWords(script: string): number;
package/dist/pacing.js ADDED
@@ -0,0 +1,7 @@
1
+ /** Пейсинг речи (docs/09 §5): 2–4 слова в секунду. */
2
+ export const WORDS_PER_SECOND_MIN = 2;
3
+ export const WORDS_PER_SECOND_MAX = 4;
4
+ export function countWords(script) {
5
+ return script.trim().split(/\s+/).filter(Boolean).length;
6
+ }
7
+ //# sourceMappingURL=pacing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pacing.js","sourceRoot":"","sources":["../src/pacing.ts"],"names":[],"mappings":"AAAA,sDAAsD;AACtD,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AAEtC,MAAM,UAAU,UAAU,CAAC,MAAc;IACvC,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;AAC3D,CAAC"}