@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,252 @@
1
+ import { Validator } from '@cfworker/json-schema'
2
+ import { DOCUMENT_JSON_SCHEMA } from './document-schema.ts'
3
+ import { isRecord } from './errors.ts'
4
+ import { captionSourceFingerprint, captionTimingProblem, stepCaptionText } from './document-caption-timings.ts'
5
+ import { validateDocumentParameters } from './parameters.ts'
6
+ import type { DocumentIssue, DocumentValidation, RichTextNode, TeachingDocument } from './contracts/document.ts'
7
+
8
+ export { DOCUMENT_JSON_SCHEMA } from './document-schema.ts'
9
+ export { DOCUMENT_ENTRY, DOCUMENT_SCHEMA_VERSION } from './contracts/document.ts'
10
+
11
+ const structural = new Validator(DOCUMENT_JSON_SCHEMA, '2020-12', false)
12
+ const escape = (key: string) => key.replaceAll('~', '~0').replaceAll('/', '~1')
13
+ const at = (path: string, key: string | number) => `${path}/${escape(String(key))}`
14
+ const own = (table: object, key: string) => Object.hasOwn(table, key)
15
+
16
+ /** The same structural and semantic checks serve agent changes, native editing and compilation. */
17
+ export function validateDocument(value: unknown): DocumentValidation {
18
+ const issues: DocumentIssue[] = []
19
+ const issue = (path: string, code: string, message: string, severity: DocumentIssue['severity'] = 'error') => { issues.push({ path, code, message, severity }) }
20
+ const active = new WeakSet<object>()
21
+ let nodes = 0
22
+ const json = (item: unknown, path: string, depth = 0): boolean => {
23
+ if (++nodes > 1_000_000 || depth > 128) { issue(path, 'json.limit', 'The document exceeds the supported JSON size or nesting limit.'); return false }
24
+ if (item === null || typeof item === 'string' || typeof item === 'boolean') return true
25
+ if (typeof item === 'number') { if (Number.isFinite(item)) return true; issue(path, 'json.number', 'JSON numbers must be finite.'); return false }
26
+ if (typeof item !== 'object') { issue(path, 'json.value', 'A document contains only JSON values.'); return false }
27
+ if (active.has(item)) { issue(path, 'json.cycle', 'A JSON value cannot contain an object cycle.'); return false }
28
+ if (!Array.isArray(item) && ![Object.prototype, null].includes(Object.getPrototypeOf(item))) { issue(path, 'json.object', 'A JSON object cannot have a custom object prototype.'); return false }
29
+ active.add(item)
30
+ const valid = Object.entries(item).every(([key, child]) => json(child, at(path, key), depth + 1))
31
+ active.delete(item)
32
+ return valid
33
+ }
34
+ if (!json(value, '')) return { issues }
35
+ let checked
36
+ try { checked = structural.validate(value) }
37
+ catch (error) { issue('', 'schema.invalid', error instanceof Error ? error.message : 'Document schema validation failed.'); return { issues } }
38
+ if (!checked.valid) {
39
+ for (const error of checked.errors.slice(0, 100)) issue(error.instanceLocation.replace(/^#/, ''), `schema.${error.keyword}`, error.error)
40
+ return { issues }
41
+ }
42
+ const doc = value as TeachingDocument
43
+ const lectureRenderer = doc.metadata.renderers?.lecture
44
+ if (isRecord(lectureRenderer) && own(lectureRenderer, 'script')) issue('/metadata/renderers/lecture/script', 'narration.external-source', 'External lecture scripts are not supported. Author narration in lecture.json narrations and bind it through each step narrationIds.')
45
+ const titleTargets = new Map(Object.keys(doc.sections).map(id => [`page:${id}`, id]))
46
+ const ids = new Map<string, string>()
47
+ const identify = (id: string, path: string) => {
48
+ const section = titleTargets.get(id)
49
+ if (section !== undefined) issue(path, 'id.reserved', `ID ${id} is reserved for the title of section ${section}.`)
50
+ const prior = ids.get(id)
51
+ if (prior !== undefined) issue(path, 'id.duplicate', `ID ${id} is already used at ${prior}.`)
52
+ else ids.set(id, path)
53
+ }
54
+ identify(doc.id, '/id')
55
+ for (const name of ['sections', 'blocks', 'steps', 'narrations', 'assets', 'definitions'] as const) {
56
+ for (const [key, entity] of Object.entries(doc[name])) {
57
+ const path = at(`/${name}`, key)
58
+ if (entity.id !== key) issue(`${path}/id`, 'id.key', 'An entity ID must match its map key.')
59
+ identify(entity.id, `${path}/id`)
60
+ }
61
+ }
62
+ const reference = (table: object, id: string, path: string, kind: string): boolean => {
63
+ if (own(table, id)) return true
64
+ issue(path, 'reference.missing', `Unknown ${kind} ${id}.`)
65
+ return false
66
+ }
67
+ for (const [index, id] of doc.sectionOrder.entries()) reference(doc.sections, id, `/sectionOrder/${index}`, 'section')
68
+ for (const id of Object.keys(doc.sections)) if (!doc.sectionOrder.includes(id)) issue(at('/sections', id), 'section.order', 'Every section must appear once in sectionOrder.')
69
+ const parents = new Map<string, string>()
70
+ const sectionOf = new Map<string, string>()
71
+ const visiting = new Set<string>()
72
+ const visit = (id: string, path: string, section: string) => {
73
+ if (!reference(doc.blocks, id, path, 'block')) return
74
+ if (visiting.has(id)) { issue(path, 'block.cycle', 'Block ownership cannot contain a cycle.'); return }
75
+ if (parents.has(id)) { issue(path, 'block.parent', `Block ${id} already belongs to ${parents.get(id)}.`); return }
76
+ parents.set(id, path); sectionOf.set(id, section); visiting.add(id)
77
+ const block = doc.blocks[id]!
78
+ if (block.type === 'container' || block.type === 'component') for (const [index, child] of (block.children ?? []).entries()) visit(child, at(at('/blocks', id), `children`) + `/${index}`, section)
79
+ visiting.delete(id)
80
+ }
81
+ for (const [id, section] of Object.entries(doc.sections)) for (const [index, child] of section.children.entries()) visit(child, at('/sections', id) + `/children/${index}`, id)
82
+ for (const id of Object.keys(doc.blocks)) if (!parents.has(id)) issue(at('/blocks', id), 'block.orphan', 'Every content block must have one section or container parent.')
83
+ const richIds = new Map<string, Set<string>>()
84
+ const rich = (node: RichTextNode | { id: string, type: string, children?: unknown[] }, path: string, collected: Set<string>) => {
85
+ identify(node.id, `${path}/id`); collected.add(node.id)
86
+ if (node.type === 'table') {
87
+ const table = node as Extract<RichTextNode, { type: 'table' }>
88
+ if ((table.headerRows ?? 0) > table.children.length) issue(`${path}/headerRows`, 'table.header', 'Header rows cannot exceed table rows.')
89
+ let spans: { from: number, to: number, until: number }[] = []
90
+ const widths = table.children.map((row, rowIndex) => {
91
+ spans = spans.filter(span => span.until > rowIndex).sort((a, b) => a.from - b.from)
92
+ let column = 0, index = 0
93
+ const added: typeof spans = []
94
+ for (const [cellIndex, cell] of row.children.entries()) {
95
+ const cellPath = `${path}/children/${rowIndex}/children/${cellIndex}`
96
+ const colspan = cell.colspan ?? 1, rowspan = cell.rowspan ?? 1
97
+ if (!Number.isSafeInteger(colspan) || !Number.isSafeInteger(rowspan)) {
98
+ issue(cellPath, 'table.span', 'Cell spans must be safe positive integers.')
99
+ continue
100
+ }
101
+ while (spans[index] && spans[index]!.from <= column) { column = Math.max(column, spans[index]!.to); index++ }
102
+ const end = column + colspan
103
+ const overlap = spans[index] && spans[index]!.from < end
104
+ if (overlap) issue(cellPath, 'table.overlap', 'A cell cannot overlap a cell spanning from an earlier row.')
105
+ if (rowIndex + rowspan > table.children.length) issue(`${cellPath}/rowspan`, 'table.span', 'A row span cannot extend beyond the table rows.')
106
+ if (!Number.isSafeInteger(end) || table.columnWidths && end > table.columnWidths.length) issue(`${cellPath}/colspan`, 'table.span', 'A cell span cannot extend beyond the declared table columns.')
107
+ if (rowspan > 1 && !overlap) added.push({ from: column, to: end, until: rowIndex + rowspan })
108
+ column = end
109
+ }
110
+ const width = spans.reduce((width, span) => Math.max(width, span.to), column)
111
+ spans.push(...added)
112
+ return width
113
+ })
114
+ if (table.headerColumns !== undefined && widths.some(width => table.headerColumns! > width)) issue(`${path}/headerColumns`, 'table.header', 'Header columns cannot exceed table columns.')
115
+ if (table.columnWidths && widths.some(width => width !== table.columnWidths!.length)) issue(`${path}/columnWidths`, 'table.widths', 'Column widths must match the logical table columns.')
116
+ }
117
+ if ('children' in node && Array.isArray(node.children)) for (const [index, child] of node.children.entries()) if (isRecord(child) && typeof child.id === 'string') rich(child as unknown as RichTextNode, `${path}/children/${index}`, collected)
118
+ if (node.type === 'extension' && 'fallback' in node && Array.isArray(node.fallback)) node.fallback.forEach((child, index) => rich(child as RichTextNode, `${path}/fallback/${index}`, collected))
119
+ }
120
+ const schema = (value: JsonSchemaLike, parameters: unknown, path: string) => {
121
+ const result = validateDocumentParameters(value, parameters)
122
+ for (const error of result.issues) issue(path + error.path, result.available ? 'parameters.schema' : 'parameters.schema-unavailable', error.message)
123
+ }
124
+ for (const [id, block] of Object.entries(doc.blocks)) {
125
+ const path = at('/blocks', id)
126
+ if (block.type === 'text') {
127
+ const collected = new Set<string>(); richIds.set(id, collected)
128
+ block.content.forEach((node, index) => rich(node, `${path}/content/${index}`, collected))
129
+ for (const [index, item] of (block.reveal?.order ?? []).entries()) if (!collected.has(item)) issue(`${path}/reveal/order/${index}`, 'reveal.target', 'Reveal items must name rich-text nodes of this block.')
130
+ } else if (block.type === 'image') {
131
+ if (block.assetId !== undefined && reference(doc.assets, block.assetId, `${path}/assetId`, 'asset') && doc.assets[block.assetId]!.kind !== 'image') issue(`${path}/assetId`, 'asset.kind', 'An image block requires an image asset.')
132
+ } else if (block.type === 'component') {
133
+ if (reference(doc.definitions, block.definitionId, `${path}/definitionId`, 'definition')) schema(doc.definitions[block.definitionId]!.parameterSchema, block.parameters, `${path}/parameters`)
134
+ } else if (block.type === 'code') {
135
+ if (block.highlight && block.origin?.highlightRegions) issue(`${path}/highlight`, 'code.highlight-owner', 'Choose authored line numbers or tracked source regions, not both.')
136
+ for (const [field, values] of [['highlight', block.highlight], ['origin/highlightRegions', block.origin?.highlightRegions]] as const) for (const step of Object.keys(values ?? {})) {
137
+ if (reference(doc.steps, step, `${path}/${field}/${escape(step)}`, 'step') && doc.steps[step]!.sectionId !== sectionOf.get(id)) issue(`${path}/${field}/${escape(step)}`, 'code.highlight-section', 'A code highlight must belong to a step of its own section.')
138
+ }
139
+ for (const [index, variant] of (block.variants ?? []).entries()) identify(variant.id, `${path}/variants/${index}/id`)
140
+ } else if (block.type === 'container') {
141
+ if (block.properties.columnWidths && block.properties.columnWidths.length !== block.children.length) issue(`${path}/properties/columnWidths`, 'container.columns', 'Column widths must match the child count.')
142
+ if (block.layout === 'tabs' && block.properties.tabTitles && block.properties.tabTitles.length !== block.children.length) issue(`${path}/properties/tabTitles`, 'container.tabs', 'Tab titles must match the child count.')
143
+ }
144
+ }
145
+ for (const [id, definition] of Object.entries(doc.definitions)) {
146
+ const states = new Set<string>()
147
+ for (const [index, state] of definition.states.entries()) {
148
+ if (states.has(state.id)) issue(at('/definitions', id) + `/states/${index}/id`, 'state.duplicate', 'Definition state IDs must be unique within the definition.')
149
+ states.add(state.id)
150
+ }
151
+ }
152
+ const effectiveVisuals = (id: string, visiting = new Set<string>()): Map<string, TeachingDocument['steps'][string]['visuals'][number]> => {
153
+ if (visiting.has(id) || !own(doc.steps, id)) return new Map()
154
+ visiting.add(id)
155
+ const step = doc.steps[id]!
156
+ const result = step.inherits ? effectiveVisuals(step.inherits, visiting) : new Map<string, TeachingDocument['steps'][string]['visuals'][number]>()
157
+ for (const visual of step.visuals) result.set(visual.blockId, { ...result.get(visual.blockId), ...visual, parameters: { ...result.get(visual.blockId)?.parameters, ...visual.parameters } })
158
+ return result
159
+ }
160
+ const orderedSteps = new Set<string>()
161
+ for (const [id, section] of Object.entries(doc.sections)) for (const [index, stepId] of section.stepOrder.entries()) {
162
+ const path = at('/sections', id) + `/stepOrder/${index}`
163
+ if (orderedSteps.has(stepId)) issue(path, 'step.owner', 'A step belongs to one section order.')
164
+ orderedSteps.add(stepId)
165
+ if (reference(doc.steps, stepId, path, 'step') && doc.steps[stepId]!.sectionId !== id) issue(path, 'step.section', 'A step must belong to the section that orders it.')
166
+ }
167
+ for (const [id, step] of Object.entries(doc.steps)) {
168
+ const path = at('/steps', id)
169
+ if (!orderedSteps.has(id)) issue(path, 'step.orphan', 'Every step must appear in its section stepOrder.')
170
+ reference(doc.sections, step.sectionId, `${path}/sectionId`, 'section')
171
+ const visuals = new Set<string>()
172
+ for (const [index, visual] of step.visuals.entries()) {
173
+ const location = `${path}/visuals/${index}`
174
+ if (visuals.has(visual.blockId)) issue(`${location}/blockId`, 'visual.duplicate', 'A step has one override per block.')
175
+ visuals.add(visual.blockId)
176
+ if (!reference(doc.blocks, visual.blockId, `${location}/blockId`, 'block')) continue
177
+ if (sectionOf.get(visual.blockId) !== step.sectionId) issue(`${location}/blockId`, 'visual.section', 'Visual state must target a block owned by the same section.')
178
+ const actual = effectiveVisuals(id).get(visual.blockId)!
179
+ const block = doc.blocks[visual.blockId]!
180
+ if (block.type === 'component') {
181
+ const definition = doc.definitions[block.definitionId]
182
+ if (definition) {
183
+ if (actual.parameters) schema(definition.parameterSchema, { ...block.parameters, ...actual.parameters }, `${location}/parameters`)
184
+ const state = actual.state === undefined ? undefined : definition.states.find(state => state.id === actual.state)
185
+ if (actual.state !== undefined && !state) issue(`${location}/state`, 'state.missing', 'The component definition does not declare this state.')
186
+ if (state?.parameterSchema) schema(state.parameterSchema, { ...block.parameters, ...actual.parameters }, `${location}/parameters`)
187
+ }
188
+ } else if (visual.state !== undefined) issue(`${location}/state`, 'state.unsupported', 'Named animation states require a component definition.')
189
+ }
190
+ for (const [index, narration] of step.narrationIds.entries()) reference(doc.narrations, narration, `${path}/narrationIds/${index}`, 'narration')
191
+ const seen = new Set<string>([id]); let inherited = step.inherits
192
+ while (inherited !== undefined) {
193
+ if (seen.has(inherited)) { issue(`${path}/inherits`, 'step.cycle', 'Step inheritance cannot contain a cycle.'); break }
194
+ seen.add(inherited)
195
+ if (!reference(doc.steps, inherited, `${path}/inherits`, 'step')) break
196
+ if (doc.steps[inherited]!.sectionId !== step.sectionId) { issue(`${path}/inherits`, 'step.section', 'Inherited steps must belong to the same section.'); break }
197
+ inherited = doc.steps[inherited]!.inherits
198
+ }
199
+ }
200
+ const cues = new Set<string>()
201
+ for (const [id, narration] of Object.entries(doc.narrations)) if (narration.provenance) {
202
+ const path = at('/narrations', id) + '/provenance/assetId'
203
+ if (reference(doc.assets, narration.provenance.assetId, path, 'asset') && doc.assets[narration.provenance.assetId]!.kind !== 'audio') issue(path, 'asset.kind', 'Transcription provenance requires the original audio asset.')
204
+ }
205
+ for (const [id, narration] of Object.entries(doc.narrations)) for (const [index, node] of narration.content.entries()) if (node.type === 'cue') {
206
+ const path = at('/narrations', id) + `/content/${index}`
207
+ identify(node.id, `${path}/id`); cues.add(node.id)
208
+ if (node.target) reference(doc.blocks, node.target.blockId, `${path}/target/blockId`, 'block')
209
+ if (node.parameters && ['pauseMs', 'durationMs', 'delayMs'].some(key => own(node.parameters!, key))) issue(`${path}/parameters`, 'production.owner', 'Cue timing belongs to production.cueTimings.')
210
+ }
211
+ const voice = (production: TeachingDocument['production']['defaults'], path: string) => {
212
+ const source = production.voiceSource
213
+ if (source?.type === 'recording' && reference(doc.assets, source.assetId, `${path}/voiceSource/assetId`, 'asset') && !['audio', 'video'].includes(doc.assets[source.assetId]!.kind)) issue(`${path}/voiceSource/assetId`, 'asset.kind', 'Recorded speech requires audio or video.')
214
+ if (source?.type === 'recording' && source.sourceRange && source.sourceRange.endMs <= source.sourceRange.startMs) issue(`${path}/voiceSource/sourceRange`, 'media.range', 'A recording range must end after it starts.')
215
+ }
216
+ voice(doc.production.defaults, '/production/defaults')
217
+ for (const [id, override] of Object.entries(doc.production.sectionOverrides ?? {})) { reference(doc.sections, id, at('/production/sectionOverrides', id), 'section'); voice(override, at('/production/sectionOverrides', id)) }
218
+ for (const [id, override] of Object.entries(doc.production.stepOverrides)) { reference(doc.steps, id, at('/production/stepOverrides', id), 'step'); voice(override, at('/production/stepOverrides', id)) }
219
+ for (const id of Object.keys(doc.production.narrationOverrides ?? {})) reference(doc.narrations, id, at('/production/narrationOverrides', id), 'narration')
220
+ for (const id of Object.keys(doc.production.cueTimings ?? {})) if (!cues.has(id)) issue(at('/production/cueTimings', id), 'cue.missing', 'Cue timing must reference an authored narration cue.')
221
+ for (const [stepId, timing] of Object.entries(doc.production.captionTimings ?? {})) {
222
+ const step = doc.steps[stepId]
223
+ const stale = !step || JSON.stringify(step.narrationIds) !== JSON.stringify(timing.narrationIds) || captionSourceFingerprint(doc, stepId) !== timing.sourceFingerprint
224
+ const problem = stale ? 'caption-source-changed' : captionTimingProblem(timing, stepCaptionText(doc, stepId))
225
+ if (problem) issue(at('/production/captionTimings', stepId), 'caption.timing-stale', 'Manual caption timing requires review: ' + problem, 'warning')
226
+ }
227
+ for (const [index, clip] of doc.production.mediaClips.entries()) {
228
+ const path = `/production/mediaClips/${index}`
229
+ identify(clip.id, `${path}/id`)
230
+ if (reference(doc.assets, clip.assetId, `${path}/assetId`, 'asset') && !['audio', 'video'].includes(doc.assets[clip.assetId]!.kind)) issue(`${path}/assetId`, 'asset.kind', 'A media clip requires audio or video.')
231
+ reference(doc.steps, clip.attachment.stepId, `${path}/attachment/stepId`, 'step')
232
+ if (clip.sourceRange.endMs <= clip.sourceRange.startMs) issue(`${path}/sourceRange`, 'media.range', 'A media range must end after it starts.')
233
+ }
234
+ const lecture = doc.metadata.renderers?.lecture
235
+ if (lecture && ['voice', 'gap', 'script'].some(key => own(lecture, key))) issue('/metadata/renderers/lecture', 'production.owner', 'Voice, narration sources and playback timing belong only to production and narration entities.')
236
+ const textTargets = new Set([...richIds.values()].flatMap(values => [...values]))
237
+ for (const id of doc.sectionOrder) textTargets.add(`page:${id}`)
238
+ for (const [id, block] of Object.entries(doc.blocks)) if (block.type === 'code' && block.value !== undefined) textTargets.add(id)
239
+ for (const [id, thread] of Object.entries(doc.comments ?? {})) {
240
+ const path = at('/comments', id)
241
+ if (id !== thread.id) issue(`${path}/id`, 'id.key', 'A thread ID must match its map key.')
242
+ identify(thread.id, `${path}/id`)
243
+ for (const edge of ['from', 'to'] as const) if (thread.anchor && !textTargets.has(thread.anchor[edge].blockId)) issue(`${path}/anchor/${edge}/blockId`, 'comment.target', 'A comment anchor must name a stable rich-text node or section title.')
244
+ for (const [index, comment] of thread.comments.entries()) {
245
+ identify(comment.id, `${path}/comments/${index}/id`)
246
+ comment.content?.forEach((node, child) => rich(node, `${path}/comments/${index}/content/${child}`, new Set()))
247
+ }
248
+ }
249
+ return issues.some(issue => issue.severity === 'error') ? { issues } : { document: doc, issues }
250
+ }
251
+
252
+ type JsonSchemaLike = TeachingDocument['definitions'][string]['parameterSchema']
@@ -0,0 +1,7 @@
1
+ /** A duration as authors write one (`{cue}` `pause`, `:durations:`): `1.2` and `1.2s` are seconds, `800ms` milliseconds; in milliseconds. */
2
+ export function parseDuration(value: string): number | undefined {
3
+ const match = /^(\d+(?:\.\d+)?)\s*(ms|s)?$/.exec(value.trim())
4
+ if (!match) return undefined
5
+ const n = Number(match[1])
6
+ return Math.round(match[2] === 'ms' ? n : n * 1000)
7
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,15 @@
1
+ export function errorMessage(error: unknown): string {
2
+ return error instanceof Error ? error.message : String(error)
3
+ }
4
+
5
+ export function isRecord(value: unknown): value is Record<string, unknown> {
6
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
7
+ }
8
+
9
+ /** What a preview needs (the viewer app, the document's code) is still being built: ask again later; not a failure. */
10
+ export class PreviewPreparing extends Error {
11
+ constructor(message = 'preview-preparing') {
12
+ super(message)
13
+ this.name = 'PreviewPreparing'
14
+ }
15
+ }
package/src/events.ts ADDED
@@ -0,0 +1,32 @@
1
+ import type { DomainEvent, EventBus } from './contracts.ts'
2
+
3
+ type Handler = (event: DomainEvent) => void
4
+ type ErrorHandler = (error: unknown, event: DomainEvent) => void
5
+
6
+ const reportToConsole: ErrorHandler = (error, event) => {
7
+ console.error(`[presmium] handler for "${event.type}" threw`, error)
8
+ }
9
+
10
+ export function createEventBus(onError: ErrorHandler = reportToConsole): EventBus {
11
+ const handlers = new Map<string, Set<Handler>>()
12
+ return {
13
+ emit(event) {
14
+ for (const handler of [...(handlers.get(event.type) ?? [])]) {
15
+ try {
16
+ handler(event)
17
+ } catch (error) {
18
+ onError(error, event)
19
+ }
20
+ }
21
+ },
22
+ on(type, handler) {
23
+ const set = handlers.get(type) ?? new Set()
24
+ handlers.set(type, set)
25
+ const h = handler as Handler
26
+ set.add(h)
27
+ return () => {
28
+ set.delete(h)
29
+ }
30
+ },
31
+ }
32
+ }
@@ -0,0 +1,25 @@
1
+ import type { RunResult } from './contracts/execution.ts'
2
+
3
+ const ALIASES: Record<string, string> = {
4
+ py: 'python',
5
+ python3: 'python',
6
+ js: 'javascript',
7
+ mjs: 'javascript',
8
+ ts: 'typescript',
9
+ }
10
+
11
+ export function languageId(lang: string): string {
12
+ const id = lang.trim().toLowerCase()
13
+ if (id.startsWith('ipython')) return 'python'
14
+ return ALIASES[id] ?? id
15
+ }
16
+
17
+ export function failedRun(executorId: string, stderr: string, durationMs = 0): RunResult {
18
+ return { ok: false, stdout: '', stderr, exitCode: null, durationMs, timedOut: false, source: 'live', executorId }
19
+ }
20
+
21
+ export const CREDENTIAL_VARIABLE = /_(?:KEY|TOKEN|SECRET|PASSWORD|APPID)$/i
22
+
23
+ export function environmentExecutor(name: string): string {
24
+ return `presmium.env.${name}`
25
+ }
@@ -0,0 +1,31 @@
1
+ function normalizeWhitespace(text: string): string {
2
+ const lines = text
3
+ .replace(/\r\n?/g, '\n')
4
+ .split('\n')
5
+ .map(line => line.replace(/\s+$/u, '').replace(/^[ \t]+/, lead => ' '.repeat(lead.replace(/\t/g, ' ').length)))
6
+ .filter(line => line.length > 0)
7
+ if (lines.length === 0) return ''
8
+ const indent = Math.min(...lines.map(line => line.length - line.trimStart().length))
9
+ return lines
10
+ .map((line) => {
11
+ const depth = line.length - line.trimStart().length - indent
12
+ const body = line.trimStart().replace(/\s+/gu, ' ').replace(/ ?([^\p{L}\p{N}_ ]) ?/gu, '$1')
13
+ return ' '.repeat(depth) + body
14
+ })
15
+ .join('\n')
16
+ }
17
+
18
+ const FNV_OFFSET = 0xcbf29ce484222325n
19
+ const FNV_PRIME = 0x100000001b3n
20
+
21
+ export function fnv1a64(text: string): string {
22
+ let hash = FNV_OFFSET
23
+ for (const byte of new TextEncoder().encode(text)) {
24
+ hash = BigInt.asUintN(64, (hash ^ BigInt(byte)) * FNV_PRIME)
25
+ }
26
+ return hash.toString(16).padStart(16, '0')
27
+ }
28
+
29
+ export function fingerprint(text: string): string {
30
+ return fnv1a64(normalizeWhitespace(text))
31
+ }
package/src/follow.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { IRSection } from './ir.ts'
2
+ import { stepTrail, trailStep } from './step-keys.ts'
3
+ import type { StepKeys } from './step-keys.ts'
4
+
5
+ /** The presenter's position by ID, so learners with another version of the document reach the same place. */
6
+ export interface FollowAnchor {
7
+ section?: string
8
+ trail?: string[]
9
+ back?: string
10
+ }
11
+
12
+ const LEARNERS = 'student'
13
+
14
+ /** For a section only some audiences have: the nearest earlier section that the other versions have too. */
15
+ export function backSection(sections: readonly Pick<IRSection, 'id' | 'audience'>[], id: string): string | undefined {
16
+ const at = sections.findIndex(section => section.id === id)
17
+ const audience = sections[at]?.audience
18
+ if (!audience?.length) return undefined
19
+ const learners = !audience.includes(LEARNERS)
20
+ return sections.slice(0, at).findLast(section => !section.audience?.length || (learners && section.audience.includes(LEARNERS)))?.id
21
+ }
22
+
23
+ /** Where a learner goes in their own version of the document when following the presenter's position. */
24
+ export function followedPlace(position: FollowAnchor & { step: number }, keysOf: (id: string) => StepKeys | undefined): { section: string, step: number } | undefined {
25
+ const { section, trail, back, step } = position
26
+ const keys = section === undefined ? undefined : keysOf(section)
27
+ if (keys) {
28
+ const other = trail !== undefined && stepTrail(keys, step).join('\n') !== trail.join('\n')
29
+ return { section: section!, step: other ? trailStep(keys, trail) : step }
30
+ }
31
+ const before = back === undefined ? undefined : keysOf(back)
32
+ return before && { section: back!, step: before.length - 1 }
33
+ }
package/src/git.ts ADDED
@@ -0,0 +1,12 @@
1
+ const GIT_SAFE_CONFIG: readonly string[] = ['core.hooksPath=/dev/null', 'core.fsmonitor=false', 'core.quotePath=false', 'commit.gpgSign=false', 'tag.gpgSign=false']
2
+
3
+ export function gitConfigArgs(): string[] {
4
+ return GIT_SAFE_CONFIG.flatMap(entry => ['-c', entry])
5
+ }
6
+
7
+ export function gitEnvironment(env: Record<string, string | undefined>): Record<string, string> {
8
+ const own: Record<string, string> = {}
9
+ for (const [name, value] of Object.entries(env)) if (value !== undefined && !/^GIT_/i.test(name)) own[name] = value
10
+ own.GIT_TERMINAL_PROMPT = '0'
11
+ return own
12
+ }
package/src/html.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { parseFragment } from 'parse5'
2
+ import type { DefaultTreeAdapterMap } from 'parse5'
3
+
4
+ export function escapeHtml(text: string): string {
5
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
6
+ }
7
+
8
+ export interface HtmlAttribute {
9
+ name: string
10
+ value: string
11
+ start: number
12
+ end: number
13
+ }
14
+
15
+ export const URL_ATTRIBUTES: ReadonlySet<string> = new Set(['src', 'poster', 'href'])
16
+
17
+ export function htmlAttributes(html: string, names: ReadonlySet<string>): HtmlAttribute[] {
18
+ const found: HtmlAttribute[] = []
19
+ const visit = (node: DefaultTreeAdapterMap['node']) => {
20
+ if ('attrs' in node) {
21
+ for (const { name, value } of node.attrs) {
22
+ const at = node.sourceCodeLocation?.attrs?.[name]
23
+ if (names.has(name) && at) found.push({ name, value, start: at.startOffset, end: at.endOffset })
24
+ }
25
+ }
26
+ if ('childNodes' in node) node.childNodes.forEach(visit)
27
+ if ('content' in node) visit(node.content)
28
+ }
29
+ visit(parseFragment(html, { sourceCodeLocationInfo: true }))
30
+ return found.sort((a, b) => a.start - b.start)
31
+ }
32
+
33
+ export function mapHtmlUrls(html: string, map: (url: string, attribute: string) => string | undefined): string {
34
+ let out = html
35
+ for (const { name, value, start, end } of htmlAttributes(html, URL_ATTRIBUTES).reverse()) {
36
+ const mapped = map(value, name)
37
+ if (mapped === undefined) continue
38
+ out = `${out.slice(0, start)}${html.slice(start, start + name.length)}="${escapeHtml(mapped)}"${out.slice(end)}`
39
+ }
40
+ return out
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ export * from './contracts.ts'
2
+ export { applyDocumentOperations } from './document-operations.ts'
3
+ export { validateDocumentChange, DOCUMENT_CHANGE_JSON_SCHEMA } from './authoring.ts'
4
+ export { allocateDocumentPage, createDocumentPage, documentPageDefaults, DOCUMENT_PAGE_DEFAULT_TIMING, duplicateDocumentPage, moveDocumentPage, pageContents, pageDeletion, pageDeletionImpact, removeDocumentPage, renameDocumentPage } from './document-pages.ts'
5
+ export { documentOutline, documentPart, DocumentReadError } from './document-reads.ts'
6
+ export { changeDocumentVoice, documentVoiceStepIds, previewDocumentVoiceChange, resolveDocumentStepVoice } from './document-voices.ts'
7
+ export { DOCUMENT_COLOR_NAMES, documentColorToken, documentColorValue } from './document-colors.ts'
8
+ export type { DocumentColorKind, DocumentColorName } from './document-colors.ts'
9
+ export { applyDocumentCommentAction, isDocumentTextLink, documentCommentTargets, validateDocumentCommentAnchor, DocumentCommentError } from './document-comments.ts'
10
+ export { applyDocumentAuthoringAction } from './document-authoring-actions.ts'
11
+ export { applyDocumentComponentAction, effectiveComponentParameters, createDocumentParameterChanges, documentComponentSection, DocumentComponentError } from './document-components.ts'
12
+ export type { DocumentStepVoice, DocumentVoiceImpact, DocumentVoiceOptions, DocumentVoiceScope } from './document-voices.ts'
13
+ export { prepareTranscriptionAdoption } from './transcription-adoption.ts'
14
+ export { productionAssetIds, recordingStepIssue, recordingSourceRange } from './recording.ts'
15
+ export * from './ir.ts'
16
+ export { createAnchorResolverRegistry, isRenderable, parseAnchor } from './anchors.ts'
17
+ export type { ParsedAnchor } from './anchors.ts'
18
+ export { diagnosticOf } from './diagnostics.ts'
19
+ export { DOCUMENT_JSON_SCHEMA, validateDocument } from './document.ts'
20
+ export { validateDocumentParameters, documentParameterAccess, documentParameterSchemaForAccess } from './parameters.ts'
21
+ export type { DocumentParameterAccess, DocumentParameterIssue, DocumentParameterSchemaReference, DocumentParameterValidation } from './parameters.ts'
22
+ export { docBlocks, irBlocks, nodeMdast } from './blocks.ts'
23
+ export { CheckRegistry } from './checks.ts'
24
+ export { errorMessage, isRecord, PreviewPreparing } from './errors.ts'
25
+ export { createEventBus } from './events.ts'
26
+ export { CREDENTIAL_VARIABLE, environmentExecutor, failedRun, languageId } from './execution.ts'
27
+ export { gitConfigArgs, gitEnvironment } from './git.ts'
28
+ export { escapeHtml, htmlAttributes, mapHtmlUrls, URL_ATTRIBUTES } from './html.ts'
29
+ export type { HtmlAttribute } from './html.ts'
30
+ export { hostName, localRequestRefusal } from './requests.ts'
31
+ export type { RequestHeaders } from './requests.ts'
32
+ export { fingerprint, fnv1a64 } from './fingerprint.ts'
33
+ export { applyDocumentCaptionTiming, captionSourceFingerprint, captionTimingProblem, stepCaptionText, narrationCaptionText, CAPTION_TIMING_MAX_SEGMENTS, DocumentCaptionTimingError } from './document-caption-timings.ts'
34
+ export type { DocumentCaptionTimingAction } from './document-caption-timings.ts'
35
+ export { backSection, followedPlace } from './follow.ts'
36
+ export type { FollowAnchor } from './follow.ts'
37
+ export { Kernel } from './kernel.ts'
38
+ export type { KernelOptions } from './kernel.ts'
39
+ export { deckLocale, localeOf } from './locale.ts'
40
+ export { ANCHOR_LOCK_PATH, AnchorLock } from './lock.ts'
41
+ export { prefixedLogger } from './logging.ts'
42
+ export { lowerMyst, roleMark } from './lower-myst.ts'
43
+ export type { LinkedType, LoweredMarkdown, LowerOptions } from './lower-myst.ts'
44
+ export { connectPreviewFrame, isPreviewActive, isPreviewEmbedded, previewAppearance, previewFrameAncestors, reportPreview } from './preview-frame.ts'
45
+ export { keyBelongsToTarget, watchFocusSource } from './keyboard.ts'
46
+ export type { PreviewFrame, PreviewPage, PreviewProblem, PreviewTextEdit, PreviewTextHandle } from './preview-frame.ts'
47
+ export { previewEditBinding } from './preview-edit.ts'
48
+ export { pageEditCapability, pinPageSource, renamePageSource, movePageSource, removePageSource, pageRemovalTargets, pageIncomingReferences, writePage } from './page-source.ts'
49
+ export type { PageEditCapability, PageEditReason, PageRemoval } from './page-source.ts'
50
+ export { propText, withProp } from './props.ts'
51
+ export { publishedUrl } from './published.ts'
52
+ export type { PublishedUrlOptions } from './published.ts'
53
+ export { blockState, blockText, blockVisible, blockStepState, chapters, isChapterTitle, mdastText, narrationAt, sectionNarration, tokenize } from './reading.ts'
54
+ export type { Chapter } from './reading.ts'
55
+ export { slideChapters, slideShape } from './slides.ts'
56
+ export { compactSteps, viewFor } from './views.ts'
57
+ export type { SlideShape } from './slides.ts'
58
+ export { jsonResponse, problemResponse, sseResponse } from './responses.ts'
59
+ export { kernelIncompatibility, ServiceRegistry } from './services.ts'
60
+ export type { StartPlan } from './services.ts'
61
+ export { remapWords, splitWords } from './speech-words.ts'
62
+ export type { TextWord, WordSpan, WrittenWord } from './speech-words.ts'
63
+ export { stepKeys, stepTrail, trailStep } from './step-keys.ts'
64
+ export type { StepKeys } from './step-keys.ts'
65
+ export { parseDuration } from './durations.ts'
66
+ export { stepStateAt } from './steps.ts'
67
+ export { ABSOLUTE_URL, NON_FILE_URL, urlSlug } from './urls.ts'
68
+ export { isButton } from './lower-myst.ts'
69
+ export { componentProps, findComponentRenderer, hasRendererExtension, WEB_RENDERER_PACKAGE, withPublishedAssets } from './renderer-components.ts'
70
+ export { htmlAsText, isProseRun } from './run-outputs.ts'
71
+ export { safeDecodeURIComponent } from './uri-decoding.ts'
72
+ export { APPROVAL_KINDS, hasRole, isPresentingRole, LOCAL_AUTHOR } from './access.ts'
73
+ export { pushStream } from './push-stream.ts'
74
+ export type { PushStream, PushStreamOptions } from './push-stream.ts'
75
+ export { aigcLabel, aigcString, speechEngineId } from './speech.ts'
76
+ export { documentVoice, resolvedVoice, speechVoice, stepVoiceReason, voiceOverrides, voiceTarget } from './voice.ts'
77
+ export { isCjk } from './speech-words.ts'
78
+ export { componentCode } from './component-code.ts'
79
+ export { jupyterKernelLanguage } from './jupyter-kernel.ts'
80
+
81
+ export { createPreviewEvents } from './preview-events.ts'
82
+ export type { PreviewEvents } from './preview-events.ts'