@hypit/hypit 0.1.3 → 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.
- package/README.md +1 -1
- package/dist/public/studio-adapter.d.ts +31 -2
- package/package.json +4 -1
- package/packages/audio-track-studio/src/index.ts +7 -4
- package/packages/build-result/README.md +58 -0
- package/packages/build-result/package.json +2 -1
- package/packages/build-result/src/decode.ts +10 -8
- package/packages/build-result/src/file-reference.ts +42 -0
- package/packages/build-result/src/index.ts +3 -0
- package/packages/build-result/src/replace-file-windows.ts +58 -0
- package/packages/build-result/src/replace-file.ts +11 -0
- package/packages/build-result/src/store.ts +44 -13
- package/packages/build-result/src/types.ts +38 -8
- package/packages/build-result/src/writer.ts +12 -1
- package/packages/build-result-fs/README.md +23 -1
- package/packages/build-result-s3/README.md +5 -0
- package/packages/build-result-s3/src/repository.ts +19 -5
- package/packages/caption-fine/README.md +17 -2
- package/packages/caption-fine/src/manifest.ts +1 -1
- package/packages/caption-fine/src/render.ts +48 -36
- package/packages/caption-fine-studio/package.json +8 -3
- package/packages/caption-fine-studio/src/index.ts +10 -1
- package/packages/cli/README.md +25 -0
- package/packages/cli/src/arguments.ts +16 -11
- package/packages/cli/src/build-planning.ts +6 -1
- package/packages/cli/src/command.ts +1 -1
- package/packages/cli/src/commands/environment.ts +42 -13
- package/packages/cli/src/machine-view.ts +12 -3
- package/packages/cli/src/main.ts +4 -0
- package/packages/cli/src/output.ts +28 -12
- package/packages/cli/src/result-export.ts +22 -13
- package/packages/cli/src/run-file.ts +19 -8
- package/packages/comment-sticker-studio/src/index.ts +2 -1
- package/packages/compiler-node/src/compiler.ts +1 -0
- package/packages/estimate/README.md +56 -3
- package/packages/estimate/src/program.ts +7 -8
- package/packages/fonts-open/README.md +5 -0
- package/packages/fonts-open/package.json +5 -1
- package/packages/fonts-open/src/studio.ts +21 -0
- package/packages/media-track-studio/src/index.ts +7 -2
- package/packages/provider-whisperx-local/README.md +57 -0
- package/packages/ranking/src/render.ts +11 -9
- package/packages/ranking-studio/README.md +11 -0
- package/packages/ranking-studio/package.json +8 -3
- package/packages/ranking-studio/src/index.ts +11 -4
- package/packages/runtime/src/catalog.ts +2 -0
- package/packages/runtime-host-node/src/index.ts +1 -0
- package/packages/runtime-local/README.md +19 -0
- package/packages/runtime-local/src/programs.ts +39 -24
- package/packages/runtime-local/src/runtime.ts +2 -0
- package/packages/script/README.md +22 -0
- package/packages/script/src/edit.ts +123 -73
- package/packages/script/src/lexical.ts +40 -6
- package/packages/script/src/parser.ts +36 -8
- package/packages/script/src/types.ts +4 -0
- package/packages/seedance-kits/README.md +3 -1
- package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
- package/packages/speech-alignment/src/align.ts +45 -21
- package/packages/studio/INSPECTOR.md +104 -0
- package/packages/studio/README.md +65 -4
- package/packages/studio/src/build-library.ts +99 -84
- package/packages/studio/src/library-media.ts +19 -0
- package/packages/studio/src/parameter-values.ts +41 -1
- package/packages/studio/src/parameters.ts +20 -21
- package/packages/studio/src/server.ts +58 -30
- package/packages/studio/src/shared.ts +20 -6
- package/packages/studio/src/studio-registry.ts +22 -0
- package/packages/studio/src/style.css +135 -144
- package/packages/studio/src/temporal-edit.ts +131 -0
- package/packages/studio/src/ui/artifact-name.ts +80 -0
- package/packages/studio/src/ui/artifact-preview.ts +99 -0
- package/packages/studio/src/ui/icons.ts +5 -1
- package/packages/studio/src/ui/library.ts +373 -192
- package/packages/studio/src/ui/main.ts +153 -31
- package/packages/studio/src/ui/material-preview.ts +1 -1
- package/packages/studio/src/ui/overlay.ts +21 -20
- package/packages/studio/src/ui/sidebar-panel.ts +31 -0
- package/packages/studio/src/ui/stage.ts +124 -8
- package/packages/studio/src/ui/timeline.ts +16 -179
- package/packages/studio/start.ts +15 -3
- package/packages/studio-adapter/README.md +21 -0
- package/packages/studio-adapter/src/index.ts +26 -1
- package/packages/temporal-markup/EDITING.md +183 -0
- package/packages/temporal-markup/README.md +4 -0
- package/packages/typography-track-studio/package.json +8 -3
- package/packages/typography-track-studio/src/index.ts +5 -1
- package/packages/video-cli/README.md +12 -3
- package/packages/video-cli/src/creation.ts +11 -5
- package/packages/whisperx/README.md +7 -0
- package/packages/workspace/src/index.ts +2 -0
- package/packages/workspace-fs-node/README.md +4 -1
- package/packages/workspace-fs-node/src/workspace.ts +3 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Companion-owned Inspector fields
|
|
2
|
+
|
|
3
|
+
The Inspector asks three questions. **Where** describes spatial placement and organization;
|
|
4
|
+
**When** describes scheduling, playback and changes over time; **How** describes appearance,
|
|
5
|
+
content and sound. These are navigation domains, not restrictions on component behavior.
|
|
6
|
+
|
|
7
|
+
| Domain | Useful pages and sections |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Where | Position, size, anchor, fitting/cropping, padding, clipping, stacking, layout |
|
|
10
|
+
| When | Placement in time, playback/cycling, source trim, entrance, sustained motion, exit, transitions, fades |
|
|
11
|
+
| How | Text/content, typography, fill/stroke, image adjustments, shadows, effect character, audio levels |
|
|
12
|
+
|
|
13
|
+
Each Companion supplies page and section IDs and labels in declaration order. These examples are
|
|
14
|
+
a shared vocabulary, not a registry of permitted pages. A new component can name a useful grouping
|
|
15
|
+
without a Studio change. Put a field where its authoring decision belongs: motion duration is When,
|
|
16
|
+
its color is How, and the object's frame is Where. One field has one owner and one location.
|
|
17
|
+
|
|
18
|
+
## Controls describe the editing task
|
|
19
|
+
|
|
20
|
+
`bindings` exposes actual author endpoints; `inspector` chooses which to show. A resolved field is
|
|
21
|
+
shown only when its binding is writable. The finite controls are independent of the three domains:
|
|
22
|
+
|
|
23
|
+
| Control | Use |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `text` | Text, identifiers, or a meaningful expression. `multiline: true` provides a textarea. |
|
|
26
|
+
| `number` | A scalar with optional limits, stepping, display scaling, or an authored suffix. |
|
|
27
|
+
| `boolean` | A two-state switch. |
|
|
28
|
+
| `select` | A finite choice, including presets, font families or numeric weights. |
|
|
29
|
+
| `color` | Hex text and a native color picker; optional `swatches` offers package-chosen colors. |
|
|
30
|
+
| `list` | An ordered value described by an array schema, such as a palette or repeated settings. |
|
|
31
|
+
| `record` | Named values described by an object schema, such as one shadow's parameters. |
|
|
32
|
+
|
|
33
|
+
Lists and records retain Apply/Reset drafts. Their schema owns value shape. There is no separate
|
|
34
|
+
palette database, font control protocol, generic CSS editor, or component-supplied DOM renderer.
|
|
35
|
+
Use text for an expression whose parts cannot meaningfully be edited as one number.
|
|
36
|
+
|
|
37
|
+
## Separate author values from display values
|
|
38
|
+
|
|
39
|
+
The DTO retains the authored `value`. A numeric presentation describes the small, reversible
|
|
40
|
+
conversion into the editor. The server applies the inverse, validates the **authored** value against
|
|
41
|
+
its public schema, and uses the author language's serializer. Source ranges and existing transactions
|
|
42
|
+
still own the write. Failed validation or recompilation does not publish a partial edit.
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
// Authored as "78%" or "240px": edit only the magnitude, keep the existing unit.
|
|
46
|
+
{ binding: "frame.width", label: "Width", domain: "where",
|
|
47
|
+
section: { id: "size", label: "Size" }, control: "number",
|
|
48
|
+
number: { suffixes: ["%", "px"], step: 1 } }
|
|
49
|
+
|
|
50
|
+
// Authored as 0.78: display 78 %, write 0.42 when the author enters 42.
|
|
51
|
+
{ binding: "appearance.opacity", label: "Opacity", domain: "how",
|
|
52
|
+
section: { id: "image", label: "Image" }, control: "number", unit: "%",
|
|
53
|
+
number: { scale: 100, minimum: 0, maximum: 100, step: 1 } }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`unit` is a label, not an instruction to append text. `suffixes` explicitly declares a string-valued
|
|
57
|
+
number and retains whichever admitted suffix the current source uses. It does not convert `%` into
|
|
58
|
+
pixels or seconds into frames. `scale` is positive; displayed value = authored magnitude × scale.
|
|
59
|
+
Limits and step in `number` refer to displayed values. A number schema's limits and integer setting
|
|
60
|
+
also inform the input when no presentation override is provided. Empty input is not silently zero.
|
|
61
|
+
|
|
62
|
+
SVML strings escape attribute delimiters and entity characters. SVS values use `formatSvsValue`.
|
|
63
|
+
Neither the UI nor a Companion writes source text through an arbitrary callback. Timeline temporal
|
|
64
|
+
gestures are separate from Inspector field conversion; see the temporal author forms for their
|
|
65
|
+
semantic-anchor and local-offset editing behavior.
|
|
66
|
+
|
|
67
|
+
## Choices retain their actual value
|
|
68
|
+
|
|
69
|
+
Existing string options are shorthand for identical value and label. Rich options can supply a
|
|
70
|
+
different label, description and one small visual hint:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
options: [
|
|
74
|
+
{ value: "brand-serif", label: "Brand Serif", description: "Project headline face",
|
|
75
|
+
preview: { kind: "font", family: "Project Serif", sample: "Aa 字幕" } },
|
|
76
|
+
{ value: "brand-sans", label: "Brand Sans" },
|
|
77
|
+
]
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
A color hint uses `{ kind: "color", color: "#FF584A" }`. Values may be strings, numbers or booleans;
|
|
81
|
+
the selected value, not its display label, is validated and written. Preview hints contain data only.
|
|
82
|
+
Options remain named keyboard-operable choices. Font hints use a family available to the Studio
|
|
83
|
+
document; declaring a hint does not install or load a font or change the composition's font bytes.
|
|
84
|
+
|
|
85
|
+
The optional `@hypit/fonts-open/studio` helper owns its catalog labels and descriptions. Caption,
|
|
86
|
+
Typography and Ranking Companions use it to reach a Style's referenced font and edit its `family`
|
|
87
|
+
attribute. It appears for an actual writable family attribute; a `media:Font` file has no such
|
|
88
|
+
attribute. The family still has to supply the authored weight and style. A local font keeps its exact
|
|
89
|
+
file binding. Changing a shared font changes every consumer; this is visible Source reuse.
|
|
90
|
+
|
|
91
|
+
Nested `referenced` declarations follow explicit author references, such as Style → Font → family.
|
|
92
|
+
They neither scan for candidates nor replace whole-value references with strings. Project components
|
|
93
|
+
can declare their own option values and binding paths through the same ABI.
|
|
94
|
+
|
|
95
|
+
## Implementation owners
|
|
96
|
+
|
|
97
|
+
- `studio-adapter` owns the data-only declarations and snapshot types.
|
|
98
|
+
- Component Companions own grouping, choices, units and references to writable author facts.
|
|
99
|
+
- Studio resolves endpoints, renders controls, converts display values and commits source edits.
|
|
100
|
+
- SVML/SVS and the component's ordinary compilation remain the authority for valid authored work.
|
|
101
|
+
|
|
102
|
+
The UI follows native numeric input behavior and named listbox options; reference material:
|
|
103
|
+
[MDN numeric inputs](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/input/number),
|
|
104
|
+
[WAI listbox pattern](https://www.w3.org/WAI/ARIA/apg/patterns/listbox/).
|
|
@@ -14,11 +14,55 @@ When `--runtime` is omitted, Studio reads the resolved project's `.hypit/runtime
|
|
|
14
14
|
selection made by `hypit runtime use`. It does not search parent projects for a
|
|
15
15
|
Profile. `--workspace` explicitly selects the project boundary; `--package-root`
|
|
16
16
|
overrides the author package resolution root when those locations intentionally differ.
|
|
17
|
+
Startup prints the project, Run, Profile and whether the Profile came from the command argument or
|
|
18
|
+
the project's selection file. Relative command-line paths start at the invoking directory;
|
|
19
|
+
`--workspace` selects the project without rebasing `--run` or `--runtime`.
|
|
17
20
|
The upper-left library is intentionally not a filesystem browser:
|
|
18
21
|
|
|
19
22
|
- Source is the exact Run + Author closure and writes back only the selected file;
|
|
20
|
-
- Tasks
|
|
21
|
-
-
|
|
23
|
+
- Tasks show one card per Build, with All, In progress and Finished filters. Active progress comes from
|
|
24
|
+
the selected Runtime's read-only control interface; completed, failed and cancelled Builds come
|
|
25
|
+
from the project's Result Repository. Saving a Result and an issue requiring attention remain
|
|
26
|
+
visible, with the source Run and available progress or failure reason.
|
|
27
|
+
- Artifacts show top-level file Outputs whose declared MIME type is image, video or audio,
|
|
28
|
+
including files already published by ongoing Builds. Composite Outputs retain their structure:
|
|
29
|
+
the library does not extract their embedded resources. Forwarded file Outputs resolve to their
|
|
30
|
+
owner; references to the same owner file share one card and retain their origins. Highlighted
|
|
31
|
+
Outputs sort first, without excluding other media.
|
|
32
|
+
|
|
33
|
+
Source, Tasks and Artifacts use the shared `ui/sidebar-panel.ts` layout and navigation buttons. Each view
|
|
34
|
+
owns its toolbar content, data and actions; the shared shell owns geometry and navigation styling.
|
|
35
|
+
|
|
36
|
+
Views load on entry; returning to Tasks preserves its loaded list and selection. Explicit Refresh reads the latest state. There is no library polling.
|
|
37
|
+
Refresh replaces the current snapshot; scrolling to the end loads more finished Results. Failed
|
|
38
|
+
refreshes preserve the last view and show the error. Artifacts show project media through an icon sidebar: All media, Videos, Images and Audio.
|
|
39
|
+
The header names the category and offers Refresh. Selecting a Task adds its name beside the category in the existing header, establishing a session-local task context without shifting the list; its arrow opens that Build's media. The context remains across tab changes. Click its name to return to the selected Task, or clear it to browse project media. Task selection does not write Source, Result or Runtime state. Selected tasks expand the complete error text with a copy action. Source details are available on each media card. A media category is sent to the server, which
|
|
40
|
+
walks Result metadata pages until it collects a useful batch of matching files or reaches the end.
|
|
41
|
+
This keeps Build storage unchanged while avoiding empty category pages. The browser appends
|
|
42
|
+
older matches on scroll; Refresh reads the latest results for the current category.
|
|
43
|
+
|
|
44
|
+
Media cards use fixed-size square cells, contain the file at its own aspect ratio, and show up to two
|
|
45
|
+
wrapped name lines below. Selection colors the thumbnail area and name separately, without an
|
|
46
|
+
outer card border. Resizing the
|
|
47
|
+
sidebar distributes spare width between columns, then adds a column when another fixed-size
|
|
48
|
+
card fits. Card size stays unchanged. Video thumbnails decode a frame in the browser; audio waveforms are also
|
|
49
|
+
browser presentation, not additional Result files. Thumbnails load as their cells enter view.
|
|
50
|
+
The CLI carries the author's component name from compilation provenance into the Result's
|
|
51
|
+
optional Output `displayName`. The public Output identifier stays intact for references;
|
|
52
|
+
Outputs without a display name show that identifier unchanged. Double-click a name or press F2
|
|
53
|
+
to edit it in full; Enter or blur saves, Escape cancels. Failed saves retain the text and show the
|
|
54
|
+
reason. A completed, failed or cancelled Build's name is written through Result Repository
|
|
55
|
+
`updatePresentation` to that exact Output's `displayName`. Ongoing Builds become editable when
|
|
56
|
+
they finish, following the existing Result presentation-edit boundary. Renaming never changes
|
|
57
|
+
Output identity, referenced media files or other Builds' names for the same file.
|
|
58
|
+
|
|
59
|
+
The composition has a frame-snapped progress slider and current/total time.
|
|
60
|
+
Clicking a card opens the file in the central preview and pauses the composition. Images need
|
|
61
|
+
no transport; video and audio share the play/mute controls and a seconds-based scrubber above
|
|
62
|
+
the playback bar. The previous/next buttons skip five seconds for media, and step one frame
|
|
63
|
+
for the composition. Back to composition restores the existing composition playhead. Selecting
|
|
64
|
+
or seeking in the composition timeline also returns to it. Opening media changes no Source,
|
|
65
|
+
Run, or Candidate selection.
|
|
22
66
|
|
|
23
67
|
No project manifest, Studio database, output-directory scan or inferred campaign
|
|
24
68
|
folder structure is involved. With no selected Runtime, finished Result tasks and
|
|
@@ -32,7 +76,10 @@ The selected targets must reach one Film and its resolved time source. Studio re
|
|
|
32
76
|
distinct Films in one view; use separate Runs/sessions for those. A SemanticTrack supplies the
|
|
33
77
|
Script lane and performance timing, including wordless Segments with media spans. A declared
|
|
34
78
|
ProgramSpace supplies authored animation time without a Script lane. Both show their component
|
|
35
|
-
tracks and use the same rendering and parameter-editing machinery.
|
|
79
|
+
tracks and use the same rendering and parameter-editing machinery. When the snapshot contains
|
|
80
|
+
a semantic timeline, its lane and label stay pinned directly below the time ruler while
|
|
81
|
+
component tracks scroll. Its presence follows the resolved semantic timeline, including
|
|
82
|
+
wordless content, rather than a component name or the presence of spoken words.
|
|
36
83
|
|
|
37
84
|
Open the URL printed by Vite. Studio requests port 5179 by default, accepts `--port`,
|
|
38
85
|
and Vite can choose another available port when it is occupied. Reuse that process
|
|
@@ -52,11 +99,25 @@ Timeline gestures use explicit temporal authority. Moving a shared Selection or
|
|
|
52
99
|
Moment edits Script and moves its consumers after recompilation. A parameter-based
|
|
53
100
|
handle edits its exact authored parameter. Fixed or derived values with no supported
|
|
54
101
|
inverse remain read-only. Seeing an entity does not promise every drag gesture.
|
|
102
|
+
|
|
103
|
+
A direct `during={selection}` move advances both endpoints by the same number of semantic
|
|
104
|
+
stops (distinct frame positions), so its duration may change. `at/for` moves its event and
|
|
105
|
+
offers a trailing duration trim; `until/for` offers the corresponding leading trim.
|
|
106
|
+
An Instant reference expression edits only its offset; a bare reference has an implicit zero offset.
|
|
107
|
+
`start/end` trims edit the corresponding expression; moving the window shifts both by the same
|
|
108
|
+
frame delta. Edited clock values and offsets are written in frames at the current ProgramSpace rate.
|
|
109
|
+
The semantic marker Inspector exposes exact anchor identities and, where a
|
|
110
|
+
declared handle supports it, offers choices among coincident anchors.
|
|
111
|
+
See [temporal author forms](../temporal-markup/EDITING.md) for the complete behavior.
|
|
112
|
+
|
|
55
113
|
Tasks and Artifacts are inspection surfaces; selecting an Artifact does not write a
|
|
56
114
|
Run Candidate. Use `build-record`/`satisfy` in the Run for explicit Output reuse.
|
|
57
115
|
|
|
58
116
|
## Component presentation
|
|
59
117
|
|
|
118
|
+
[Inspector fields](INSPECTOR.md) describes Where/When/How grouping, numeric unit conversion,
|
|
119
|
+
rich choices, fonts, color suggestions and exact Source writeback.
|
|
120
|
+
|
|
60
121
|
Studio is an application boundary. Core and domain computation do not import it or
|
|
61
122
|
register UI metadata. The installed Distribution explicitly selects one independent
|
|
62
123
|
Studio Companion per supported official domain. Packages actually selected by the
|
|
@@ -82,7 +143,7 @@ source binding is never shown merely because Studio can reach it. Material layer
|
|
|
82
143
|
Resource id or Surface identity, never a Studio HTTP URL. Studio always owns
|
|
83
144
|
time formatting and transport resolution, so chrome and material cannot hide a
|
|
84
145
|
title or its time.
|
|
85
|
-
Studio owns session-wide behavior and chrome: Companion assembly,
|
|
146
|
+
Studio owns session-wide behavior and chrome: Companion assembly, single-row overlap display,
|
|
86
147
|
fallback defaults, selection treatment, playback, zoom,
|
|
87
148
|
scrolling, the finite Inspector control set and source mutation transport. A
|
|
88
149
|
companion cannot ship arbitrary DOM or CSS into the application.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fileReferenceIdentity } from "@hypit/build-result";
|
|
1
2
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
3
|
|
|
3
4
|
import { buildIdCreatedAt } from "@hypit/protocol";
|
|
@@ -5,17 +6,16 @@ import type { BuildView, NodeRuntimeHost, RuntimeHostTransientExecution } from "
|
|
|
5
6
|
import type { StoredValue, TypeRef } from "@hypit/protocol";
|
|
6
7
|
import type {
|
|
7
8
|
BuildResultFileRange,
|
|
8
|
-
BuildResultFileRef,
|
|
9
9
|
BuildResultManifest,
|
|
10
10
|
BuildResultRepository,
|
|
11
11
|
FinishedBuildResultManifest,
|
|
12
|
-
RepositoryBuildResultOutput,
|
|
13
12
|
} from "@hypit/build-result";
|
|
14
13
|
|
|
15
14
|
import { resolveBuildResultValue } from "@hypit/cli";
|
|
16
15
|
import { videoCliDistribution } from "@hypit/video-cli";
|
|
17
16
|
|
|
18
|
-
import type { StudioArtifactView, StudioLibraryView, StudioTaskView } from "./shared.js";
|
|
17
|
+
import type { StudioArtifactView, StudioLibraryRequest, StudioLibraryView, StudioTaskView } from "./shared.js";
|
|
18
|
+
import { mergeStudioArtifacts } from "./library-media.js";
|
|
19
19
|
|
|
20
20
|
type RuntimeControl = Awaited<ReturnType<NodeRuntimeHost["openControl"]>>;
|
|
21
21
|
|
|
@@ -24,7 +24,8 @@ export type StudioBuildLibrary = {
|
|
|
24
24
|
readonly runtime?: Pick<RuntimeControl, "activity">;
|
|
25
25
|
/** Runtime-owned, disposable execution for the current authoring session. */
|
|
26
26
|
readonly transientExecution?: RuntimeHostTransientExecution;
|
|
27
|
-
readonly library: (
|
|
27
|
+
readonly library: (request: StudioLibraryRequest) => Promise<StudioLibraryView>;
|
|
28
|
+
readonly renameArtifact: (build: string, output: string, displayName: string | null) => Promise<string | undefined>;
|
|
28
29
|
readonly resolveHistoricalOutput: (
|
|
29
30
|
build: string,
|
|
30
31
|
output: string,
|
|
@@ -36,7 +37,6 @@ export type StudioBuildLibrary = {
|
|
|
36
37
|
readonly openArtifact: (
|
|
37
38
|
build: string,
|
|
38
39
|
output: string,
|
|
39
|
-
valuePath: string,
|
|
40
40
|
) => Promise<{
|
|
41
41
|
readonly mediaType: string;
|
|
42
42
|
readonly size: number;
|
|
@@ -51,7 +51,7 @@ function projectPath(root: string, path: string): string {
|
|
|
51
51
|
|
|
52
52
|
function isWithin(root: string, path: string): boolean {
|
|
53
53
|
const rel = relative(resolve(root), projectPath(root, path));
|
|
54
|
-
return rel === "" || (!rel.startsWith(
|
|
54
|
+
return rel === "" || (rel !== ".." && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -65,15 +65,9 @@ function presentedPath(root: string, path: string): string {
|
|
|
65
65
|
return (relative(resolve(root), absolute) || ".").split(sep).join("/");
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
function buildBelongsTo(root: string, view: BuildView): boolean {
|
|
69
|
-
return view.source !== undefined
|
|
70
|
-
&& (isWithin(root, view.run?.path ?? view.source.path) || isWithin(root, view.source.path));
|
|
71
|
-
}
|
|
72
|
-
|
|
73
68
|
function taskStatus(view: BuildView): StudioTaskView["status"] {
|
|
74
|
-
if (view.issue !== undefined) return "
|
|
69
|
+
if (view.issue !== undefined) return "attention";
|
|
75
70
|
if (view.activity === "submitting" || view.activity === "ready") return "queued";
|
|
76
|
-
if (view.activity === "saving-result") return "active";
|
|
77
71
|
return view.activity;
|
|
78
72
|
}
|
|
79
73
|
|
|
@@ -83,18 +77,20 @@ function taskView(
|
|
|
83
77
|
result?: BuildResultManifest,
|
|
84
78
|
): StudioTaskView {
|
|
85
79
|
if (view.source === undefined) throw new Error(`Active Build ${view.id} has no source`);
|
|
80
|
+
const detail = view.issue?.message ?? view.stop?.reason;
|
|
86
81
|
return {
|
|
87
82
|
id: view.id,
|
|
88
83
|
...(result?.title === undefined ? {} : { title: result.title }),
|
|
89
84
|
...(result?.note === undefined ? {} : { note: result.note }),
|
|
90
85
|
...(result?.highlightedOutputs === undefined ? {} : { highlightedOutputs: result.highlightedOutputs }),
|
|
91
86
|
createdAt: view.createdAt,
|
|
87
|
+
ongoing: true,
|
|
92
88
|
status: taskStatus(view),
|
|
89
|
+
...(detail === undefined ? {} : { detail }),
|
|
90
|
+
...(view.requests === undefined ? {} : { requests: view.requests }),
|
|
93
91
|
source: presentedPath(root, view.source.path),
|
|
94
92
|
...(view.run === undefined ? {} : { run: presentedPath(root, view.run.path) }),
|
|
95
93
|
targets: result?.targets ?? view.targets,
|
|
96
|
-
acceptedRecords: view.acceptedRecords,
|
|
97
|
-
outstandingCommands: view.outstandingCommands,
|
|
98
94
|
operations: view.operations.map((operation) => ({
|
|
99
95
|
status: operation.status,
|
|
100
96
|
...(operation.progress?.phase === undefined ? {} : { phase: operation.progress.phase }),
|
|
@@ -112,107 +108,117 @@ function resultTaskView(root: string, result: FinishedBuildResultManifest): Stud
|
|
|
112
108
|
...(result.note === undefined ? {} : { note: result.note }),
|
|
113
109
|
...(result.highlightedOutputs === undefined ? {} : { highlightedOutputs: result.highlightedOutputs }),
|
|
114
110
|
createdAt: buildIdCreatedAt(result.id)!,
|
|
111
|
+
finishedAt: result.finishedAt,
|
|
112
|
+
ongoing: false,
|
|
115
113
|
status: result.outcome,
|
|
114
|
+
...(result.failure === undefined ? {} : { detail: result.failure }),
|
|
116
115
|
source: presentedPath(root, result.source.path),
|
|
117
116
|
...(result.run === undefined ? {} : { run: presentedPath(root, result.run.path) }),
|
|
118
117
|
targets: result.targets,
|
|
119
|
-
acceptedRecords: Object.keys(result.outputs).length,
|
|
120
|
-
outstandingCommands: 0,
|
|
121
118
|
operations: [],
|
|
122
119
|
};
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
function
|
|
126
|
-
return
|
|
127
|
-
? `${current}[${segment}]`
|
|
128
|
-
: /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(segment)
|
|
129
|
-
? `${current}.${segment}`
|
|
130
|
-
: `${current}[${JSON.stringify(segment)}]`, "$");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function filesInResolvedOutput(resolved: RepositoryBuildResultOutput): readonly {
|
|
134
|
-
readonly valuePath: string;
|
|
135
|
-
readonly file: BuildResultFileRef;
|
|
136
|
-
}[] {
|
|
137
|
-
if (resolved.value.kind === "build-file") return [{ valuePath: "$", file: resolved.value }];
|
|
138
|
-
if (resolved.value.kind === "value") {
|
|
139
|
-
return resolved.value.document.resources.map((binding) => ({
|
|
140
|
-
valuePath: presentedValuePath(binding.at),
|
|
141
|
-
file: binding.file,
|
|
142
|
-
}));
|
|
143
|
-
}
|
|
144
|
-
return [];
|
|
122
|
+
function isMedia(mediaType: string): boolean {
|
|
123
|
+
return /^(image|video|audio)\//u.test(mediaType);
|
|
145
124
|
}
|
|
146
125
|
|
|
147
126
|
async function artifactsForResults(
|
|
148
127
|
root: string,
|
|
149
128
|
repository: BuildResultRepository,
|
|
150
129
|
manifests: readonly BuildResultManifest[],
|
|
130
|
+
media?: StudioLibraryRequest["media"],
|
|
151
131
|
): Promise<readonly StudioArtifactView[]> {
|
|
152
132
|
const nested = await Promise.all(manifests.flatMap((manifest) =>
|
|
153
|
-
Object.
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
133
|
+
Object.entries(manifest.outputs).map(async ([output, entry]): Promise<StudioArtifactView[]> => {
|
|
134
|
+
// Composite Outputs remain intact. Listing media never opens their Value Documents.
|
|
135
|
+
if (entry.value.kind !== "build-file" && entry.value.kind !== "external-file" && entry.value.kind !== "build-output") return [];
|
|
136
|
+
const description = entry.value.kind === "build-file" || entry.value.kind === "external-file"
|
|
137
|
+
? { kind: "resource", mediaType: entry.value.mediaType }
|
|
138
|
+
: await repository.describeOutput(manifest.id, output);
|
|
139
|
+
if (description?.kind !== "resource" || (description.mediaType === undefined || !isMedia(description.mediaType))) return [];
|
|
140
|
+
if (media !== undefined && !description.mediaType.startsWith(`${media}/`)) return [];
|
|
141
|
+
const resolved = entry.value.kind === "build-file"
|
|
142
|
+
? { build: manifest.id, output, value: entry.value }
|
|
143
|
+
: await repository.resolve(manifest.id, output);
|
|
144
|
+
if (resolved === undefined || (resolved.value.kind !== "build-file" && resolved.value.kind !== "external-file")) return [];
|
|
145
|
+
const file = resolved.value;
|
|
146
|
+
const source = presentedPath(root, manifest.source.path);
|
|
147
|
+
const run = manifest.run === undefined ? undefined : presentedPath(root, manifest.run.path);
|
|
148
|
+
return [{
|
|
149
|
+
id: fileReferenceIdentity(resolved.build, file),
|
|
158
150
|
build: manifest.id,
|
|
159
151
|
createdAt: buildIdCreatedAt(manifest.id)!,
|
|
160
152
|
output,
|
|
153
|
+
nameEditable: manifest.outcome !== undefined,
|
|
154
|
+
...(entry.displayName === undefined ? {} : { displayName: entry.displayName }),
|
|
161
155
|
highlighted: manifest.highlightedOutputs?.includes(output) === true,
|
|
162
156
|
...(manifest.title === undefined ? {} : { buildTitle: manifest.title }),
|
|
163
157
|
...(manifest.note === undefined ? {} : { buildNote: manifest.note }),
|
|
164
|
-
|
|
165
|
-
ownerBuild: resolved.build,
|
|
158
|
+
...(file.kind === "external-file" ? {} : { ownerBuild: file.build ?? resolved.build }),
|
|
166
159
|
ownerOutput: resolved.output,
|
|
167
|
-
filePath: file.path,
|
|
160
|
+
filePath: file.kind === "external-file" ? file.uri : file.path,
|
|
168
161
|
size: file.size,
|
|
169
162
|
mediaType: file.mediaType,
|
|
170
|
-
source
|
|
171
|
-
...(
|
|
172
|
-
|
|
163
|
+
source,
|
|
164
|
+
...(run === undefined ? {} : { run }),
|
|
165
|
+
origins: [{ build: manifest.id, output, source, ...(run === undefined ? {} : { run }) }],
|
|
166
|
+
}];
|
|
173
167
|
})));
|
|
174
|
-
return nested.flat()
|
|
175
|
-
Number(right.highlighted) - Number(left.highlighted)
|
|
176
|
-
|| right.createdAt - left.createdAt
|
|
177
|
-
|| left.output.localeCompare(right.output)
|
|
178
|
-
|| left.valuePath.localeCompare(right.valuePath));
|
|
168
|
+
return mergeStudioArtifacts(nested.flat());
|
|
179
169
|
}
|
|
180
170
|
|
|
181
|
-
/**
|
|
182
|
-
export async function readStudioLibrary(input: {
|
|
171
|
+
/** Query only the selected library view, through Runtime and Repository interfaces. */
|
|
172
|
+
export async function readStudioLibrary(input: StudioLibraryRequest & {
|
|
183
173
|
readonly profile?: string;
|
|
184
174
|
readonly workspaceRoot: string;
|
|
185
175
|
readonly runtime?: Pick<RuntimeControl, "activity">;
|
|
186
176
|
readonly results: BuildResultRepository;
|
|
187
|
-
readonly before?: string;
|
|
188
177
|
}): Promise<StudioLibraryView> {
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|| isWithin(input.workspaceRoot, manifest.source.path));
|
|
196
|
-
const resultsByBuild = new Map(manifests.map((manifest) => [manifest.id, manifest]));
|
|
178
|
+
const matches = (item: { readonly id: string; readonly source: { readonly path: string }; readonly run?: { readonly path: string } }) =>
|
|
179
|
+
(isWithin(input.workspaceRoot, item.run?.path ?? item.source.path) || isWithin(input.workspaceRoot, item.source.path))
|
|
180
|
+
&& (input.build === undefined || item.id === input.build)
|
|
181
|
+
&& (input.run === undefined || (item.run !== undefined
|
|
182
|
+
&& projectPath(input.workspaceRoot, item.run.path) === projectPath(input.workspaceRoot, input.run)));
|
|
183
|
+
// Read activity before Results so a Build that finishes during this query is still represented.
|
|
197
184
|
const active = input.runtime === undefined || input.before !== undefined
|
|
198
|
-
? []
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
? [] : (await input.runtime.activity()).builds;
|
|
186
|
+
const views = active.filter((view) => view.source !== undefined && matches({ ...view, source: view.source }));
|
|
187
|
+
let page = input.build === undefined
|
|
188
|
+
? await input.results.browse({ limit: 25, ...(input.before === undefined ? {} : { before: input.before }) })
|
|
189
|
+
: { results: [await input.results.read(input.build)].filter((item): item is BuildResultManifest => item !== undefined) };
|
|
190
|
+
const manifests = page.results.filter(matches);
|
|
191
|
+
const resultsByBuild = new Map(manifests.map((manifest) => [manifest.id, manifest]));
|
|
192
|
+
await Promise.all(views.filter((view) => !resultsByBuild.has(view.id)).map(async (view) => {
|
|
193
|
+
const result = await input.results.read(view.id);
|
|
194
|
+
if (result !== undefined && matches(result)) resultsByBuild.set(view.id, result);
|
|
195
|
+
}));
|
|
196
|
+
let artifacts: readonly StudioArtifactView[] = [];
|
|
197
|
+
if (input.section === "artifacts") {
|
|
198
|
+
artifacts = await artifactsForResults(input.workspaceRoot, input.results, [...resultsByBuild.values()], input.media);
|
|
199
|
+
// Page through Result metadata until there is a useful media page. The repository still
|
|
200
|
+
// owns Build order and cursors; no media directory or secondary persistent index is needed.
|
|
201
|
+
while (artifacts.length < 25 && "next" in page && page.next !== undefined) {
|
|
202
|
+
const cursor = page.next;
|
|
203
|
+
page = await input.results.browse({ limit: 25, before: cursor });
|
|
204
|
+
if ("next" in page && page.next === cursor) throw new Error("Result pagination did not advance");
|
|
205
|
+
artifacts = mergeStudioArtifacts([...artifacts,
|
|
206
|
+
...await artifactsForResults(input.workspaceRoot, input.results, page.results.filter(matches), input.media)]);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
201
209
|
return {
|
|
210
|
+
section: input.section,
|
|
202
211
|
environment: resolve(input.workspaceRoot),
|
|
203
212
|
...(input.profile === undefined ? {} : { runtime: resolve(input.profile) }),
|
|
204
|
-
...(page.next
|
|
205
|
-
tasks: [
|
|
206
|
-
...views.map((view) => taskView(
|
|
207
|
-
input.workspaceRoot,
|
|
208
|
-
view,
|
|
209
|
-
resultsByBuild.get(view.id),
|
|
210
|
-
)),
|
|
213
|
+
...("next" in page && page.next !== undefined ? { next: page.next } : {}),
|
|
214
|
+
tasks: input.section !== "tasks" ? [] : [
|
|
215
|
+
...views.map((view) => taskView(input.workspaceRoot, view, resultsByBuild.get(view.id))),
|
|
211
216
|
...manifests
|
|
212
|
-
.filter((manifest)
|
|
217
|
+
.filter((manifest): manifest is FinishedBuildResultManifest => manifest.outcome !== undefined
|
|
218
|
+
&& !views.some((view) => view.id === manifest.id))
|
|
213
219
|
.map((manifest) => resultTaskView(input.workspaceRoot, manifest)),
|
|
214
220
|
].sort((left, right) => right.createdAt - left.createdAt || left.id.localeCompare(right.id)),
|
|
215
|
-
artifacts
|
|
221
|
+
artifacts,
|
|
216
222
|
};
|
|
217
223
|
}
|
|
218
224
|
|
|
@@ -245,31 +251,40 @@ export async function openStudioBuildLibrary(
|
|
|
245
251
|
throw error;
|
|
246
252
|
}
|
|
247
253
|
const results = openedResults.repository;
|
|
254
|
+
let presentationWrite: Promise<unknown> = Promise.resolve();
|
|
248
255
|
return {
|
|
249
256
|
...(resolvedProfile === undefined ? {} : { profile: resolvedProfile }),
|
|
250
257
|
...(runtime === undefined ? {} : { runtime }),
|
|
251
258
|
...(transientExecution === undefined ? {} : { transientExecution }),
|
|
252
|
-
async library(
|
|
259
|
+
async library(request) {
|
|
253
260
|
return await readStudioLibrary({
|
|
254
261
|
...(resolvedProfile === undefined ? {} : { profile: resolvedProfile }),
|
|
255
262
|
workspaceRoot,
|
|
256
263
|
...(runtime === undefined ? {} : { runtime }),
|
|
257
264
|
results,
|
|
258
|
-
...
|
|
265
|
+
...request,
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
async renameArtifact(build, output, displayName) {
|
|
269
|
+
const saved = presentationWrite.then(async () => {
|
|
270
|
+
const updated = await results.updatePresentation(build, { outputDisplayNames: { [output]: displayName } });
|
|
271
|
+
return updated.outputs[output]?.displayName;
|
|
259
272
|
});
|
|
273
|
+
presentationWrite = saved.catch(() => undefined);
|
|
274
|
+
return await saved;
|
|
260
275
|
},
|
|
261
276
|
async resolveHistoricalOutput(build, output) {
|
|
262
277
|
return await resolveBuildResultValue(results, build, output);
|
|
263
278
|
},
|
|
264
|
-
async openArtifact(build, output
|
|
279
|
+
async openArtifact(build, output) {
|
|
265
280
|
const resolved = await results.resolve(build, output);
|
|
266
281
|
if (resolved === undefined) return undefined;
|
|
267
|
-
|
|
268
|
-
|
|
282
|
+
if ((resolved.value.kind !== "build-file" && resolved.value.kind !== "external-file") || !isMedia(resolved.value.mediaType)) return undefined;
|
|
283
|
+
const file = resolved.value;
|
|
269
284
|
return {
|
|
270
|
-
mediaType:
|
|
271
|
-
size:
|
|
272
|
-
open: async (range) => await results.openFile(resolved.build,
|
|
285
|
+
mediaType: file.mediaType,
|
|
286
|
+
size: file.size,
|
|
287
|
+
open: async (range) => await results.openFile(resolved.build, file, range),
|
|
273
288
|
};
|
|
274
289
|
},
|
|
275
290
|
async close() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { StudioArtifactView } from "./shared.js";
|
|
2
|
+
|
|
3
|
+
/** Merge known references, without reading or comparing media bytes. */
|
|
4
|
+
export function mergeStudioArtifacts(items: readonly StudioArtifactView[]): StudioArtifactView[] {
|
|
5
|
+
const files = new Map<string, StudioArtifactView>();
|
|
6
|
+
for (const item of items) {
|
|
7
|
+
const prior = files.get(item.id);
|
|
8
|
+
if (prior === undefined) {
|
|
9
|
+
files.set(item.id, item);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const latest = item.createdAt > prior.createdAt ? item : prior;
|
|
13
|
+
const origins = new Map([...prior.origins, ...item.origins].map((origin) =>
|
|
14
|
+
[JSON.stringify([origin.build, origin.output]), origin]));
|
|
15
|
+
files.set(item.id, { ...latest, highlighted: prior.highlighted || item.highlighted, origins: [...origins.values()] });
|
|
16
|
+
}
|
|
17
|
+
return [...files.values()].sort((left, right) => Number(right.highlighted) - Number(left.highlighted)
|
|
18
|
+
|| right.createdAt - left.createdAt || left.output.localeCompare(right.output));
|
|
19
|
+
}
|
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
import type { CanonicalValue, ValueSchema } from "@hypit/protocol";
|
|
2
2
|
import { formatSvsValue } from "@hypit/svs";
|
|
3
|
+
import type { StudioInspectorField, StudioParameterOption, StudioNumberPresentation } from "@hypit/studio-adapter";
|
|
4
|
+
|
|
5
|
+
export const parameterOption = (option: StudioParameterOption): Exclude<StudioParameterOption, string> =>
|
|
6
|
+
typeof option === "string" ? { value: option, label: option } : option;
|
|
7
|
+
|
|
8
|
+
// Keep binary floating-point residue out of decimal controls (0.55 × 100 is 55).
|
|
9
|
+
const scaledDecimal = (value: number, scale: number): number =>
|
|
10
|
+
scale === 1 ? value : Number((value * scale).toPrecision(15));
|
|
11
|
+
|
|
12
|
+
export function parameterNumber(value: CanonicalValue, presentation: StudioNumberPresentation = {}): { value: number; suffix: string } {
|
|
13
|
+
let held = value;
|
|
14
|
+
let suffix = "";
|
|
15
|
+
if (presentation.suffixes !== undefined) {
|
|
16
|
+
if (typeof held !== "string") throw new Error("A suffixed number must be authored as text.");
|
|
17
|
+
const text = held.trim();
|
|
18
|
+
const found = [...presentation.suffixes].sort((a, b) => b.length - a.length).find(item => text.endsWith(item));
|
|
19
|
+
if (found === undefined) throw new Error("The authored unit is not supported by this field.");
|
|
20
|
+
suffix = found;
|
|
21
|
+
held = text.slice(0, text.length - suffix.length).trim();
|
|
22
|
+
}
|
|
23
|
+
if (typeof held !== "number" && (typeof held !== "string" || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/iu.test(held.trim()))) {
|
|
24
|
+
throw new Error("This field needs a numeric value.");
|
|
25
|
+
}
|
|
26
|
+
const number = scaledDecimal(Number(held), presentation.scale ?? 1);
|
|
27
|
+
if (!Number.isFinite(number)) throw new Error("This field needs a finite number.");
|
|
28
|
+
return { value: number, suffix };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Decode the displayed edit before validating and serializing the authored value. */
|
|
32
|
+
export function parameterAuthorValue(field: StudioInspectorField, displayed: CanonicalValue): CanonicalValue {
|
|
33
|
+
if (field.control !== "number") return displayed;
|
|
34
|
+
if (typeof displayed !== "number" || !Number.isFinite(displayed)) throw new Error(`${field.label} expects a finite number.`);
|
|
35
|
+
const presentation = field.number ?? {};
|
|
36
|
+
if (presentation.minimum !== undefined && displayed < presentation.minimum) throw new Error(`${field.label} must be at least ${presentation.minimum}.`);
|
|
37
|
+
if (presentation.maximum !== undefined && displayed > presentation.maximum) throw new Error(`${field.label} must be at most ${presentation.maximum}.`);
|
|
38
|
+
const value = scaledDecimal(displayed, 1 / (presentation.scale ?? 1));
|
|
39
|
+
if (!Number.isFinite(value)) throw new Error(`${field.label} cannot be represented.`);
|
|
40
|
+
return presentation.suffixes === undefined ? value : `${value}${parameterNumber(field.value, presentation).suffix}`;
|
|
41
|
+
}
|
|
3
42
|
|
|
4
43
|
function fail(path: string, expectation: string): never {
|
|
5
44
|
throw new Error(`${path} ${expectation}.`);
|
|
@@ -76,6 +115,7 @@ export function validateParameterValue(value: CanonicalValue, schema: ValueSchem
|
|
|
76
115
|
|
|
77
116
|
export function serializeParameterValue(value: CanonicalValue, language: "svml" | "svs" | "svrun"): string {
|
|
78
117
|
if (language === "svs") return formatSvsValue(value);
|
|
79
|
-
if (typeof value === "string"
|
|
118
|
+
if (typeof value === "string") return value.replaceAll("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
119
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
80
120
|
throw new Error(`${language.toUpperCase()} parameter bindings currently accept scalar values only.`);
|
|
81
121
|
}
|