@scenar/core 0.8.0 → 0.9.1

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/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export type { StepTimeline } from "./timeline/compute-step-timeline.js";
10
10
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
11
11
  export { deriveStepFromTime } from "./timeline/derive-step.js";
12
12
  export { getStepDurationMs } from "./timeline/step-duration.js";
13
+ export type { ScenarioShot } from "./timeline/collect-shots.js";
14
+ export { SHOT_NAME_PATTERN, collectScenarioShots, collectShotNames, } from "./timeline/collect-shots.js";
13
15
  export { CLICK_DELAY_MS, TYPE_CHAR_DELAY_MS, HOVER_HOLD_MS, DRAG_SETTLE_MS, CAMERA_TRANSITION_MS, VIEWPORT_SETTLE_MS, } from "./timing/constants.js";
14
16
  export { findScrollParent, scrollTargetIntoView, scrollTargetIntoViewInstant, } from "./dom/scroll.js";
15
17
  export { CURSOR_TARGET_ATTRIBUTE, SCROLL_TARGET_ATTRIBUTE, HOVER_STATE_ATTRIBUTE, DRAG_STATE_ATTRIBUTE, cursorTargetSelector, scrollTargetSelector, } from "./targeting/data-attributes.js";
package/index.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EACV,UAAU,EACV,UAAU,EACV,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,YAAY,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,YAAY,EACV,UAAU,EACV,UAAU,EACV,0BAA0B,GAC3B,MAAM,2BAA2B,CAAC;AAGnC,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAG9E,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAGjC,YAAY,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,YAAY,EAAE,YAAY,EAAE,MAAM,qCAAqC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAG/B,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EACV,gBAAgB,EAChB,kBAAkB,EAClB,uBAAuB,EACvB,yBAAyB,GAC1B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AACvE,YAAY,EACV,yBAAyB,EACzB,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC"}
package/index.js CHANGED
@@ -5,6 +5,7 @@ export { computeCursorPosition } from "./cursor/compute-position.js";
5
5
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
6
6
  export { deriveStepFromTime } from "./timeline/derive-step.js";
7
7
  export { getStepDurationMs } from "./timeline/step-duration.js";
8
+ export { SHOT_NAME_PATTERN, collectScenarioShots, collectShotNames, } from "./timeline/collect-shots.js";
8
9
  // Timing constants
9
10
  export { CLICK_DELAY_MS, TYPE_CHAR_DELAY_MS, HOVER_HOLD_MS, DRAG_SETTLE_MS, CAMERA_TRANSITION_MS, VIEWPORT_SETTLE_MS, } from "./timing/constants.js";
10
11
  // DOM scroll utilities
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,sEAAsE;AAgBtE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,mBAAmB;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,0EAA0E;AAC1E,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAQ7B,uEAAuE;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4BAA4B;AAC5B,sEAAsE;AAgBtE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,UAAU,EACV,4BAA4B,GAC7B,MAAM,yBAAyB,CAAC;AAIjC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAIrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAC;AAC1E,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAEhE,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,gBAAgB,GACjB,MAAM,6BAA6B,CAAC;AAErC,mBAAmB;AACnB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,GACnB,MAAM,uBAAuB,CAAC;AAE/B,uBAAuB;AACvB,OAAO,EACL,gBAAgB,EAChB,oBAAoB,EACpB,2BAA2B,GAC5B,MAAM,iBAAiB,CAAC;AAEzB,oCAAoC;AACpC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACpB,oBAAoB,GACrB,MAAM,gCAAgC,CAAC;AAExC,0EAA0E;AAC1E,OAAO,EACL,mBAAmB,EACnB,6BAA6B,EAC7B,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,qBAAqB,CAAC;AAQ7B,uEAAuE;AACvE,OAAO,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scenar/core",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "description": "Pure TypeScript types, timing, and utilities for the Scenar scenario engine.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -29,5 +29,21 @@ export interface ScenarioStep<T> {
29
29
  * share the same atPercent, they fire in array order.
30
30
  */
31
31
  readonly interactions?: readonly StepAction[];
32
+ /**
33
+ * Names this step as a still-capture point for `scenar shoot`.
34
+ *
35
+ * The capture happens at the step's settled *end* — after every
36
+ * interaction (and its tail: click dispatch, per-char typing, camera
37
+ * tween) has fired — so a shot-bearing step's duration must fit its
38
+ * interaction tails.
39
+ *
40
+ * Shots are addressed by this name, never by step index, so inserting a
41
+ * beat into a scenario never renumbers existing docs references. Names
42
+ * must be unique within a scenario and kebab-case
43
+ * (`^[a-z0-9]+(-[a-z0-9]+)*$`) — each becomes a filename
44
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed
45
+ * bundle. Steps without a `shot` are simply walked through.
46
+ */
47
+ readonly shot?: string;
32
48
  }
33
49
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scenario/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;CAC/C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/scenario/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD;;;;GAIG;AACH,MAAM,WAAW,YAAY,CAAC,CAAC;IAC7B,uDAAuD;IACvD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,UAAU,EAAE,CAAC;IAC9C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB"}
package/src/index.ts CHANGED
@@ -31,6 +31,12 @@ export type { StepTimeline } from "./timeline/compute-step-timeline.js";
31
31
  export { computeStepTimeline } from "./timeline/compute-step-timeline.js";
32
32
  export { deriveStepFromTime } from "./timeline/derive-step.js";
33
33
  export { getStepDurationMs } from "./timeline/step-duration.js";
34
+ export type { ScenarioShot } from "./timeline/collect-shots.js";
35
+ export {
36
+ SHOT_NAME_PATTERN,
37
+ collectScenarioShots,
38
+ collectShotNames,
39
+ } from "./timeline/collect-shots.js";
34
40
 
35
41
  // Timing constants
36
42
  export {
@@ -31,4 +31,20 @@ export interface ScenarioStep<T> {
31
31
  * share the same atPercent, they fire in array order.
32
32
  */
33
33
  readonly interactions?: readonly StepAction[];
34
+ /**
35
+ * Names this step as a still-capture point for `scenar shoot`.
36
+ *
37
+ * The capture happens at the step's settled *end* — after every
38
+ * interaction (and its tail: click dispatch, per-char typing, camera
39
+ * tween) has fired — so a shot-bearing step's duration must fit its
40
+ * interaction tails.
41
+ *
42
+ * Shots are addressed by this name, never by step index, so inserting a
43
+ * beat into a scenario never renumbers existing docs references. Names
44
+ * must be unique within a scenario and kebab-case
45
+ * (`^[a-z0-9]+(-[a-z0-9]+)*$`) — each becomes a filename
46
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed
47
+ * bundle. Steps without a `shot` are simply walked through.
48
+ */
49
+ readonly shot?: string;
34
50
  }
@@ -0,0 +1,116 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { computeStepTimeline } from "./compute-step-timeline.js";
3
+ import { SHOT_NAME_PATTERN, collectScenarioShots, collectShotNames } from "./collect-shots.js";
4
+
5
+ describe("collectScenarioShots", () => {
6
+ it("resolves each shot to the settled end of its step", () => {
7
+ const steps = [
8
+ { delayMs: 0, shot: "opening" },
9
+ { delayMs: 1000 },
10
+ { delayMs: 2000, shot: "detail-open" },
11
+ ];
12
+ const timeline = computeStepTimeline(steps, null);
13
+ // Starts: [0, 1000, 3000]; total: 3000 + 3000 (final dwell).
14
+ expect(collectScenarioShots(steps, timeline)).toEqual([
15
+ { name: "opening", timeMs: 999, stepIndex: 0 },
16
+ { name: "detail-open", timeMs: 5999, stepIndex: 2 },
17
+ ]);
18
+ });
19
+
20
+ it("places a final-step shot inside the final dwell", () => {
21
+ const steps = [{ delayMs: 0 }, { delayMs: 500, shot: "finale" }];
22
+ const timeline = computeStepTimeline(steps, null);
23
+ expect(collectScenarioShots(steps, timeline)).toEqual([
24
+ { name: "finale", timeMs: timeline.totalDurationMs - 1, stepIndex: 1 },
25
+ ]);
26
+ });
27
+
28
+ it("resolves shot times against narration-driven step durations", () => {
29
+ const steps = [{ delayMs: 0, shot: "narrated" }, { delayMs: 500 }];
30
+ const timeline = computeStepTimeline(steps, {
31
+ steps: [{ src: "a.mp3", durationMs: 4000 }, null],
32
+ });
33
+ // Step 1 starts at max(500, 4000) = 4000; the shot lands just before it.
34
+ expect(collectScenarioShots(steps, timeline)).toEqual([
35
+ { name: "narrated", timeMs: 3999, stepIndex: 0 },
36
+ ]);
37
+ });
38
+
39
+ it("returns an empty list when no step declares a shot", () => {
40
+ const steps = [{ delayMs: 0 }, { delayMs: 1000 }];
41
+ expect(collectScenarioShots(steps, computeStepTimeline(steps, null))).toEqual([]);
42
+ });
43
+
44
+ it("rejects a non-kebab-case name, naming the step", () => {
45
+ const steps = [{ delayMs: 0 }, { delayMs: 100, shot: "Detail_Open" }];
46
+ expect(() =>
47
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
48
+ ).toThrowError(/step 1 declares shot "Detail_Open"/);
49
+ });
50
+
51
+ it.each(["-leading", "trailing-", "double--dash", "UPPER", "with.dot", "with space", ""])(
52
+ 'rejects invalid shot name "%s"',
53
+ (name) => {
54
+ const steps = [{ delayMs: 0, shot: name }];
55
+ expect(() =>
56
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
57
+ ).toThrowError(/not kebab-case/);
58
+ },
59
+ );
60
+
61
+ it.each(["a", "shot-1", "agent-detail", "step2-open"])(
62
+ 'accepts valid shot name "%s"',
63
+ (name) => {
64
+ expect(SHOT_NAME_PATTERN.test(name)).toBe(true);
65
+ },
66
+ );
67
+
68
+ it("rejects duplicate names, naming both steps", () => {
69
+ const steps = [
70
+ { delayMs: 0, shot: "opening" },
71
+ { delayMs: 100 },
72
+ { delayMs: 100, shot: "opening" },
73
+ ];
74
+ expect(() =>
75
+ collectScenarioShots(steps, computeStepTimeline(steps, null)),
76
+ ).toThrowError(/steps 0 and 2 both declare shot "opening"/);
77
+ });
78
+ });
79
+
80
+ describe("collectShotNames", () => {
81
+ it("returns the declared names in step order, without needing a timeline", () => {
82
+ const steps = [
83
+ { delayMs: 0, shot: "opening" },
84
+ { delayMs: 1000 },
85
+ { delayMs: 2000, shot: "detail-open" },
86
+ ];
87
+ expect(collectShotNames(steps)).toEqual(["opening", "detail-open"]);
88
+ });
89
+
90
+ it("returns an empty list when no step declares a shot", () => {
91
+ expect(collectShotNames([{ delayMs: 0 }, { delayMs: 1000 }])).toEqual([]);
92
+ });
93
+
94
+ it("throws the same validation errors as collectScenarioShots", () => {
95
+ expect(() => collectShotNames([{ delayMs: 0, shot: "Not_Kebab" }])).toThrowError(
96
+ /not kebab-case/,
97
+ );
98
+ expect(() =>
99
+ collectShotNames([
100
+ { delayMs: 0, shot: "opening" },
101
+ { delayMs: 100, shot: "opening" },
102
+ ]),
103
+ ).toThrowError(/steps 0 and 1 both declare shot "opening"/);
104
+ });
105
+
106
+ it("agrees with collectScenarioShots on every valid scenario (shared walk)", () => {
107
+ const steps = [
108
+ { delayMs: 0, shot: "opening" },
109
+ { delayMs: 500 },
110
+ { delayMs: 700, shot: "midway" },
111
+ { delayMs: 300, shot: "finale" },
112
+ ];
113
+ const resolved = collectScenarioShots(steps, computeStepTimeline(steps, null));
114
+ expect(collectShotNames(steps)).toEqual(resolved.map((shot) => shot.name));
115
+ });
116
+ });
@@ -0,0 +1,114 @@
1
+ import type { StepTimeline } from "./compute-step-timeline.js";
2
+
3
+ /**
4
+ * Minimal step shape for shot collection. Accepts any ScenarioStep<T>
5
+ * without caring about the data payload.
6
+ */
7
+ interface StepWithShot {
8
+ delayMs: number;
9
+ shot?: string;
10
+ }
11
+
12
+ /** A named still-capture point resolved to its position on the timeline. */
13
+ export interface ScenarioShot {
14
+ /** The step's declared `shot` name (validated kebab-case, unique). */
15
+ readonly name: string;
16
+ /**
17
+ * The capture time: the step's settled end. For step i this is
18
+ * `stepStartTimesMs[i + 1] - 1`; for the final step it is
19
+ * `totalDurationMs - 1`, which already includes the final dwell (or the
20
+ * final narration clip, whichever is longer).
21
+ */
22
+ readonly timeMs: number;
23
+ /** Index of the step that declared the shot. */
24
+ readonly stepIndex: number;
25
+ }
26
+
27
+ /**
28
+ * The shape a `shot` name must have. Each name becomes a filename
29
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
30
+ * so the charset is deliberately the strict kebab-case subset of what the
31
+ * deploy path contract admits.
32
+ */
33
+ export const SHOT_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
34
+
35
+ /**
36
+ * Walk the steps and validate every declared `shot`, returning name +
37
+ * declaring index in step order — the single validation pass behind both
38
+ * {@link collectShotNames} and {@link collectScenarioShots}, so a name
39
+ * that passes one can never fail the other.
40
+ *
41
+ * Throws on the two authoring mistakes that must never reach a deployed
42
+ * bundle: a name that cannot be a clean filename/URL segment, and a
43
+ * duplicate name (shots are addressed by name, never by index — a
44
+ * collision would silently overwrite a still). Error messages name the
45
+ * offending step index so the fix is local.
46
+ */
47
+ function collectDeclaredShots(
48
+ steps: readonly StepWithShot[],
49
+ ): Array<{ name: string; stepIndex: number }> {
50
+ const declared: Array<{ name: string; stepIndex: number }> = [];
51
+ const seen = new Map<string, number>();
52
+
53
+ for (let i = 0; i < steps.length; i++) {
54
+ const name = steps[i]?.shot;
55
+ if (name === undefined) continue;
56
+
57
+ if (!SHOT_NAME_PATTERN.test(name)) {
58
+ throw new Error(
59
+ `step ${i} declares shot "${name}", which is not kebab-case ` +
60
+ `(expected ${SHOT_NAME_PATTERN}); it becomes a filename and a URL segment`,
61
+ );
62
+ }
63
+ const firstIndex = seen.get(name);
64
+ if (firstIndex !== undefined) {
65
+ throw new Error(
66
+ `steps ${firstIndex} and ${i} both declare shot "${name}"; ` +
67
+ `shot names must be unique within a scenario`,
68
+ );
69
+ }
70
+ seen.set(name, i);
71
+ declared.push({ name, stepIndex: i });
72
+ }
73
+
74
+ return declared;
75
+ }
76
+
77
+ /**
78
+ * The validated shot names declared by a scenario, in step order —
79
+ * everything about a scenario's shots that is knowable without a
80
+ * timeline. `scenar pack` records this list in the bundle's
81
+ * scenario.json so tooling can learn a bundle's shots (or that it has
82
+ * none) without booting a browser.
83
+ *
84
+ * Throws on the same authoring mistakes as {@link collectScenarioShots};
85
+ * see {@link collectDeclaredShots}.
86
+ */
87
+ export function collectShotNames(steps: readonly StepWithShot[]): string[] {
88
+ return collectDeclaredShots(steps).map((shot) => shot.name);
89
+ }
90
+
91
+ /**
92
+ * Resolve every `shot`-bearing step to its named capture point, in
93
+ * timeline order.
94
+ *
95
+ * Validation (and the errors it throws) is shared with
96
+ * {@link collectShotNames}; see {@link collectDeclaredShots}.
97
+ *
98
+ * The `timeline` must be computed from the same steps (and the same
99
+ * narration manifest) the capture will walk — shot times are meaningless
100
+ * against any other timeline.
101
+ */
102
+ export function collectScenarioShots(
103
+ steps: readonly StepWithShot[],
104
+ timeline: StepTimeline,
105
+ ): ScenarioShot[] {
106
+ return collectDeclaredShots(steps).map(({ name, stepIndex }) => {
107
+ const nextStartMs = timeline.stepStartTimesMs[stepIndex + 1];
108
+ return {
109
+ name,
110
+ timeMs: (nextStartMs ?? timeline.totalDurationMs) - 1,
111
+ stepIndex,
112
+ };
113
+ });
114
+ }
@@ -0,0 +1,55 @@
1
+ import type { StepTimeline } from "./compute-step-timeline.js";
2
+ /**
3
+ * Minimal step shape for shot collection. Accepts any ScenarioStep<T>
4
+ * without caring about the data payload.
5
+ */
6
+ interface StepWithShot {
7
+ delayMs: number;
8
+ shot?: string;
9
+ }
10
+ /** A named still-capture point resolved to its position on the timeline. */
11
+ export interface ScenarioShot {
12
+ /** The step's declared `shot` name (validated kebab-case, unique). */
13
+ readonly name: string;
14
+ /**
15
+ * The capture time: the step's settled end. For step i this is
16
+ * `stepStartTimesMs[i + 1] - 1`; for the final step it is
17
+ * `totalDurationMs - 1`, which already includes the final dwell (or the
18
+ * final narration clip, whichever is longer).
19
+ */
20
+ readonly timeMs: number;
21
+ /** Index of the step that declared the shot. */
22
+ readonly stepIndex: number;
23
+ }
24
+ /**
25
+ * The shape a `shot` name must have. Each name becomes a filename
26
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
27
+ * so the charset is deliberately the strict kebab-case subset of what the
28
+ * deploy path contract admits.
29
+ */
30
+ export declare const SHOT_NAME_PATTERN: RegExp;
31
+ /**
32
+ * The validated shot names declared by a scenario, in step order —
33
+ * everything about a scenario's shots that is knowable without a
34
+ * timeline. `scenar pack` records this list in the bundle's
35
+ * scenario.json so tooling can learn a bundle's shots (or that it has
36
+ * none) without booting a browser.
37
+ *
38
+ * Throws on the same authoring mistakes as {@link collectScenarioShots};
39
+ * see {@link collectDeclaredShots}.
40
+ */
41
+ export declare function collectShotNames(steps: readonly StepWithShot[]): string[];
42
+ /**
43
+ * Resolve every `shot`-bearing step to its named capture point, in
44
+ * timeline order.
45
+ *
46
+ * Validation (and the errors it throws) is shared with
47
+ * {@link collectShotNames}; see {@link collectDeclaredShots}.
48
+ *
49
+ * The `timeline` must be computed from the same steps (and the same
50
+ * narration manifest) the capture will walk — shot times are meaningless
51
+ * against any other timeline.
52
+ */
53
+ export declare function collectScenarioShots(steps: readonly StepWithShot[], timeline: StepTimeline): ScenarioShot[];
54
+ export {};
55
+ //# sourceMappingURL=collect-shots.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-shots.d.ts","sourceRoot":"","sources":["../../src/timeline/collect-shots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D;;;GAGG;AACH,UAAU,YAAY;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,4EAA4E;AAC5E,MAAM,WAAW,YAAY;IAC3B,sEAAsE;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AA4C5D;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,GAAG,MAAM,EAAE,CAEzE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,YAAY,EAAE,EAC9B,QAAQ,EAAE,YAAY,GACrB,YAAY,EAAE,CAShB"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The shape a `shot` name must have. Each name becomes a filename
3
+ * (`stills/<shot>.<theme>.png`) and a URL segment in the deployed bundle,
4
+ * so the charset is deliberately the strict kebab-case subset of what the
5
+ * deploy path contract admits.
6
+ */
7
+ export const SHOT_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/;
8
+ /**
9
+ * Walk the steps and validate every declared `shot`, returning name +
10
+ * declaring index in step order — the single validation pass behind both
11
+ * {@link collectShotNames} and {@link collectScenarioShots}, so a name
12
+ * that passes one can never fail the other.
13
+ *
14
+ * Throws on the two authoring mistakes that must never reach a deployed
15
+ * bundle: a name that cannot be a clean filename/URL segment, and a
16
+ * duplicate name (shots are addressed by name, never by index — a
17
+ * collision would silently overwrite a still). Error messages name the
18
+ * offending step index so the fix is local.
19
+ */
20
+ function collectDeclaredShots(steps) {
21
+ const declared = [];
22
+ const seen = new Map();
23
+ for (let i = 0; i < steps.length; i++) {
24
+ const name = steps[i]?.shot;
25
+ if (name === undefined)
26
+ continue;
27
+ if (!SHOT_NAME_PATTERN.test(name)) {
28
+ throw new Error(`step ${i} declares shot "${name}", which is not kebab-case ` +
29
+ `(expected ${SHOT_NAME_PATTERN}); it becomes a filename and a URL segment`);
30
+ }
31
+ const firstIndex = seen.get(name);
32
+ if (firstIndex !== undefined) {
33
+ throw new Error(`steps ${firstIndex} and ${i} both declare shot "${name}"; ` +
34
+ `shot names must be unique within a scenario`);
35
+ }
36
+ seen.set(name, i);
37
+ declared.push({ name, stepIndex: i });
38
+ }
39
+ return declared;
40
+ }
41
+ /**
42
+ * The validated shot names declared by a scenario, in step order —
43
+ * everything about a scenario's shots that is knowable without a
44
+ * timeline. `scenar pack` records this list in the bundle's
45
+ * scenario.json so tooling can learn a bundle's shots (or that it has
46
+ * none) without booting a browser.
47
+ *
48
+ * Throws on the same authoring mistakes as {@link collectScenarioShots};
49
+ * see {@link collectDeclaredShots}.
50
+ */
51
+ export function collectShotNames(steps) {
52
+ return collectDeclaredShots(steps).map((shot) => shot.name);
53
+ }
54
+ /**
55
+ * Resolve every `shot`-bearing step to its named capture point, in
56
+ * timeline order.
57
+ *
58
+ * Validation (and the errors it throws) is shared with
59
+ * {@link collectShotNames}; see {@link collectDeclaredShots}.
60
+ *
61
+ * The `timeline` must be computed from the same steps (and the same
62
+ * narration manifest) the capture will walk — shot times are meaningless
63
+ * against any other timeline.
64
+ */
65
+ export function collectScenarioShots(steps, timeline) {
66
+ return collectDeclaredShots(steps).map(({ name, stepIndex }) => {
67
+ const nextStartMs = timeline.stepStartTimesMs[stepIndex + 1];
68
+ return {
69
+ name,
70
+ timeMs: (nextStartMs ?? timeline.totalDurationMs) - 1,
71
+ stepIndex,
72
+ };
73
+ });
74
+ }
75
+ //# sourceMappingURL=collect-shots.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collect-shots.js","sourceRoot":"","sources":["../../src/timeline/collect-shots.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,0BAA0B,CAAC;AAE5D;;;;;;;;;;;GAWG;AACH,SAAS,oBAAoB,CAC3B,KAA8B;IAE9B,MAAM,QAAQ,GAA+C,EAAE,CAAC;IAChE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC;QAC5B,IAAI,IAAI,KAAK,SAAS;YAAE,SAAS;QAEjC,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,KAAK,CACb,QAAQ,CAAC,mBAAmB,IAAI,6BAA6B;gBAC3D,aAAa,iBAAiB,4CAA4C,CAC7E,CAAC;QACJ,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAClC,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,KAAK,CACb,SAAS,UAAU,QAAQ,CAAC,uBAAuB,IAAI,KAAK;gBAC1D,6CAA6C,CAChD,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;QAClB,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAA8B;IAC7D,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,KAA8B,EAC9B,QAAsB;IAEtB,OAAO,oBAAoB,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE;QAC7D,MAAM,WAAW,GAAG,QAAQ,CAAC,gBAAgB,CAAC,SAAS,GAAG,CAAC,CAAC,CAAC;QAC7D,OAAO;YACL,IAAI;YACJ,MAAM,EAAE,CAAC,WAAW,IAAI,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC;YACrD,SAAS;SACV,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}