rvlib-shipkit 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (140) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +1 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +5 -2
  12. package/src/cli/cmd_Commands.ts +9 -0
  13. package/src/cli/cmd_Review.ts +974 -0
  14. package/src/cli/cmd_Sound.ts +209 -0
  15. package/src/cli/cmd_Time.ts +112 -0
  16. package/src/cli/cmd_Trailer.ts +264 -6
  17. package/src/cli/kit/verb.ts +22 -4
  18. package/src/cli.ts +8 -4
  19. package/src/collections/collection-types.ts +6 -1
  20. package/src/collections/providers.ts +3 -0
  21. package/src/collections/review.ts +70 -0
  22. package/src/core/app-icon.ts +10 -6
  23. package/src/core/layout.ts +2 -0
  24. package/src/reflect/prompt-corpus.ts +15 -119
  25. package/src/reflect/transcript.ts +254 -0
  26. package/src/repo-config.ts +3 -0
  27. package/src/review/anim.ts +228 -0
  28. package/src/review/auto.ts +141 -0
  29. package/src/review/format.ts +158 -0
  30. package/src/review/http.ts +23 -0
  31. package/src/review/marks.ts +43 -0
  32. package/src/review/media.ts +45 -0
  33. package/src/review/model.ts +896 -0
  34. package/src/review/project.ts +126 -0
  35. package/src/review/role.ts +48 -0
  36. package/src/review/routes.ts +239 -0
  37. package/src/review/store.ts +1011 -0
  38. package/src/review/threads.ts +237 -0
  39. package/src/review/trailer-desk.ts +385 -0
  40. package/src/review/trailer-model.ts +183 -0
  41. package/src/review/trailer-routes.ts +136 -0
  42. package/src/review/unit.ts +183 -0
  43. package/src/review/watchers.ts +54 -0
  44. package/src/serve/embed.ts +35 -0
  45. package/src/serve/hub-app/activity-view.tsx +1 -1
  46. package/src/serve/hub-app/activity.css +0 -4
  47. package/src/serve/hub-app/admin-svc.ts +1 -1
  48. package/src/serve/hub-app/admin-view.tsx +1 -1
  49. package/src/serve/hub-app/dock-view.tsx +5 -4
  50. package/src/serve/hub-app/hash-route.ts +36 -1
  51. package/src/serve/hub-app/header-bar.tsx +3 -1
  52. package/src/serve/hub-app/home-st.ts +17 -3
  53. package/src/serve/hub-app/home-strip.tsx +3 -2
  54. package/src/serve/hub-app/home.html +1 -0
  55. package/src/serve/hub-app/hub-app.tsx +80 -11
  56. package/src/serve/hub-app/hub.css +83 -14
  57. package/src/serve/hub-app/icons.tsx +15 -0
  58. package/src/serve/hub-app/index.html +1 -0
  59. package/src/serve/hub-app/insights-view.tsx +4 -3
  60. package/src/serve/hub-app/keys.ts +14 -10
  61. package/src/serve/hub-app/layout-st.ts +63 -2
  62. package/src/serve/hub-app/md.ts +32 -0
  63. package/src/serve/hub-app/planned-view.tsx +3 -2
  64. package/src/serve/hub-app/planned.css +0 -1
  65. package/src/serve/hub-app/project-list.css +26 -6
  66. package/src/serve/hub-app/project-list.tsx +46 -10
  67. package/src/serve/hub-app/projects-svc.ts +7 -2
  68. package/src/serve/hub-app/repo-sections.ts +15 -3
  69. package/src/serve/hub-app/repo-st.ts +61 -4
  70. package/src/serve/hub-app/review/clip-st.ts +258 -0
  71. package/src/serve/hub-app/review/kind-view.tsx +240 -0
  72. package/src/serve/hub-app/review/kit.tsx +113 -0
  73. package/src/serve/hub-app/review/review-answer.tsx +269 -0
  74. package/src/serve/hub-app/review/review-board.tsx +46 -0
  75. package/src/serve/hub-app/review/review-list.tsx +156 -0
  76. package/src/serve/hub-app/review/review-page.tsx +96 -0
  77. package/src/serve/hub-app/review/review-st.ts +902 -0
  78. package/src/serve/hub-app/review/review-views.tsx +648 -0
  79. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  80. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  81. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  82. package/src/serve/hub-app/review.css +389 -0
  83. package/src/serve/hub-app/theme-st.ts +56 -0
  84. package/src/serve/hub-app/tree-build.ts +4 -0
  85. package/src/serve/hub-app/tree-row.tsx +1 -1
  86. package/src/serve/hub-app/tree-svc.ts +4 -0
  87. package/src/serve/hub-app/wiki-st.ts +190 -0
  88. package/src/serve/hub-app/wikis-view.tsx +134 -5
  89. package/src/serve/hub-runtime.ts +9 -7
  90. package/src/serve/project-gutter-route.ts +25 -1
  91. package/src/serve/project-gutter.ts +1 -1
  92. package/src/serve/same-origin.ts +5 -1
  93. package/src/serve/serve-hub.ts +15 -0
  94. package/src/shipkit-files.ts +9 -5
  95. package/src/shot/client.ts +17 -5
  96. package/src/shot/shooter.ts +19 -3
  97. package/src/shot/shot-ask.ts +9 -1
  98. package/src/shot/stepped-clip.ts +53 -0
  99. package/src/trailer/adapter.ts +90 -6
  100. package/src/trailer/build.ts +459 -59
  101. package/src/trailer/ffmpeg.ts +22 -0
  102. package/src/trailer/fonts.ts +55 -0
  103. package/src/trailer/footage.ts +112 -43
  104. package/src/trailer/grammar.ts +864 -127
  105. package/src/trailer/graph.ts +308 -49
  106. package/src/trailer/holes.ts +72 -0
  107. package/src/trailer/load-unit.ts +20 -3
  108. package/src/trailer/look.ts +20 -3
  109. package/src/trailer/motion.ts +207 -0
  110. package/src/trailer/passage.ts +129 -0
  111. package/src/trailer/plan.ts +82 -23
  112. package/src/trailer/prune.ts +204 -0
  113. package/src/trailer/score/compose.ts +256 -0
  114. package/src/trailer/score/dsp.ts +272 -0
  115. package/src/trailer/score/gm-render.ts +43 -0
  116. package/src/trailer/score/gm.ts +90 -0
  117. package/src/trailer/score/midi.ts +143 -0
  118. package/src/trailer/score/palette.ts +188 -0
  119. package/src/trailer/score/pattern.ts +98 -0
  120. package/src/trailer/score/score.ts +228 -0
  121. package/src/trailer/score/sound.ts +123 -0
  122. package/src/trailer/score/synth.ts +383 -0
  123. package/src/trailer/sound/audio.ts +148 -0
  124. package/src/trailer/sound/brand.ts +42 -0
  125. package/src/trailer/sound/comfy.ts +38 -0
  126. package/src/trailer/sound/eleven.ts +72 -0
  127. package/src/trailer/sound/fluid.ts +71 -0
  128. package/src/trailer/sound/gen.ts +139 -0
  129. package/src/trailer/sound/index.ts +60 -0
  130. package/src/trailer/sound/presets.ts +69 -0
  131. package/src/trailer/sound/run.ts +189 -0
  132. package/src/trailer/states.ts +76 -0
  133. package/src/trailer/stitch.ts +92 -9
  134. package/src/trailer/titles.ts +471 -150
  135. package/src/trailer/url.ts +18 -3
  136. package/src/trailer/vocab.ts +34 -4
  137. package/src/trailer/words.ts +275 -0
  138. package/src/wiki/graph-mount.ts +3 -0
  139. package/src/wiki/graph-page.ts +45 -9
  140. package/src/wiki/graph-pages.ts +128 -0
@@ -8,15 +8,19 @@
8
8
  // on the home page
9
9
  // trailer({...}) what `sk trailer` cannot read from a file: the
10
10
  // sounds the repo resolves by id (trailer/adapter.ts)
11
+ // review({...}) the repo's parts of the review store: its subject
12
+ // kinds, topics, sources, auto rules, the dispatcher
13
+ // (review/unit.ts)
11
14
  //
12
15
  // the one constraint a reader breaks first: every kind is IMPORTED by the
13
16
  // process that reads it (discovery, pm's config child, the daemon), so a file
14
17
  // keeps its top level cheap and puts heavy work inside a function.
15
18
  import { check, type CheckDef } from './check.ts'
19
+ import { review, type AutoRule, type KindDef, type ReviewDef, type ReviewSource, type ReviewUnit } from './review/unit.ts'
16
20
  import { trailer, type TrailerDef, type TrailerUnit } from './trailer/adapter.ts'
17
21
 
18
- export { check, trailer }
19
- export type { TrailerDef, TrailerUnit }
22
+ export { check, review, trailer }
23
+ export type { AutoRule, KindDef, ReviewDef, ReviewSource, ReviewUnit, TrailerDef, TrailerUnit }
20
24
 
21
25
  /** one button beside a global process: a command, or a function of the file */
22
26
  export type ProcessAction = {
@@ -73,7 +77,7 @@ export type WidgetDef = {
73
77
  export type GlobalProcessUnit = GlobalProcessDef & { kind: 'globalProcess' }
74
78
  export type WidgetUnit = WidgetDef & { kind: 'widget' }
75
79
 
76
- export const UNIT_KINDS = ['globalProcess', 'widget', 'trailer'] as const
80
+ export const UNIT_KINDS = ['globalProcess', 'widget', 'trailer', 'review'] as const
77
81
 
78
82
  const KEBAB = /^[a-z0-9][a-z0-9-]*$/
79
83
  const words = (v: unknown): boolean => Array.isArray(v) && v.length > 0 && v.every((w) => typeof w === 'string')
@@ -127,9 +131,9 @@ export function widget(d: WidgetDef): WidgetUnit {
127
131
  }
128
132
 
129
133
  /** the kind a default export says it is: a unit, else a check */
130
- export function unitKind(d: unknown): 'globalProcess' | 'widget' | 'trailer' | 'check' {
134
+ export function unitKind(d: unknown): (typeof UNIT_KINDS)[number] | 'check' {
131
135
  const k = (d as { kind?: unknown } | null)?.kind
132
- return k === 'globalProcess' || k === 'widget' || k === 'trailer' ? k : 'check'
136
+ return UNIT_KINDS.includes(k as (typeof UNIT_KINDS)[number]) ? (k as (typeof UNIT_KINDS)[number]) : 'check'
133
137
  }
134
138
 
135
139
  export type { CheckDef }
@@ -8,7 +8,7 @@ import type { ShotDone } from './shooter.ts'
8
8
  /** the global process id pm knows the service by (shot.shipkit.ts) */
9
9
  export const SHOT_PROCESS = 'shot'
10
10
 
11
- export type ShotInput = { url: string; out: string; viewport?: Viewport; dpr?: number; reload?: boolean; waitMs?: number }
11
+ export type ShotInput = { url: string; out: string; viewport?: Viewport; dpr?: number; reload?: boolean; waitMs?: number; step?: string; at?: number }
12
12
  export type ClipInput = ShotInput & { seconds: number; fps?: number; dom?: boolean }
13
13
  export type ShotResult = ShotDone & { via: 'warm' | 'started' | 'local' }
14
14
 
@@ -21,13 +21,25 @@ const shotAsk = (i: ShotInput): ShotAsk => ({
21
21
  waitMs: i.waitMs ?? DEFAULT_WAIT_MS,
22
22
  })
23
23
 
24
- /** a png of `url` at `out` (absolute), through the warm browser */
25
- export const shotUrl = (i: ShotInput, log: (l: string) => void = () => {}): Promise<ShotResult> => ask('/shot', shotAsk(i), log)
24
+ /** a service of an older shipkit takes a stepped ask, ignores the step and shoots the page as it loads: never kept */
25
+ function stepped(i: { step?: string }, r: ShotResult): void {
26
+ if (i.step && r.stepped !== true) throw new Error(`the shot service on ${shotUrlBase()} did not step the page (it says: ${r.note}); it runs an older shipkit: sk gp restart ${SHOT_PROCESS}`)
27
+ }
28
+
29
+ /** a png of `url` at `out` (absolute), through the warm browser; with `step`, the page drawn at `at` first */
30
+ export async function shotUrl(i: ShotInput, log: (l: string) => void = () => {}): Promise<ShotResult> {
31
+ const r = await ask('/shot', { ...shotAsk(i), ...(i.step ? { step: i.step, at: i.at ?? 0 } : {}) }, log)
32
+ stepped(i, r)
33
+ return r
34
+ }
26
35
 
27
36
  /** an mp4 of `seconds` of `url` at `out` (absolute): a page that is a canvas through its WebCodecs, any other page (or
28
37
  * `dom`) as chromium paints it */
29
- export const clipUrl = (i: ClipInput, log: (l: string) => void = () => {}): Promise<ShotResult> =>
30
- ask('/clip', { ...shotAsk(i), seconds: i.seconds, fps: i.fps ?? DEFAULT_FPS, dom: i.dom ?? false } satisfies ClipAsk, log)
38
+ export async function clipUrl(i: ClipInput, log: (l: string) => void = () => {}): Promise<ShotResult> {
39
+ const r = await ask('/clip', { ...shotAsk(i), seconds: i.seconds, fps: i.fps ?? DEFAULT_FPS, dom: i.dom ?? false, ...(i.step ? { step: i.step } : {}) } satisfies ClipAsk, log)
40
+ stepped(i, r)
41
+ return r
42
+ }
31
43
 
32
44
  /** null when nothing listens (start it), else the service's answer; a refusal throws its reason */
33
45
  async function post(route: string, a: ShotAsk): Promise<ShotDone | null> {
@@ -59,7 +59,7 @@ export const SHOT_NICE = 10
59
59
  /** a page that paints nothing once the screencast starts is filmed from one still taken after this */
60
60
  const FIRST_FRAME_MS = 1000
61
61
 
62
- export type ShotDone = { out: string; ms: number; phases: Record<string, number>; errors: string[]; note: string }
62
+ export type ShotDone = { out: string; ms: number; phases: Record<string, number>; errors: string[]; note: string; stepped?: true }
63
63
 
64
64
  type Slot = { key: string; url: string; ctx: Context; page: Page; cdp: Cdp; errors: string[]; frozen: boolean }
65
65
 
@@ -169,6 +169,8 @@ export class Shooter {
169
169
  const phases: Record<string, number> = {}
170
170
  const t0 = performance.now()
171
171
  let move = pageMove(this.#slot == null || this.#slot.page.isClosed() ? null : { url: this.#slot.url, key: this.#slot.key }, a)
172
+ // a stepped still draws its moment on a fresh load, never on a page some other shot left at another time
173
+ if (a.step && move === 'none') move = 'reload'
172
174
  if (move === 'boot') {
173
175
  const old = this.#slot
174
176
  this.#slot = null
@@ -200,13 +202,22 @@ export class Shooter {
200
202
  let t = performance.now()
201
203
  const how = await waitReady(() => slot.page.evaluate(readMark), move === 'none' ? 0 : a.waitMs, { now: () => performance.now(), sleep: (n) => slot.page.waitForTimeout(n) })
202
204
  phases['ready'] = ms(t)
205
+ if (a.step) {
206
+ t = performance.now()
207
+ const { stepTo } = await import('./stepped-clip.ts')
208
+ if (!(await slot.page.evaluate(stepTo, [a.step, a.at ?? 0] as [string, number]))) throw new Error(`the page has no window.${a.step}(t): a stepped page draws the time it is given there`)
209
+ await waitReady(() => slot.page.evaluate(readMark), 0, { now: () => performance.now(), sleep: (n) => slot.page.waitForTimeout(n) })
210
+ // the warm page now shows that moment: the next plain shot of this url loads it again
211
+ slot.url = ''
212
+ phases['step'] = ms(t)
213
+ }
203
214
  t = performance.now()
204
215
  const r = await slot.cdp.send('Page.captureScreenshot', { format: 'png', optimizeForSpeed: true })
205
216
  phases['capture'] = ms(t)
206
217
  mkdirSync(dirname(a.out), { recursive: true })
207
218
  await Bun.write(a.out, Buffer.from(String(r['data']), 'base64'))
208
- const note = `${move === 'none' ? 'warm page' : thawed ? 'frozen page reloaded' : move}, ${how === 'ready' ? 'ready mark' : move === 'none' ? 'no wait' : `${a.waitMs} ms after load`}`
209
- return { out: a.out, ms: ms(t0), phases, errors: [...slot.errors], note }
219
+ const note = `${move === 'none' ? 'warm page' : thawed ? 'frozen page reloaded' : move}, ${how === 'ready' ? 'ready mark' : move === 'none' ? 'no wait' : `${a.waitMs} ms after load`}${a.step ? `, drawn at ${a.at ?? 0}s through window.${a.step}(t)` : ''}`
220
+ return { out: a.out, ms: ms(t0), phases, errors: [...slot.errors], note, ...(a.step ? { stepped: true as const } : {}) }
210
221
  }
211
222
 
212
223
  async #clip(a: ClipAsk): Promise<ShotDone> {
@@ -223,6 +234,11 @@ export class Shooter {
223
234
  await waitReady(() => page.evaluate(readMark), a.waitMs, { now: () => performance.now(), sleep: (n) => page.waitForTimeout(n) })
224
235
  phases['load'] = ms(t)
225
236
  t = performance.now()
237
+ if (a.step) {
238
+ const note = await (await import('./stepped-clip.ts')).stepPage(await ctx.newCDPSession(page), page, a as ClipAsk & { step: string })
239
+ phases['film'] = ms(t)
240
+ return { out: a.out, ms: ms(t0), phases, errors, note, stepped: true }
241
+ }
226
242
  const path: ClipPath = clipPathOf({ dom: a.dom === true, canvas: await page.evaluate(largestCanvas), viewport: a.viewport })
227
243
  const why = path === 'canvas' ? await page.evaluate(installClip, { seconds: a.seconds, fps: a.fps, quantizer: CLIP_QUANTIZER, keyEvery: KEY_EVERY } satisfies PageClipAsk) : null
228
244
  if (path === 'dom' || why != null) {
@@ -17,6 +17,9 @@ export type ShotAsk = {
17
17
  reload: boolean
18
18
  /** a page with no ready mark is shot this long after its load (a warm page at once) */
19
19
  waitMs: number
20
+ /** a still of a stepped page: `window[step](at)` on a fresh load, then its ready mark, then the shot */
21
+ step?: string
22
+ at?: number
20
23
  }
21
24
 
22
25
  export type ClipAsk = ShotAsk & {
@@ -24,6 +27,9 @@ export type ClipAsk = ShotAsk & {
24
27
  fps: number
25
28
  /** film the page itself even when a canvas fills it (dom-clip.ts); a page with no big canvas is filmed so anyway */
26
29
  dom?: boolean
30
+ /** step the page frame by frame (stepped-clip.ts): before each frame `window[step](t)`, t its time in seconds, then
31
+ * its ready mark; a slow page films as smooth as a fast one */
32
+ step?: string
27
33
  }
28
34
 
29
35
  export const DEFAULT_SHOT_PORT = 4796
@@ -113,7 +119,9 @@ export const isClip = (a: ShotAsk | ClipAsk): a is ClipAsk => 'seconds' in a
113
119
 
114
120
  /** a request held past this (a clip: past its own seconds) fails loud and frees the turn */
115
121
  export const REQUEST_MAX_MS = 120_000
116
- export const capMsOf = (a: ShotAsk | ClipAsk): number => REQUEST_MAX_MS + (isClip(a) ? a.seconds * 1000 : 0)
122
+ export const capMsOf = (a: ShotAsk | ClipAsk): number => REQUEST_MAX_MS + (isClip(a) ? (a.step ? a.seconds * a.fps * STEP_FRAME_MS : a.seconds * 1000) : 0)
123
+ /** a stepped frame's budget in the request's cap: a page that draws a frame slower than this is no page to step */
124
+ export const STEP_FRAME_MS = 1000
117
125
 
118
126
  // ── the ready mark ──────────────────────────────────────────────────────────
119
127
  // A page that knows when it is drawn says so: `<meta name="shot-ready"
@@ -0,0 +1,53 @@
1
+ // a clip filmed frame by frame (wiki build/trailer.md, film page): before each frame the page is asked to draw the
2
+ // frame's time, `window[step](t)`, then the shot service waits for its ready mark and two painted frames, takes one
3
+ // screenshot, and ffmpeg encodes the frames in order. The page's own speed never shows: a frame that takes a second to
4
+ // draw sits beside one that took a millisecond. The page owns its clock while it is stepped
5
+ import { mkdirSync } from 'fs'
6
+ import { dirname } from 'path'
7
+ import { domEncodeArgs } from './dom-clip.ts'
8
+ import { type ClipAsk, readMark, waitReady } from './shot-ask.ts'
9
+
10
+ type Cdp = { send(method: string, params?: Record<string, unknown>): Promise<Record<string, unknown>> }
11
+ type Page = { evaluate<R, A>(fn: (a: A) => R | Promise<R>, arg: A): Promise<R>; waitForTimeout(ms: number): Promise<void> }
12
+
13
+ /** the frames a stepped clip holds, and each one's time */
14
+ export const stepTimes = (seconds: number, fps: number): number[] => Array.from({ length: Math.max(1, Math.round(seconds * fps)) }, (_, i) => +(i / fps).toFixed(6))
15
+
16
+ /** IN THE PAGE: the hook asked to draw time t; false when the page has no such hook */
17
+ export const stepTo = async ([hook, t]: [string, number]): Promise<boolean> => {
18
+ const f = (window as unknown as Record<string, unknown>)[hook]
19
+ if (typeof f !== 'function') return false
20
+ await (f as (t: number) => unknown)(t)
21
+ await new Promise<void>((ok) => requestAnimationFrame(() => requestAnimationFrame(() => ok())))
22
+ return true
23
+ }
24
+
25
+ export async function stepPage(cdp: Cdp, page: Page, a: ClipAsk & { step: string }): Promise<string> {
26
+ const ffmpeg = Bun.which('ffmpeg') ?? (process.platform === 'darwin' ? '/opt/homebrew/bin/ffmpeg' : null)
27
+ if (ffmpeg == null) throw new Error('ffmpeg is not installed: brew install ffmpeg')
28
+ mkdirSync(dirname(a.out), { recursive: true })
29
+ const times = stepTimes(a.seconds, a.fps)
30
+ const p = Bun.spawn([ffmpeg, ...domEncodeArgs(a.fps, times.length, a.out)], { stdin: 'pipe', stdout: 'ignore', stderr: 'pipe' })
31
+ const stderr = new Response(p.stderr).text()
32
+ const t0 = performance.now()
33
+ let px = ''
34
+ try {
35
+ for (const t of times) {
36
+ if (!(await page.evaluate(stepTo, [a.step, t] as [string, number]))) throw new Error(`the page has no window.${a.step}(t): a stepped page draws the time it is given there`)
37
+ await waitReady(() => page.evaluate(readMark, undefined), 0, { now: () => performance.now(), sleep: (n) => page.waitForTimeout(n) })
38
+ const shot = await cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: 92 })
39
+ const jpeg = Buffer.from(String(shot['data']), 'base64')
40
+ // the frame's pixels, from the jpeg's start of frame
41
+ const sof = jpeg.indexOf(Buffer.from([0xff, 0xc0]))
42
+ if (px === '' && sof >= 0) px = `${jpeg.readUInt16BE(sof + 7)}x${jpeg.readUInt16BE(sof + 5)}`
43
+ p.stdin.write(jpeg)
44
+ await p.stdin.flush()
45
+ }
46
+ } finally {
47
+ await Promise.resolve(p.stdin.end()).catch(() => undefined)
48
+ }
49
+ const err = await stderr
50
+ if ((await p.exited) !== 0) throw new Error(`ffmpeg encode → ${a.out}: ${err.trim().split('\n').slice(-6).join('\n') || `exit ${p.exitCode}`}`)
51
+ const s = (performance.now() - t0) / 1000
52
+ return `${times.length} frames ${px} at ${a.fps} fps, each stepped through window.${a.step}(t) and its ready mark (${(s / times.length * 1000).toFixed(0)} ms a frame), H.264 by ffmpeg`
53
+ }
@@ -1,23 +1,107 @@
1
- // what a repo adds to the trailer core, all optional. Filming is never here: a film line calls the project CLI's
2
- // film rows (vocab.ts). A `trailer({...})` unit in any `<name>.shipkit.ts` resolves the sounds that are not files.
3
- // Discovered like every unit file (load-unit.ts), and read only when a cut names such a sound
1
+ // what a repo adds to the trailer core, all optional (wiki build/trailer.md, The trailer unit). Filming is never here:
2
+ // a film line calls the project CLI's film rows (vocab.ts). A `trailer({...})` unit in any `<name>.shipkit.ts`
3
+ // resolves the sounds that are not files, names the paths a take's world is, checks the content the language cannot
4
+ // know, refines one film line's take, and turns on the guards that read the repo's own words. Discovered like every
5
+ // unit file (load-unit.ts)
6
+ import type { SoundRun, TakeLength } from './grammar.ts'
7
+ import { type Words, wordsProblem } from './words.ts'
4
8
 
9
+ /** the repo's sounds by id; null: not the repo's, the house makes it (src/trailer/sound/, design call 9) */
5
10
  export interface TrailerSounds {
6
11
  /** a music id to an audio file of at least `seconds`, cached by the repo */
7
- music?(id: string, o: { take?: string; seconds: number }): Promise<string>
12
+ music?(id: string, o: { take?: string; seconds: number }): Promise<string | null>
8
13
  /** an sfx id to an audio file */
9
- sfx?(id: string): Promise<string>
14
+ sfx?(id: string): Promise<string | null>
10
15
  /** a voice line, found by its text, to an audio file */
11
- voice?(text: string): Promise<string>
16
+ voice?(text: string): Promise<string | null>
17
+ }
18
+
19
+ /** a sound line the house makes (`resolveSound` and `soundStates` of src/trailer/sound/index.ts, the shape it reads):
20
+ * the line read, the prompt, or a `.mid` file's path (`text`), how it is made (`seed`, `by`, `for`, `hit`), what a music
21
+ * cue covers (`seconds`), or the line's own `run` command */
22
+ export interface SoundRequest {
23
+ kind: 'music' | 'sfx' | 'voice'
24
+ text: string
25
+ seed?: number
26
+ by?: string
27
+ for?: number
28
+ hit?: boolean
29
+ seconds?: number
30
+ line: number
31
+ run?: SoundRun
32
+ /** the cut's folder: a `reads` file is found beside it first, then from the repo root */
33
+ dir: string
34
+ /** the text is a quoted prompt, never an id */
35
+ prompt?: boolean
36
+ /** music: the take of a song id */
37
+ take?: string
38
+ }
39
+
40
+ /** a film line of the project CLI as the unit sees it: its words as written, the text of each `@<file>` word */
41
+ export interface TrailerFilmLine {
42
+ line: number
43
+ /** the shot or footage block it films */
44
+ shot: string
45
+ argv: string[]
46
+ output: 'still' | 'clip'
47
+ files: Record<string, string>
48
+ }
49
+
50
+ /** what `check` reads: the cut's film lines and the sounds it names by id */
51
+ export interface TrailerCheckInput {
52
+ file: string
53
+ dir: string
54
+ size: [number, number]
55
+ fps: number
56
+ films: TrailerFilmLine[]
57
+ /** each music cue that names an id; `opening`: the header's, the one the cut starts on */
58
+ music: { line: number; id: string; take?: string; from: number; opening: boolean }[]
59
+ sfx: { line: number; id: string }[]
60
+ voices: { line: number; text: string }[]
61
+ }
62
+
63
+ /** an error at its line; `warn`: said, never blocking */
64
+ export interface TrailerFinding {
65
+ line: number
66
+ message: string
67
+ warn?: true
68
+ }
69
+
70
+ /** one film line's take over its row's defaults: its version, how long it films, a fixed world, what its key reads */
71
+ export interface TakeAnswer {
72
+ version?: number | string
73
+ length?: TakeLength
74
+ /** the take's world for good (`rev:<commit>`): never stale */
75
+ world?: string
76
+ /** what the key reads instead of the words */
77
+ recipe?: unknown
12
78
  }
13
79
 
14
80
  export interface TrailerDef {
15
81
  sound?: TrailerSounds
82
+ /** the paths a take's world is (`['src', 'public']`); default the whole tree, trailer/ and .shipkit/ left out */
83
+ stamp?: string[]
84
+ /** in process, before filming, in a few ms: the content the language cannot know */
85
+ check?(c: TrailerCheckInput): TrailerFinding[] | Promise<TrailerFinding[]>
86
+ /** one film line's take, when its words change what its row says */
87
+ take?(f: TrailerFilmLine & { version: number | string | null; length: TakeLength }): TakeAnswer | undefined
88
+ /** the `states` a film prints: parts a cut may land on (`ignore`), parts a shot never opens or ends on (`underWay`) */
89
+ states?: { ignore?: string[]; underWay?: string[] }
90
+ /** a clip take that drops its picture for a few frames is filmed again once, then refused (holes.ts) */
91
+ holes?: boolean
92
+ /** the words a `film page` of this repo takes (words.ts): each checked at its line, its query made plain */
93
+ page?: { words: Words }
16
94
  }
17
95
 
18
96
  export type TrailerUnit = TrailerDef & { kind: 'trailer' }
19
97
 
20
98
  export function trailer(d: TrailerDef): TrailerUnit {
21
99
  for (const [k, f] of Object.entries(d.sound ?? {})) if (typeof f !== 'function') throw new Error(`trailer: sound.${k} is a function returning an audio file`)
100
+ for (const k of ['check', 'take'] as const) if (d[k] !== undefined && typeof d[k] !== 'function') throw new Error(`trailer: ${k} is a function`)
101
+ if (d.page !== undefined) {
102
+ const why = wordsProblem(d.page.words) ?? Object.entries(d.page.words).flatMap(([k, w]) => (w.arg !== undefined ? [`page word ${k} declares arg: a page word has a key (it is the url's query)`] : []))[0] ?? null
103
+ if (why !== null) throw new Error(`trailer: ${why}`)
104
+ }
105
+ if (d.stamp !== undefined && (!Array.isArray(d.stamp) || d.stamp.some((p) => typeof p !== 'string' || p === '' || p.startsWith('/')))) throw new Error('trailer: stamp is a list of repo relative paths')
22
106
  return { ...d, kind: 'trailer' }
23
107
  }