@hypit/hypit 0.1.2 → 0.1.4

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 (94) hide show
  1. package/README.md +1 -1
  2. package/dist/public/studio-adapter.d.ts +31 -2
  3. package/package.json +4 -1
  4. package/packages/audio-track-studio/src/index.ts +7 -4
  5. package/packages/build-result/README.md +58 -0
  6. package/packages/build-result/package.json +2 -1
  7. package/packages/build-result/src/decode.ts +10 -8
  8. package/packages/build-result/src/file-reference.ts +42 -0
  9. package/packages/build-result/src/index.ts +3 -0
  10. package/packages/build-result/src/replace-file-windows.ts +58 -0
  11. package/packages/build-result/src/replace-file.ts +11 -0
  12. package/packages/build-result/src/store.ts +44 -13
  13. package/packages/build-result/src/types.ts +38 -8
  14. package/packages/build-result/src/writer.ts +12 -1
  15. package/packages/build-result-fs/README.md +23 -1
  16. package/packages/build-result-s3/README.md +5 -0
  17. package/packages/build-result-s3/src/repository.ts +19 -5
  18. package/packages/caption-fine/README.md +17 -2
  19. package/packages/caption-fine/src/manifest.ts +1 -1
  20. package/packages/caption-fine/src/render.ts +48 -36
  21. package/packages/caption-fine-studio/package.json +8 -3
  22. package/packages/caption-fine-studio/src/index.ts +10 -1
  23. package/packages/cli/README.md +25 -0
  24. package/packages/cli/src/arguments.ts +16 -11
  25. package/packages/cli/src/build-planning.ts +6 -1
  26. package/packages/cli/src/command.ts +1 -1
  27. package/packages/cli/src/commands/environment.ts +42 -13
  28. package/packages/cli/src/machine-view.ts +12 -3
  29. package/packages/cli/src/main.ts +4 -0
  30. package/packages/cli/src/output.ts +28 -12
  31. package/packages/cli/src/result-export.ts +22 -13
  32. package/packages/cli/src/run-file.ts +19 -8
  33. package/packages/comment-sticker-studio/src/index.ts +2 -1
  34. package/packages/compiler-node/src/compiler.ts +1 -0
  35. package/packages/estimate/README.md +56 -3
  36. package/packages/estimate/src/program.ts +7 -8
  37. package/packages/fonts-open/README.md +5 -0
  38. package/packages/fonts-open/package.json +5 -1
  39. package/packages/fonts-open/src/studio.ts +21 -0
  40. package/packages/gpt-image-kits/README.md +17 -12
  41. package/packages/gpt-image-kits/kits/phone-ugc-v1.svs +8 -8
  42. package/packages/media-track-studio/src/index.ts +7 -2
  43. package/packages/provider-whisperx-local/README.md +57 -0
  44. package/packages/ranking/src/render.ts +11 -9
  45. package/packages/ranking-studio/README.md +11 -0
  46. package/packages/ranking-studio/package.json +8 -3
  47. package/packages/ranking-studio/src/index.ts +11 -4
  48. package/packages/runtime/src/catalog.ts +2 -0
  49. package/packages/runtime-host-node/src/index.ts +1 -0
  50. package/packages/runtime-local/README.md +19 -0
  51. package/packages/runtime-local/src/programs.ts +39 -24
  52. package/packages/runtime-local/src/runtime.ts +2 -0
  53. package/packages/script/README.md +22 -0
  54. package/packages/script/src/edit.ts +123 -73
  55. package/packages/script/src/lexical.ts +40 -6
  56. package/packages/script/src/parser.ts +36 -8
  57. package/packages/script/src/types.ts +4 -0
  58. package/packages/seedance-kits/README.md +3 -1
  59. package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
  60. package/packages/speech-alignment/src/align.ts +45 -21
  61. package/packages/studio/INSPECTOR.md +104 -0
  62. package/packages/studio/README.md +65 -4
  63. package/packages/studio/src/build-library.ts +99 -84
  64. package/packages/studio/src/library-media.ts +19 -0
  65. package/packages/studio/src/parameter-values.ts +41 -1
  66. package/packages/studio/src/parameters.ts +20 -21
  67. package/packages/studio/src/server.ts +58 -30
  68. package/packages/studio/src/shared.ts +20 -6
  69. package/packages/studio/src/studio-registry.ts +22 -0
  70. package/packages/studio/src/style.css +135 -144
  71. package/packages/studio/src/temporal-edit.ts +131 -0
  72. package/packages/studio/src/ui/artifact-name.ts +80 -0
  73. package/packages/studio/src/ui/artifact-preview.ts +99 -0
  74. package/packages/studio/src/ui/icons.ts +5 -1
  75. package/packages/studio/src/ui/library.ts +373 -192
  76. package/packages/studio/src/ui/main.ts +153 -31
  77. package/packages/studio/src/ui/material-preview.ts +1 -1
  78. package/packages/studio/src/ui/overlay.ts +21 -20
  79. package/packages/studio/src/ui/sidebar-panel.ts +31 -0
  80. package/packages/studio/src/ui/stage.ts +124 -8
  81. package/packages/studio/src/ui/timeline.ts +16 -179
  82. package/packages/studio/start.ts +15 -3
  83. package/packages/studio-adapter/README.md +21 -0
  84. package/packages/studio-adapter/src/index.ts +26 -1
  85. package/packages/temporal-markup/EDITING.md +183 -0
  86. package/packages/temporal-markup/README.md +4 -0
  87. package/packages/typography-track-studio/package.json +8 -3
  88. package/packages/typography-track-studio/src/index.ts +5 -1
  89. package/packages/video-cli/README.md +12 -3
  90. package/packages/video-cli/src/creation.ts +11 -5
  91. package/packages/whisperx/README.md +7 -0
  92. package/packages/workspace/src/index.ts +2 -0
  93. package/packages/workspace-fs-node/README.md +4 -1
  94. package/packages/workspace-fs-node/src/workspace.ts +3 -1
package/README.md CHANGED
@@ -26,7 +26,7 @@
26
26
  <p align="center">
27
27
  <a href="https://hypit.ai"><img alt="Visit our website" src="https://img.shields.io/badge/Visit%20our%20website-DF3C68?style=for-the-badge&logo=data:image/svg%2Bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjI1Ny4wMyAyODcuNDggNDg2Ljk0IDQyNi4wNCI%2BPHBhdGggZmlsbD0iI2ZmZiIgZD0iTTczMy4wMDEsNDYyLjU5MmMtMTEuNDQtMTYuNTEtMzAuMjg1LTI2LjM0NC01MC4zNTMtMjYuMzQ0aC0xNzguNzA1Yy0yMy40OTMsMC00Ny42NzUsMTQuMjM0LTU2LjYyOCwzNS45NjdsLTU0LjA2NSwxNDUuMDUzYy04LjgsMjEuNTIzLTguNjg2LDQ2LjAxMSw0LjI0Nyw2NS4zNTIsMTIuOTksMTkuNDE4LDM3LjAzOCwzMC44OTcsNjAuMzk3LDMwLjg5N2gxNjQuNDljMjcuMDEzLDAsNTEuNDYzLTE2Ljk4OSw2MC45MTQtNDIuMjhsNTYuODAxLTE1Mi4yMjdjNy4wMjEtMTguODI1LDQuMzgxLTM5LjkwOC03LjA1OS01Ni4zOTl2LS4wMzhoLS4wMzh2LjAxOVpNNjMzLjk3OCw2NTIuODEzYy0xLjc5OCw0LjgyMS02LjQ4NSw4LjA3My0xMS42MzIsOC4wNzNoLTE1OS42NWMtOC4zMDMsMC0xMi43OC01LjM1Ny0xNC4zMjktNy42MzNzLTQuNzgzLTguNDc1LTEuNjA3LTE2LjE0N2w0NS42MjgtMTI2LjcyNWM0LjM4MS0xMi4xNDgsMTUuNDU4LTIwLjc5NiwyOC4zNTItMjEuNDY1LjYzMS0uMDM4LDEuMjgyLS4wMzgsMS45NTEtLjAzOGgxNDQuMjY5czIzLjkzMywyLjcxNywxNy43NzMsMjcuOTUxbC01MC43NTUsMTM2LjAwNGgwdi0uMDE5Wk0zNTYuMjUsNjIzLjEyMnMtNDIuNzc3LTIyLjE3My0zMy43ODYtNjIuNDgzbDU2Ljc2Mi0xNTcuMzE2YzkuMTQ1LTI1LjM0OSwzMy4yMTItNDIuMjYxLDYwLjE2OC00Mi4yNjFoMTgxLjc4NWMxMS45NTcsMCwyMy4xNDksNS43OTcsMzAuMDU1LDE1LjU3M2wyNi41NzMsMzcuNjVoLTIxNi4xMjVjLTE2LjY0NCwwLTMxLjU0NywxMC4zNS0zNy4zNDQsMjUuOTYxbC02OC4wODgsMTgyLjg5NWgwdi0uMDE5Wk0yOTIuMDY0LDU0OS41NDNzLTQyLjc5Ny0yMi4xNzMtMzMuNzg2LTYyLjQ4M2w1Ni43NjItMTU3LjMxNmM5LjE2NC0yNS4zNDksMzMuMjMxLTQyLjI2MSw2MC4xNjgtNDIuMjYxaDE3Mi4wMDljMTEuOTU3LDAsMjMuMTQ5LDUuNzk3LDMwLjA1NSwxNS41NzNsMjYuNTczLDM3LjY1aC0yMDYuMzExYy0xNi42NDQsMC0zMS41NDcsMTAuMzUtMzcuMzQ0LDI1Ljk2MWwtNjguMDg4LDE4Mi44OTVoLS4wMzh2LS4wMTlaIi8%2BPC9zdmc%2B"></a>
28
28
  <a href="https://discord.gg/85hnyQnxpn"><img alt="Join our Discord" src="https://img.shields.io/badge/Join%20our%20Discord-5865F2?style=for-the-badge&logo=discord&logoColor=white"></a>
29
- <a href="https://t.me/hypit"><img alt="Join our Telegram" src="https://img.shields.io/badge/Join%20our%20Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white"></a>
29
+ <a href="https://t.me/hypitai"><img alt="Join our Telegram" src="https://img.shields.io/badge/Join%20our%20Telegram-26A5E4?style=for-the-badge&logo=telegram&logoColor=white"></a>
30
30
  <a href="https://x.com/hypitai"><img alt="Follow @hypitai on X" src="https://img.shields.io/badge/Follow%20%40hypitai-000000?style=for-the-badge&logo=x&logoColor=white"></a>
31
31
  </p>
32
32
 
@@ -159,6 +159,28 @@ type StudioTemporalBinding = {
159
159
  declare const studioParameterControls: readonly ["text", "number", "boolean", "select", "color", "list", "record"];
160
160
  type StudioParameterControl = typeof studioParameterControls[number];
161
161
  type StudioParameterLanguage = "svml" | "svs" | "svrun";
162
+ /** Data-only choices. Packages supply meaning; Studio owns all option rendering. */
163
+ type StudioParameterOption = string | {
164
+ readonly value: string | number | boolean;
165
+ readonly label: string;
166
+ readonly description?: string;
167
+ readonly preview?: {
168
+ readonly kind: "color";
169
+ readonly color: string;
170
+ } | {
171
+ readonly kind: "font";
172
+ readonly family: string;
173
+ readonly sample?: string;
174
+ };
175
+ };
176
+ /** Displayed number = authored number × scale. Suffixes are retained, never converted. */
177
+ type StudioNumberPresentation = {
178
+ readonly scale?: number;
179
+ readonly suffixes?: readonly string[];
180
+ readonly minimum?: number;
181
+ readonly maximum?: number;
182
+ readonly step?: number;
183
+ };
162
184
  /** A real author endpoint. Its existence never implies Inspector visibility. */
163
185
  type StudioSourceBinding = {
164
186
  readonly id: string;
@@ -203,8 +225,12 @@ type StudioInspectorFieldDeclaration = {
203
225
  readonly summary?: string;
204
226
  /** Omit when the binding's public schema selects the finite Studio control. */
205
227
  readonly control?: StudioParameterControl;
206
- readonly options?: readonly string[];
228
+ readonly options?: readonly StudioParameterOption[];
207
229
  readonly unit?: string;
230
+ readonly number?: StudioNumberPresentation;
231
+ readonly multiline?: boolean;
232
+ /** Optional suggested colors, independent of the accepted color value. */
233
+ readonly swatches?: readonly string[];
208
234
  };
209
235
  /** Resolved Inspector DTO. Studio renders it and executes its exact source write. */
210
236
  type StudioInspectorField = Omit<StudioInspectorFieldDeclaration, "control"> & {
@@ -314,6 +340,7 @@ type StudioEntityDisplay = {
314
340
  readonly layers: readonly StudioDisplayLayer[];
315
341
  };
316
342
  type StudioLaneDescription = {
343
+ /** Height of this single timeline lane; overlapping entities share it. */
317
344
  readonly heightPx: number;
318
345
  readonly groupId?: string;
319
346
  readonly attachedTo?: string;
@@ -484,10 +511,12 @@ type StudioEntityDraft = {
484
511
  readonly display: StudioEntityDisplay;
485
512
  readonly startFrame: number;
486
513
  readonly endFrameExclusive: number;
514
+ /** Back-to-front timeline order. Ties preserve projection order. Selection raises only the timeline item. */
487
515
  readonly stackOrder: number;
488
516
  readonly elementRange?: Range;
489
517
  readonly markerId?: string;
490
518
  readonly presentId?: string;
519
+ /** Rendered parts belonging to this entity, including phases beyond its editable timeline interval. */
491
520
  readonly renderIds?: readonly string[];
492
521
  /** Resolved author references that differ per derived entity, such as one Cue's actual Style. */
493
522
  readonly parameterReferences?: Readonly<Record<string, string>>;
@@ -683,4 +712,4 @@ declare function childEntities(context: StudioTrackCompanionContext, items: read
683
712
  }[], entity: StudioTimelinePresentation["entity"], chrome: StudioTimelinePresentation["chrome"]): readonly StudioEntityDraft[];
684
713
 
685
714
  export { artifactPreview, authoredChildFor, childEntities, createStudioCompanionHostFacet, createStudioTrackCompanionHostFacet, previewLayer, requiredReferencedValue, requiredSurfaceValue, sameSurfaceValue, studioCompanionHostAbi, studioContributionFromPackage, studioParameterControls, studioTrackCompanionsFromPackage, temporalBindingsFor, temporalLineageFor, temporalSemanticSource, textLayer };
686
- export type { Range, StudioCandidateProvenance, StudioCompanionContribution, StudioCompanionHostFacet, StudioDisplayLayer, StudioEditCoordinate, StudioEditHandle, StudioEditSource, StudioEditSourceRole, StudioEntityDisplay, StudioEntityDraft, StudioFilmCompanion, StudioIcon, StudioInspectorDomain, StudioInspectorField, StudioInspectorFieldDeclaration, StudioInspectorPageDeclaration, StudioInspectorSectionDeclaration, StudioLaneAttachment, StudioLaneDescription, StudioMaterialPreview, StudioObservedValue, StudioPackageContribution, StudioParameterControl, StudioParameterLanguage, StudioPlacement, StudioPlacementChild, StudioPreviewSource, StudioRecipeBindingDeclaration, StudioRecipeReferenceBindingDeclaration, StudioResolvedTrack, StudioScriptAdjustment, StudioScriptCompanion, StudioScriptSourceMap, StudioSemanticAnchor, StudioSemanticEditTarget, StudioSemanticSegment, StudioSemanticTimeline, StudioSemanticToken, StudioSnapTarget, StudioSourceBinding, StudioSourceBindingDeclaration, StudioSpan, StudioTemporalAuthority, StudioTemporalBinding, StudioTemporalConsumer, StudioTemporalConsumerInput, StudioTemporalInstantProjection, StudioTemporalLineage, StudioTemporalPhase, StudioTemporalProjection, StudioTemporalSource, StudioTemporalWindowProjection, StudioTimelineGesture, StudioTimelinePresentation, StudioTimelineTone, StudioTrackCompanion, StudioTrackCompanionContext, StudioTrackFamily, StudioTrackTrace, StudioViewRole };
715
+ export type { Range, StudioCandidateProvenance, StudioCompanionContribution, StudioCompanionHostFacet, StudioDisplayLayer, StudioEditCoordinate, StudioEditHandle, StudioEditSource, StudioEditSourceRole, StudioEntityDisplay, StudioEntityDraft, StudioFilmCompanion, StudioIcon, StudioInspectorDomain, StudioInspectorField, StudioInspectorFieldDeclaration, StudioInspectorPageDeclaration, StudioInspectorSectionDeclaration, StudioLaneAttachment, StudioLaneDescription, StudioMaterialPreview, StudioNumberPresentation, StudioObservedValue, StudioPackageContribution, StudioParameterControl, StudioParameterLanguage, StudioParameterOption, StudioPlacement, StudioPlacementChild, StudioPreviewSource, StudioRecipeBindingDeclaration, StudioRecipeReferenceBindingDeclaration, StudioResolvedTrack, StudioScriptAdjustment, StudioScriptCompanion, StudioScriptSourceMap, StudioSemanticAnchor, StudioSemanticEditTarget, StudioSemanticSegment, StudioSemanticTimeline, StudioSemanticToken, StudioSnapTarget, StudioSourceBinding, StudioSourceBindingDeclaration, StudioSpan, StudioTemporalAuthority, StudioTemporalBinding, StudioTemporalConsumer, StudioTemporalConsumerInput, StudioTemporalInstantProjection, StudioTemporalLineage, StudioTemporalPhase, StudioTemporalProjection, StudioTemporalSource, StudioTemporalWindowProjection, StudioTimelineGesture, StudioTimelinePresentation, StudioTimelineTone, StudioTrackCompanion, StudioTrackCompanionContext, StudioTrackFamily, StudioTrackTrace, StudioViewRole };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,6 +115,8 @@
115
115
  "packages/*/*.html",
116
116
  "packages/*/package.json",
117
117
  "packages/*/README.md",
118
+ "packages/temporal-markup/EDITING.md",
119
+ "packages/studio/INSPECTOR.md",
118
120
  "services/image-opencv/pyproject.toml",
119
121
  "services/image-opencv/package.json",
120
122
  "services/image-opencv/uv.lock",
@@ -259,6 +261,7 @@
259
261
  },
260
262
  "dependencies": {
261
263
  "@puppeteer/browsers": "3.2.2",
264
+ "koffi": "3.2.1",
262
265
  "puppeteer-core": "25.10.0",
263
266
  "sharp": "0.35.3",
264
267
  "cmu-pronouncing-dictionary": "3.0.0",
@@ -48,22 +48,25 @@ export const audioTrackStudioTrackCompanions: readonly StudioTrackCompanion[] =
48
48
  ],
49
49
  inspector: [
50
50
  {
51
- binding: "playback", label: "Playback", domain: "how",
51
+ binding: "playback", label: "Playback", domain: "when",
52
52
  page: { id: "playback", label: "Playback" }, section: { id: "playback", label: "Playback" },
53
53
  control: "select", options: ["once", "once-start", "once-end", "loop", "loop-start", "loop-end", "stretch"],
54
54
  },
55
55
  ...(["trim-start", "trim-end"] as const).map((binding) => ({
56
- binding, label: binding === "trim-start" ? "Trim Start" : "Trim End", domain: "how" as const,
57
- page: { id: "playback", label: "Playback" }, section: { id: "trim", label: "Trim" }, control: "text" as const,
56
+ binding, label: binding === "trim-start" ? "Trim Start" : "Trim End", domain: "when" as const,
57
+ page: { id: "playback", label: "Playback" }, section: { id: "trim", label: "Trim" }, control: "number" as const,
58
+ number: { suffixes: ["ms", "s", "f"], minimum: 0 },
58
59
  })),
59
60
  ...(["min-rate", "max-rate", "gain"] as const).map((binding) => ({
60
61
  binding, label: binding === "min-rate" ? "Minimum Rate" : binding === "max-rate" ? "Maximum Rate" : "Gain",
61
62
  domain: "how" as const, page: { id: "mix", label: "Mix" }, section: { id: "mix", label: "Mix" },
62
63
  control: "number" as const,
64
+ unit: "%", number: { scale: 100, minimum: 0, step: 1 },
63
65
  })),
64
66
  ...(["fade-in", "fade-out"] as const).map((binding) => ({
65
67
  binding, label: binding === "fade-in" ? "Fade In" : "Fade Out", domain: "when" as const,
66
- page: { id: "fade", label: "Fade" }, section: { id: "fade", label: "Fade" }, control: "text" as const,
68
+ page: { id: "fade", label: "Fade" }, section: { id: "fade", label: "Fade" }, control: "number" as const,
69
+ number: { suffixes: ["ms", "s", "f"], minimum: 0 },
67
70
  })),
68
71
  ],
69
72
  requiredValues: ["program"], project: projectAudio,
@@ -0,0 +1,58 @@
1
+ # `@hypit/build-result`
2
+
3
+ Build Results retain accepted public Outputs and the attempt's final outcome. Execution state belongs
4
+ to Runtime. A Result records which value satisfied each Output; a new Output record does not imply a
5
+ new media file.
6
+
7
+ ## Values and file ownership
8
+
9
+ | Value source | Persisted representation |
10
+ | --- | --- |
11
+ | New Resource without an existing durable address | One file owned by this Build |
12
+ | Explicit Workspace file | `external-file` with the Workspace's URI |
13
+ | Resource from an earlier Result | `build-file` with its original `build` and `path` |
14
+ | Entire earlier public Output | `build-output` pointing to its terminal Build and Output |
15
+ | Composite value | A Value Document whose Resource bindings follow the same rules at every depth |
16
+
17
+ A local `build-file` omits `build` when its owner is the containing Result. When imported into another
18
+ Build, its owner becomes explicit. Arrays and nested objects do not change ownership: a new Composite
19
+ can save new structure while referencing old video, audio and images. Only newly produced Resources
20
+ get new files. The encoder recognizes Resource values, independently of their domain Type or field name.
21
+
22
+ An `external-file` stays live. Replacing that file changes subsequent reads; deleting it leaves an
23
+ unavailable dependency. The stored size describes the file when admitted; `describeFile` and public
24
+ Output resolution obtain its current size. Readers use the explicit address without searching for
25
+ alternatives or comparing file contents.
26
+
27
+ ## Passing references through execution
28
+
29
+ Workspace attachments may supply a `location` URI. The Node compiler preserves it, and the CLI maps
30
+ these addresses and explicitly imported Result files to the compilation's Resource identities. It
31
+ passes that map as `BuildResultSeed.resourceReferences` alongside whole-Output `forwards`.
32
+
33
+ The Result writer keeps this per-Build map in its private writer state while publishing. It uses the
34
+ map for both direct Resource Outputs and all nested Resource bindings. Persistent Output documents
35
+ contain the references they actually use; private writer state is removed at the terminal outcome.
36
+ Core still receives ordinary typed Records, Candidates and Resource identities. It has no knowledge
37
+ of file paths or Result repositories. Runtime may stage input bytes for execution independently of
38
+ Result publication.
39
+
40
+ Repositories own address access. `describeFile` obtains metadata and `openFile` streams bytes or a byte
41
+ range. The filesystem and S3 implementations accept `ExternalFileAccess` for external addresses; the
42
+ Node default opens Workspace `file:` URIs. A custom adapter can supply a different address reader.
43
+ Selecting S3 for Result storage does not implicitly upload external file dependencies.
44
+
45
+ `fileReferenceIdentity` identifies the explicit URI or owning Build and path. Studio uses this identity
46
+ to group multiple uses of a file. Separate files remain separate even when their bytes happen to match.
47
+
48
+ An explicit `hypit get` export collects the selected Output's referenced bytes into its destination.
49
+ Composite exports rewrite bindings to local files, including references with different owners but the
50
+ same relative filename. This makes that export self-contained; ordinary Build publication preserves
51
+ references. Moving editable projects also requires preserving or updating their external file addresses.
52
+
53
+ Published Outputs can carry an optional author `displayName` for browsers such as Studio.
54
+ It is presentation text only: Output names, forwarding and file ownership do not change.
55
+
56
+ `updatePresentation` accepts `outputDisplayNames`, keyed by exact public Output name. Each
57
+ nonempty string sets that Output’s `displayName`; null removes it. Like title and note editing,
58
+ this applies to finished Results and preserves all values, file references and Output identifiers.
@@ -8,6 +8,7 @@
8
8
  ".": "./src/index.ts"
9
9
  },
10
10
  "dependencies": {
11
- "@hypit/protocol": "workspace:*"
11
+ "@hypit/protocol": "workspace:*",
12
+ "koffi": "3.2.1"
12
13
  }
13
14
  }
@@ -70,14 +70,9 @@ function resultPath(value: unknown, subject: string): string {
70
70
 
71
71
  function outputValue(value: unknown, subject: string): BuildResultOutputValue {
72
72
  const item = object(value, subject);
73
- if (item.kind === "build-file") {
73
+ if (item.kind === "build-file" || item.kind === "external-file") {
74
74
  assertBuildResultFileRef(item, subject);
75
- return {
76
- kind: "build-file",
77
- path: item.path,
78
- size: item.size,
79
- mediaType: item.mediaType,
80
- };
75
+ return { ...item };
81
76
  }
82
77
  if (item.kind === "build-output") {
83
78
  return {
@@ -106,6 +101,7 @@ function outputs(value: unknown, subject: string): Readonly<Record<string, Build
106
101
  text(name, `${subject} Output name`);
107
102
  const item = object(value, `${subject}[${JSON.stringify(name)}]`);
108
103
  return [name, {
104
+ ...(item.displayName === undefined ? {} : { displayName: text(item.displayName, `${subject}.displayName`) }),
109
105
  type: typeRef(item.type, `${subject}[${JSON.stringify(name)}].type`),
110
106
  value: outputValue(item.value, `${subject}[${JSON.stringify(name)}].value`),
111
107
  }] as const;
@@ -255,7 +251,7 @@ export function decodeBuildResultWriterState(
255
251
  if (logicalOutputs.has(output)) throw new Error(`${subject} publishes Logical Output ${output} more than once`);
256
252
  names.add(name);
257
253
  logicalOutputs.add(output);
258
- return { name, output };
254
+ return { name, output, ...(published.displayName === undefined ? {} : { displayName: text(published.displayName, `${subject}.displayName`) }) };
259
255
  });
260
256
  const forwardedOutputs = new Set<string>();
261
257
  const forwards = item.forwards.map((value, index) => {
@@ -270,6 +266,12 @@ export function decodeBuildResultWriterState(
270
266
  };
271
267
  });
272
268
  return {
269
+ ...(item.resourceReferences === undefined ? {} : { resourceReferences: Object.fromEntries(
270
+ Object.entries(object(item.resourceReferences, `${subject}.resourceReferences`)).map(([resource, reference]) => {
271
+ assertBuildResultFileRef(reference, `${subject}.resourceReferences.${resource}`);
272
+ return [resource, reference];
273
+ }),
274
+ ) }),
273
275
  resources: pathMap(item.resources, `${subject}.resources`),
274
276
  values: pathMap(item.values, `${subject}.values`),
275
277
  publishedOutputs,
@@ -0,0 +1,42 @@
1
+ import { createReadStream } from "node:fs";
2
+ import { stat } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { BuildResultFileRange, BuildResultFileRef } from "./types.js";
5
+
6
+ /** Address access belongs to the Repository adapter, not the value or domain graph. */
7
+ export type ExternalFileAccess = {
8
+ size(uri: string): Promise<number>;
9
+ open(uri: string, range?: BuildResultFileRange): Promise<AsyncIterable<Uint8Array>>;
10
+ };
11
+
12
+ /** The Node distribution's external files are explicit file: URLs supplied by its Workspace. */
13
+ export const localExternalFiles: ExternalFileAccess = {
14
+ async size(uri) { return (await stat(fileURLToPath(uri))).size; },
15
+ async open(uri, range) {
16
+ if (range !== undefined) {
17
+ const size = await localExternalFiles.size(uri);
18
+ if (!Number.isSafeInteger(range.start) || range.start < 0
19
+ || !Number.isSafeInteger(range.endExclusive) || range.endExclusive <= range.start
20
+ || range.endExclusive > size) throw new Error(`Invalid file range for ${uri}`);
21
+ }
22
+ return createReadStream(fileURLToPath(uri), range === undefined ? {} : { start: range.start, end: range.endExclusive - 1 });
23
+ },
24
+ };
25
+
26
+ export function fileReferenceIdentity(owner: string, file: BuildResultFileRef): string {
27
+ return file.kind === "external-file" ? JSON.stringify(["external", file.uri])
28
+ : JSON.stringify(["result", file.build ?? owner, file.path]);
29
+ }
30
+
31
+ /** Make a relative Result file reference independent of its enclosing value document. */
32
+ export function ownedFileReference(owner: string, file: BuildResultFileRef): BuildResultFileRef {
33
+ return file.kind === "build-file" ? { ...file, build: file.build ?? owner } : file;
34
+ }
35
+
36
+ /** Reopening an external reference reads its current metadata, without comparing versions. */
37
+ export async function currentFileReference(
38
+ file: BuildResultFileRef,
39
+ access: ExternalFileAccess = localExternalFiles,
40
+ ): Promise<BuildResultFileRef> {
41
+ return file.kind === "external-file" ? { ...file, size: await access.size(file.uri) } : file;
42
+ }
@@ -23,3 +23,6 @@ export {
23
23
  } from "./store.js";
24
24
  export type * from "./types.js";
25
25
  export type * from "./writer.js";
26
+
27
+ export { currentFileReference, fileReferenceIdentity, ownedFileReference, localExternalFiles } from "./file-reference.js";
28
+ export type { ExternalFileAccess } from "./file-reference.js";
@@ -0,0 +1,58 @@
1
+ import { toNamespacedPath } from "node:path";
2
+ import koffi from "koffi";
3
+
4
+ const kernel = koffi.load("kernel32.dll");
5
+ const createFile = kernel.func("intptr_t __stdcall CreateFileW(const char16_t *path, uint32_t access, uint32_t share, void *security, uint32_t disposition, uint32_t attributes, intptr_t templateFile)");
6
+ const setInformation = kernel.func("int __stdcall SetFileInformationByHandle(intptr_t file, int infoClass, void *info, uint32_t size)");
7
+ const closeHandle = kernel.func("int __stdcall CloseHandle(intptr_t file)");
8
+ const getLastError = kernel.func("uint32_t __stdcall GetLastError()");
9
+
10
+ const renameInfo = koffi.struct({
11
+ Flags: "uint32_t",
12
+ RootDirectory: "void *",
13
+ FileNameLength: "uint32_t",
14
+ FileName: koffi.array("uint16_t", 1),
15
+ });
16
+
17
+ const DELETE = 0x00010000;
18
+ const SHARE_READ_WRITE_DELETE = 0x7;
19
+ const OPEN_EXISTING = 3;
20
+ const FILE_ATTRIBUTE_NORMAL = 0x80;
21
+ const FileRenameInfoEx = 22;
22
+ const REPLACE_IF_EXISTS = 0x1;
23
+ const POSIX_SEMANTICS = 0x2;
24
+
25
+ function failure(syscall: string, from: string, to: string): Error {
26
+ const win32Code = getLastError() as number;
27
+ return Object.assign(new Error(`Windows file replacement failed (${win32Code}): '${from}' -> '${to}'`), {
28
+ code: `WIN32_${win32Code}`, win32Code, syscall, path: from, dest: to,
29
+ });
30
+ }
31
+
32
+ /**
33
+ * FileRenameInfoEx preserves open readers of the old file while publishing the new name.
34
+ * Node's Windows rename uses MoveFileExW, whose replacement semantics differ here.
35
+ * https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/4217551b-d2c0-42cb-9dc1-69a716cf6d0c
36
+ */
37
+ export function replaceWindowsFile(from: string, to: string): void {
38
+ const filename = Buffer.from(toNamespacedPath(to), "utf16le");
39
+ const nameOffset = koffi.offsetof(renameInfo, "FileName");
40
+ // Win32 requires a NUL-terminated FileName even though FileNameLength excludes
41
+ // that terminator. Buffer.alloc leaves the final WCHAR zero-initialized.
42
+ // https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-file_rename_info
43
+ const info = Buffer.alloc(Math.max(koffi.sizeof(renameInfo), nameOffset + filename.length + 2));
44
+ info.writeUInt32LE(REPLACE_IF_EXISTS | POSIX_SEMANTICS, koffi.offsetof(renameInfo, "Flags"));
45
+ info.writeUInt32LE(filename.length, koffi.offsetof(renameInfo, "FileNameLength"));
46
+ filename.copy(info, nameOffset);
47
+
48
+ const handle = createFile(toNamespacedPath(from), DELETE, SHARE_READ_WRITE_DELETE,
49
+ null, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0) as number | bigint;
50
+ if (handle === -1 || handle === -1n) throw failure("CreateFileW", from, to);
51
+ try {
52
+ if (!setInformation(handle, FileRenameInfoEx, info, info.length)) {
53
+ throw failure("SetFileInformationByHandle", from, to);
54
+ }
55
+ } finally {
56
+ closeHandle(handle);
57
+ }
58
+ }
@@ -0,0 +1,11 @@
1
+ import { rename } from "node:fs/promises";
2
+
3
+ /** Publish a complete file without changing the bytes held by existing readers. */
4
+ export async function replaceFile(from: string, to: string): Promise<void> {
5
+ if (process.platform === "win32") {
6
+ const { replaceWindowsFile } = await import("./replace-file-windows.js");
7
+ replaceWindowsFile(from, to);
8
+ } else {
9
+ await rename(from, to);
10
+ }
11
+ }
@@ -1,3 +1,5 @@
1
+ import { currentFileReference, localExternalFiles } from "./file-reference.js";
2
+ import type { ExternalFileAccess } from "./file-reference.js";
1
3
  import { randomUUID } from "node:crypto";
2
4
  import {
3
5
  mkdir,
@@ -40,6 +42,7 @@ import {
40
42
  encodeBuildResultManifest,
41
43
  } from "./decode.js";
42
44
  import { syncBuildResultOutputs } from "./writer.js";
45
+ import { replaceFile } from "./replace-file.js";
43
46
 
44
47
  const manifestName = "result.json";
45
48
  const writerStateName = ".writer.json";
@@ -83,6 +86,22 @@ export function applyBuildResultPresentation(
83
86
  if (highlighted.length === 0) delete base.highlightedOutputs;
84
87
  else base.highlightedOutputs = highlighted;
85
88
  }
89
+ if (update.outputDisplayNames !== undefined) {
90
+ const outputs = { ...manifest.outputs };
91
+ for (const [name, value] of Object.entries(update.outputDisplayNames)) {
92
+ const entry = manifest.outputs[name];
93
+ assert(Object.hasOwn(manifest.outputs, name) && entry !== undefined,
94
+ `Build Result ${manifest.id} has no Output ${name}`);
95
+ const { displayName: _priorName, ...rest } = entry;
96
+ if (value === null) outputs[name] = rest;
97
+ else {
98
+ const displayName = value.trim();
99
+ assert(displayName.length > 0, "Output display name must not be empty");
100
+ outputs[name] = { ...rest, displayName };
101
+ }
102
+ }
103
+ return { ...base, outputs };
104
+ }
86
105
  return base;
87
106
  }
88
107
 
@@ -95,7 +114,7 @@ async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
95
114
  const temporary = `${path}.part-${randomUUID()}`;
96
115
  try {
97
116
  await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { flag: "wx" });
98
- await rename(temporary, path);
117
+ await replaceFile(temporary, path);
99
118
  } catch (error) {
100
119
  await rm(temporary, { force: true });
101
120
  throw error;
@@ -123,7 +142,7 @@ async function copyArtifactAtomic(
123
142
  await output.write(chunk);
124
143
  }
125
144
  await output.close();
126
- await rename(temporary, destination);
145
+ await replaceFile(temporary, destination);
127
146
  } catch (error) {
128
147
  await output.close().catch(() => undefined);
129
148
  await rm(temporary, { force: true });
@@ -221,6 +240,7 @@ export async function resolveBuildResultOutput(
221
240
  root: string,
222
241
  build: string,
223
242
  output: string,
243
+ externalFiles: ExternalFileAccess = localExternalFiles,
224
244
  ): Promise<ResolvedBuildResultOutput | undefined> {
225
245
  const seen = new Set<string>();
226
246
  let currentBuild = build;
@@ -254,14 +274,14 @@ export async function resolveBuildResultOutput(
254
274
  };
255
275
  }
256
276
  const terminalKind = (entry.value as { readonly kind?: unknown }).kind;
257
- assert(terminalKind === "build-file" || terminalKind === "inline",
277
+ assert(terminalKind === "build-file" || terminalKind === "external-file" || terminalKind === "inline",
258
278
  `Build ${currentBuild} Output ${currentOutput} has unsupported Result value kind ${String(terminalKind)}`);
259
279
  return {
260
280
  build: currentBuild,
261
281
  output: currentOutput,
262
282
  directory,
263
283
  type: entry.type,
264
- value: entry.value,
284
+ value: entry.value.kind === "external-file" ? await currentFileReference(entry.value, externalFiles) : entry.value,
265
285
  };
266
286
  }
267
287
  }
@@ -270,6 +290,7 @@ export async function describeBuildResultOutput(
270
290
  root: string,
271
291
  build: string,
272
292
  output: string,
293
+ externalFiles: ExternalFileAccess = localExternalFiles,
273
294
  ): Promise<RepositoryBuildResultOutputDescription | undefined> {
274
295
  const seen = new Set<string>();
275
296
  let currentBuild = build;
@@ -286,8 +307,9 @@ export async function describeBuildResultOutput(
286
307
  currentOutput = entry.value.output;
287
308
  continue;
288
309
  }
289
- return entry.value.kind === "build-file"
290
- ? { type: entry.type, kind: "resource", size: entry.value.size, mediaType: entry.value.mediaType }
310
+ const file = entry.value.kind === "external-file" ? await currentFileReference(entry.value, externalFiles) : entry.value;
311
+ return file.kind === "build-file" || file.kind === "external-file"
312
+ ? { type: entry.type, kind: "resource", size: file.size, mediaType: file.mediaType }
291
313
  : entry.value.kind === "value"
292
314
  ? { type: entry.type, kind: "composite" }
293
315
  : { type: entry.type, kind: "scalar" };
@@ -334,12 +356,12 @@ export class FileBuildResult {
334
356
  this.directory = resolve(directory);
335
357
  }
336
358
 
337
- static async create(root: string, seed: BuildResultSeed): Promise<FileBuildResult> {
359
+ static async create(root: string, seed: BuildResultSeed, externalFiles: ExternalFileAccess = localExternalFiles): Promise<FileBuildResult> {
338
360
  assertOrderedBuildId(seed.id);
339
361
  assertBuildResultSeed(seed);
340
362
  const forwards = await normalizeBuildResultForwards({
341
363
  read: async (build) => await readBuildResult(buildResultDirectory(root, build)),
342
- resolve: async (build, output) => await resolveBuildResultOutput(root, build, output),
364
+ resolve: async (build, output) => await resolveBuildResultOutput(root, build, output, externalFiles),
343
365
  }, seed.forwards ?? []);
344
366
  const directory = buildResultDirectory(root, seed.id);
345
367
  await mkdir(dirname(directory), { recursive: true });
@@ -360,6 +382,7 @@ export class FileBuildResult {
360
382
  resources: {},
361
383
  values: {},
362
384
  publishedOutputs: seed.publishedOutputs,
385
+ ...(seed.resourceReferences === undefined ? {} : { resourceReferences: seed.resourceReferences }),
363
386
  forwards,
364
387
  });
365
388
  await rename(temporary, directory);
@@ -405,6 +428,7 @@ export class FileBuildResult {
405
428
  },
406
429
  },
407
430
  });
431
+ if (!updated.changed) return manifest;
408
432
  await writeJsonAtomic(join(this.directory, writerStateName), updated.writer);
409
433
  await writeManifestAtomic(join(this.directory, manifestName), updated.manifest);
410
434
  return updated.manifest;
@@ -434,15 +458,17 @@ export class FileBuildResult {
434
458
 
435
459
  /** Default zero-configuration project repository backed by one ordinary directory tree. */
436
460
  export class FileBuildResultRepository implements BuildResultRepository {
461
+ readonly externalFiles: ExternalFileAccess;
437
462
  readonly root: string;
438
463
 
439
- constructor(root: string) {
464
+ constructor(root: string, externalFiles: ExternalFileAccess = localExternalFiles) {
465
+ this.externalFiles = externalFiles;
440
466
  assert(root.trim().length > 0, "Build Result root must not be empty");
441
467
  this.root = resolve(root);
442
468
  }
443
469
 
444
470
  async create(seed: BuildResultSeed): Promise<FileBuildResult> {
445
- return await FileBuildResult.create(this.root, seed);
471
+ return await FileBuildResult.create(this.root, seed, this.externalFiles);
446
472
  }
447
473
 
448
474
  async openWriter(build: string): Promise<FileBuildResult | undefined> {
@@ -477,22 +503,27 @@ export class FileBuildResultRepository implements BuildResultRepository {
477
503
  }
478
504
 
479
505
  async describeOutput(build: string, output: string): Promise<RepositoryBuildResultOutputDescription | undefined> {
480
- return await describeBuildResultOutput(this.root, build, output);
506
+ return await describeBuildResultOutput(this.root, build, output, this.externalFiles);
481
507
  }
482
508
 
483
509
  async resolve(build: string, output: string): Promise<RepositoryBuildResultOutput | undefined> {
484
- const resolvedOutput = await resolveBuildResultOutput(this.root, build, output);
510
+ const resolvedOutput = await resolveBuildResultOutput(this.root, build, output, this.externalFiles);
485
511
  if (resolvedOutput === undefined) return undefined;
486
512
  const { directory: _directory, ...portable } = resolvedOutput;
487
513
  return portable;
488
514
  }
489
515
 
516
+ async describeFile(_build: string, file: BuildResultFileRef): Promise<BuildResultFileRef> {
517
+ return await currentFileReference(file, this.externalFiles);
518
+ }
519
+
490
520
  async openFile(
491
521
  build: string,
492
522
  file: BuildResultFileRef,
493
523
  range?: BuildResultFileRange,
494
524
  ): Promise<AsyncIterable<Uint8Array> | undefined> {
495
- const directory = buildResultDirectory(this.root, build);
525
+ if (file.kind === "external-file") return await this.externalFiles.open(file.uri, range);
526
+ const directory = buildResultDirectory(this.root, file.build ?? build);
496
527
  const path = containedResultPath(directory, file.path);
497
528
  if (!await exists(path)) return undefined;
498
529
  if (range === undefined) return createReadStream(path);