@lincyaw/kernel 0.0.0-stage → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +32 -2
  3. package/package.json +41 -3
  4. package/src/access.ts +18 -0
  5. package/src/anchors.ts +58 -0
  6. package/src/authoring.ts +56 -0
  7. package/src/blocks.ts +19 -0
  8. package/src/checks.ts +35 -0
  9. package/src/component-code.ts +6 -0
  10. package/src/contracts/agent.ts +76 -0
  11. package/src/contracts/anchors.ts +46 -0
  12. package/src/contracts/authoring-actions.ts +60 -0
  13. package/src/contracts/authoring-clients.ts +213 -0
  14. package/src/contracts/authoring.ts +180 -0
  15. package/src/contracts/checks.ts +29 -0
  16. package/src/contracts/classroom.ts +49 -0
  17. package/src/contracts/diagnostics.ts +37 -0
  18. package/src/contracts/document-audio.ts +150 -0
  19. package/src/contracts/document-comments.ts +31 -0
  20. package/src/contracts/document-history.ts +63 -0
  21. package/src/contracts/document-image.ts +32 -0
  22. package/src/contracts/document-reads.ts +57 -0
  23. package/src/contracts/document-voice.ts +44 -0
  24. package/src/contracts/document.ts +240 -0
  25. package/src/contracts/execution.ts +118 -0
  26. package/src/contracts/locale.ts +3 -0
  27. package/src/contracts/offline.ts +2 -0
  28. package/src/contracts/operations.ts +8 -0
  29. package/src/contracts/plugin.ts +183 -0
  30. package/src/contracts/presenter-panels.ts +17 -0
  31. package/src/contracts/preview.ts +386 -0
  32. package/src/contracts/production-assets.ts +61 -0
  33. package/src/contracts/production.ts +109 -0
  34. package/src/contracts/projects.ts +59 -0
  35. package/src/contracts/server.ts +108 -0
  36. package/src/contracts/service.ts +8 -0
  37. package/src/contracts/speech-settings.ts +16 -0
  38. package/src/contracts/speech.ts +149 -0
  39. package/src/contracts/steps.ts +15 -0
  40. package/src/contracts/transcription-adoption.ts +79 -0
  41. package/src/contracts/transcription.ts +112 -0
  42. package/src/contracts/voice.ts +97 -0
  43. package/src/contracts/voiceover-execution.ts +68 -0
  44. package/src/contracts/workspace.ts +458 -0
  45. package/src/contracts.ts +35 -0
  46. package/src/diagnostics.ts +7 -0
  47. package/src/document-authoring-actions.ts +47 -0
  48. package/src/document-caption-timings.ts +64 -0
  49. package/src/document-colors.ts +25 -0
  50. package/src/document-comments.ts +118 -0
  51. package/src/document-components.ts +251 -0
  52. package/src/document-operations.ts +132 -0
  53. package/src/document-pages.ts +215 -0
  54. package/src/document-reads.ts +81 -0
  55. package/src/document-schema.ts +85 -0
  56. package/src/document-voices.ts +153 -0
  57. package/src/document.ts +252 -0
  58. package/src/durations.ts +7 -0
  59. package/src/errors.ts +15 -0
  60. package/src/events.ts +32 -0
  61. package/src/execution.ts +25 -0
  62. package/src/fingerprint.ts +31 -0
  63. package/src/follow.ts +33 -0
  64. package/src/git.ts +12 -0
  65. package/src/html.ts +41 -0
  66. package/src/index.ts +82 -0
  67. package/src/ir.ts +367 -0
  68. package/src/jupyter-kernel.ts +9 -0
  69. package/src/kernel.ts +190 -0
  70. package/src/keyboard.ts +43 -0
  71. package/src/locale.ts +13 -0
  72. package/src/lock.ts +81 -0
  73. package/src/logging.ts +11 -0
  74. package/src/lower-myst.ts +312 -0
  75. package/src/node/assets.ts +23 -0
  76. package/src/node/authoring-actions.ts +50 -0
  77. package/src/node/authoring-clients.ts +14 -0
  78. package/src/node/document-audio.ts +22 -0
  79. package/src/node/document-history.ts +11 -0
  80. package/src/node/document-image.ts +8 -0
  81. package/src/node/document-reads.ts +10 -0
  82. package/src/node/document-voice.ts +7 -0
  83. package/src/node/ffmpeg.ts +29 -0
  84. package/src/node/file-version.ts +6 -0
  85. package/src/node/fonts.ts +22 -0
  86. package/src/node/index.ts +29 -0
  87. package/src/node/installed-packages.ts +11 -0
  88. package/src/node/lines.ts +24 -0
  89. package/src/node/locale.ts +7 -0
  90. package/src/node/network.ts +11 -0
  91. package/src/node/operator-speech.ts +91 -0
  92. package/src/node/packages.ts +74 -0
  93. package/src/node/paths.ts +7 -0
  94. package/src/node/projects.ts +7 -0
  95. package/src/node/resources.ts +86 -0
  96. package/src/node/revision.ts +6 -0
  97. package/src/node/run-directory.ts +70 -0
  98. package/src/node/strip-types.mjs +15 -0
  99. package/src/node/transcription-adoption.ts +9 -0
  100. package/src/node/transcription.ts +10 -0
  101. package/src/node/voiceover-routes.ts +73 -0
  102. package/src/node/voiceover.ts +199 -0
  103. package/src/page-source.ts +359 -0
  104. package/src/parameters.ts +81 -0
  105. package/src/preview-edit.ts +18 -0
  106. package/src/preview-events.ts +57 -0
  107. package/src/preview-frame.ts +1220 -0
  108. package/src/props.ts +17 -0
  109. package/src/published.ts +11 -0
  110. package/src/push-stream.ts +67 -0
  111. package/src/reading.ts +96 -0
  112. package/src/recording.ts +38 -0
  113. package/src/renderer-components.ts +30 -0
  114. package/src/requests.ts +28 -0
  115. package/src/responses.ts +49 -0
  116. package/src/run-outputs.ts +15 -0
  117. package/src/runner.py +454 -0
  118. package/src/services.ts +146 -0
  119. package/src/slides.ts +36 -0
  120. package/src/speech-words.ts +76 -0
  121. package/src/speech.ts +13 -0
  122. package/src/step-keys.ts +38 -0
  123. package/src/steps.ts +7 -0
  124. package/src/transcription-adoption.ts +105 -0
  125. package/src/uri-decoding.ts +8 -0
  126. package/src/urls.ts +13 -0
  127. package/src/views.ts +47 -0
  128. package/src/voice.ts +86 -0
@@ -0,0 +1,386 @@
1
+ import { isRecord } from '../errors.ts'
2
+ import type { ProductionBinding, ProductionPlaybackCommand, ProductionPlaybackState } from './production.ts'
3
+
4
+ export const PREVIEW_PROTOCOL = 1
5
+
6
+ export const PREVIEW_NAME = 'presmium-preview'
7
+
8
+ export const PREVIEW_PARAM = 'presmium-preview'
9
+
10
+ /** The workbench's appearance when it loads a preview (`dark` or `light`), so that the page's first paint matches it. */
11
+ export const PREVIEW_APPEARANCE_PARAM = 'presmium-appearance'
12
+
13
+ export const PREVIEW_REVISION = 'presmiumRevision'
14
+
15
+ export const AUTHOR_MARKS_CLASS = 'pm-author-marks'
16
+
17
+ /** Read-only, version-bound control of an individual document widget. */
18
+ export interface BlockPreviewCommand {
19
+ type: 'presmium:block-preview-command'
20
+ document: string
21
+ blockId: string
22
+ revision: string
23
+ sectionId: string
24
+ stepId: string
25
+ action: 'seek' | 'pause' | 'sync'
26
+ /** The parent's current navigation intent, including repeat visits to the same step. */
27
+ navigation?: string
28
+ /** Omitted and document initial/reload seeks show the completed step; start is an explicit playback boundary. */
29
+ at?: 'start' | 'end'
30
+ }
31
+
32
+ export interface BlockPreviewPosition {
33
+ type: 'presmium:block-preview-position'
34
+ document: string
35
+ blockId: string
36
+ revision: string
37
+ sectionId: string
38
+ stepId: string
39
+ navigation: string
40
+ }
41
+
42
+ export interface BlockPreviewPlaybackIdentity {
43
+ document: string
44
+ blockId: string
45
+ revision: string
46
+ request: number
47
+ }
48
+
49
+ export interface BlockPreviewPlaybackRequest extends BlockPreviewPlaybackIdentity {
50
+ type: 'presmium:block-preview-playback-request'
51
+ sectionId: string
52
+ stepId: string
53
+ navigation: string
54
+ }
55
+
56
+ export interface BlockPreviewPlaybackGranted extends BlockPreviewPlaybackIdentity {
57
+ type: 'presmium:block-preview-playback-granted'
58
+ error?: string
59
+ navigation: string
60
+ }
61
+
62
+ export interface BlockPreviewPlaybackCancelled extends BlockPreviewPlaybackIdentity {
63
+ type: 'presmium:block-preview-playback-cancelled'
64
+ }
65
+
66
+ export interface PreviewAnchor {
67
+ section: string
68
+ block?: string
69
+ }
70
+
71
+ /** A box in the preview page's viewport, in CSS pixels. */
72
+ export interface PreviewRect {
73
+ x: number
74
+ y: number
75
+ width: number
76
+ height: number
77
+ }
78
+
79
+ /** A point in the preview page's viewport, in CSS pixels. */
80
+ export interface PreviewPoint {
81
+ x: number
82
+ y: number
83
+ }
84
+
85
+ export interface PreviewPin {
86
+ id: string
87
+ section: string
88
+ block: string
89
+ label: string
90
+ }
91
+
92
+ export interface PreviewPosition {
93
+ section: string
94
+ step: number
95
+ trail: string[]
96
+ view?: string
97
+ }
98
+
99
+ /** Source-oriented edits from the block editor. Opaque blocks retain their source verbatim. */
100
+ export interface PreviewEditedBlock {
101
+ id: string
102
+ source?: string
103
+ at?: number
104
+ markdown?: string
105
+ scene?: string
106
+ durations?: number[]
107
+ }
108
+
109
+ /** The source of one anchored block, read by the workbench from its current IR binding. */
110
+ export interface PreviewSource {
111
+ part: 'definition' | 'block'
112
+ path: string
113
+ language: string
114
+ content: string
115
+ version: string
116
+ shared: number
117
+ }
118
+
119
+ /** Fixed live-viewer channels; no arbitrary upstream URL crosses the preview boundary. */
120
+ export const PREVIEW_PRESENTATION_PARAM = 'presmium-presentation'
121
+ /** `playback`: the workbench plays this page, so Space and the left and right arrows pressed in it go to the workbench. */
122
+ export const PREVIEW_KEYS_PARAM = 'presmium-keys'
123
+ export const PREVIEW_EVENTS_PARAM = 'presmium-events'
124
+ export type PreviewEventTarget =
125
+ | { topic: 'viewer-site', document: string, app: string, lang: string, form: 'web' | 'slides' }
126
+ | { topic: 'viewer-control', document: string, window: string }
127
+ export type PreviewEventMessage = { type: 'open' | 'error' } | { type: 'event', event: string, data: string }
128
+ export type PreviewEventSubscription = (target: PreviewEventTarget, received: (message: PreviewEventMessage) => void) => () => void
129
+
130
+ export function isPreviewEventTarget(value: unknown): value is PreviewEventTarget {
131
+ if (!isRecord(value) || !(typeof value.document === 'string' && value.document.length <= 1024)) return false
132
+ if (value.topic === 'viewer-site') return Object.keys(value).every(key => ['topic', 'document', 'app', 'lang', 'form'].includes(key))
133
+ && bounded(value.app, 200) && (typeof value.lang === 'string' && value.lang.length <= 80) && (value.form === 'web' || value.form === 'slides')
134
+ return value.topic === 'viewer-control' && Object.keys(value).every(key => ['topic', 'document', 'window'].includes(key))
135
+ && typeof value.window === 'string' && /^[a-zA-Z0-9-]{1,80}$/.test(value.window)
136
+ }
137
+ const eventMessage = (value: unknown): value is PreviewEventMessage => isRecord(value) && (value.type === 'open' || value.type === 'error'
138
+ || value.type === 'event' && bounded(value.event, 40) && (typeof value.data === 'string' && value.data.length <= (2 << 20)))
139
+
140
+ export type PreviewCommand =
141
+ | { type: 'presentation-confirmed', revision: string }
142
+ | { type: 'events-ready', connection: string }
143
+ | { type: 'events-message', connection: string, id: string, message: PreviewEventMessage }
144
+ | { type: 'production-playback', request: number, control: ProductionPlaybackCommand }
145
+ | { type: 'document-history', action: 'undo' | 'redo', request: number }
146
+ | { type: 'document-history-reset' }
147
+ | { type: 'pause-playback', request: number }
148
+ | { type: 'playback-granted', request: number }
149
+ | { type: 'flush', request: number }
150
+ | { type: 'edit-result', target: PreviewAnchor, request: number, revision?: string, binding?: string, error?: string }
151
+ | { type: 'blocks-result', section: string, request: number, revision?: string, error?: string }
152
+ | { type: 'source-result', section: string, block: string, request: number, source?: PreviewSource, error?: string }
153
+ | { type: 'source-saved', section: string, block: string, request: number, sourceVersion?: string, error?: string }
154
+ | { type: 'source-created', section: string, request: number, block?: string, error?: string }
155
+ | { type: 'page-insert', section: string, id: string, request: number }
156
+ | { type: 'page-created', section: string, request: number, page?: string, error?: string }
157
+ | { type: 'go', section: string, step?: number, trail?: string[], view?: string }
158
+ | { type: 'highlight', target: PreviewAnchor | null }
159
+ | { type: 'pick', on: boolean }
160
+ | { type: 'pins', targets: PreviewPin[] }
161
+ | { type: 'marks', on: boolean }
162
+ | { type: 'appearance', dark: boolean }
163
+ | { type: 'activity', active: boolean, request: number }
164
+ /** Edits the text of a block (a section's heading when there is no block) in place, the caret at `at`; null ends editing and puts the text back. */
165
+ | { type: 'edit', target: PreviewAnchor | null, at?: PreviewPoint, revision?: string, binding?: string }
166
+ /** Makes the blocks (or sections) blink a few times, the first brought into view: what the AI just changed. */
167
+ | { type: 'flash', targets: PreviewAnchor[] }
168
+
169
+ export type PreviewCommandType = PreviewCommand['type']
170
+
171
+ /** A workbench command the page's keyboard asks for while it has the focus. */
172
+ export type PreviewShortcut = 'undo' | 'redo' | 'dismiss' | 'save' | 'play-pause' | 'previous' | 'next'
173
+ const PREVIEW_SHORTCUTS: readonly string[] = ['undo', 'redo', 'dismiss', 'save', 'play-pause', 'previous', 'next'] satisfies readonly PreviewShortcut[]
174
+
175
+ export type PreviewReport =
176
+ | { type: 'events-subscribe', connection: string, id: string, target: PreviewEventTarget }
177
+ | { type: 'events-unsubscribe', connection: string, id: string }
178
+ | { type: 'presentation-action', action: 'refresh' | 'exit' }
179
+ | { type: 'production-playback-state', state: ProductionPlaybackState }
180
+ | { type: 'production-playback-result', request: number, binding: ProductionBinding, state?: ProductionPlaybackState, error?: string }
181
+ | { type: 'document-history-state', section?: string, undo: boolean, redo: boolean }
182
+ | { type: 'document-history-result', request: number, handled: boolean, error?: string }
183
+ | { type: 'playback-paused', request: number, error?: string }
184
+ | { type: 'playback-request', request: number }
185
+ | { type: 'flushed', request: number, error?: string }
186
+ | { type: 'blocks-edited', section: string, request: number, revision: string, blocks: PreviewEditedBlock[] }
187
+ | { type: 'source-read', section: string, block: string, request: number, part?: 'definition' | 'block' }
188
+ | { type: 'source-save', section: string, block: string, request: number, path: string, base: string, content: string, part?: 'definition' | 'block' }
189
+ | { type: 'source-create', section: string, id: string, after?: string, request: number, revision: string }
190
+ | { type: 'source-draft', dirty: boolean }
191
+ /** Aggregate unsaved body blocks, independent of generated-source and narration drafts. */
192
+ | { type: 'body-draft', dirty: boolean }
193
+ /** Aggregate unacknowledged native title and complex-text drafts. */
194
+ | { type: 'native-draft', dirty: boolean }
195
+ | { type: 'page-create', section: string, id: string, request: number, revision: string }
196
+ | { type: 'page-create-error', request: number, error: string }
197
+ | { type: 'narrate', section: string, step: number, navigate?: boolean }
198
+ | { type: 'ready', renderer: 'web' | 'slides', document: string, revision?: string, capabilities: PreviewCommandType[], eventConnection?: string }
199
+ | ({ type: 'position' } & PreviewPosition)
200
+ | { type: 'picked', section: string, block?: string, step?: number, trail?: string[], rect?: PreviewRect, point?: PreviewPoint, clicks?: number }
201
+ | { type: 'selection', selection: (PreviewAnchor & { text: string, rect?: PreviewRect, obstacles?: PreviewRect[] }) | null }
202
+ /** Where the picked block or the selection is after the page scrolled or resized; null when it is out of sight. */
203
+ | { type: 'rect', rect: PreviewRect | null, obstacles?: PreviewRect[] }
204
+ /** Where the blocks the AI changed are in the preview's viewport while they blink, and again as the page scrolls (at most 16). */
205
+ | { type: 'flashed', rects: PreviewRect[] }
206
+ | { type: 'editing', target: PreviewAnchor, active: boolean }
207
+ /** The text of a block edited in place, before and after (its `textContent`); sent only when it changed. */
208
+ | { type: 'edited', target: PreviewAnchor, before: string, after: string, request?: number, revision?: string, binding?: string }
209
+ | { type: 'updated', revision?: string }
210
+ | { type: 'activity', active: boolean, request: number, revision?: string, error?: string }
211
+ | { type: 'report', level: 'error' | 'warning', plugin?: string, message: string, resolved?: boolean }
212
+ | { type: 'shortcut', command: PreviewShortcut }
213
+
214
+ export type PreviewEnvelope<M> = M & { protocol: typeof PREVIEW_NAME, version: number, nonce: string }
215
+
216
+ export function previewEnvelope<M extends PreviewCommand | PreviewReport>(nonce: string, message: M): PreviewEnvelope<M> {
217
+ return { ...message, protocol: PREVIEW_NAME, version: PREVIEW_PROTOCOL, nonce }
218
+ }
219
+
220
+ const text = (value: unknown): value is string => typeof value === 'string'
221
+ const optional = <T>(value: unknown, is: (v: unknown) => v is T) => value === undefined || is(value)
222
+ const finite = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
223
+ const positive = (value: unknown): value is number => finite(value) && value > 0
224
+ const nonnegative = (value: unknown): value is number => finite(value) && value >= 0
225
+ const productionId = (value: unknown): value is string => typeof value === 'string' && value.length > 0 && value.length <= 200
226
+ const productionBinding = (value: unknown): value is ProductionBinding => isRecord(value) && isRecord(value.source)
227
+ && typeof value.source.documentId === 'string' && value.source.documentId.length > 0 && value.source.documentId.length <= 200
228
+ && typeof value.source.version === 'string' && /^[\da-f]{20}$/.test(value.source.version)
229
+ && typeof value.source.fingerprint === 'string' && /^[\da-f]{16}$/.test(value.source.fingerprint)
230
+ && typeof value.irRevision === 'string' && value.irRevision.length > 0 && value.irRevision.length <= 200
231
+ && typeof value.planId === 'string' && value.planId.length > 0 && value.planId.length <= 200
232
+ const productionState = (value: unknown): value is ProductionPlaybackState => {
233
+ if (!isRecord(value) || !productionBinding(value.binding) || !['loading', 'ready', 'estimated', 'unavailable', 'failed'].includes(String(value.status)) || !optional(value.error, text)) return false
234
+ if (value.position === undefined) return true
235
+ const position = value.position
236
+ return isRecord(position) && nonnegative(position.timeMs) && nonnegative(position.durationMs) && position.timeMs <= position.durationMs
237
+ && optional(position.stepTimeMs, nonnegative) && optional(position.sectionId, text) && optional(position.stepId, text) && optional(position.waitingFor, text)
238
+ && typeof position.playing === 'boolean' && positive(position.rate) && typeof position.trying === 'boolean'
239
+ }
240
+ const productionCommand = (value: unknown): value is ProductionPlaybackCommand => isRecord(value) && productionBinding(value.binding)
241
+ && (value.action === 'load' || value.action === 'pause' || value.action === 'play' || value.action === 'seek' && nonnegative(value.timeMs)
242
+ || value.action === 'step' && productionId(value.sectionId) && productionId(value.stepId) && optional(value.utteranceId, productionId) || value.action === 'rate' && positive(value.rate))
243
+ const step = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 0
244
+ const request = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0
245
+ const strings = (value: unknown): value is string[] => Array.isArray(value) && value.length <= 10_000 && value.every(text)
246
+ const anchor = (value: unknown): value is PreviewAnchor => isRecord(value) && text(value.section) && optional(value.block, text)
247
+ const length = (value: unknown): value is number => typeof value === 'number' && Number.isFinite(value)
248
+ const rect = (value: unknown): value is PreviewRect => isRecord(value) && length(value.x) && length(value.y) && length(value.width) && length(value.height) && value.width >= 0 && value.height >= 0
249
+ const obstacles = (value: unknown): value is PreviewRect[] => Array.isArray(value) && value.length <= 16 && value.every(rect)
250
+ const point = (value: unknown): value is PreviewPoint => isRecord(value) && length(value.x) && length(value.y)
251
+ const clicks = (value: unknown): value is number => Number.isInteger(value) && (value as number) >= 1 && (value as number) <= 3
252
+ const toAnchor = (value: PreviewAnchor): PreviewAnchor => ({ section: value.section, ...value.block ? { block: value.block } : {} })
253
+ const toRect = (value: PreviewRect): PreviewRect => ({ x: value.x, y: value.y, width: value.width, height: value.height })
254
+ const toPoint = (value: PreviewPoint): PreviewPoint => ({ x: value.x, y: value.y })
255
+ const MAX_EDITED = 200_000
256
+ const bounded = (value: unknown, max: number): value is string => text(value) && value.length > 0 && value.length <= max
257
+ const editBaseline = (m: Record<string, unknown>) => m.revision === undefined && m.binding === undefined || bounded(m.revision, 200) && bounded(m.binding, 200)
258
+ const editedRequest = (m: Record<string, unknown>) => m.request === undefined && m.revision === undefined && m.binding === undefined || request(m.request) && bounded(m.revision, 200) && bounded(m.binding, 200)
259
+ const sourceTarget = (m: Record<string, unknown>) => bounded(m.section, 200) && bounded(m.block, 200) && request(m.request)
260
+ const sourcePart = (value: unknown): value is 'definition' | 'block' => value === 'definition' || value === 'block'
261
+ const pageId = (value: unknown): value is string => text(value) && /^page-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(value)
262
+ const sourceValue = (value: unknown): value is PreviewSource => isRecord(value) && sourcePart(value.part) && bounded(value.path, 4096) && bounded(value.language, 100) && text(value.content) && value.content.length <= MAX_EDITED && bounded(value.version, 200) && request(value.shared) && value.shared > 0
263
+
264
+ export function toBlockPreviewCommand(value: unknown): BlockPreviewCommand | undefined {
265
+ if (!isRecord(value) || value.type !== 'presmium:block-preview-command' || !text(value.document)
266
+ || !text(value.blockId) || !value.blockId || !text(value.revision) || !value.revision
267
+ || !text(value.sectionId) || !value.sectionId || !text(value.stepId) || !value.stepId
268
+ || value.action !== 'seek' && value.action !== 'pause' && value.action !== 'sync'
269
+ || (value.action === 'pause' ? !optional(value.navigation, value => bounded(value, 200)) : !bounded(value.navigation, 200))
270
+ || value.at !== undefined && value.at !== 'start' && value.at !== 'end') return undefined
271
+ return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, action: value.action, ...value.navigation !== undefined ? { navigation: value.navigation as string } : {}, ...value.at !== undefined ? { at: value.at } : {} }
272
+ }
273
+
274
+ export function toBlockPreviewPosition(value: unknown): BlockPreviewPosition | undefined {
275
+ if (!isRecord(value) || value.type !== 'presmium:block-preview-position' || !text(value.document)
276
+ || !bounded(value.blockId, 4096) || !bounded(value.revision, 200) || !bounded(value.sectionId, 4096)
277
+ || !bounded(value.stepId, 4096) || !bounded(value.navigation, 200)) return undefined
278
+ return { type: value.type, document: value.document, blockId: value.blockId, revision: value.revision, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation }
279
+ }
280
+
281
+ const playbackIdentity = (value: unknown): value is Record<string, unknown> & BlockPreviewPlaybackIdentity => isRecord(value) && text(value.document) && bounded(value.blockId, 4096) && bounded(value.revision, 200) && request(value.request)
282
+ const toPlaybackIdentity = (value: BlockPreviewPlaybackIdentity): BlockPreviewPlaybackIdentity => ({ document: value.document, blockId: value.blockId, revision: value.revision, request: value.request })
283
+
284
+ export function toBlockPreviewPlaybackRequest(value: unknown): BlockPreviewPlaybackRequest | undefined {
285
+ return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-request' && bounded(value.sectionId, 4096) && bounded(value.stepId, 4096) && bounded(value.navigation, 200)
286
+ ? { ...toPlaybackIdentity(value), type: value.type, sectionId: value.sectionId, stepId: value.stepId, navigation: value.navigation } : undefined
287
+ }
288
+
289
+ export function toBlockPreviewPlaybackGranted(value: unknown): BlockPreviewPlaybackGranted | undefined {
290
+ return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-granted' && optional(value.error, text) && bounded(value.navigation, 200)
291
+ ? { ...toPlaybackIdentity(value), type: value.type, navigation: value.navigation, ...value.error !== undefined ? { error: value.error as string } : {} } : undefined
292
+ }
293
+
294
+ export function toBlockPreviewPlaybackCancelled(value: unknown): BlockPreviewPlaybackCancelled | undefined {
295
+ return playbackIdentity(value) && value.type === 'presmium:block-preview-playback-cancelled' ? { ...toPlaybackIdentity(value), type: value.type } : undefined
296
+ }
297
+
298
+ export function isPreviewEnvelope(data: unknown, nonce: string): data is PreviewEnvelope<{ type: string }> {
299
+ return isRecord(data) && data.protocol === PREVIEW_NAME && data.nonce === nonce && typeof data.version === 'number' && text(data.type)
300
+ }
301
+
302
+ export function toPreviewCommand(data: unknown, nonce: string): PreviewCommand | undefined {
303
+ if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
304
+ const m = data as Record<string, unknown>
305
+ switch (m.type) {
306
+ case 'presentation-confirmed': return productionId(m.revision) ? { type: 'presentation-confirmed', revision: m.revision } : undefined
307
+ case 'events-ready': return productionId(m.connection) ? { type: 'events-ready', connection: m.connection } : undefined
308
+ case 'events-message': return productionId(m.connection) && productionId(m.id) && eventMessage(m.message) ? { type: 'events-message', connection: m.connection, id: m.id, message: m.message } : undefined
309
+ case 'production-playback': return request(m.request) && productionCommand(m.control) ? { type: 'production-playback', request: m.request, control: m.control } : undefined
310
+ case 'document-history': return request(m.request) && (m.action === 'undo' || m.action === 'redo') ? { type: 'document-history', request: m.request, action: m.action } : undefined
311
+ case 'document-history-reset': return { type: 'document-history-reset' }
312
+ case 'pause-playback':
313
+ case 'playback-granted': return request(m.request) ? { type: m.type, request: m.request } : undefined
314
+ case 'flush': return request(m.request) ? { type: 'flush', request: m.request } : undefined
315
+ case 'edit-result': return anchor(m.target) && request(m.request) && editBaseline(m) && optional(m.error, text) ? { type: 'edit-result', target: toAnchor(m.target), request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
316
+ case 'blocks-result': return text(m.section) && request(m.request) && optional(m.revision, text) && optional(m.error, text) ? { type: 'blocks-result', section: m.section, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
317
+ case 'source-result': return sourceTarget(m) && optional(m.source, sourceValue) && optional(m.error, text) ? { type: 'source-result', section: m.section as string, block: m.block as string, request: m.request as number, ...m.source === undefined ? {} : { source: m.source as PreviewSource }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
318
+ case 'source-saved': return sourceTarget(m) && optional(m.sourceVersion, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-saved', section: m.section as string, block: m.block as string, request: m.request as number, ...m.sourceVersion === undefined ? {} : { sourceVersion: m.sourceVersion as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
319
+ case 'source-created': return bounded(m.section, 200) && request(m.request) && optional(m.block, (value): value is string => bounded(value, 200)) && optional(m.error, text) ? { type: 'source-created', section: m.section, request: m.request, ...m.block === undefined ? {} : { block: m.block as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
320
+ case 'page-insert': return bounded(m.section, 200) && pageId(m.id) && request(m.request) ? { type: 'page-insert', section: m.section, id: m.id, request: m.request } : undefined
321
+ case 'page-created': return bounded(m.section, 200) && request(m.request) && optional(m.page, pageId) && optional(m.error, text) ? { type: 'page-created', section: m.section, request: m.request, ...m.page === undefined ? {} : { page: m.page as string }, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
322
+ case 'go': return text(m.section) && optional(m.step, step) && optional(m.trail, strings) && optional(m.view, text) ? { type: 'go', section: m.section, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.view ? { view: m.view as string } : {} } : undefined
323
+ case 'highlight': return m.target === null || anchor(m.target) ? { type: 'highlight', target: m.target === null ? null : { section: m.target.section, ...m.target.block ? { block: m.target.block } : {} } } : undefined
324
+ case 'pick':
325
+ case 'marks': return typeof m.on === 'boolean' ? { type: m.type, on: m.on } : undefined
326
+ case 'appearance': return typeof m.dark === 'boolean' ? { type: 'appearance', dark: m.dark } : undefined
327
+ case 'activity': return typeof m.active === 'boolean' && request(m.request) ? { type: 'activity', active: m.active, request: m.request } : undefined
328
+ case 'edit': return (m.target === null || anchor(m.target)) && optional(m.at, point) && editBaseline(m) ? { type: 'edit', target: m.target === null ? null : toAnchor(m.target), ...m.at ? { at: toPoint(m.at as PreviewPoint) } : {}, ...m.revision === undefined ? {} : { revision: m.revision as string, binding: m.binding as string } } : undefined
329
+ case 'pins': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(t => isRecord(t) && text(t.id) && text(t.section) && text(t.block) && text(t.label))
330
+ ? { type: 'pins', targets: (m.targets as PreviewPin[]).map(({ id, section, block, label }) => ({ id, section, block, label })) }
331
+ : undefined
332
+ case 'flash': return Array.isArray(m.targets) && m.targets.length <= 1000 && m.targets.every(anchor) ? { type: 'flash', targets: (m.targets as PreviewAnchor[]).map(toAnchor) } : undefined
333
+ default: return undefined
334
+ }
335
+ }
336
+
337
+ export function toPreviewReport(data: unknown, nonce: string): PreviewReport | undefined {
338
+ if (!isPreviewEnvelope(data, nonce) || data.version !== PREVIEW_PROTOCOL) return undefined
339
+ const m = data as Record<string, unknown>
340
+ switch (m.type) {
341
+ case 'events-subscribe': return productionId(m.connection) && productionId(m.id) && isPreviewEventTarget(m.target) ? { type: 'events-subscribe', connection: m.connection, id: m.id, target: m.target } : undefined
342
+ case 'events-unsubscribe': return productionId(m.connection) && productionId(m.id) ? { type: 'events-unsubscribe', connection: m.connection, id: m.id } : undefined
343
+ case 'presentation-action': return m.action === 'refresh' || m.action === 'exit' ? { type: 'presentation-action', action: m.action } : undefined
344
+ case 'production-playback-state': return productionState(m.state) ? { type: 'production-playback-state', state: m.state } : undefined
345
+ case 'production-playback-result': return request(m.request) && productionBinding(m.binding) && optional(m.state, productionState) && optional(m.error, text)
346
+ ? { type: 'production-playback-result', request: m.request, binding: m.binding, ...m.state ? { state: m.state as ProductionPlaybackState } : {}, ...m.error !== undefined ? { error: m.error as string } : {} } : undefined
347
+ case 'document-history-state': return optional(m.section, text) && typeof m.undo === 'boolean' && typeof m.redo === 'boolean' ? { type: 'document-history-state', ...m.section === undefined ? {} : { section: m.section as string }, undo: m.undo, redo: m.redo } : undefined
348
+ case 'document-history-result': return request(m.request) && typeof m.handled === 'boolean' && optional(m.error, text) ? { type: 'document-history-result', request: m.request, handled: m.handled, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
349
+ case 'playback-request': return request(m.request) ? { type: 'playback-request', request: m.request } : undefined
350
+ case 'playback-paused': return request(m.request) && optional(m.error, text) ? { type: 'playback-paused', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
351
+ case 'flushed': return request(m.request) && optional(m.error, text) ? { type: 'flushed', request: m.request, ...m.error === undefined ? {} : { error: m.error as string } } : undefined
352
+ case 'blocks-edited': return text(m.section) && request(m.request) && text(m.revision) && Array.isArray(m.blocks) && m.blocks.length <= 2000 && m.blocks.every(b => isRecord(b) && text(b.id) && b.id.length <= 200 && optional(b.source, text) && optional(b.at, step) && optional(b.markdown, (v): v is string => text(v) && v.length <= MAX_EDITED) && optional(b.scene, text) && optional(b.durations, (v): v is number[] => Array.isArray(v) && v.length <= 1000 && v.every(n => typeof n === 'number' && Number.isFinite(n) && n >= 0))) ? { type: 'blocks-edited', section: m.section, request: m.request, revision: m.revision, blocks: m.blocks as unknown as PreviewEditedBlock[] } : undefined
353
+ case 'source-read': return sourceTarget(m) && optional(m.part, sourcePart) ? { type: 'source-read', section: m.section as string, block: m.block as string, request: m.request as number, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
354
+ case 'source-save': return sourceTarget(m) && optional(m.part, sourcePart) && bounded(m.path, 4096) && bounded(m.base, 200) && text(m.content) && m.content.length <= MAX_EDITED ? { type: 'source-save', section: m.section as string, block: m.block as string, request: m.request as number, path: m.path as string, base: m.base as string, content: m.content, ...m.part === undefined ? {} : { part: m.part as 'definition' | 'block' } } : undefined
355
+ case 'source-create': return bounded(m.section, 200) && text(m.id) && /^scene-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(m.id) && request(m.request) && bounded(m.revision, 200) && optional(m.after, (value): value is string => bounded(value, 200)) ? { type: 'source-create', section: m.section, id: m.id, request: m.request, revision: m.revision, ...m.after === undefined ? {} : { after: m.after as string } } : undefined
356
+ case 'source-draft': return typeof m.dirty === 'boolean' ? { type: 'source-draft', dirty: m.dirty } : undefined
357
+ case 'body-draft': return typeof m.dirty === 'boolean' ? { type: 'body-draft', dirty: m.dirty } : undefined
358
+ case 'native-draft': return typeof m.dirty === 'boolean' ? { type: 'native-draft', dirty: m.dirty } : undefined
359
+ case 'page-create': return bounded(m.section, 200) && pageId(m.id) && request(m.request) && bounded(m.revision, 200) ? { type: 'page-create', section: m.section, id: m.id, request: m.request, revision: m.revision } : undefined
360
+ case 'page-create-error': return request(m.request) && bounded(m.error, 4000) ? { type: 'page-create-error', request: m.request, error: m.error } : undefined
361
+ case 'narrate': return text(m.section) && step(m.step) && optional(m.navigate, (value): value is boolean => typeof value === 'boolean') ? { type: 'narrate', section: m.section, step: m.step, ...m.navigate === undefined ? {} : { navigate: m.navigate as boolean } } : undefined
362
+ case 'ready': return (m.renderer === 'web' || m.renderer === 'slides') && text(m.document) && optional(m.revision, text) && strings(m.capabilities) && optional(m.eventConnection, productionId) ? { type: 'ready', renderer: m.renderer, document: m.document, ...m.revision ? { revision: m.revision as string } : {}, capabilities: m.capabilities as PreviewCommandType[], ...m.eventConnection ? { eventConnection: m.eventConnection as string } : {} } : undefined
363
+ case 'position': return text(m.section) && step(m.step) && strings(m.trail) && optional(m.view, text) ? { type: 'position', section: m.section, step: m.step, trail: m.trail, ...m.view ? { view: m.view as string } : {} } : undefined
364
+ case 'picked': return text(m.section) && optional(m.block, text) && optional(m.step, step) && optional(m.trail, strings) && optional(m.rect, rect) && optional(m.point, point) && optional(m.clicks, clicks)
365
+ ? { type: 'picked', section: m.section, ...m.block ? { block: m.block as string } : {}, ...m.step === undefined ? {} : { step: m.step as number }, ...m.trail ? { trail: m.trail as string[] } : {}, ...m.rect ? { rect: toRect(m.rect as PreviewRect) } : {}, ...m.point ? { point: toPoint(m.point as PreviewPoint) } : {}, ...m.clicks === undefined ? {} : { clicks: m.clicks as number } }
366
+ : undefined
367
+ case 'selection': {
368
+ if (m.selection === null) return { type: 'selection', selection: null }
369
+ const selected = m.selection
370
+ return isRecord(selected) && text(selected.text) && anchor(selected) && optional(selected.rect, rect) && optional(selected.obstacles, obstacles)
371
+ ? { type: 'selection', selection: { ...toAnchor(selected), text: selected.text, ...selected.rect ? { rect: toRect(selected.rect as PreviewRect) } : {}, ...selected.obstacles !== undefined ? { obstacles: (selected.obstacles as PreviewRect[]).map(toRect) } : {} } }
372
+ : undefined
373
+ }
374
+ case 'flashed': return Array.isArray(m.rects) && m.rects.length <= 16 && m.rects.every(rect) ? { type: 'flashed', rects: (m.rects as PreviewRect[]).map(toRect) } : undefined
375
+ case 'rect': return (m.rect === null || rect(m.rect)) && optional(m.obstacles, obstacles) ? { type: 'rect', rect: m.rect === null ? null : toRect(m.rect), ...m.obstacles !== undefined ? { obstacles: (m.obstacles as PreviewRect[]).map(toRect) } : {} } : undefined
376
+ case 'editing': return anchor(m.target) && typeof m.active === 'boolean' ? { type: 'editing', target: toAnchor(m.target), active: m.active } : undefined
377
+ case 'edited': return anchor(m.target) && text(m.before) && text(m.after) && m.before.length <= MAX_EDITED && m.after.length <= MAX_EDITED && editedRequest(m) ? { type: 'edited', target: toAnchor(m.target), before: m.before, after: m.after, ...m.request === undefined ? {} : { request: m.request as number, revision: m.revision as string, binding: m.binding as string } } : undefined
378
+ case 'updated': return optional(m.revision, text) ? { type: 'updated', ...m.revision ? { revision: m.revision as string } : {} } : undefined
379
+ case 'activity': return typeof m.active === 'boolean' && request(m.request) && optional(m.revision, text) && optional(m.error, text)
380
+ ? { type: 'activity', active: m.active, request: m.request, ...m.revision === undefined ? {} : { revision: m.revision as string }, ...m.error === undefined ? {} : { error: m.error as string } }
381
+ : undefined
382
+ case 'report': return (m.level === 'error' || m.level === 'warning') && text(m.message) && optional(m.plugin, text) && (m.resolved === undefined || typeof m.resolved === 'boolean') ? { type: 'report', level: m.level, message: m.message, ...m.plugin ? { plugin: m.plugin as string } : {}, ...m.resolved === undefined ? {} : { resolved: m.resolved as boolean } } : undefined
383
+ case 'shortcut': return typeof m.command === 'string' && PREVIEW_SHORTCUTS.includes(m.command) ? { type: 'shortcut', command: m.command as PreviewShortcut } : undefined
384
+ default: return undefined
385
+ }
386
+ }
@@ -0,0 +1,61 @@
1
+ import type { Asset, MediaClip, TimeRange } from './document.ts'
2
+ import type { ProductionBinding, ProductionPlan, ProductionPlanQuery } from './production.ts'
3
+ import type { CanonicalSource } from '../ir.ts'
4
+ import type { DocumentChangeAnswer } from './workspace.ts'
5
+ import { defineService } from './service.ts'
6
+
7
+ /** Actual local audio metadata, derived from a bounded, regular file rather than authored duration. */
8
+ export interface ProductionAssetInspection {
9
+ assetId: string
10
+ mediaId: string
11
+ kind: 'audio'
12
+ sha256: string
13
+ durationMs: number
14
+ byteLength: number
15
+ mediaType: string
16
+ codec: string
17
+ sampleRate: number
18
+ channels: number
19
+ }
20
+ export interface ProductionAssetQuery extends ProductionPlanQuery { assetId: string }
21
+ export interface ProductionAssetAnswer extends ProductionPlanQuery { asset: ProductionAssetInspection }
22
+
23
+ /** Multipart fields are change (this JSON) and file (the original bytes). Import does not attach a clip. */
24
+ export const DOCUMENT_AUDIO_MAX_BYTES = 16 << 20
25
+ export interface DocumentAudioImport { id: string, baseDocumentVersion: string }
26
+ export interface DocumentAudioImportAnswer extends DocumentChangeAnswer { assetId: string, inspection: ProductionAssetInspection }
27
+ /** Reads a declared asset directly from the saved document, including assets not yet used by a clip. */
28
+ export interface DocumentAudioInspectionAnswer { document: string, version: string, fingerprint: string, inspection: ProductionAssetInspection }
29
+ export type DocumentMediaClipChange =
30
+ | { id: string, baseDocumentVersion: string, binding: ProductionBinding, clip: MediaClip }
31
+ | { id: string, baseDocumentVersion: string, removeClipId: string }
32
+
33
+ export class ProductionAudioRefusal extends Error {
34
+ readonly status: number
35
+ readonly code: string
36
+ constructor(status: number, code: string) { super(code); this.status = status; this.code = code }
37
+ }
38
+ /** Node-only media capabilities. Workspace authoring owns the transaction; the plugin owns real media inspection. */
39
+ export interface ProductionAudioPlanQuery { source: CanonicalSource, irRevision?: string }
40
+ export interface ProductionAudioAuthoring {
41
+ inspectBytes(assetId: string, bytes: Uint8Array, signal?: AbortSignal): Promise<{ inspection: ProductionAssetInspection, extension: string }>
42
+ inspectAsset(asset: Asset, signal?: AbortSignal): Promise<ProductionAssetInspection>
43
+ /** Cache-only projection of an existing real IR; this never compiles or synthesizes. */
44
+ readPlan(query: ProductionAudioPlanQuery, signal?: AbortSignal): Promise<ProductionPlan>
45
+ validateClips(binding: ProductionBinding, clips: MediaClip[], assets: Record<string, Asset>, signal?: AbortSignal): Promise<void>
46
+ }
47
+ export const PRODUCTION_AUDIO_AUTHORING = defineService<ProductionAudioAuthoring>('presmium.productionAudioAuthoring')
48
+
49
+ /** One non-destructive use of an inspected file, placed inside an already confirmed step. */
50
+ export interface ProductionAudioClip {
51
+ id: string
52
+ assetId: string
53
+ mediaId: string
54
+ sectionId: string
55
+ stepId: string
56
+ startMs: number
57
+ endMs: number
58
+ sourceRange: TimeRange
59
+ volume: number
60
+ muted: boolean
61
+ }
@@ -0,0 +1,109 @@
1
+ import type { CanonicalSource } from '../ir.ts'
2
+ import type { TrackEvent } from './operations.ts'
3
+ import type { TimeRange } from './document.ts'
4
+ import type { ProductionAssetInspection } from './production-assets.ts'
5
+
6
+ /** The same supported rates are used by the author controls and the actual production driver. */
7
+ export const PRODUCTION_PLAYBACK_RATES = [0.5, 0.75, 1, 1.25, 1.5, 2] as const
8
+
9
+ /** One immutable derived plan; a cache change can change planId without changing the saved document. */
10
+ export interface ProductionBinding { source: CanonicalSource, irRevision: string, planId: string }
11
+ export function sameProductionBinding(a: ProductionBinding, b: ProductionBinding): boolean {
12
+ return a.planId === b.planId && a.irRevision === b.irRevision && a.source.documentId === b.source.documentId && a.source.version === b.source.version && a.source.fingerprint === b.source.fingerprint
13
+ }
14
+ export interface ProductionPlaybackPosition {
15
+ timeMs: number
16
+ stepTimeMs?: number
17
+ sectionId?: string
18
+ stepId?: string
19
+ durationMs: number
20
+ playing: boolean
21
+ rate: number
22
+ trying: boolean
23
+ /** Exact missing utterance that paused this playback intent before its estimated interval. */
24
+ waitingFor?: string
25
+ }
26
+ export interface ProductionPlaybackState {
27
+ binding: ProductionBinding
28
+ status: 'loading' | 'ready' | 'estimated' | 'unavailable' | 'failed'
29
+ position?: ProductionPlaybackPosition
30
+ error?: string
31
+ }
32
+ export type ProductionPlaybackCommand =
33
+ | { binding: ProductionBinding, action: 'load' | 'pause' | 'play' }
34
+ | { binding: ProductionBinding, action: 'seek', timeMs: number }
35
+ /** Selects an event boundary even when several stable steps share the same time. */
36
+ | { binding: ProductionBinding, action: 'step', sectionId: string, stepId: string, utteranceId?: string }
37
+ | { binding: ProductionBinding, action: 'rate', rate: number }
38
+ export interface ProductionPlanQuery { source: CanonicalSource, irRevision: string }
39
+ /** clipId is a plan resource ID: either an exact speech key or an inspected asset mediaId. */
40
+ export interface ProductionMediaQuery extends ProductionBinding { clipId: string }
41
+ export type ProductionTiming = 'confirmed' | 'estimated' | 'unknown'
42
+
43
+ /** All ranges use milliseconds in the single document-wide playback clock. Unknown boundaries are omitted. */
44
+ export interface ProductionRange { startMs?: number, endMs?: number, timing: ProductionTiming }
45
+ export interface ProductionAudio {
46
+ clipId: string
47
+ key: string
48
+ durationMs: number
49
+ /** Digest of the actual cached audio bytes, when available. Never inferred from a cache key. */
50
+ sha256?: string
51
+ }
52
+ export interface ProductionSegment extends ProductionRange {
53
+ id: string
54
+ sectionId: string
55
+ stepId: string
56
+ /** A cue can advance through several steps inside this one audio/caption segment. */
57
+ stepIds?: string[]
58
+ narrationIds?: string[]
59
+ sourceStepId?: string
60
+ blockId?: string
61
+ utteranceId?: string
62
+ label: string
63
+ status?: 'cached' | 'recorded' | 'silent' | 'missing' | 'blocked'
64
+ alignment?: 'words' | 'proportional' | 'none' | 'manual'
65
+ audio?: ProductionAudio
66
+ /** Imported media is distinct from a synthesized speech-cache entry. */
67
+ media?: ProductionAssetInspection
68
+ mediaClipId?: string
69
+ assetId?: string
70
+ sourceRange?: TimeRange
71
+ volume?: number
72
+ muted?: boolean
73
+ waveform?: number[]
74
+ waveformStatus?: 'available' | 'unavailable'
75
+ }
76
+ export interface ProductionTrack { kind: 'visual' | 'voice' | 'caption', segments: ProductionSegment[] }
77
+ export interface CaptionTimingSource {
78
+ available: boolean
79
+ reason?: string
80
+ sourceFingerprint: string
81
+ playbackFingerprint?: string
82
+ durationMs?: number
83
+ narrationIds: string[]
84
+ }
85
+ export interface ProductionStep extends ProductionRange { captionTiming?: CaptionTimingSource, stepId: string, index: number, holdMs?: number, animationMs?: number }
86
+ export interface ProductionPage extends ProductionRange { sectionId: string, title: string, steps: ProductionStep[] }
87
+ export interface ProductionDiagnostic {
88
+ code: string
89
+ severity: 'info' | 'warning' | 'error'
90
+ message: string
91
+ sectionId?: string
92
+ stepId?: string
93
+ blockId?: string
94
+ narrationId?: string
95
+ assetId?: string
96
+ mediaClipId?: string
97
+ }
98
+ export interface ProductionPlan {
99
+ binding: ProductionBinding
100
+ durationMs?: number
101
+ timing: ProductionTiming
102
+ pages: ProductionPage[]
103
+ tracks: ProductionTrack[]
104
+ events: TrackEvent[]
105
+ diagnostics: ProductionDiagnostic[]
106
+ /** Preview may buffer at missing speech; export still requires a confirmed complete plan. */
107
+ canPreview: boolean
108
+ canExport: boolean
109
+ }
@@ -0,0 +1,59 @@
1
+ import type { DocumentOperation, OperationAnswer } from './workspace.ts'
2
+ import type { CanonicalSource } from '../ir.ts'
3
+ import type { Member } from './server.ts'
4
+
5
+ export const PROJECT_MAX_REQUEST_CHARS = 4096
6
+ export const PROJECT_MAX_RESPONSE_CHARS = 7168
7
+ export const PROJECT_MAX_PLANS = 64
8
+ export const PROJECT_PLAN_TTL_MS = 300_000
9
+ export type ProjectOperation = DocumentOperation | { op: 'retitle', dir: string, title: string }
10
+ export interface ProjectCaller { member: Member, origin: 'editor' | 'agent', boundDocument?: string }
11
+ export interface ProjectObservation { snapshot?: string, configuration: string }
12
+ export interface ProjectCapabilities {
13
+ available: boolean
14
+ actions: Record<ProjectOperation['op'], { available: boolean, reasons: string[] }>
15
+ reasons: string[]
16
+ observation: ProjectObservation
17
+ verification: 'configuration-only'
18
+ rootDirectoryOperations: false
19
+ limits: { requestChars: number, responseChars: number, backupEntries: number, backupBytes: number, plans: number, ttlMs: number }
20
+ }
21
+ export interface ProjectPlanRequest { id: string, operation: ProjectOperation }
22
+ export interface ProjectPlan {
23
+ id: string
24
+ planId: string
25
+ digest: string
26
+ operation: ProjectOperation
27
+ observation: ProjectObservation
28
+ tree?: string
29
+ source?: CanonicalSource
30
+ documentId?: string
31
+ expiresAt: string
32
+ impact: { added: string[], removed: string[], files: number, bytes: number, referenceDocuments: string[], referencePolicy: 'preserve-or-refuse' }
33
+ }
34
+ export interface ProjectApply { planId: string, digest: string, confirmDeletion?: true }
35
+ export interface ProjectApplied extends OperationAnswer {
36
+ id: string
37
+ planId: string
38
+ snapshot?: string
39
+ tree?: string
40
+ source?: CanonicalSource
41
+ documentId?: string
42
+ revisionTruncated?: true
43
+ fileCount?: number
44
+ verification: { saved: true, readback: 'verified', compiled: 'not-requested' }
45
+ }
46
+ /** Directory management borrows the Web writer; metadata retitle borrows canonical document CAS. */
47
+ export interface ProjectAuthoring {
48
+ capabilities(caller: ProjectCaller): Promise<ProjectCapabilities>
49
+ plan(request: ProjectPlanRequest, caller: ProjectCaller): Promise<ProjectPlan>
50
+ readPlan(request: ProjectApply, caller: ProjectCaller): Promise<ProjectPlan>
51
+ apply(request: ProjectApply, caller: ProjectCaller, signal?: AbortSignal): Promise<ProjectApplied>
52
+ }
53
+ const text = { type: 'string', minLength: 1, maxLength: 200 } as const
54
+ const dir = { type: 'string', maxLength: 200 } as const
55
+ const operation = (op: string, properties: Record<string, unknown>, required: string[]) => ({ type: 'object', additionalProperties: false, properties: { op: { const: op }, dir, ...properties }, required: ['op', 'dir', ...required] })
56
+ export const PROJECT_PLAN_SCHEMA = { type: 'object', additionalProperties: false, required: ['id', 'operation'], properties: {
57
+ id: text, operation: { oneOf: [operation('create', { title: text, template: text }, []), operation('copy', { to: text, title: text }, ['to']), operation('rename', { to: text }, ['to']), operation('delete', {}, []), operation('retitle', { title: text }, ['title'])] },
58
+ } } as const
59
+ export const PROJECT_APPLY_SCHEMA = { type: 'object', additionalProperties: false, required: ['planId', 'digest'], properties: { planId: text, digest: { type: 'string', pattern: '^[a-f0-9]{64}$' }, confirmDeletion: { const: true } } } as const