@hypit/hypit 0.1.13 → 0.2.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 (130) hide show
  1. package/README.md +37 -3
  2. package/bin/hypit.mjs +0 -2
  3. package/dist/public/browser-capture.d.ts +9 -3
  4. package/dist/public/caption.d.ts +2 -0
  5. package/dist/public/narrative.d.ts +2 -0
  6. package/dist/public/runtime-kit.d.ts +2 -2
  7. package/dist/public/speech.d.ts +2 -0
  8. package/examples/semantic-composition/packages/responsive-explainer/README.md +1 -1
  9. package/package.json +2 -1
  10. package/packages/browser-capture/README.md +9 -2
  11. package/packages/browser-capture/package.json +5 -0
  12. package/packages/browser-capture/src/browser.ts +22 -8
  13. package/packages/browser-capture/src/index.ts +8 -2
  14. package/packages/caption/README.md +1 -1
  15. package/packages/caption/src/display.ts +1 -0
  16. package/packages/caption-fine/README.md +19 -5
  17. package/packages/caption-fine/package.json +1 -0
  18. package/packages/caption-fine/src/joined-box.ts +114 -0
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +99 -92
  21. package/packages/caption-fine/src/spacing.ts +8 -61
  22. package/packages/caption-fine-studio/src/index.ts +2 -4
  23. package/packages/cli/README.md +3 -2
  24. package/packages/cli/package.json +1 -0
  25. package/packages/cli/src/arguments.ts +2 -2
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +21 -9
  28. package/packages/cli/src/machine-view.ts +1 -1
  29. package/packages/cli/src/main.ts +2 -1
  30. package/packages/cli/src/oauth.ts +50 -7
  31. package/packages/cli/src/output.ts +1 -0
  32. package/packages/credential-store-file/README.md +60 -0
  33. package/packages/credential-store-file/package.json +21 -0
  34. package/packages/credential-store-file/src/activation.ts +29 -0
  35. package/packages/credential-store-file/src/index.ts +1 -0
  36. package/packages/credential-store-file/src/store.ts +89 -0
  37. package/packages/credential-store-os/src/store.ts +5 -1
  38. package/packages/fonts-open/src/surface.ts +8 -2
  39. package/packages/hyperframes/README.md +16 -2
  40. package/packages/hyperframes/src/browser-program.ts +6 -1
  41. package/packages/hyperframes/src/document.ts +31 -21
  42. package/packages/hyperframes/src/project.ts +11 -20
  43. package/packages/media-execution/README.md +7 -0
  44. package/packages/media-execution/src/execute.ts +5 -5
  45. package/packages/media-execution/src/index.ts +1 -1
  46. package/packages/media-execution/src/process-env.ts +20 -0
  47. package/packages/media-execution/src/surface.ts +83 -76
  48. package/packages/narrative/README.md +3 -1
  49. package/packages/narrative/src/schema.ts +1 -0
  50. package/packages/narrative/src/types.ts +2 -0
  51. package/packages/package-loader-node/README.md +10 -0
  52. package/packages/package-loader-node/src/index.ts +1 -0
  53. package/packages/package-loader-node/src/loader.ts +25 -5
  54. package/packages/package-loader-node/src/location.ts +11 -2
  55. package/packages/provider-hyperframes-local/README.md +81 -8
  56. package/packages/provider-hyperframes-local/package.json +13 -3
  57. package/packages/provider-hyperframes-local/src/activation.ts +20 -6
  58. package/packages/provider-hyperframes-local/src/browser-install.ts +5 -0
  59. package/packages/provider-hyperframes-local/src/browser.ts +118 -0
  60. package/packages/provider-hyperframes-local/src/capture-bootstrap.ts +4 -6
  61. package/packages/provider-hyperframes-local/src/capture-exit.ts +24 -0
  62. package/packages/provider-hyperframes-local/src/capture-process.ts +29 -52
  63. package/packages/provider-hyperframes-local/src/capture-worker.ts +2 -0
  64. package/packages/provider-hyperframes-local/src/capture.ts +12 -5
  65. package/packages/provider-hyperframes-local/src/opaque-capture.ts +46 -18
  66. package/packages/provider-hyperframes-local/src/options.ts +2 -2
  67. package/packages/provider-hyperframes-local/src/process-tree.ts +82 -0
  68. package/packages/provider-hyperframes-local/src/process.ts +22 -0
  69. package/packages/provider-hyperframes-local/src/program.ts +25 -33
  70. package/packages/provider-hyperframes-local/src/provider.ts +4 -7
  71. package/packages/provider-hyperframes-local/src/render.ts +9 -4
  72. package/packages/provider-whisperx-local/README.md +26 -10
  73. package/packages/provider-whisperx-local/src/activation.ts +10 -1
  74. package/packages/provider-whisperx-local/src/program.ts +8 -4
  75. package/packages/runtime-host-node/README.md +5 -0
  76. package/packages/runtime-host-node/src/index.ts +8 -2
  77. package/packages/runtime-host-node/src/packages.ts +13 -7
  78. package/packages/runtime-kit/README.md +6 -2
  79. package/packages/runtime-kit/src/index.ts +2 -2
  80. package/packages/runtime-local/README.md +16 -1
  81. package/packages/runtime-local/src/config.ts +1 -1
  82. package/packages/runtime-local/src/credentials.ts +10 -5
  83. package/packages/runtime-local/src/host.ts +2 -0
  84. package/packages/runtime-local/src/index.ts +1 -0
  85. package/packages/runtime-local/src/programs.ts +53 -17
  86. package/packages/script/README.md +81 -34
  87. package/packages/script/bin/migrate-0.2.mjs +90 -0
  88. package/packages/script/src/edit.ts +28 -60
  89. package/packages/script/src/lexical.ts +59 -130
  90. package/packages/script/src/manifest.ts +11 -11
  91. package/packages/script/src/narrative.ts +4 -2
  92. package/packages/script/src/parser.ts +140 -193
  93. package/packages/script/src/types.ts +2 -3
  94. package/packages/studio/src/preview/runtime-shim.ts +17 -3
  95. package/packages/studio/src/ui/code.ts +1 -1
  96. package/packages/studio/src/ui/main.ts +2 -2
  97. package/packages/studio/src/ui/markers.ts +3 -3
  98. package/packages/studio/src/ui/selection.ts +1 -1
  99. package/packages/studio/src/ui/syntax.ts +42 -21
  100. package/packages/temporal-markup/README.md +1 -1
  101. package/packages/video-cli/README.md +14 -6
  102. package/packages/video-cli/package.json +1 -0
  103. package/packages/video-cli/src/capture.ts +17 -2
  104. package/packages/video-cli/src/creation.ts +4 -6
  105. package/packages/video-cli/src/distribution.ts +3 -10
  106. package/packages/video-cli/src/media.ts +10 -3
  107. package/packages/video-cli/src/version.ts +1 -1
  108. package/packages/whisperx/README.md +56 -3
  109. package/packages/whisperx/src/component.ts +2 -5
  110. package/packages/whisperx/src/evidence.ts +4 -3
  111. package/packages/whisperx/src/index.ts +1 -0
  112. package/packages/whisperx/src/manifest.ts +2 -3
  113. package/packages/whisperx/src/surface.ts +3 -6
  114. package/packages/whisperx/src/types.ts +9 -1
  115. package/packages/yt-dlp/README.md +10 -3
  116. package/packages/yt-dlp/package.json +5 -0
  117. package/packages/yt-dlp/src/download.ts +11 -29
  118. package/packages/yt-dlp/src/environment.ts +36 -0
  119. package/packages/yt-dlp/src/index.ts +1 -0
  120. package/services/whisperx/README.md +45 -12
  121. package/services/whisperx/src/hypit_whisperx_service/application.py +3 -0
  122. package/services/whisperx/src/hypit_whisperx_service/check.py +7 -0
  123. package/services/whisperx/src/hypit_whisperx_service/config.py +7 -0
  124. package/services/whisperx/src/hypit_whisperx_service/engine.py +21 -5
  125. package/services/whisperx/src/hypit_whisperx_service/models.py +131 -0
  126. package/services/whisperx/src/hypit_whisperx_service/prepare.py +21 -3
  127. package/services/whisperx/src/hypit_whisperx_service/resources.py +19 -3
  128. package/services/yt-dlp/README.md +14 -15
  129. package/services/yt-dlp/pyproject.toml +1 -1
  130. package/services/yt-dlp/uv.lock +513 -2
@@ -293,6 +293,8 @@ async function bringUpOwned(
293
293
  onProgress?.({ id: program.id, phase: "checking" });
294
294
  const initial = await program.probe();
295
295
  if (initial.state === "ready") {
296
+ const prepared = await prepareOwned(root, program, endpoint, onProgress);
297
+ if (prepared.state.state !== "ready") return prepared;
296
298
  onProgress?.({ id: program.id, phase: "ready" });
297
299
  return { ...base, action: "already-running", state: initial };
298
300
  }
@@ -306,6 +308,10 @@ async function bringUpOwned(
306
308
  const installLogPath = join(directory(root, program), "install.log");
307
309
  const existingPid = await readPid(root, program);
308
310
  if (existingPid !== undefined && processAlive(existingPid)) {
311
+ if (program.installation !== undefined) {
312
+ const prepared = await prepareOwned(root, program, endpoint, onProgress);
313
+ if (prepared.state.state !== "ready") return prepared;
314
+ }
309
315
  release();
310
316
  onProgress?.({ id: program.id, phase: "waiting", logPath,
311
317
  detail: `observing existing process ${existingPid}` });
@@ -320,23 +326,9 @@ async function bringUpOwned(
320
326
  }
321
327
  if (existingPid !== undefined) await rm(join(directory(root, program), "process.pid"), { force: true });
322
328
  if (program.installation !== undefined) {
323
- const installation = await program.installation.probe();
324
- if (installation.state !== "ready" || program.installation.prepareBeforeStart === true) {
325
- await mkdir(directory(root, program), { recursive: true });
326
- await rotateLog(installLogPath);
327
- for (const command of program.installation.commands) {
328
- onProgress?.({ id: program.id, phase: "installing", logPath: installLogPath, detail: commandLabel(command) });
329
- const result = await run(root, command, installLogPath);
330
- if (!result.ok) {
331
- return { ...base, action: "unchanged", state: initial, logPath: installLogPath, detail: `${command.command} failed: ${result.detail}` };
332
- }
333
- }
334
- const after = await program.installation.probe();
335
- if (after.state !== "ready") {
336
- return { ...base, action: "unchanged", state: initial, logPath: installLogPath, detail: `installation is ${after.state}: ${after.detail}` };
337
- }
338
- installed = true;
339
- }
329
+ const prepared = await prepareOwned(root, program, endpoint, onProgress, program.installation.prepareBeforeStart === true);
330
+ if (prepared.state.state !== "ready") return prepared;
331
+ installed = prepared.action === "installed";
340
332
  }
341
333
  if (program.start === undefined) {
342
334
  // Nothing to keep running: installation was the whole job.
@@ -435,6 +427,50 @@ async function bringUp(
435
427
  } finally { release(); }
436
428
  }
437
429
 
430
+ /** Resource preparation is independent of process readiness. Caller owns the lifecycle lock. */
431
+ async function prepareOwned(
432
+ root: string, program: ManagedProgram, endpoint: string,
433
+ onProgress?: (event: ManagedProgramProgress) => void, reconcile = false,
434
+ ): Promise<ManagedProgramReport> {
435
+ const base = { id: program.id, endpoint };
436
+ const installation = program.installation;
437
+ if (installation === undefined) return { ...base, action: "unchanged", state: await program.probe() };
438
+ const logPath = join(directory(root, program), "install.log");
439
+ const before = await installation.probe();
440
+ if (before.state === "ready" && !reconcile) return { ...base, action: "unchanged", state: before };
441
+ await mkdir(root, { recursive: true });
442
+ await rotateLog(logPath);
443
+ for (const command of installation.commands) {
444
+ onProgress?.({ id: program.id, phase: "installing", logPath, detail: commandLabel(command) });
445
+ const result = await run(root, command, logPath);
446
+ if (!result.ok) return { ...base, action: "unchanged", state: { state: "down", detail: `${command.command} failed: ${result.detail}` }, installationLogPath: logPath };
447
+ }
448
+ const state = await installation.probe();
449
+ return { ...base, action: state.state === "ready" ? "installed" : "unchanged", state, installationLogPath: logPath };
450
+ }
451
+
452
+ /** Prepare selected resources without starting, stopping or trusting a running process. */
453
+ export async function prepareManagedPrograms(
454
+ path: string, options: ManagedProgramOptions = {},
455
+ ): Promise<{ readonly dataRoot: string; readonly programs: readonly ManagedProgramReport[] }> {
456
+ const { dataRoot, programs } = await declaredManagedPrograms(path, options);
457
+ const reports = await Promise.all(independentPrograms(programs).map(async ({ instance, program }): Promise<ManagedProgramReport> => {
458
+ const home = directory(dataRoot, program);
459
+ await mkdir(home, { recursive: true });
460
+ const { tryProgramLock } = await import("./program-lock.js");
461
+ const release = tryProgramLock(home);
462
+ if (release === undefined) return { id: program.id, endpoint: instance, action: "unchanged", state: { state: "down", detail: "another command owns this Program's preparation; inspect its logs" } };
463
+ try {
464
+ options.onProgress?.({ id: program.id, phase: "checking" });
465
+ const report = await prepareOwned(dataRoot, program, instance, options.onProgress);
466
+ if (report.state.state === "ready") options.onProgress?.({ id: program.id, phase: "ready" });
467
+ return report;
468
+ }
469
+ finally { release(); }
470
+ }));
471
+ return { dataRoot, programs: reports };
472
+ }
473
+
438
474
  /** Install when needed and start every external program the Runtime Profile implies. */
439
475
  export async function bringManagedProgramsUp(
440
476
  path: string,
@@ -10,7 +10,7 @@ know that Script, Segment or Narrative exist.
10
10
 
11
11
  ```svml
12
12
  <script id="story">
13
- @answer
13
+ @{answer}
14
14
 
15
15
  <opening>
16
16
  <ALICE> I will make the first point.
@@ -18,7 +18,7 @@ know that Script, Segment or Narrative exist.
18
18
  </opening>
19
19
 
20
20
  <pause/>
21
- @/answer
21
+ @{/answer}
22
22
  </script>
23
23
  ```
24
24
 
@@ -65,8 +65,8 @@ CaptionDocument contracts.
65
65
  Role Cue or the Segment close ends that turn.
66
66
  - **Dual Text**: one authored speech span with separate display and spoken projections, written
67
67
  `<display text | spoken text>`. In `<display text|>`, omitted speech inherits the displayed prose.
68
- - **Selection marker**: a named semantic range, written `@name ... @/name`.
69
- - **Moment marker**: a named semantic point, written `@name!`.
68
+ - **Selection marker**: a named semantic range, written `@{name} ... @{/name}`.
69
+ - **Moment marker**: a named semantic point, written `@{name!}`.
70
70
  - **CaptionDocument**: the Script-owned caption truth; it contains **Display Words**,
71
71
  **Alignment Units** and **Cue Breaks**. It contains no seconds or frames.
72
72
  - **Token attribute**: a flat postfix display-word annotation such as `really{emphasis}` or
@@ -86,7 +86,7 @@ Caption Styles choose whether to highlight, reveal or keep the text steady.
86
86
 
87
87
  In a shared side, markers and display attributes do not enter either text projection. Its speech
88
88
  Tokens retain offsets into the actually written left-hand text, so Studio can move an anchor inside
89
- `<组@beat!件化|>` without expanding the shorthand. Attributes still annotate the preceding display
89
+ `<组@{beat!}件化|>` without expanding the shorthand. Attributes still annotate the preceding display
90
90
  word, not the whole group. Whitespace-only speech is omitted; a group with no spoken text on either
91
91
  side is invalid. This adds no new public value type or protocol version.
92
92
 
@@ -101,8 +101,8 @@ Units; it does not split the Segment, cut the picture or end a Selection.
101
101
  <script id="story">
102
102
  <exchange>
103
103
  <HOST> I use it || every day, || since <2012 | twenty twelve>.
104
- <GUEST> Even @proof on holiday @/proof?
105
- <HOST> @answer! Especially then.
104
+ <GUEST> Even @{proof} on holiday @{/proof}?
105
+ <HOST> @{answer!} Especially then.
106
106
  </exchange>
107
107
  </script>
108
108
  ```
@@ -117,19 +117,53 @@ following characters. Display punctuation attaches to neighboring words without
117
117
  These units support precise timing and highlighting. A Caption Cue can hold a whole phrase of them;
118
118
  `||` chooses its handoff independently of character counts or visual line wrapping.
119
119
 
120
+ Annotations do not create speech boundaries. Comments are transparent (`hel<!--note-->lo`
121
+ remains `hello`); a postfix attribute or `||` inside a word is invalid. Script analyzes a complete
122
+ prose run before binding these constructs. An explicit Dual correspondence and a speaker/Segment
123
+ boundary remain authored structure. Shared Dual groups still expose their internal speech anchors.
124
+
125
+ A Dual display side is literal authored text, including symbols and emoji: `<😀|smile>` and
126
+ `<.|dot>` have explicit speech correspondence and require no invented speech token for the symbol.
127
+ A literal-only display is one display surface within that correspondence.
128
+
129
+ ## Display spelling and separators
130
+
131
+ Script preserves the normalized display spelling independently of speech tokenization. Ordinary
132
+ whitespace runs become one space; leading/trailing whitespace in a Turn and padding at the edges
133
+ of a Dual Text side are omitted. No language-specific rule removes a Chinese space or inserts a
134
+ space between numeric and Korean/Latin tokens. `是的 就是这样`, `3개월`, `3 개월`, `3D` and `3 D`
135
+ therefore remain distinct as authored. Source newlines are prose formatting, not Caption Cue breaks.
136
+ Use `||` for Cues and a family's layout controls for visual rows.
137
+
138
+ Each `CaptionDisplayWord.separatorBefore` is `""` or `" "`, relative to the preceding displayed word
139
+ in its Turn. Together with `text`, it carries the display spelling to consumers; it is not a speech
140
+ Token and has no timing. A renderer suppresses the leading separator at a displayed line/Cue start.
141
+ Shared groups retain internal separators and individual speech anchors: `<New York|>` is one
142
+ Alignment Unit with a space inside, while `<3D|>` has none. Grouping is a creative choice, not a
143
+ workaround for preserving spelling.
144
+
120
145
  ## Selection and Moment affinity
121
146
 
147
+ Every marker starts with `@{` and ends with `}`. The complete marker is zero-width in speech and
148
+ display; surrounding prose whitespace remains prose. All control sigils belong inside the braces:
149
+ `@{beat!}` is a Moment, whereas `@{part}!` opens a Selection followed by a literal exclamation mark.
150
+ Names match `[a-z][a-z0-9_-]{0,63}`; whitespace and nesting inside a marker are invalid.
151
+ A marker cannot split a speech Token or separate it from attached punctuation: place
152
+ `@{beat!}“测试”`, not `“@{beat!}测试”`. `hello{emphasis}` is a postfix display attribute; `@{part}`
153
+ is consumed as one marker and cannot be mistaken for that attribute. Write a literal `@{part}` as
154
+ `\@\{part\}`.
155
+
122
156
  A marker selects the adjacent semantic anchor; it does not write a timecode. Inside a spoken
123
157
  passage, its affinity normally chooses a neighboring word boundary:
124
158
 
125
159
  | Marker | Boundary |
126
160
  | --- | --- |
127
- | `@name` | Selection opens at the next word's start: right affinity. |
128
- | `~@name` | Selection opens at the previous word's end: left affinity. |
129
- | `@/name` | Selection closes at the previous word's end: left affinity. |
130
- | `@/name~` | Selection closes at the next word's start: right affinity. |
131
- | `@name!` | Moment at the next word's start: right affinity. |
132
- | `~@name!` | Moment at the previous word's end: left affinity. |
161
+ | `@{name}` | Selection opens at the next word's start: right affinity. |
162
+ | `@{~name}` | Selection opens at the previous word's end: left affinity. |
163
+ | `@{/name}` | Selection closes at the previous word's end: left affinity. |
164
+ | `@{/name~}` | Selection closes at the next word's start: right affinity. |
165
+ | `@{name!}` | Moment at the next word's start: right affinity. |
166
+ | `@{~name!}` | Moment at the previous word's end: left affinity. |
133
167
 
134
168
  At structural edges, the parser retains the corresponding Segment or program boundary rather than
135
169
  inventing a neighboring word. Selection and Moment ids share one namespace. Selections can overlap,
@@ -138,37 +172,50 @@ cross and span Segments; unlike tags, they do not need to nest.
138
172
  To join two visual Selections without exposing their inter-word pause, match affinity on both sides:
139
173
 
140
174
  ```text
141
- @coffee my coffee @/coffee ~@smoothie my smoothie @/smoothie
142
- @coffee my coffee @/coffee~ @smoothie my smoothie @/smoothie
175
+ @{coffee} my coffee @{/coffee} @{~smoothie} my smoothie @{/smoothie}
176
+ @{coffee} my coffee @{/coffee~} @{smoothie} my smoothie @{/smoothie}
143
177
  ```
144
178
 
145
179
  These are alternative spellings, not two occurrences to put in the same Script. In the first,
146
180
  “coffee” ends both the first Window and the gap's left boundary, so the smoothie Selection owns the
147
181
  pause. In the second, the next “my” starts both touching boundaries, so the coffee Selection owns it.
148
- Plain `@/coffee @smoothie` leaves the gap between previous word end and next word start outside both.
182
+ Plain `@{/coffee} @{smoothie}` leaves the gap between previous word end and next word start outside both.
149
183
  The media consumer still decides playback and visual coverage inside those projected Windows.
150
184
 
151
185
  ## Marker writeback
152
186
 
153
187
  `adjustScriptSelection` and `adjustScriptMoment` accept explicit anchor identities. A Selection's
154
- two endpoints are written together. Script owns their legal source sites: token punctuation and
155
- postfix attributes stay attached, Segment boundaries stay structural, and coincident markers are
156
- ordered together. Writeback reads the current source, removes the markers, normalizes ordinary
157
- same-line prose, and places the markers at their requested anchors. Repeated spaces and tabs in
158
- prose become necessary separators; punctuation uses the same attachment rules as Script's text
159
- projection. Newlines, blank lines and each line's leading spaces/tabs remain intact. A marker at
160
- the start of a line is placed after its indentation; whitespace after that marker is prose, not
161
- additional indentation. Marker-only lines remain blank when the marker leaves. Comments, tags,
162
- Dual Text and display attributes retain their own syntax and are not passed through prose cleanup.
163
-
164
- Normalization keeps lexical units unchanged and does not restore earlier whitespace spellings.
165
- It has no persistent formatting state: the same text structure and anchor relationships produce
166
- the same spelling on repeated edits. Empty self-closing Segments expand when needed to make their
167
- distinct boundaries writable.
168
-
169
- Writeback reparses the result to retain the intended identities and unchanged narrative/caption
170
- content. These checks concern authored order, not frame order. A consumer's temporal projection
171
- determines whether the resulting Instant or Window is usable.
188
+ two endpoints are moved together; unrelated markers and all source prose remain untouched. Names
189
+ terminate at `}`, so writeback never inserts a separating space or normalizes surrounding prose.
190
+ A no-op adjustment returns the original source. Script owns legal insertion sites: Tokens and
191
+ postfix attributes stay together, Dual Text markers follow the actual speech side, and coincident
192
+ markers written together have deterministic order. Empty self-closing Segments expand only when a
193
+ requested boundary needs an interior insertion site.
194
+
195
+ Writeback reparses the result to retain the requested bindings and unchanged speech, display and
196
+ Narrative/Caption content. Source offsets remain parser-private; no formatting history is stored.
197
+
198
+ ## Explicit migration from 0.1
199
+
200
+ The 0.2 parser rejects bare `@name` markers. Preview migration from the repository or installed
201
+ Distribution root, then explicitly write the reviewed result:
202
+
203
+ ```sh
204
+ node packages/script/bin/migrate-0.2.mjs /path/to/film.svml
205
+ node packages/script/bin/migrate-0.2.mjs /path/to/film.svml --write
206
+ ```
207
+
208
+ Use `--body` for a file containing a raw Script body rather than outer SVML. The tool converts
209
+ markers only inside Script bodies, leaves comments and escapes intact, and does not touch provider
210
+ prompt references such as `@image1`. It neither installs anything nor runs during a build.
211
+
212
+ The tool changes marker spelling, not marker placement. Move a marker that separates a word from
213
+ its attached quote or punctuation to the complete word boundary before using that source.
214
+
215
+ Review authored whitespace after migration: spaces previously discarded by Chinese/punctuation
216
+ normalization now appear. The tool preserves source spaces rather than guessing the author's intent.
217
+ Regenerate affected Narrative, caption and Build results with the new reader/writer together;
218
+ protocol identities remain `@1`. Existing rendered media is not modified by source migration.
172
219
 
173
220
  ## Complete authored content and narrow exports
174
221
 
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ /** Explicit, one-time marker migration. Never called by parsing, building, or Studio. */
3
+ import { readFile, writeFile } from "node:fs/promises";
4
+ import { resolve } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ export function migrateScriptBody(source) {
8
+ let result = "";
9
+ for (let cursor = 0; cursor < source.length;) {
10
+ if (source[cursor] === "\\") {
11
+ result += source.slice(cursor, cursor + 2);
12
+ cursor += 2;
13
+ continue;
14
+ }
15
+ if (source.startsWith("<!--", cursor)) {
16
+ const close = source.indexOf("-->", cursor + 4);
17
+ if (close < 0) throw new Error("Unclosed Script comment");
18
+ result += source.slice(cursor, close + 3);
19
+ cursor = close + 3;
20
+ continue;
21
+ }
22
+ if (source.startsWith("@{", cursor)) {
23
+ const close = source.indexOf("}", cursor + 2);
24
+ if (close < 0) throw new Error("Unclosed delimited Script marker");
25
+ result += source.slice(cursor, close + 1);
26
+ cursor = close + 1;
27
+ continue;
28
+ }
29
+ const rest = source.slice(cursor);
30
+ const match = /^@\/([a-z][a-z0-9_-]{0,63})(~)/u.exec(rest)
31
+ ?? /^@\/([a-z][a-z0-9_-]{0,63})(?![A-Za-z0-9_-])/u.exec(rest);
32
+ const open = match ? undefined : /^(~)?@([a-z][a-z0-9_-]{0,63})(!)/u.exec(rest)
33
+ ?? /^(~)?@([a-z][a-z0-9_-]{0,63})(?![A-Za-z0-9_-])/u.exec(rest);
34
+ if (match) {
35
+ result += `@{/${match[1]}${match[2] ?? ""}}`;
36
+ cursor += match[0].length;
37
+ } else if (open) {
38
+ result += `@{${open[1] ?? ""}${open[2]}${open[3] ?? ""}}`;
39
+ cursor += open[0].length;
40
+ } else {
41
+ result += source[cursor++];
42
+ }
43
+ }
44
+ return result;
45
+ }
46
+
47
+ /** Change Script raw bodies only; package references and generation prompt tags stay untouched. */
48
+ export function migrateSvml(source) {
49
+ let result = "";
50
+ let cursor = 0;
51
+ const token = /<!--[\s\S]*?-->|<(?:[A-Za-z_][\w.-]*:)?script(?=[\s/>])(?:"[^"]*"|'[^']*'|[^'">])*>/gu;
52
+ for (let match = token.exec(source); match; match = token.exec(source)) {
53
+ if (match[0].startsWith("<!--") || match[0].endsWith("/>")) continue;
54
+ const name = /^<([^\s>]+)/u.exec(match[0])[1];
55
+ const close = `</${name}>`;
56
+ const start = match.index + match[0].length;
57
+ let end = start;
58
+ while (end < source.length) {
59
+ if (source[end] === "\\") { end += 2; continue; }
60
+ if (source.startsWith("<!--", end)) {
61
+ const stop = source.indexOf("-->", end + 4);
62
+ if (stop < 0) throw new Error("Unclosed Script comment");
63
+ end = stop + 3;
64
+ continue;
65
+ }
66
+ if (source.startsWith(close, end)) break;
67
+ end++;
68
+ }
69
+ if (end >= source.length) throw new Error(`Missing ${close}`);
70
+ result += source.slice(cursor, start) + migrateScriptBody(source.slice(start, end)) + close;
71
+ cursor = end + close.length;
72
+ token.lastIndex = cursor;
73
+ }
74
+ return result + source.slice(cursor);
75
+ }
76
+
77
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
78
+ const args = process.argv.slice(2);
79
+ const flags = new Set(args.filter(arg => arg.startsWith("--")));
80
+ const paths = args.filter(arg => !arg.startsWith("--"));
81
+ if (paths.length !== 1 || [...flags].some(flag => !["--write", "--body"].includes(flag))) {
82
+ console.error("Usage: node migrate-0.2.mjs <source.svml> [--body] [--write]\nDefault: print migrated SVML. --body: input is a raw Script body. --write: replace the named file.");
83
+ process.exitCode = 2;
84
+ } else {
85
+ const source = await readFile(paths[0], "utf8");
86
+ const migrated = flags.has("--body") ? migrateScriptBody(source) : migrateSvml(source);
87
+ if (flags.has("--write")) await writeFile(paths[0], migrated);
88
+ else process.stdout.write(migrated);
89
+ }
90
+ }
@@ -3,7 +3,6 @@ import type { SourceRange } from "@hypit/protocol";
3
3
  import { canonicalStringify } from "@hypit/protocol";
4
4
  import { captionDocument, narrativeValue } from "./narrative.js";
5
5
  import { parseScript } from "./parser.js";
6
- import { cleanHorizontalProse } from "./lexical.js";
7
6
  import type { Affinity, ParsedNarrative, SemanticAnchor } from "./types.js";
8
7
 
9
8
  export type ScriptAnchorEditSite = {
@@ -75,8 +74,8 @@ export function scriptAnchorEditSites(parsed: ParsedNarrative): readonly ScriptA
75
74
  }
76
75
 
77
76
  function marker(id: string, edge: "open" | "close", affinity: Affinity): string {
78
- if (edge === "open") return affinity === "left" ? `~@${id}` : `@${id}`;
79
- return affinity === "left" ? `@/${id}` : `@/${id}~`;
77
+ if (edge === "open") return `@{${affinity === "left" ? "~" : ""}${id}}`;
78
+ return `@{/${id}${affinity === "right" ? "~" : ""}}`;
80
79
  }
81
80
 
82
81
  function applyEdits(source: string, edits: readonly Edit[]): string {
@@ -100,61 +99,32 @@ type AdjustmentInput = {
100
99
 
101
100
  type NamedAnchor = { readonly id: string; readonly edge: "open" | "close" | "moment"; readonly anchorId: string };
102
101
 
103
- /** Normalize only parser-owned prose. Indentation is read from this edit's input, never stored. */
104
- function normalizedProse(source: string, parsed: ParsedNarrative, original: string): string {
105
- const bodyStarts = new Set(parsed.segments.flatMap(segment => [segment.contentRange.start,
106
- ...segment.atoms.filter(atom => atom.kind === "role").map(atom => atom.range.end)]));
107
- const bodyEnds = new Set(parsed.segments.map(segment => segment.contentRange.end));
108
- const indentation = original.split(/\r\n|\r|\n/u).map(line => /^[ \t]*/u.exec(line)![0].length);
109
- const lines: Array<{ start: number; end: number; indentEnd: number }> = [];
110
- let offset = 0;
111
- const parts = source.split(/(\r\n|\r|\n)/u);
112
- for (let index = 0; index < parts.length; index += 2) {
113
- const text = parts[index]!;
114
- lines.push({ start: offset, end: offset + text.length, indentEnd: offset + indentation[index / 2]! });
115
- offset += text.length + (parts[index + 1]?.length ?? 0);
116
- }
117
- const edits: Edit[] = [];
118
- for (const range of parsed.proseRanges) for (const line of lines) {
119
- const start = Math.max(range.start, line.indentEnd);
120
- const end = Math.min(range.end, line.end);
121
- if (start >= end) continue;
122
- let text = cleanHorizontalProse(source.slice(start, end));
123
- const outside = !parsed.segments.some(segment => range.start >= segment.contentRange.start && range.end <= segment.contentRange.end);
124
- if (start === line.indentEnd || bodyStarts.has(start) || outside) text = text.replace(/^[ \t]+/u, "");
125
- if (end === line.end || bodyEnds.has(end) || outside) text = text.replace(/[ \t]+$/u, "");
126
- edits.push({ range: { start, end }, replacement: text });
127
- }
128
- return applyEdits(source, edits);
129
- }
130
-
131
- /** One canonical boundary spelling, independent of the order of previous gestures. */
102
+ /** Move only the requested markers. Delimited names never require editing prose separators. */
132
103
  function rewrite(input: AdjustmentInput, markers: readonly NamedAnchor[]): string {
133
- const ranges = [
134
- ...input.parsed.moments.map((item) => item.range),
135
- ...input.parsed.selections.flatMap((item) => [item.open.range, item.close.range]),
136
- ].map((range) => ({ start: range.start - input.parsed.sourceRange.start, end: range.end - input.parsed.sourceRange.start }))
137
- .sort((a, b) => a.start - b.start);
138
- const edits: Edit[] = ranges.map(range => ({ range, replacement: "" }));
104
+ const original = namedAnchors(input.parsed);
105
+ const changedNames = new Set(markers.filter(value => original.find(item => item.id === value.id && item.edge === value.edge)?.anchorId !== value.anchorId).map(value => value.id));
106
+ const moved = markers.filter(value => changedNames.has(value.id));
107
+ if (moved.length === 0) return input.source;
108
+ const origin = input.parsed.sourceRange.start;
109
+ const edits: Edit[] = moved.map(value => {
110
+ const range = value.edge === "moment"
111
+ ? input.parsed.moments.find(item => item.id === value.id)!.range
112
+ : input.parsed.selections.find(item => item.id === value.id)![value.edge].range;
113
+ return { range: { start: range.start - origin, end: range.end - origin }, replacement: "" };
114
+ });
139
115
  for (const segment of input.parsed.segments) {
140
- if (segment.selfClosing) edits.push({ range: { start: segment.range.start - input.parsed.sourceRange.start, end: segment.range.end - input.parsed.sourceRange.start }, replacement: `<${segment.id}></${segment.id}>` });
116
+ if (segment.selfClosing && moved.some(value => value.anchorId === segment.startAnchorId || value.anchorId === segment.endAnchorId)) {
117
+ edits.push({ range: { start: segment.range.start - origin, end: segment.range.end - origin }, replacement: `<${segment.id}></${segment.id}>` });
118
+ }
141
119
  }
142
- const unmarked = applyEdits(input.source, edits);
143
- const base = normalizedProse(unmarked, parseScript(input.sourceName, unmarked), input.source);
144
- const parsed = parseScript(input.sourceName, base);
120
+ const base = applyEdits(input.source, edits);
121
+ const parsed = parseScript(input.sourceName, base, origin);
145
122
  const sites = new Map(scriptAnchorEditSites(parsed).map((site, order) => [site.anchorId, { ...site, order }]));
146
123
  const groups = new Map<number, Array<NamedAnchor & { affinity: Affinity; order: number }>>();
147
- for (const value of markers) {
124
+ for (const value of moved) {
148
125
  const site = sites.get(value.anchorId);
149
126
  if (!site) throw new Error(`Semantic Anchor ${value.anchorId} does not exist.`);
150
- let offset = site.offset;
151
- const lineStart = Math.max(base.lastIndexOf("\n", offset - 1), base.lastIndexOf("\r", offset - 1)) + 1;
152
- const indentEnd = lineStart + /^[ \t]*/u.exec(base.slice(lineStart))![0].length;
153
- if (offset < indentEnd) offset = indentEnd;
154
- // Reuse horizontal separators without moving a word marker ahead of line indentation.
155
- let gapStart = offset;
156
- while (gapStart > 0 && /[ \t]/u.test(base[gapStart - 1]!)) gapStart -= 1;
157
- if (gapStart > 0 && !/[\r\n]/u.test(base[gapStart - 1]!)) offset = gapStart;
127
+ const offset = site.offset - origin;
158
128
  const group = groups.get(offset) ?? [];
159
129
  group.push({ ...value, affinity: site.affinity, order: site.order });
160
130
  groups.set(offset, group);
@@ -163,18 +133,13 @@ function rewrite(input: AdjustmentInput, markers: readonly NamedAnchor[]): strin
163
133
  group.sort((a, b) => a.order - b.order
164
134
  || (a.edge === "open" ? 0 : a.edge === "moment" ? 1 : 2) - (b.edge === "open" ? 0 : b.edge === "moment" ? 1 : 2)
165
135
  || a.id.localeCompare(b.id));
166
- let replacement = group.map((item) => item.edge === "moment"
167
- ? `${item.affinity === "left" ? "~" : ""}@${item.id}!`
136
+ const replacement = group.map(item => item.edge === "moment"
137
+ ? `@{${item.affinity === "left" ? "~" : ""}${item.id}!}`
168
138
  : marker(item.id, item.edge, item.affinity)).join("");
169
- let end = offset;
170
- while (end < base.length && /[ \t]/u.test(base[end]!)) end += 1;
171
- if (end > offset) replacement += " ";
172
- // Only undelimited names followed by an ASCII name character need a separator.
173
- if (/[a-z0-9_-]$/u.test(replacement) && /[A-Za-z0-9_-]/u.test(base[offset] ?? "")) replacement += " ";
174
- return { range: { start: offset, end }, replacement };
139
+ return { range: { start: offset, end: offset }, replacement };
175
140
  });
176
141
  const next = applyEdits(base, insertions);
177
- const reparsed = parseScript(input.sourceName, next);
142
+ const reparsed = parseScript(input.sourceName, next, origin);
178
143
  const actual = namedAnchors(reparsed);
179
144
  if (canonicalStringify(actual) !== canonicalStringify(markers)) {
180
145
  throw new Error("Script marker adjustment did not preserve the requested semantic bindings.");
@@ -183,6 +148,9 @@ function rewrite(input: AdjustmentInput, markers: readonly NamedAnchor[]): strin
183
148
  const content = (value: ParsedNarrative) => ({
184
149
  ...narrativeValue(value, "comparison") as Record<string, unknown>, selections: [], moments: [],
185
150
  caption: captionDocument(value, "caption", "comparison"),
151
+ speech: value.serializations.speech,
152
+ dialogue: value.serializations.dialogue,
153
+ display: value.captionProjection.text,
186
154
  });
187
155
  if (canonicalStringify(content(input.parsed)) !== canonicalStringify(content(reparsed))) {
188
156
  throw new Error("Script marker adjustment changed authored content.");