@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.
Files changed (92) 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/media-track-studio/src/index.ts +7 -2
  41. package/packages/provider-whisperx-local/README.md +57 -0
  42. package/packages/ranking/src/render.ts +11 -9
  43. package/packages/ranking-studio/README.md +11 -0
  44. package/packages/ranking-studio/package.json +8 -3
  45. package/packages/ranking-studio/src/index.ts +11 -4
  46. package/packages/runtime/src/catalog.ts +2 -0
  47. package/packages/runtime-host-node/src/index.ts +1 -0
  48. package/packages/runtime-local/README.md +19 -0
  49. package/packages/runtime-local/src/programs.ts +39 -24
  50. package/packages/runtime-local/src/runtime.ts +2 -0
  51. package/packages/script/README.md +22 -0
  52. package/packages/script/src/edit.ts +123 -73
  53. package/packages/script/src/lexical.ts +40 -6
  54. package/packages/script/src/parser.ts +36 -8
  55. package/packages/script/src/types.ts +4 -0
  56. package/packages/seedance-kits/README.md +3 -1
  57. package/packages/seedance-kits/kits/speaker-v1.svs +5 -5
  58. package/packages/speech-alignment/src/align.ts +45 -21
  59. package/packages/studio/INSPECTOR.md +104 -0
  60. package/packages/studio/README.md +65 -4
  61. package/packages/studio/src/build-library.ts +99 -84
  62. package/packages/studio/src/library-media.ts +19 -0
  63. package/packages/studio/src/parameter-values.ts +41 -1
  64. package/packages/studio/src/parameters.ts +20 -21
  65. package/packages/studio/src/server.ts +58 -30
  66. package/packages/studio/src/shared.ts +20 -6
  67. package/packages/studio/src/studio-registry.ts +22 -0
  68. package/packages/studio/src/style.css +135 -144
  69. package/packages/studio/src/temporal-edit.ts +131 -0
  70. package/packages/studio/src/ui/artifact-name.ts +80 -0
  71. package/packages/studio/src/ui/artifact-preview.ts +99 -0
  72. package/packages/studio/src/ui/icons.ts +5 -1
  73. package/packages/studio/src/ui/library.ts +373 -192
  74. package/packages/studio/src/ui/main.ts +153 -31
  75. package/packages/studio/src/ui/material-preview.ts +1 -1
  76. package/packages/studio/src/ui/overlay.ts +21 -20
  77. package/packages/studio/src/ui/sidebar-panel.ts +31 -0
  78. package/packages/studio/src/ui/stage.ts +124 -8
  79. package/packages/studio/src/ui/timeline.ts +16 -179
  80. package/packages/studio/start.ts +15 -3
  81. package/packages/studio-adapter/README.md +21 -0
  82. package/packages/studio-adapter/src/index.ts +26 -1
  83. package/packages/temporal-markup/EDITING.md +183 -0
  84. package/packages/temporal-markup/README.md +4 -0
  85. package/packages/typography-track-studio/package.json +8 -3
  86. package/packages/typography-track-studio/src/index.ts +5 -1
  87. package/packages/video-cli/README.md +12 -3
  88. package/packages/video-cli/src/creation.ts +11 -5
  89. package/packages/whisperx/README.md +7 -0
  90. package/packages/workspace/src/index.ts +2 -0
  91. package/packages/workspace-fs-node/README.md +4 -1
  92. package/packages/workspace-fs-node/src/workspace.ts +3 -1
@@ -238,8 +238,16 @@ function referencedParameters(input: {
238
238
  const file = sourceFor(input.root, target.sourcePath, input.files);
239
239
  if (file === undefined) return [];
240
240
  return input.declarations.flatMap((declaration) => {
241
+ const nestedPath = target.references[declaration.name];
242
+ const nested = nestedPath === undefined || declaration.referenced === undefined ? [] : referencedParameters({
243
+ root: input.root, files: input.files, draft: input.draft, placements: input.placements,
244
+ referenceName: `${input.referenceName}.${declaration.name}`,
245
+ referencePath: nestedPath,
246
+ ...(target.resolvedReferences[declaration.name] === undefined ? {} : { referenceRef: target.resolvedReferences[declaration.name] }),
247
+ declarations: declaration.referenced,
248
+ });
241
249
  const range = target.attributeValueRanges[declaration.name];
242
- if (range === undefined) return [];
250
+ if (range === undefined) return nested;
243
251
  const preimage = file.text.slice(range.start, range.end);
244
252
  const reference = target.references[declaration.name];
245
253
  const value = reference === undefined ? (target.attributes[declaration.name] ?? preimage) : reference;
@@ -263,7 +271,7 @@ function referencedParameters(input: {
263
271
  ...(!writable
264
272
  ? { disabledReason: reference === undefined ? "该几何值由组件声明为只读。" : "引用由作者在 SVML 中绑定,面板不替换引用关系。" }
265
273
  : {}),
266
- } satisfies StudioSourceBinding];
274
+ } satisfies StudioSourceBinding, ...nested];
267
275
  });
268
276
  }
269
277
 
@@ -484,7 +492,7 @@ export function resolveTimelineEditHandles(
484
492
  moveEffect?: "translate-window" | "move-start",
485
493
  ): StudioEditHandle => {
486
494
  const unavailable = affected.find(({ endpoint }) => endpoint.authority.kind === "fixed");
487
- if (unavailable !== undefined) return disabled(gesture, "该端点由 Program 或 Segment 结构固定,不能从组件时间线反写。");
495
+ if (unavailable !== undefined) return disabled(gesture, "该时间表达未开放时间轴回写。");
488
496
  const projected: ({ readonly missing: string } | StudioEditSource)[] = [];
489
497
  for (const { endpoint, role } of affected) {
490
498
  if (endpoint.authority.kind !== "parameter") continue;
@@ -527,24 +535,15 @@ export function resolveTimelineEditHandles(
527
535
  }
528
536
  const start = { endpoint: projection.start, role: "start" as const };
529
537
  const end = { endpoint: projection.end, role: "end" as const };
530
- const move = projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start"
531
- ? [start]
532
- : projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end"
533
- ? [end]
534
- : [start, end];
535
- const trimStart = projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end"
536
- ? [start]
537
- : projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start"
538
- ? [start, end]
539
- : [start];
540
- const trimEnd = projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start"
541
- ? [end]
542
- : projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end"
543
- ? [start, end]
544
- : [end];
538
+ if (projection.end.authority.kind === "parameter" && projection.end.authority.relation === "after-start") {
539
+ return [handle("move", [start], "translate-window"), handle("trim-end", [end])];
540
+ }
541
+ if (projection.start.authority.kind === "parameter" && projection.start.authority.relation === "before-end") {
542
+ return [handle("move", [end], "translate-window"), handle("trim-start", [start])];
543
+ }
545
544
  return [
546
- handle("move", move, "translate-window"),
547
- handle("trim-start", trimStart),
548
- handle("trim-end", trimEnd),
545
+ handle("move", [start, end], "translate-window"),
546
+ handle("trim-start", [start]),
547
+ handle("trim-end", [end]),
549
548
  ];
550
549
  }
@@ -15,12 +15,13 @@ import type { ServedFile } from "./compile.js";
15
15
  import type { StudioDomain } from "./domain.js";
16
16
  import type { StudioCompanionRegistry } from "./studio-registry.js";
17
17
  import { loadStudioRun } from "./run.js";
18
- import { serializeParameterValue, validateParameterValue } from "./parameter-values.js";
18
+ import { parameterAuthorValue, parameterOption, serializeParameterValue, validateParameterValue } from "./parameter-values.js";
19
19
  import { readStudioSession } from "./session.js";
20
- import type { Range, StudioFailure, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
20
+ import type { Range, StudioFailure, StudioLibraryRequest, StudioLibraryView, StudioMutation, StudioSnapshot } from "./shared.js";
21
21
  import { createStudioStoryboard } from "./storyboard.js";
22
22
  import type { StudioStoryboard } from "./storyboard.js";
23
23
  import { findSurfacePreview } from "./surface-preview.js";
24
+ import { formatTemporalPointEdit, semanticGestureSpan } from "./temporal-edit.js";
24
25
  import { replaceSourceFiles } from "./source-transaction.js";
25
26
 
26
27
  export type StudioPluginOptions = {
@@ -93,8 +94,9 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
93
94
  const watchedFiles = new Set<string>();
94
95
  const storyboards = new Map<string, Promise<StudioStoryboard>>();
95
96
 
96
- const readLibrary = async (before?: string): Promise<StudioLibraryView> => {
97
- return await options.buildLibrary?.library(before) ?? {
97
+ const readLibrary = async (request: StudioLibraryRequest): Promise<StudioLibraryView> => {
98
+ return await options.buildLibrary?.library(request) ?? {
99
+ section: request.section,
98
100
  environment: options.workspaceRoot,
99
101
  tasks: [],
100
102
  artifacts: [],
@@ -271,7 +273,7 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
271
273
  throw new Error("A timeline target must use valid whole frames.");
272
274
  }
273
275
  if (temporal.kind === "window") {
274
- if (mutation.gesture === "move"
276
+ if (mutation.gesture === "move" && handle.semantic?.kind !== "selection"
275
277
  && endFrameExclusive - startFrame !== clip.endFrameExclusive - clip.startFrame) {
276
278
  throw new Error("Move must preserve the Window duration.");
277
279
  }
@@ -287,6 +289,7 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
287
289
 
288
290
  const patches: Patch[] = [];
289
291
  const semanticTarget = mutation.target.semantic;
292
+ if (handle.semantic !== undefined && semanticTarget === undefined) throw new Error("A semantic edit requires explicit target anchors.");
290
293
  if (semanticTarget !== undefined) {
291
294
  if (handle.semantic?.kind !== semanticTarget.kind) {
292
295
  throw new Error(`Entity ${mutation.entityId} is not bound to a writable ${semanticTarget.kind}.`);
@@ -298,15 +301,9 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
298
301
  || script.narrativeId !== current.semantic.narrativeId) {
299
302
  throw new Error("The timeline entity and writable Script do not belong to the selected Narrative.");
300
303
  }
301
- if (semanticTarget.kind === "selection") {
302
- const startIndex = current.semantic.anchors.findIndex((anchor) => anchor.id === semanticTarget.startAnchorId);
303
- const endIndex = current.semantic.anchors.findIndex((anchor) => anchor.id === semanticTarget.endAnchorId);
304
- if (startIndex < 0 || endIndex < 0 || startIndex >= endIndex
305
- || current.semantic.anchors[startIndex]!.frame >= current.semantic.anchors[endIndex]!.frame) {
306
- throw new Error("A Selection must span two ordered semantic Anchors with positive time.");
307
- }
308
- } else if (!current.semantic.anchors.some((anchor) => anchor.id === semanticTarget.anchorId)) {
309
- throw new Error(`Moment Anchor ${semanticTarget.anchorId} does not exist in the current semantic Candidate.`);
304
+ const projected = semanticGestureSpan(current.semantic.anchors, handle, semanticTarget);
305
+ if (projected === undefined || projected.startFrame !== startFrame || projected.endFrameExclusive !== endFrameExclusive) {
306
+ throw new Error("The semantic edit does not produce the requested timeline projection.");
310
307
  }
311
308
  const absolute = resolve(options.workspaceRoot, script.sourcePath);
312
309
  const source = await readFile(absolute, "utf8");
@@ -366,20 +363,15 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
366
363
  }
367
364
  return snapshot?.semantic?.moments.find((candidate) => candidate.id === id)?.frame;
368
365
  };
369
- const projectedPointValue = (endpoint: StudioTemporalInstantProjection, desired: number): string => {
370
- if (endpoint.reference === "absolute") return frame(desired);
371
- const base = projectionBaseFrame(endpoint);
372
- if (base === undefined) throw new Error(`The ${endpoint.reference} projection base is unavailable.`);
373
- const offset = desired - base;
374
- return offset === 0 ? endpoint.reference : `${endpoint.reference}${offset > 0 ? "+" : ""}${offset}f`;
375
- };
366
+ const projectedPointValue = (endpoint: StudioTemporalInstantProjection, desired: number): string =>
367
+ formatTemporalPointEdit(endpoint.reference, desired, projectionBaseFrame(endpoint));
376
368
  const writeEndpoint = (
377
369
  endpoint: StudioTemporalInstantProjection,
378
370
  desired: number,
379
371
  role: "start" | "end",
380
372
  ): void => {
381
373
  if (desired === endpoint.frame) return;
382
- if (endpoint.authority.kind === "fixed") throw new Error(`The ${role} endpoint is structurally fixed.`);
374
+ if (endpoint.authority.kind === "fixed") throw new Error(`The ${role} endpoint has no timeline write target.`);
383
375
  if (endpoint.authority.kind === "semantic") {
384
376
  if (semanticFrame(endpoint) !== desired) throw new Error(`The ${role} endpoint does not match its semantic Anchor.`);
385
377
  return;
@@ -413,20 +405,22 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
413
405
  if (parameter === undefined) {
414
406
  throw new Error(`Entity ${mutation.entityId} has no writable parameter ${mutation.parameterId}.`);
415
407
  }
408
+ const authorValue = parameterAuthorValue(parameter, mutation.value);
416
409
  if (parameter.schema !== undefined) {
417
- validateParameterValue(mutation.value, parameter.schema, parameter.label);
410
+ validateParameterValue(authorValue, parameter.schema, parameter.label);
418
411
  } else if (parameter.control === "boolean" && typeof mutation.value !== "boolean") {
419
412
  throw new Error(`${parameter.label} expects true or false.`);
420
413
  } else if (parameter.control === "number" && (typeof mutation.value !== "number" || !Number.isFinite(mutation.value))) {
421
414
  throw new Error(`${parameter.label} expects a number.`);
422
- } else if ((parameter.control === "text" || parameter.control === "color" || parameter.control === "select")
415
+ } else if ((parameter.control === "text" || parameter.control === "color")
423
416
  && typeof mutation.value !== "string") {
424
417
  throw new Error(`${parameter.label} expects text.`);
425
418
  }
426
- if (parameter.options !== undefined && (typeof mutation.value !== "string" || !parameter.options.includes(mutation.value))) {
419
+ if (parameter.options !== undefined && !parameter.options.some(option => parameterOption(option).value === authorValue)) {
427
420
  throw new Error(`${parameter.label} does not accept ${mutation.value}.`);
428
421
  }
429
- const encoded = serializeParameterValue(mutation.value, parameter.language);
422
+ if (parameter.control === "color") validateParameterValue(authorValue, { kind: "string", format: "color" }, parameter.label);
423
+ const encoded = serializeParameterValue(authorValue, parameter.language);
430
424
  const replacement = `${parameter.source.prefix ?? ""}${encoded}${parameter.source.suffix ?? ""}`;
431
425
  return replacement === parameter.source.preimage ? [] : [{
432
426
  ...parameter.source,
@@ -525,6 +519,28 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
525
519
  })();
526
520
  return;
527
521
  }
522
+ if (request.method === "PUT" && url.pathname === "/__studio/artifact-name") {
523
+ void (async () => {
524
+ try {
525
+ const chunks: Buffer[] = [];
526
+ for await (const chunk of request) chunks.push(Buffer.from(chunk));
527
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf8")) as {
528
+ build?: unknown; output?: unknown; displayName?: unknown;
529
+ };
530
+ if (typeof body.build !== "string" || typeof body.output !== "string"
531
+ || (typeof body.displayName !== "string" && body.displayName !== null)) {
532
+ json(response, 400, { error: "Expected Build, Output and displayName" });
533
+ return;
534
+ }
535
+ if (options.buildLibrary === undefined) throw new Error("Result Repository is unavailable");
536
+ const displayName = await options.buildLibrary.renameArtifact(body.build, body.output, body.displayName);
537
+ json(response, 200, { displayName: displayName ?? null });
538
+ } catch (error) {
539
+ json(response, 422, { error: error instanceof Error ? error.message : String(error) });
540
+ }
541
+ })();
542
+ return;
543
+ }
528
544
  if (request.method === "POST" && url.pathname === "/__studio/mutation") {
529
545
  void (async () => {
530
546
  try {
@@ -577,8 +593,21 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
577
593
  return;
578
594
  }
579
595
  if (url.pathname === "/__studio/library") {
596
+ const section = url.searchParams.get("section");
597
+ if (section !== "tasks" && section !== "artifacts") {
598
+ json(response, 400, { error: "Choose tasks or artifacts with the section parameter" });
599
+ return;
600
+ }
601
+ const media = url.searchParams.get("media") ?? undefined;
602
+ if (media !== undefined && media !== "image" && media !== "video" && media !== "audio") {
603
+ json(response, 400, { error: "Choose image, video or audio with the media parameter" });
604
+ return;
605
+ }
580
606
  const before = url.searchParams.get("before") ?? undefined;
581
- void readLibrary(before).then(
607
+ const run = url.searchParams.get("run") ?? undefined;
608
+ const build = url.searchParams.get("build") ?? undefined;
609
+ void readLibrary({ section, ...(media === undefined ? {} : { media }), ...(before === undefined ? {} : { before }),
610
+ ...(run === undefined ? {} : { run }), ...(build === undefined ? {} : { build }) }).then(
582
611
  (view) => json(response, 200, view),
583
612
  (error) => json(response, 500, { error: error instanceof Error ? error.message : String(error) }),
584
613
  );
@@ -662,13 +691,12 @@ export function studioPlugin(options: StudioPluginOptions): Plugin {
662
691
  void (async () => {
663
692
  const build = url.searchParams.get("build");
664
693
  const output = url.searchParams.get("output");
665
- const valuePath = url.searchParams.get("path");
666
- if (build === null || output === null || valuePath === null || options.buildLibrary === undefined) {
694
+ if (build === null || output === null || options.buildLibrary === undefined) {
667
695
  response.statusCode = 404;
668
696
  response.end();
669
697
  return;
670
698
  }
671
- const artifact = await options.buildLibrary.openArtifact(build, output, valuePath);
699
+ const artifact = await options.buildLibrary.openArtifact(build, output);
672
700
  if (artifact === undefined) {
673
701
  response.statusCode = 404;
674
702
  response.end();
@@ -190,12 +190,14 @@ export type StudioTaskView = {
190
190
  readonly note?: string;
191
191
  readonly highlightedOutputs?: readonly string[];
192
192
  readonly createdAt: number;
193
- readonly status: "queued" | "running" | "waiting" | "complete" | "failed" | "cancelled" | "active" | "blocked" | "unknown";
193
+ readonly finishedAt?: number;
194
+ readonly ongoing: boolean;
195
+ readonly status: "queued" | "running" | "waiting" | "complete" | "failed" | "cancelled" | "saving-result" | "attention";
196
+ readonly detail?: string;
197
+ readonly requests?: { readonly total: number; readonly completed: number };
194
198
  readonly source: string;
195
199
  readonly run?: string;
196
200
  readonly targets: readonly string[];
197
- readonly acceptedRecords: number;
198
- readonly outstandingCommands: number;
199
201
  readonly operations: readonly {
200
202
  readonly status: "pending" | "completed" | "failed" | "cancelled";
201
203
  readonly phase?: string;
@@ -206,7 +208,10 @@ export type StudioTaskView = {
206
208
  };
207
209
 
208
210
  export type StudioArtifactView = {
209
- /** Build-qualified identity derived from the public Output and its value path. */
211
+ readonly nameEditable?: boolean;
212
+ /** Author-provided display name; the Output identifier remains unchanged. */
213
+ readonly displayName?: string;
214
+ /** File-owner identity; references to the same file share one card. */
210
215
  readonly id: string;
211
216
  readonly build: string;
212
217
  readonly createdAt: number;
@@ -214,18 +219,27 @@ export type StudioArtifactView = {
214
219
  readonly highlighted: boolean;
215
220
  readonly buildTitle?: string;
216
221
  readonly buildNote?: string;
217
- readonly valuePath: string;
218
- readonly ownerBuild: string;
222
+ readonly ownerBuild?: string;
219
223
  readonly ownerOutput: string;
220
224
  readonly filePath: string;
221
225
  readonly size: number;
222
226
  readonly mediaType: string;
223
227
  readonly source: string;
224
228
  readonly run?: string;
229
+ readonly origins: readonly { readonly build: string; readonly output: string; readonly run?: string; readonly source: string }[];
230
+ };
231
+
232
+ export type StudioLibraryRequest = {
233
+ readonly media?: "image" | "video" | "audio";
234
+ readonly section: "tasks" | "artifacts";
235
+ readonly before?: string;
236
+ readonly run?: string;
237
+ readonly build?: string;
225
238
  };
226
239
 
227
240
  /** Read-only view of the Runtime context selected for this Studio environment. */
228
241
  export type StudioLibraryView = {
242
+ readonly section: StudioLibraryRequest["section"];
229
243
  readonly environment: string;
230
244
  readonly runtime?: string;
231
245
  /** Cursor for the next older page of immutable Build Results. */
@@ -14,6 +14,7 @@ import type {
14
14
  StudioSpan,
15
15
  } from "@hypit/studio-adapter";
16
16
  import { studioParameterControls } from "@hypit/studio-adapter";
17
+ import { parameterOption } from "./parameter-values.js";
17
18
  import { compositionTypes } from "@hypit/composition";
18
19
  import { sameModule, sameType } from "@hypit/protocol";
19
20
  import type { ModuleRef, TypeRef } from "@hypit/protocol";
@@ -67,6 +68,27 @@ function validateInspector(
67
68
  if (field.control === "select" && (field.options === undefined || field.options.length === 0)) {
68
69
  throw new Error(`${subject} Inspector field ${field.binding} has no select options`);
69
70
  }
71
+ const choices = (field.options ?? []).map(parameterOption);
72
+ if (choices.some(option => !option.label.trim() || !["string", "number", "boolean"].includes(typeof option.value)
73
+ || typeof option.value === "number" && !Number.isFinite(option.value))
74
+ || new Set(choices.map(option => JSON.stringify(option.value))).size !== choices.length) {
75
+ throw new Error(`${subject} Inspector field ${field.binding} has invalid or repeated choices`);
76
+ }
77
+ const numeric = field.number;
78
+ if (numeric !== undefined) {
79
+ if (field.control !== "number" || numeric.scale !== undefined && (!Number.isFinite(numeric.scale) || numeric.scale <= 0)
80
+ || numeric.step !== undefined && (!Number.isFinite(numeric.step) || numeric.step <= 0)
81
+ || numeric.minimum !== undefined && !Number.isFinite(numeric.minimum)
82
+ || numeric.maximum !== undefined && !Number.isFinite(numeric.maximum)
83
+ || numeric.minimum !== undefined && numeric.maximum !== undefined && numeric.minimum > numeric.maximum
84
+ || numeric.suffixes !== undefined && (numeric.suffixes.length === 0 || numeric.suffixes.some(suffix => !suffix.trim()))) {
85
+ throw new Error(`${subject} Inspector field ${field.binding} has an invalid numeric presentation`);
86
+ }
87
+ }
88
+ if (field.multiline && field.control !== "text") throw new Error(`${subject} multiline editing requires text`);
89
+ if (field.swatches !== undefined && (field.control !== "color" || field.swatches.some(color => !/^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/iu.test(color)))) {
90
+ throw new Error(`${subject} color suggestions require hex colors`);
91
+ }
70
92
  }
71
93
  }
72
94