@vosjs/shared 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,23 +1,24 @@
1
- # @vosso/shared
1
+ # @vosjs/shared
2
2
 
3
- Small, dependency-free helpers and types shared across the vosso apps, the
4
- studio's document model and the vos CLI plugin. Consumed in-source and bundled
5
- into [`@vosso/vos-plugin`](../vos-plugin). MIT.
3
+ The small shared layer under the `vos` CLI and the document model
4
+ (`@vosjs/studio-core`). Dependency-free. MIT.
6
5
 
7
- ## Exports
6
+ ## Subpaths
8
7
 
9
- ```ts
10
- import { formatLabel, generateId, sleep, safeJsonParse } from '@vosso/shared'
11
- ```
8
+ | Import | What it holds |
9
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------- |
10
+ | `@vosjs/shared` | The font and typeface catalogs (`FONT_CATALOG`, `TYPEFACE_CATALOG`) and their lookups and URL helpers |
11
+ | `@vosjs/shared/params` | The remix-knob contract of `config.params`: `ParamSpec`, `readParams`, `applyParamValue` |
12
+ | `@vosjs/shared/frontmatter` | A small `---` frontmatter parser for recipe files |
13
+ | `@vosjs/shared/timelineEdits` | `TimelineEdit` and the source wrapper that applies edits over a program's `createTimeline` |
12
14
 
13
- | Utility | Description |
14
- | ------------------------------- | -------------------------------------------------------------------- |
15
- | `formatLabel(filename)` | Turn a slug/filename into a title (`'basic-fade'` `'Basic Fade'`). |
16
- | `generateId()` | A random UUID (`crypto.randomUUID`). |
17
- | `sleep(ms)` | Promise that resolves after `ms`. |
18
- | `safeJsonParse(json, fallback)` | Parse JSON, returning `fallback` on error. |
15
+ The catalogs are generated data: the families, weights and typefaces the
16
+ hosted catalog serves, with their URLs. They are the one place this package
17
+ names a host, and a consumer may point `FONT_CDN_BASE`-shaped URLs elsewhere.
19
18
 
20
- Subpaths: `@vosso/shared/types` and `@vosso/shared/utils`.
19
+ ## What is deliberately not here
21
20
 
22
- Keep this package lean it's the lowest layer, so anything with a heavier
23
- dependency or a product opinion belongs in `@vosso/studio-core` or the app.
21
+ Anything one platform decides: quota and plan tables, attribution, a
22
+ hosted endpoint's wire types, a hosted changelog's differ. Those live beside
23
+ the service that enforces or serves them. Keep this package the lowest
24
+ layer: a product opinion belongs in `@vosjs/studio-core` or the host.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export { formatLabel, generateId, safeJsonParse, sleep } from './utils/index.js';
2
-
3
1
  /**
4
2
  * Generated by the vosso build; do not edit by hand.
5
3
  * Curation lives in the build script; re-run it (then `pnpm assets:push`) to
package/dist/index.js CHANGED
@@ -1,10 +1,3 @@
1
- import "./chunk-K7EIJSYQ.js";
2
- import {
3
- formatLabel,
4
- generateId,
5
- safeJsonParse,
6
- sleep
7
- } from "./chunk-RDD5SU6G.js";
8
1
  import {
9
2
  FONT_CATALOG,
10
3
  FONT_CDN_BASE,
@@ -50,11 +43,7 @@ export {
50
43
  fontFaceUrl,
51
44
  fontManifest,
52
45
  fontStack,
53
- formatLabel,
54
- generateId,
55
46
  nearestFontWeight,
56
- safeJsonParse,
57
- sleep,
58
47
  typefaceUrl
59
48
  };
60
49
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/typefaceCatalog.ts"],"sourcesContent":["/**\n * Generated by the vosso build; do not edit by hand.\n * Curation lives in the build script; regenerate + `pnpm assets:push`, then\n * commit this file. One catalog: the 3D prop panel, take lowering, and the\n * CLI lints all read it.\n */\n\nexport interface TypefaceEntry {\n /** Catalog family name (matches the webfont catalog). */\n family: string\n /** File slug — typefaces/{slug}.typeface.json on assets.vos.so. */\n slug: string\n /** The converted weight (one file per family; extrusion wants bold). */\n weight: number\n}\n\nexport const TYPEFACE_CDN_BASE = 'https://assets.vos.so/typefaces'\n\n/** The house default 3D face. */\nexport const DEFAULT_TYPEFACE_SLUG = 'lexend'\n\nexport const TYPEFACE_CATALOG: TypefaceEntry[] = [\n { family: 'Lexend', slug: 'lexend', weight: 700 },\n { family: 'Space Grotesk', slug: 'space-grotesk', weight: 700 },\n { family: 'Bebas Neue', slug: 'bebas-neue', weight: 400 },\n { family: 'Archivo Black', slug: 'archivo-black', weight: 400 },\n { family: 'Anton', slug: 'anton', weight: 400 },\n { family: 'Righteous', slug: 'righteous', weight: 400 },\n { family: 'Playfair Display', slug: 'playfair-display', weight: 700 },\n { family: 'Fraunces', slug: 'fraunces', weight: 700 },\n { family: 'Pacifico', slug: 'pacifico', weight: 400 },\n { family: 'JetBrains Mono', slug: 'jetbrains-mono', weight: 700 },\n]\n\nexport function typefaceUrl(slug: string): string {\n return `${TYPEFACE_CDN_BASE}/${slug}.typeface.json`\n}\n\n/** Case-insensitive lookup by slug or family name (null = not in catalog). */\nexport function findTypeface(nameOrSlug: string): TypefaceEntry | null {\n const needle = nameOrSlug.trim().toLowerCase()\n return (\n TYPEFACE_CATALOG.find(\n (t) => t.slug === needle || t.family.toLowerCase() === needle,\n ) ?? null\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBO,IAAM,oBAAoB;AAG1B,IAAM,wBAAwB;AAE9B,IAAM,mBAAoC;AAAA,EAC/C,EAAE,QAAQ,UAAU,MAAM,UAAU,QAAQ,IAAI;AAAA,EAChD,EAAE,QAAQ,iBAAiB,MAAM,iBAAiB,QAAQ,IAAI;AAAA,EAC9D,EAAE,QAAQ,cAAc,MAAM,cAAc,QAAQ,IAAI;AAAA,EACxD,EAAE,QAAQ,iBAAiB,MAAM,iBAAiB,QAAQ,IAAI;AAAA,EAC9D,EAAE,QAAQ,SAAS,MAAM,SAAS,QAAQ,IAAI;AAAA,EAC9C,EAAE,QAAQ,aAAa,MAAM,aAAa,QAAQ,IAAI;AAAA,EACtD,EAAE,QAAQ,oBAAoB,MAAM,oBAAoB,QAAQ,IAAI;AAAA,EACpE,EAAE,QAAQ,YAAY,MAAM,YAAY,QAAQ,IAAI;AAAA,EACpD,EAAE,QAAQ,YAAY,MAAM,YAAY,QAAQ,IAAI;AAAA,EACpD,EAAE,QAAQ,kBAAkB,MAAM,kBAAkB,QAAQ,IAAI;AAClE;AAEO,SAAS,YAAY,MAAsB;AAChD,SAAO,GAAG,iBAAiB,IAAI,IAAI;AACrC;AAGO,SAAS,aAAa,YAA0C;AACrE,QAAM,SAAS,WAAW,KAAK,EAAE,YAAY;AAC7C,SACE,iBAAiB;AAAA,IACf,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,OAAO,YAAY,MAAM;AAAA,EACzD,KAAK;AAET;","names":[]}
1
+ {"version":3,"sources":["../src/typefaceCatalog.ts"],"sourcesContent":["/**\n * Generated by the vosso build; do not edit by hand.\n * Curation lives in the build script; regenerate + `pnpm assets:push`, then\n * commit this file. One catalog: the 3D prop panel, take lowering, and the\n * CLI lints all read it.\n */\n\nexport interface TypefaceEntry {\n /** Catalog family name (matches the webfont catalog). */\n family: string\n /** File slug — typefaces/{slug}.typeface.json on assets.vos.so. */\n slug: string\n /** The converted weight (one file per family; extrusion wants bold). */\n weight: number\n}\n\nexport const TYPEFACE_CDN_BASE = 'https://assets.vos.so/typefaces'\n\n/** The house default 3D face. */\nexport const DEFAULT_TYPEFACE_SLUG = 'lexend'\n\nexport const TYPEFACE_CATALOG: TypefaceEntry[] = [\n { family: 'Lexend', slug: 'lexend', weight: 700 },\n { family: 'Space Grotesk', slug: 'space-grotesk', weight: 700 },\n { family: 'Bebas Neue', slug: 'bebas-neue', weight: 400 },\n { family: 'Archivo Black', slug: 'archivo-black', weight: 400 },\n { family: 'Anton', slug: 'anton', weight: 400 },\n { family: 'Righteous', slug: 'righteous', weight: 400 },\n { family: 'Playfair Display', slug: 'playfair-display', weight: 700 },\n { family: 'Fraunces', slug: 'fraunces', weight: 700 },\n { family: 'Pacifico', slug: 'pacifico', weight: 400 },\n { family: 'JetBrains Mono', slug: 'jetbrains-mono', weight: 700 },\n]\n\nexport function typefaceUrl(slug: string): string {\n return `${TYPEFACE_CDN_BASE}/${slug}.typeface.json`\n}\n\n/** Case-insensitive lookup by slug or family name (null = not in catalog). */\nexport function findTypeface(nameOrSlug: string): TypefaceEntry | null {\n const needle = nameOrSlug.trim().toLowerCase()\n return (\n TYPEFACE_CATALOG.find(\n (t) => t.slug === needle || t.family.toLowerCase() === needle,\n ) ?? null\n )\n}\n"],"mappings":";;;;;;;;;;;AAgBO,IAAM,oBAAoB;AAG1B,IAAM,wBAAwB;AAE9B,IAAM,mBAAoC;AAAA,EAC/C,EAAE,QAAQ,UAAU,MAAM,UAAU,QAAQ,IAAI;AAAA,EAChD,EAAE,QAAQ,iBAAiB,MAAM,iBAAiB,QAAQ,IAAI;AAAA,EAC9D,EAAE,QAAQ,cAAc,MAAM,cAAc,QAAQ,IAAI;AAAA,EACxD,EAAE,QAAQ,iBAAiB,MAAM,iBAAiB,QAAQ,IAAI;AAAA,EAC9D,EAAE,QAAQ,SAAS,MAAM,SAAS,QAAQ,IAAI;AAAA,EAC9C,EAAE,QAAQ,aAAa,MAAM,aAAa,QAAQ,IAAI;AAAA,EACtD,EAAE,QAAQ,oBAAoB,MAAM,oBAAoB,QAAQ,IAAI;AAAA,EACpE,EAAE,QAAQ,YAAY,MAAM,YAAY,QAAQ,IAAI;AAAA,EACpD,EAAE,QAAQ,YAAY,MAAM,YAAY,QAAQ,IAAI;AAAA,EACpD,EAAE,QAAQ,kBAAkB,MAAM,kBAAkB,QAAQ,IAAI;AAClE;AAEO,SAAS,YAAY,MAAsB;AAChD,SAAO,GAAG,iBAAiB,IAAI,IAAI;AACrC;AAGO,SAAS,aAAa,YAA0C;AACrE,QAAM,SAAS,WAAW,KAAK,EAAE,YAAY;AAC7C,SACE,iBAAiB;AAAA,IACf,CAAC,MAAM,EAAE,SAAS,UAAU,EAAE,OAAO,YAAY,MAAM;AAAA,EACzD,KAAK;AAET;","names":[]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vosjs/shared",
3
- "version": "0.3.0",
4
- "description": "The small shared layer under the vos CLI and the vosso studio: the semantic differ, the free-tier limits, frontmatter, params, the font and typeface catalogs.",
3
+ "version": "0.4.0",
4
+ "description": "The small shared layer under the vos CLI and the document model: the font and typeface catalogs, the remix params contract, frontmatter, and the timeline-edit wrapper.",
5
5
  "license": "MIT",
6
6
  "author": "vosso",
7
7
  "homepage": "https://vos.so",
@@ -21,18 +21,6 @@
21
21
  "types": "./dist/index.d.ts",
22
22
  "import": "./dist/index.js"
23
23
  },
24
- "./types": {
25
- "types": "./dist/types/index.d.ts",
26
- "import": "./dist/types/index.js"
27
- },
28
- "./utils": {
29
- "types": "./dist/utils/index.d.ts",
30
- "import": "./dist/utils/index.js"
31
- },
32
- "./diff": {
33
- "types": "./dist/diff/index.d.ts",
34
- "import": "./dist/diff/index.js"
35
- },
36
24
  "./frontmatter": {
37
25
  "types": "./dist/frontmatter.d.ts",
38
26
  "import": "./dist/frontmatter.js"
@@ -41,10 +29,6 @@
41
29
  "types": "./dist/params.d.ts",
42
30
  "import": "./dist/params.js"
43
31
  },
44
- "./limits": {
45
- "types": "./dist/limits.d.ts",
46
- "import": "./dist/limits.js"
47
- },
48
32
  "./timelineEdits": {
49
33
  "types": "./dist/timelineEdits.d.ts",
50
34
  "import": "./dist/timelineEdits.js"
@@ -1 +0,0 @@
1
- //# sourceMappingURL=chunk-K7EIJSYQ.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,25 +0,0 @@
1
- // src/utils/index.ts
2
- function formatLabel(filename) {
3
- return filename.replace(/\.[^/.]+$/, "").split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
4
- }
5
- function generateId() {
6
- return crypto.randomUUID();
7
- }
8
- function sleep(ms) {
9
- return new Promise((resolve) => setTimeout(resolve, ms));
10
- }
11
- function safeJsonParse(json, fallback) {
12
- try {
13
- return JSON.parse(json);
14
- } catch {
15
- return fallback;
16
- }
17
- }
18
-
19
- export {
20
- formatLabel,
21
- generateId,
22
- sleep,
23
- safeJsonParse
24
- };
25
- //# sourceMappingURL=chunk-RDD5SU6G.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/utils/index.ts"],"sourcesContent":["/**\n * Shared Utilities\n * Common utility functions used across frontend and backend\n */\n\n/**\n * Format a filename to a human-readable label\n * @example formatLabel('basic-fade') => 'Basic Fade'\n */\nexport function formatLabel(filename: string): string {\n return filename\n .replace(/\\.[^/.]+$/, '') // Remove extension\n .split(/[-_]/)\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n .join(' ')\n}\n\n/**\n * Generate a unique ID\n */\nexport function generateId(): string {\n return crypto.randomUUID()\n}\n\n/**\n * Sleep for a specified duration\n */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/**\n * Safely parse JSON with a fallback\n */\nexport function safeJsonParse<T>(json: string, fallback: T): T {\n try {\n return JSON.parse(json) as T\n } catch {\n return fallback\n }\n}\n"],"mappings":";AASO,SAAS,YAAY,UAA0B;AACpD,SAAO,SACJ,QAAQ,aAAa,EAAE,EACvB,MAAM,MAAM,EACZ,IAAI,CAAC,SAAS,KAAK,OAAO,CAAC,EAAE,YAAY,IAAI,KAAK,MAAM,CAAC,EAAE,YAAY,CAAC,EACxE,KAAK,GAAG;AACb;AAKO,SAAS,aAAqB;AACnC,SAAO,OAAO,WAAW;AAC3B;AAKO,SAAS,MAAM,IAA2B;AAC/C,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AAKO,SAAS,cAAiB,MAAc,UAAgB;AAC7D,MAAI;AACF,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -1,65 +0,0 @@
1
- /**
2
- * The semantic differ: pure functions over two
3
- * immutable version states — no I/O, no recorded gesture log ("derive,
4
- * don't record"). Runs identically in Workers, the CLI, and the studio;
5
- * same two inputs ⇒ byte-identical ops and summary.
6
- */
7
- interface Op {
8
- op: 'add' | 'remove' | 'modify';
9
- /**
10
- * Doc tracks: zoom | tilt | speed | overlay | object | audio, plus scoped
11
- * singletons (segments | frame | cursor | cam | export | camera).
12
- * Config tracks: knob (param key space) | look | data | code | config.
13
- */
14
- track: string;
15
- /** Span/clip id, param key, function name, or field name. */
16
- id: string;
17
- /** Changed properties: key → [from, to]. Absent on code ops. */
18
- props?: Record<string, [unknown, unknown]>;
19
- /** Code-function modifies only: magnitude of the line-level change. */
20
- lines?: number;
21
- /**
22
- * True when the change is a cascaded consequence of another gesture
23
- * (e.g. a trim shifting downstream spans), not a direct edit.
24
- */
25
- cascade?: boolean;
26
- }
27
- /** One intervening version's worth of changes, as the pull contract ships it. */
28
- interface VersionChanges {
29
- versionId: string;
30
- versionNumber: number;
31
- origin: string;
32
- label: string | null;
33
- note: string | null;
34
- ops: Op[];
35
- summary: string;
36
- }
37
- /** Stable composite key for net-effect folding. */
38
- declare function opKey(op: Op): string;
39
-
40
- type AnyRecord = Record<string, unknown>;
41
-
42
- declare function diffConfig(a: AnyRecord, b: AnyRecord): Op[];
43
-
44
- declare function diffDoc(a: AnyRecord, b: AnyRecord): Op[];
45
-
46
- /**
47
- * Net effect per node across a sequence of op sets (the coalescing rules):
48
- * initial→final only, no intermediate drags. add→modify folds into the add;
49
- * add→remove vanishes; modify chains keep the first `from` and last `to`
50
- * (dropping props that net to no change); remove→add of the same id is one
51
- * modify (the node was replaced in place). Output is deterministically
52
- * ordered: track declaration order, then output-time anchor, then id.
53
- */
54
- declare function coalesce(opsInOrder: readonly Op[]): Op[];
55
-
56
- /**
57
- * Deterministic prose over an op list — the line that lands in the agent's
58
- * context ("zoom z3: end 4.2s→4.6s, level 2.2→1.8; tilt t1 removed").
59
- * Same ops ⇒ byte-identical string; unit-pinned like the geometry mirrors.
60
- */
61
- declare function summarize(ops: readonly Op[], options?: {
62
- maxItems?: number;
63
- }): string;
64
-
65
- export { type Op, type VersionChanges, coalesce, diffConfig, diffDoc, opKey, summarize };
@@ -1,568 +0,0 @@
1
- // src/diff/internal.ts
2
- function isRecord(v) {
3
- return !!v && typeof v === "object" && !Array.isArray(v);
4
- }
5
- function deepEqual(a, b) {
6
- if (Object.is(a, b)) return true;
7
- if (typeof a !== typeof b) return false;
8
- if (Array.isArray(a) && Array.isArray(b)) {
9
- return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]));
10
- }
11
- if (isRecord(a) && isRecord(b)) {
12
- const ka = Object.keys(a);
13
- const kb = Object.keys(b);
14
- if (ka.length !== kb.length) return false;
15
- return ka.every((k) => deepEqual(a[k], b[k]));
16
- }
17
- return false;
18
- }
19
- function shallowDiff(a, b, ignore = /* @__PURE__ */ new Set()) {
20
- const props = {};
21
- const keys = [.../* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)])].sort();
22
- for (const k of keys) {
23
- if (ignore.has(k)) continue;
24
- if (!deepEqual(a[k], b[k])) props[k] = [a[k], b[k]];
25
- }
26
- return Object.keys(props).length ? props : void 0;
27
- }
28
- function diffIdTrack(track, a, b) {
29
- const ops = [];
30
- const aById = new Map((a ?? []).map((n) => [String(n.id), n]));
31
- const bById = new Map((b ?? []).map((n) => [String(n.id), n]));
32
- for (const [id, node] of aById) {
33
- if (!bById.has(id)) {
34
- ops.push({ op: "remove", track, id, props: pickAnchor(node) });
35
- }
36
- }
37
- for (const [id, node] of bById) {
38
- const prev = aById.get(id);
39
- if (!prev) {
40
- ops.push({ op: "add", track, id, props: pickAnchor(node) });
41
- continue;
42
- }
43
- const props = shallowDiff(prev, node, ID_KEYS);
44
- if (props) ops.push({ op: "modify", track, id, props });
45
- }
46
- return ops;
47
- }
48
- var ID_KEYS = /* @__PURE__ */ new Set(["id"]);
49
- function pickAnchor(node) {
50
- const out = {};
51
- for (const k of ["in", "out", "at", "start", "kind", "key"]) {
52
- if (node[k] !== void 0) out[k] = [void 0, node[k]];
53
- }
54
- return Object.keys(out).length ? out : void 0;
55
- }
56
- function lineDelta(a, b) {
57
- const count = (s) => {
58
- const m = /* @__PURE__ */ new Map();
59
- for (const line of s.split("\n")) m.set(line, (m.get(line) ?? 0) + 1);
60
- return m;
61
- };
62
- const ca = count(a);
63
- const cb = count(b);
64
- let removed = 0;
65
- let added = 0;
66
- for (const [line, n] of ca) removed += Math.max(0, n - (cb.get(line) ?? 0));
67
- for (const [line, n] of cb) added += Math.max(0, n - (ca.get(line) ?? 0));
68
- return Math.max(added, removed);
69
- }
70
-
71
- // src/diff/diffConfig.ts
72
- var FUNCTION_FIELDS = ["setup", "createContent", "createTimeline", "onFrame"];
73
- var CONFIG_FIELDS = ["duration", "camera", "postprocessing", "elements"];
74
- function diffConfig(a, b) {
75
- const ops = [];
76
- const aParams = paramsByKey(a);
77
- const bParams = paramsByKey(b);
78
- const aData = isRecord(a.data) ? a.data : {};
79
- const bData = isRecord(b.data) ? b.data : {};
80
- const removedKeys = [];
81
- const addedKeys = [];
82
- for (const key of aParams.keys()) if (!bParams.has(key)) removedKeys.push(key);
83
- for (const key of bParams.keys()) if (!aParams.has(key)) addedKeys.push(key);
84
- const renamed = /* @__PURE__ */ new Map();
85
- for (const oldKey of [...removedKeys]) {
86
- const oldSpec = aParams.get(oldKey);
87
- const match = addedKeys.find(
88
- (newKey) => specEqualIgnoringIdentity(oldSpec, bParams.get(newKey))
89
- );
90
- if (match) {
91
- renamed.set(oldKey, match);
92
- removedKeys.splice(removedKeys.indexOf(oldKey), 1);
93
- addedKeys.splice(addedKeys.indexOf(match), 1);
94
- }
95
- }
96
- for (const [oldKey, newKey] of renamed) {
97
- const props = {
98
- key: [oldKey, newKey]
99
- };
100
- const fromVal = valueOf(aData, aParams.get(oldKey), oldKey);
101
- const toVal = valueOf(bData, bParams.get(newKey), newKey);
102
- if (!deepEqual(fromVal, toVal)) props.value = [fromVal, toVal];
103
- ops.push({ op: "modify", track: "knob", id: newKey, props });
104
- }
105
- for (const key of removedKeys) {
106
- ops.push({ op: "remove", track: "knob", id: key });
107
- }
108
- for (const key of addedKeys) {
109
- const spec = bParams.get(key);
110
- ops.push({
111
- op: "add",
112
- track: "knob",
113
- id: key,
114
- props: {
115
- kind: [void 0, spec.kind],
116
- value: [void 0, valueOf(bData, spec, key)]
117
- }
118
- });
119
- }
120
- for (const [key, aSpec] of aParams) {
121
- const bSpec = bParams.get(key);
122
- if (!bSpec) continue;
123
- const props = {};
124
- const aVal = valueOf(aData, aSpec, key);
125
- const bVal = valueOf(bData, bSpec, key);
126
- if (!deepEqual(aVal, bVal)) props.value = [aVal, bVal];
127
- const specDiff = shallowDiff(aSpec, bSpec, KNOB_IDENTITY);
128
- if (specDiff) {
129
- if ("default" in specDiff && "value" in props) delete specDiff.default;
130
- Object.assign(props, specDiff);
131
- }
132
- if (Object.keys(props).length) {
133
- ops.push({ op: "modify", track: "knob", id: key, props });
134
- }
135
- }
136
- const declared = /* @__PURE__ */ new Set([...aParams.keys(), ...bParams.keys()]);
137
- const dataKeys = [
138
- .../* @__PURE__ */ new Set([...Object.keys(aData), ...Object.keys(bData)])
139
- ].sort();
140
- for (const key of dataKeys) {
141
- if (declared.has(key)) continue;
142
- const from = aData[key];
143
- const to = bData[key];
144
- if (deepEqual(from, to)) continue;
145
- const op = from === void 0 ? "add" : to === void 0 ? "remove" : "modify";
146
- ops.push({ op, track: "data", id: key, props: { value: [from, to] } });
147
- }
148
- ops.push(
149
- ...diffIdTrack("look", looksAsNodes(a.presets), looksAsNodes(b.presets))
150
- );
151
- for (const fn of FUNCTION_FIELDS) {
152
- const va = a[fn];
153
- const vb = b[fn];
154
- const fa = typeof va === "string" ? va : "";
155
- const fb = typeof vb === "string" ? vb : "";
156
- if (fa === fb) continue;
157
- ops.push({
158
- op: "modify",
159
- track: "code",
160
- id: fn,
161
- lines: lineDelta(fa, fb)
162
- });
163
- }
164
- for (const field of CONFIG_FIELDS) {
165
- if (deepEqual(a[field], b[field])) continue;
166
- ops.push({
167
- op: "modify",
168
- track: "config",
169
- id: field,
170
- props: { value: [a[field], b[field]] }
171
- });
172
- }
173
- return ops;
174
- }
175
- var KNOB_IDENTITY = /* @__PURE__ */ new Set(["key", "label", "order"]);
176
- function paramsByKey(config) {
177
- const out = /* @__PURE__ */ new Map();
178
- if (!Array.isArray(config.params)) return out;
179
- for (const entry of config.params) {
180
- if (isRecord(entry) && typeof entry.key === "string" && entry.key) {
181
- out.set(entry.key, entry);
182
- }
183
- }
184
- return out;
185
- }
186
- function valueOf(data, spec, key) {
187
- return data[key] !== void 0 ? data[key] : spec.default;
188
- }
189
- function specEqualIgnoringIdentity(a, b) {
190
- return !shallowDiff(a, b, KNOB_IDENTITY);
191
- }
192
- function looksAsNodes(presets) {
193
- if (!Array.isArray(presets)) return [];
194
- return presets.filter((p) => isRecord(p) && typeof p.name === "string").map((p) => ({ ...p, id: p.name }));
195
- }
196
-
197
- // src/diff/diffDoc.ts
198
- var ID_TRACKS = [
199
- ["zoom", "zoom"],
200
- ["tilt", "tilt"],
201
- ["camMove", "camMotion"],
202
- ["speed", "speed"],
203
- ["rejected", "rejected"],
204
- ["overlay", "overlays"],
205
- ["object", "objects"],
206
- ["audio", "audio"]
207
- ];
208
- var SCOPED_FIELDS = ["frame", "cursor", "cam", "export"];
209
- function diffDoc(a, b) {
210
- const ops = [];
211
- const pa = isRecord(a.program) ? a.program : void 0;
212
- const pb = isRecord(b.program) ? b.program : void 0;
213
- if (pa || pb) {
214
- ops.push(
215
- ...diffConfig(
216
- pa && isRecord(pa.config) ? pa.config : {},
217
- pb && isRecord(pb.config) ? pb.config : {}
218
- )
219
- );
220
- const ea = pa && isRecord(pa.tweenEdits) ? pa.tweenEdits : {};
221
- const eb = pb && isRecord(pb.tweenEdits) ? pb.tweenEdits : {};
222
- for (const key of /* @__PURE__ */ new Set([...Object.keys(ea), ...Object.keys(eb)])) {
223
- const va = isRecord(ea[key]) ? ea[key] : void 0;
224
- const vb = isRecord(eb[key]) ? eb[key] : void 0;
225
- if (va && !vb) ops.push({ op: "remove", track: "tween", id: `#${key}` });
226
- else if (!va && vb) ops.push({ op: "add", track: "tween", id: `#${key}` });
227
- else if (va && vb) {
228
- const props = shallowDiff(va, vb);
229
- if (props)
230
- ops.push({ op: "modify", track: "tween", id: `#${key}`, props });
231
- }
232
- }
233
- const da = programLength(pa);
234
- const db = programLength(pb);
235
- if (da !== db && (pa?.duration !== void 0 || pb?.duration !== void 0)) {
236
- ops.push({
237
- op: "modify",
238
- track: "config",
239
- id: "duration",
240
- props: { value: [da, db] }
241
- });
242
- }
243
- }
244
- for (const [track, field] of ID_TRACKS) {
245
- ops.push(...diffIdTrack(track, asNodeList(a[field]), asNodeList(b[field])));
246
- }
247
- const speedOps = ops.filter((o) => o.track === "speed");
248
- if (speedOps.length) {
249
- const [oa, ob] = [outputDuration(a), outputDuration(b)];
250
- if (oa !== null && ob !== null && Math.abs(oa - ob) >= 0.05) {
251
- speedOps[0].props = { ...speedOps[0].props ?? {}, output: [oa, ob] };
252
- }
253
- }
254
- const aSeg = asNodeList(a.segments) ?? [];
255
- const bSeg = asNodeList(b.segments) ?? [];
256
- if (!deepEqual(a.segments, b.segments)) {
257
- const props = {};
258
- if (aSeg.length !== bSeg.length) props.count = [aSeg.length, bSeg.length];
259
- const [da, db] = [keptDuration(aSeg), keptDuration(bSeg)];
260
- if (da !== db) props.duration = [da, db];
261
- ops.push({
262
- op: "modify",
263
- track: "segments",
264
- id: "segments",
265
- props: Object.keys(props).length ? props : { value: ["edited", "edited"] }
266
- });
267
- }
268
- for (const field of SCOPED_FIELDS) {
269
- const va = a[field];
270
- const vb = b[field];
271
- const fa = isRecord(va) ? va : {};
272
- const fb = isRecord(vb) ? vb : {};
273
- const props = shallowDiff(fa, fb);
274
- if (props) ops.push({ op: "modify", track: field, id: field, props });
275
- }
276
- const cameraProps = {};
277
- for (const k of ["zoomStyle", "tiltStyle", "micGain"]) {
278
- if (!deepEqual(a[k], b[k])) cameraProps[k] = [a[k], b[k]];
279
- }
280
- if (!deepEqual(a.zoomParams, b.zoomParams)) {
281
- cameraProps.zoomParams = [a.zoomParams, b.zoomParams];
282
- }
283
- if (Object.keys(cameraProps).length) {
284
- ops.push({
285
- op: "modify",
286
- track: "camera",
287
- id: "camera",
288
- props: cameraProps
289
- });
290
- }
291
- return ops;
292
- }
293
- function asNodeList(v) {
294
- if (!Array.isArray(v)) return void 0;
295
- return v.filter(isRecord);
296
- }
297
- function programLength(program) {
298
- if (!program) return null;
299
- if (typeof program.duration === "number") return program.duration;
300
- const cfg = isRecord(program.config) ? program.config : void 0;
301
- return cfg && typeof cfg.duration === "number" ? cfg.duration : null;
302
- }
303
- function outputDuration(doc) {
304
- const source = isRecord(doc.source) ? doc.source : void 0;
305
- const meta = source && isRecord(source.meta) ? source.meta : void 0;
306
- const durS = isRecord(doc.program) ? programLength(doc.program) : meta && typeof meta.durationMs === "number" ? meta.durationMs / 1e3 : null;
307
- const explicit = (asNodeList(doc.segments) ?? []).map((s) => ({
308
- in: typeof s.in === "number" ? s.in : 0,
309
- out: typeof s.out === "number" ? s.out : 0
310
- })).filter((s) => s.out > s.in);
311
- const segments = explicit.length ? explicit : durS !== null ? [{ in: 0, out: durS }] : null;
312
- if (!segments) return null;
313
- const spans = (asNodeList(doc.speed) ?? []).map((s) => ({
314
- in: typeof s.in === "number" ? s.in : 0,
315
- out: typeof s.out === "number" ? s.out : 0,
316
- rate: typeof s.rate === "number" && s.rate > 0 ? s.rate : 1
317
- })).filter((s) => s.out > s.in);
318
- let total = 0;
319
- for (const seg of segments) {
320
- total += seg.out - seg.in;
321
- for (const sp of spans) {
322
- const ov = Math.min(seg.out, sp.out) - Math.max(seg.in, sp.in);
323
- if (ov > 0) total -= ov - ov / sp.rate;
324
- }
325
- }
326
- return Math.round(total * 1e3) / 1e3;
327
- }
328
- function keptDuration(segments) {
329
- let total = 0;
330
- for (const s of segments) {
331
- const start = typeof s.in === "number" ? s.in : 0;
332
- const end = typeof s.out === "number" ? s.out : start;
333
- total += Math.max(0, end - start);
334
- }
335
- return Math.round(total * 1e3) / 1e3;
336
- }
337
-
338
- // src/diff/types.ts
339
- function opKey(op) {
340
- return `${op.track}\0${op.id}`;
341
- }
342
-
343
- // src/diff/coalesce.ts
344
- function coalesce(opsInOrder) {
345
- const byNode = /* @__PURE__ */ new Map();
346
- for (const next of opsInOrder) {
347
- const key = opKey(next);
348
- const prev = byNode.get(key);
349
- if (!prev) {
350
- byNode.set(key, cloneOp(next));
351
- continue;
352
- }
353
- const folded = foldPair(prev, next);
354
- if (folded) byNode.set(key, folded);
355
- else byNode.delete(key);
356
- }
357
- return [...byNode.values()].sort(compareOps);
358
- }
359
- function foldPair(prev, next) {
360
- if (prev.op === "add" && next.op === "remove") return null;
361
- if (prev.op === "add" && next.op === "modify") {
362
- return { ...prev, props: mergeProps(prev.props, next.props) };
363
- }
364
- if (next.op === "remove") return { ...next };
365
- if (prev.op === "remove" && next.op === "add") {
366
- return { ...next, op: "modify", props: mergeProps(prev.props, next.props) };
367
- }
368
- if (prev.op === "modify" && next.op === "modify") {
369
- const merged = mergeProps(prev.props, next.props, true);
370
- const lines = prev.lines !== void 0 || next.lines !== void 0 ? (prev.lines ?? 0) + (next.lines ?? 0) : void 0;
371
- if (!merged && lines === void 0) return null;
372
- return { ...prev, props: merged, lines };
373
- }
374
- return { ...next };
375
- }
376
- function mergeProps(a, b, dropNoNet = false) {
377
- if (!a) return b ? { ...b } : void 0;
378
- if (!b) return { ...a };
379
- const out = { ...a };
380
- for (const [k, [from, to]] of Object.entries(b)) {
381
- out[k] = k in out ? [out[k][0], to] : [from, to];
382
- }
383
- if (dropNoNet) {
384
- for (const [k, [from, to]] of Object.entries(out)) {
385
- if (JSON.stringify(from) === JSON.stringify(to)) delete out[k];
386
- }
387
- }
388
- return Object.keys(out).length ? out : void 0;
389
- }
390
- function cloneOp(op) {
391
- return { ...op, props: op.props ? { ...op.props } : void 0 };
392
- }
393
- var TRACK_ORDER = [
394
- "segments",
395
- "frame",
396
- "camera",
397
- "cursor",
398
- "cam",
399
- "zoom",
400
- "tilt",
401
- "speed",
402
- "overlay",
403
- "object",
404
- "audio",
405
- "export",
406
- "knob",
407
- "look",
408
- "data",
409
- "code",
410
- "config"
411
- ];
412
- function compareOps(a, b) {
413
- const ta = TRACK_ORDER.indexOf(a.track);
414
- const tb = TRACK_ORDER.indexOf(b.track);
415
- if (ta !== tb) return (ta === -1 ? 99 : ta) - (tb === -1 ? 99 : tb);
416
- const anchorA = timeAnchor(a);
417
- const anchorB = timeAnchor(b);
418
- if (anchorA !== anchorB) return anchorA - anchorB;
419
- return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
420
- }
421
- function timeAnchor(op) {
422
- for (const k of ["in", "at", "start"]) {
423
- const pair = op.props?.[k];
424
- if (pair) {
425
- const v = pair[1] ?? pair[0];
426
- if (typeof v === "number") return v;
427
- }
428
- }
429
- return Number.MAX_SAFE_INTEGER;
430
- }
431
-
432
- // src/diff/summarize.ts
433
- function summarize(ops, options = {}) {
434
- const max = options.maxItems ?? 12;
435
- if (ops.length === 0) return "no changes";
436
- const shown = ops.slice(0, max);
437
- const parts = shown.map(phrase);
438
- const hidden = ops.length - shown.length;
439
- if (hidden > 0)
440
- parts.push(`and ${hidden} smaller change${hidden === 1 ? "" : "s"}`);
441
- return parts.join("; ");
442
- }
443
- function phrase(op) {
444
- if (op.track === "code") {
445
- return `code ${op.id} changed (${op.lines ?? 0} line${op.lines === 1 ? "" : "s"})`;
446
- }
447
- if (op.track === "knob") return knobPhrase(op);
448
- if (op.track === "config") {
449
- if (op.id === "duration" && op.props?.value) {
450
- const [from, to] = op.props.value;
451
- return `duration ${fmt(from, true)}\u2192${fmt(to, true)}`;
452
- }
453
- return `${op.id} changed`;
454
- }
455
- if (op.track === "data") {
456
- const pair = op.props?.value;
457
- if (op.op === "add") return `data ${op.id} set to ${fmt(pair?.[1])}`;
458
- if (op.op === "remove") return `data ${op.id} removed`;
459
- return `data ${op.id}: ${fmt(pair?.[0])}\u2192${fmt(pair?.[1])}`;
460
- }
461
- if (op.track === "look") {
462
- if (op.op === "add") return `look "${op.id}" added`;
463
- if (op.op === "remove") return `look "${op.id}" removed`;
464
- return `look "${op.id}" changed`;
465
- }
466
- if (op.track === "segments") {
467
- const d = op.props?.duration;
468
- if (d) return `trim: kept footage ${fmt(d[0], true)}\u2192${fmt(d[1], true)}`;
469
- return "segments edited";
470
- }
471
- if (SCOPED_TRACKS.has(op.track)) {
472
- return `${op.track} ${propList(op.props)}`;
473
- }
474
- const name = `${op.track} ${op.id}`;
475
- const output = op.props?.output;
476
- const outputNote = output ? `output ${fmt(output[0], true)}\u2192${fmt(output[1], true)}` : null;
477
- if (op.op === "add") {
478
- const range = rangeOf(op);
479
- const bits = [range, outputNote].filter(Boolean).join(", ");
480
- return bits ? `${name} added (${bits})` : `${name} added`;
481
- }
482
- if (op.op === "remove")
483
- return outputNote ? `${name} removed (${outputNote})` : `${name} removed`;
484
- return `${name}: ${propList(op.props)}`;
485
- }
486
- function knobPhrase(op) {
487
- const rename = op.props?.key;
488
- if (rename) return `replaced knob ${fmt(rename[0])} with ${fmt(rename[1])}`;
489
- if (op.op === "add") {
490
- const kind = (op.props?.kind ?? [])[1];
491
- return typeof kind === "string" ? `knob ${op.id} added (${kind})` : `knob ${op.id} added`;
492
- }
493
- if (op.op === "remove") return `knob ${op.id} removed`;
494
- const value = op.props?.value;
495
- const parts = [];
496
- if (value) parts.push(`${fmt(value[0])}\u2192${fmt(value[1])}`);
497
- for (const [k, [from, to]] of Object.entries(op.props ?? {})) {
498
- if (k === "value") continue;
499
- parts.push(`${k} ${fmt(from)}\u2192${fmt(to)}`);
500
- }
501
- return `knob ${op.id}: ${parts.join(", ")}`;
502
- }
503
- var SCOPED_TRACKS = /* @__PURE__ */ new Set(["frame", "cursor", "cam", "export", "camera"]);
504
- var PROP_LABELS = {
505
- in: "start",
506
- out: "end",
507
- cx: "focus x",
508
- cy: "focus y"
509
- };
510
- var TIME_PROPS = /* @__PURE__ */ new Set([
511
- "in",
512
- "out",
513
- "at",
514
- "start",
515
- "end",
516
- "duration",
517
- "output"
518
- ]);
519
- function propList(props) {
520
- if (!props) return "changed";
521
- return Object.entries(props).map(([k, [from, to]]) => {
522
- const time = TIME_PROPS.has(k);
523
- return `${PROP_LABELS[k] ?? k} ${fmt(from, time)}\u2192${fmt(to, time)}`;
524
- }).join(", ");
525
- }
526
- function rangeOf(op) {
527
- const startPair = anchorPair(op, ["in", "at", "start"]);
528
- const start = startPair ? startPair[1] : void 0;
529
- if (typeof start !== "number") return null;
530
- const endPair = anchorPair(op, ["out"]);
531
- const end = endPair ? endPair[1] : void 0;
532
- return typeof end === "number" ? `${fmt(start, true)}\u2013${fmt(end, true)}` : `at ${fmt(start, true)}`;
533
- }
534
- function anchorPair(op, keys) {
535
- for (const k of keys) {
536
- const pair = op.props?.[k];
537
- if (pair) return pair;
538
- }
539
- return void 0;
540
- }
541
- var FMT_STRING_MAX = 40;
542
- function fmt(v, time = false) {
543
- if (v === void 0) return "\u2205";
544
- if (v === null) return "null";
545
- if (typeof v === "number") {
546
- const rounded = Math.round(v * 100) / 100;
547
- return time ? `${rounded}s` : String(rounded);
548
- }
549
- if (typeof v === "boolean") return v ? "on" : "off";
550
- if (typeof v === "string") {
551
- if (v === "") return '""';
552
- const flat = v.replace(/\s*\n\s*/g, " ");
553
- const clipped = flat.length > FMT_STRING_MAX ? `${flat.slice(0, FMT_STRING_MAX - 1)}\u2026` : flat;
554
- return clipped === v ? quoteIfNeeded(clipped) : `"${clipped}"`;
555
- }
556
- return "changed";
557
- }
558
- function quoteIfNeeded(s) {
559
- return /^[\w.#-]+$/.test(s) ? s : `"${s}"`;
560
- }
561
- export {
562
- coalesce,
563
- diffConfig,
564
- diffDoc,
565
- opKey,
566
- summarize
567
- };
568
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../src/diff/internal.ts","../../src/diff/diffConfig.ts","../../src/diff/diffDoc.ts","../../src/diff/types.ts","../../src/diff/coalesce.ts","../../src/diff/summarize.ts"],"sourcesContent":["import type { Op } from './types'\n\nexport type AnyRecord = Record<string, unknown>\n\nexport function isRecord(v: unknown): v is AnyRecord {\n return !!v && typeof v === 'object' && !Array.isArray(v)\n}\n\nexport function deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true\n if (typeof a !== typeof b) return false\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => deepEqual(v, b[i]))\n }\n if (isRecord(a) && isRecord(b)) {\n const ka = Object.keys(a)\n const kb = Object.keys(b)\n if (ka.length !== kb.length) return false\n return ka.every((k) => deepEqual(a[k], b[k]))\n }\n return false\n}\n\n/**\n * Shallow property diff of two node objects: changed keys → [from, to].\n * Nested values compare deep but report as whole-value pairs — span/clip\n * props are overwhelmingly scalars, and a whole-value pair stays honest for\n * the rest. `ignore` keys (identity fields) never report.\n */\nexport function shallowDiff(\n a: AnyRecord,\n b: AnyRecord,\n ignore: ReadonlySet<string> = new Set(),\n): Record<string, [unknown, unknown]> | undefined {\n const props: Record<string, [unknown, unknown]> = {}\n const keys = [...new Set([...Object.keys(a), ...Object.keys(b)])].sort()\n for (const k of keys) {\n if (ignore.has(k)) continue\n if (!deepEqual(a[k], b[k])) props[k] = [a[k], b[k]]\n }\n return Object.keys(props).length ? props : undefined\n}\n\n/**\n * Diff two id-keyed node lists into add/remove/modify ops. Identity is the\n * id — a moved span with the same id is a modify (the trap case); a\n * remove+add pair is two different nodes by construction.\n */\nexport function diffIdTrack(\n track: string,\n a: readonly AnyRecord[] | undefined,\n b: readonly AnyRecord[] | undefined,\n): Op[] {\n const ops: Op[] = []\n const aById = new Map((a ?? []).map((n) => [String(n.id), n]))\n const bById = new Map((b ?? []).map((n) => [String(n.id), n]))\n for (const [id, node] of aById) {\n if (!bById.has(id)) {\n ops.push({ op: 'remove', track, id, props: pickAnchor(node) })\n }\n }\n for (const [id, node] of bById) {\n const prev = aById.get(id)\n if (!prev) {\n ops.push({ op: 'add', track, id, props: pickAnchor(node) })\n continue\n }\n const props = shallowDiff(prev, node, ID_KEYS)\n if (props) ops.push({ op: 'modify', track, id, props })\n }\n return ops\n}\n\nconst ID_KEYS = new Set(['id'])\n\n/**\n * Anchor props carried on add/remove ops so the summary can place the node\n * in time (\"zoom z5 added (0.8s–2.4s)\") without shipping the whole node.\n */\nfunction pickAnchor(\n node: AnyRecord,\n): Record<string, [unknown, unknown]> | undefined {\n const out: Record<string, [unknown, unknown]> = {}\n for (const k of ['in', 'out', 'at', 'start', 'kind', 'key']) {\n if (node[k] !== undefined) out[k] = [undefined, node[k]]\n }\n return Object.keys(out).length ? out : undefined\n}\n\n/** Magnitude of a function-string change: max(lines added, lines removed). */\nexport function lineDelta(a: string, b: string): number {\n const count = (s: string): Map<string, number> => {\n const m = new Map<string, number>()\n for (const line of s.split('\\n')) m.set(line, (m.get(line) ?? 0) + 1)\n return m\n }\n const ca = count(a)\n const cb = count(b)\n let removed = 0\n let added = 0\n for (const [line, n] of ca) removed += Math.max(0, n - (cb.get(line) ?? 0))\n for (const [line, n] of cb) added += Math.max(0, n - (ca.get(line) ?? 0))\n return Math.max(added, removed)\n}\n","import {\n deepEqual,\n diffIdTrack,\n isRecord,\n lineDelta,\n shallowDiff,\n} from './internal'\nimport type { AnyRecord } from './internal'\nimport type { Op } from './types'\n\n/**\n * Semantic diff over two VosConfigJson states: param keys are\n * program mode's stable ids, so knob values and specs diff fully typed;\n * function-string bodies collapse to a magnitude (\"createContent changed,\n * 12 lines\") — the accepted asymmetry, in the direction nobody reads JSON.\n */\n\nconst FUNCTION_FIELDS = ['setup', 'createContent', 'createTimeline', 'onFrame']\n\n/** Top-level structured fields worth reporting individually. */\nconst CONFIG_FIELDS = ['duration', 'camera', 'postprocessing', 'elements']\n\nexport function diffConfig(a: AnyRecord, b: AnyRecord): Op[] {\n const ops: Op[] = []\n\n const aParams = paramsByKey(a)\n const bParams = paramsByKey(b)\n const aData = isRecord(a.data) ? a.data : {}\n const bData = isRecord(b.data) ? b.data : {}\n\n // --- Knobs: spec + value changes, keyed by param key -------------------\n const removedKeys: string[] = []\n const addedKeys: string[] = []\n for (const key of aParams.keys()) if (!bParams.has(key)) removedKeys.push(key)\n for (const key of bParams.keys()) if (!aParams.has(key)) addedKeys.push(key)\n\n // Rename detection (the \"replaced knob\" trap): a removed and an added key\n // whose specs match ignoring identity/labeling are ONE knob renamed.\n const renamed = new Map<string, string>() // old → new\n for (const oldKey of [...removedKeys]) {\n const oldSpec = aParams.get(oldKey)!\n const match = addedKeys.find((newKey) =>\n specEqualIgnoringIdentity(oldSpec, bParams.get(newKey)!),\n )\n if (match) {\n renamed.set(oldKey, match)\n removedKeys.splice(removedKeys.indexOf(oldKey), 1)\n addedKeys.splice(addedKeys.indexOf(match), 1)\n }\n }\n\n for (const [oldKey, newKey] of renamed) {\n const props: Record<string, [unknown, unknown]> = {\n key: [oldKey, newKey],\n }\n const fromVal = valueOf(aData, aParams.get(oldKey)!, oldKey)\n const toVal = valueOf(bData, bParams.get(newKey)!, newKey)\n if (!deepEqual(fromVal, toVal)) props.value = [fromVal, toVal]\n ops.push({ op: 'modify', track: 'knob', id: newKey, props })\n }\n for (const key of removedKeys) {\n ops.push({ op: 'remove', track: 'knob', id: key })\n }\n for (const key of addedKeys) {\n const spec = bParams.get(key)!\n ops.push({\n op: 'add',\n track: 'knob',\n id: key,\n props: {\n kind: [undefined, spec.kind],\n value: [undefined, valueOf(bData, spec, key)],\n },\n })\n }\n for (const [key, aSpec] of aParams) {\n const bSpec = bParams.get(key)\n if (!bSpec) continue\n const props: Record<string, [unknown, unknown]> = {}\n const aVal = valueOf(aData, aSpec, key)\n const bVal = valueOf(bData, bSpec, key)\n if (!deepEqual(aVal, bVal)) props.value = [aVal, bVal]\n const specDiff = shallowDiff(aSpec, bSpec, KNOB_IDENTITY)\n if (specDiff) {\n // Default changes only matter when they aren't already the value change\n // (applyParamValue bakes default AND data together).\n if ('default' in specDiff && 'value' in props) delete specDiff.default\n Object.assign(props, specDiff)\n }\n if (Object.keys(props).length) {\n ops.push({ op: 'modify', track: 'knob', id: key, props })\n }\n }\n\n // --- Data keys not declared as knobs ------------------------------------\n const declared = new Set([...aParams.keys(), ...bParams.keys()])\n const dataKeys = [\n ...new Set([...Object.keys(aData), ...Object.keys(bData)]),\n ].sort()\n for (const key of dataKeys) {\n if (declared.has(key)) continue\n const from = aData[key]\n const to = bData[key]\n if (deepEqual(from, to)) continue\n const op: Op['op'] =\n from === undefined ? 'add' : to === undefined ? 'remove' : 'modify'\n ops.push({ op, track: 'data', id: key, props: { value: [from, to] } })\n }\n\n // --- Looks (presets), keyed by name -------------------------------------\n ops.push(\n ...diffIdTrack('look', looksAsNodes(a.presets), looksAsNodes(b.presets)),\n )\n\n // --- Function strings collapse to magnitude -----------------------------\n for (const fn of FUNCTION_FIELDS) {\n const va = a[fn]\n const vb = b[fn]\n const fa = typeof va === 'string' ? va : ''\n const fb = typeof vb === 'string' ? vb : ''\n if (fa === fb) continue\n ops.push({\n op: 'modify',\n track: 'code',\n id: fn,\n lines: lineDelta(fa, fb),\n })\n }\n\n // --- Remaining structured top-level fields ------------------------------\n for (const field of CONFIG_FIELDS) {\n if (deepEqual(a[field], b[field])) continue\n ops.push({\n op: 'modify',\n track: 'config',\n id: field,\n props: { value: [a[field], b[field]] },\n })\n }\n\n return ops\n}\n\n/** Spec fields that are identity/labeling, not behavior. */\nconst KNOB_IDENTITY = new Set(['key', 'label', 'order'])\n\ninterface ParamSpecLike extends AnyRecord {\n key: string\n kind?: unknown\n default?: unknown\n}\n\nfunction paramsByKey(config: AnyRecord): Map<string, ParamSpecLike> {\n const out = new Map<string, ParamSpecLike>()\n if (!Array.isArray(config.params)) return out\n for (const entry of config.params) {\n if (isRecord(entry) && typeof entry.key === 'string' && entry.key) {\n out.set(entry.key, entry as ParamSpecLike)\n }\n }\n return out\n}\n\n/** A knob's effective value: data wins (what playback reads), else default. */\nfunction valueOf(data: AnyRecord, spec: ParamSpecLike, key: string): unknown {\n return data[key] !== undefined ? data[key] : spec.default\n}\n\nfunction specEqualIgnoringIdentity(\n a: ParamSpecLike,\n b: ParamSpecLike,\n): boolean {\n return !shallowDiff(a, b, KNOB_IDENTITY)\n}\n\nfunction looksAsNodes(presets: unknown): AnyRecord[] {\n if (!Array.isArray(presets)) return []\n return presets\n .filter((p): p is AnyRecord => isRecord(p) && typeof p.name === 'string')\n .map((p) => ({ ...p, id: p.name }))\n}\n","import { diffConfig } from './diffConfig'\nimport { deepEqual, diffIdTrack, isRecord, shallowDiff } from './internal'\nimport type { AnyRecord } from './internal'\nimport type { Op } from './types'\n\n/**\n * Semantic diff over two ProjectDoc states (wired end-to-end once hosted\n * versions persist take docs). Every editable node has a stable id, so id-keyed\n * tracks diff exactly; singleton surfaces report as scoped modifies. Typed\n * structurally (Record-based) — the differ must not depend on studio-core.\n */\n\n/** Track name → doc field, for the id-keyed span/clip tracks. */\nconst ID_TRACKS: readonly [track: string, field: string][] = [\n ['zoom', 'zoom'],\n ['tilt', 'tilt'],\n ['camMove', 'camMotion'],\n ['speed', 'speed'],\n ['rejected', 'rejected'],\n ['overlay', 'overlays'],\n ['object', 'objects'],\n ['audio', 'audio'],\n]\n\n/** Singleton surfaces that report as one scoped modify each. */\nconst SCOPED_FIELDS = ['frame', 'cursor', 'cam', 'export'] as const\n\nexport function diffDoc(a: AnyRecord, b: AnyRecord): Op[] {\n const ops: Op[] = []\n\n // The program anchor: its config diffs as a program does (code /\n // knob / data / look / config tracks), and its tween-timing overlay as\n // one `tween` track keyed by spec index.\n const pa = isRecord(a.program) ? a.program : undefined\n const pb = isRecord(b.program) ? b.program : undefined\n if (pa || pb) {\n ops.push(\n ...diffConfig(\n pa && isRecord(pa.config) ? pa.config : {},\n pb && isRecord(pb.config) ? pb.config : {},\n ),\n )\n const ea = pa && isRecord(pa.tweenEdits) ? pa.tweenEdits : {}\n const eb = pb && isRecord(pb.tweenEdits) ? pb.tweenEdits : {}\n for (const key of new Set([...Object.keys(ea), ...Object.keys(eb)])) {\n const va = isRecord(ea[key]) ? ea[key] : undefined\n const vb = isRecord(eb[key]) ? eb[key] : undefined\n if (va && !vb) ops.push({ op: 'remove', track: 'tween', id: `#${key}` })\n else if (!va && vb) ops.push({ op: 'add', track: 'tween', id: `#${key}` })\n else if (va && vb) {\n const props = shallowDiff(va, vb)\n if (props)\n ops.push({ op: 'modify', track: 'tween', id: `#${key}`, props })\n }\n }\n // A changed `program.duration` (the anchor's own length) reports as the\n // config's duration would; the config's own duration already did above.\n const da = programLength(pa)\n const db = programLength(pb)\n if (\n da !== db &&\n (pa?.duration !== undefined || pb?.duration !== undefined)\n ) {\n ops.push({\n op: 'modify',\n track: 'config',\n id: 'duration',\n props: { value: [da, db] },\n })\n }\n }\n\n for (const [track, field] of ID_TRACKS) {\n ops.push(...diffIdTrack(track, asNodeList(a[field]), asNodeList(b[field])))\n }\n\n // A speed edit's cost in OUTPUT seconds rides the first\n // speed op, so the history line says what the cut gained or lost\n // (\"speed sp0: rate 2→4, output 25s→22.5s\").\n const speedOps = ops.filter((o) => o.track === 'speed')\n if (speedOps.length) {\n const [oa, ob] = [outputDuration(a), outputDuration(b)]\n if (oa !== null && ob !== null && Math.abs(oa - ob) >= 0.05) {\n speedOps[0].props = { ...(speedOps[0].props ?? {}), output: [oa, ob] }\n }\n }\n\n // Segments: the trim surface. Report count + kept-duration movement, not\n // the raw arrays (payloads stay small; the compare player shows the rest).\n const aSeg = asNodeList(a.segments) ?? []\n const bSeg = asNodeList(b.segments) ?? []\n if (!deepEqual(a.segments, b.segments)) {\n const props: Record<string, [unknown, unknown]> = {}\n if (aSeg.length !== bSeg.length) props.count = [aSeg.length, bSeg.length]\n const [da, db] = [keptDuration(aSeg), keptDuration(bSeg)]\n if (da !== db) props.duration = [da, db]\n ops.push({\n op: 'modify',\n track: 'segments',\n id: 'segments',\n props: Object.keys(props).length\n ? props\n : { value: ['edited', 'edited'] },\n })\n }\n\n for (const field of SCOPED_FIELDS) {\n const va = a[field]\n const vb = b[field]\n const fa = isRecord(va) ? va : {}\n const fb = isRecord(vb) ? vb : {}\n const props = shallowDiff(fa, fb)\n if (props) ops.push({ op: 'modify', track: field, id: field, props })\n }\n\n // Camera personality: style + overrides + wand intensity as ONE surface.\n const cameraProps: Record<string, [unknown, unknown]> = {}\n for (const k of ['zoomStyle', 'tiltStyle', 'micGain'] as const) {\n if (!deepEqual(a[k], b[k])) cameraProps[k] = [a[k], b[k]]\n }\n if (!deepEqual(a.zoomParams, b.zoomParams)) {\n cameraProps.zoomParams = [a.zoomParams, b.zoomParams]\n }\n if (Object.keys(cameraProps).length) {\n ops.push({\n op: 'modify',\n track: 'camera',\n id: 'camera',\n props: cameraProps,\n })\n }\n\n return ops\n}\n\nfunction asNodeList(v: unknown): AnyRecord[] | undefined {\n if (!Array.isArray(v)) return undefined\n return v.filter(isRecord)\n}\n\n/**\n * OUTPUT duration of a doc: kept segments with speed spans applied — the\n * same arithmetic the lowering's rate map performs, kept structural so the\n * differ stays dependency-free. Null when the doc cannot say (no source\n * duration and no explicit segments).\n */\nfunction programLength(program: AnyRecord | undefined): number | null {\n if (!program) return null\n if (typeof program.duration === 'number') return program.duration\n const cfg = isRecord(program.config) ? program.config : undefined\n return cfg && typeof cfg.duration === 'number' ? cfg.duration : null\n}\n\nfunction outputDuration(doc: AnyRecord): number | null {\n // A program document is one source span, its own length: its speed\n // spans rate it the way a recording's rate the footage.\n const source = isRecord(doc.source) ? doc.source : undefined\n const meta = source && isRecord(source.meta) ? source.meta : undefined\n const durS = isRecord(doc.program)\n ? programLength(doc.program)\n : meta && typeof meta.durationMs === 'number'\n ? meta.durationMs / 1000\n : null\n const explicit = (asNodeList(doc.segments) ?? [])\n .map((s) => ({\n in: typeof s.in === 'number' ? s.in : 0,\n out: typeof s.out === 'number' ? s.out : 0,\n }))\n .filter((s) => s.out > s.in)\n const segments = explicit.length\n ? explicit\n : durS !== null\n ? [{ in: 0, out: durS }]\n : null\n if (!segments) return null\n const spans = (asNodeList(doc.speed) ?? [])\n .map((s) => ({\n in: typeof s.in === 'number' ? s.in : 0,\n out: typeof s.out === 'number' ? s.out : 0,\n rate: typeof s.rate === 'number' && s.rate > 0 ? s.rate : 1,\n }))\n .filter((s) => s.out > s.in)\n let total = 0\n for (const seg of segments) {\n total += seg.out - seg.in\n for (const sp of spans) {\n const ov = Math.min(seg.out, sp.out) - Math.max(seg.in, sp.in)\n if (ov > 0) total -= ov - ov / sp.rate\n }\n }\n return Math.round(total * 1000) / 1000\n}\n\nfunction keptDuration(segments: AnyRecord[]): number {\n let total = 0\n for (const s of segments) {\n const start = typeof s.in === 'number' ? s.in : 0\n const end = typeof s.out === 'number' ? s.out : start\n total += Math.max(0, end - start)\n }\n return Math.round(total * 1000) / 1000\n}\n","/**\n * The semantic differ: pure functions over two\n * immutable version states — no I/O, no recorded gesture log (\"derive,\n * don't record\"). Runs identically in Workers, the CLI, and the studio;\n * same two inputs ⇒ byte-identical ops and summary.\n */\n\nexport interface Op {\n op: 'add' | 'remove' | 'modify'\n /**\n * Doc tracks: zoom | tilt | speed | overlay | object | audio, plus scoped\n * singletons (segments | frame | cursor | cam | export | camera).\n * Config tracks: knob (param key space) | look | data | code | config.\n */\n track: string\n /** Span/clip id, param key, function name, or field name. */\n id: string\n /** Changed properties: key → [from, to]. Absent on code ops. */\n props?: Record<string, [unknown, unknown]>\n /** Code-function modifies only: magnitude of the line-level change. */\n lines?: number\n /**\n * True when the change is a cascaded consequence of another gesture\n * (e.g. a trim shifting downstream spans), not a direct edit.\n */\n cascade?: boolean\n}\n\n/** One intervening version's worth of changes, as the pull contract ships it. */\nexport interface VersionChanges {\n versionId: string\n versionNumber: number\n origin: string\n label: string | null\n note: string | null\n ops: Op[]\n summary: string\n}\n\n/** Stable composite key for net-effect folding. */\nexport function opKey(op: Op): string {\n return `${op.track}\u0000${op.id}`\n}\n","import { opKey } from './types'\nimport type { Op } from './types'\n\n/**\n * Net effect per node across a sequence of op sets (the coalescing rules):\n * initial→final only, no intermediate drags. add→modify folds into the add;\n * add→remove vanishes; modify chains keep the first `from` and last `to`\n * (dropping props that net to no change); remove→add of the same id is one\n * modify (the node was replaced in place). Output is deterministically\n * ordered: track declaration order, then output-time anchor, then id.\n */\nexport function coalesce(opsInOrder: readonly Op[]): Op[] {\n const byNode = new Map<string, Op>()\n\n for (const next of opsInOrder) {\n const key = opKey(next)\n const prev = byNode.get(key)\n if (!prev) {\n byNode.set(key, cloneOp(next))\n continue\n }\n const folded = foldPair(prev, next)\n if (folded) byNode.set(key, folded)\n else byNode.delete(key)\n }\n\n return [...byNode.values()].sort(compareOps)\n}\n\nfunction foldPair(prev: Op, next: Op): Op | null {\n // add then remove: the node never existed for the reader.\n if (prev.op === 'add' && next.op === 'remove') return null\n // add then modify: still an add, with the final properties.\n if (prev.op === 'add' && next.op === 'modify') {\n return { ...prev, props: mergeProps(prev.props, next.props) }\n }\n // modify then remove: the removal is the story.\n if (next.op === 'remove') return { ...next }\n // remove then add: replaced in place — one modify.\n if (prev.op === 'remove' && next.op === 'add') {\n return { ...next, op: 'modify', props: mergeProps(prev.props, next.props) }\n }\n // modify then modify: first from, last to; drop props that net out.\n if (prev.op === 'modify' && next.op === 'modify') {\n const merged = mergeProps(prev.props, next.props, true)\n const lines =\n prev.lines !== undefined || next.lines !== undefined\n ? (prev.lines ?? 0) + (next.lines ?? 0)\n : undefined\n if (!merged && lines === undefined) return null\n return { ...prev, props: merged, lines }\n }\n // Remaining combinations (add→add, remove→remove) shouldn't occur between\n // well-formed sets; keep the latest as the honest answer.\n return { ...next }\n}\n\nfunction mergeProps(\n a: Op['props'],\n b: Op['props'],\n dropNoNet = false,\n): Op['props'] {\n if (!a) return b ? { ...b } : undefined\n if (!b) return { ...a }\n const out: NonNullable<Op['props']> = { ...a }\n for (const [k, [from, to]] of Object.entries(b)) {\n out[k] = k in out ? [out[k][0], to] : [from, to]\n }\n if (dropNoNet) {\n for (const [k, [from, to]] of Object.entries(out)) {\n if (JSON.stringify(from) === JSON.stringify(to)) delete out[k]\n }\n }\n return Object.keys(out).length ? out : undefined\n}\n\nfunction cloneOp(op: Op): Op {\n return { ...op, props: op.props ? { ...op.props } : undefined }\n}\n\n/** Track presentation order: content tracks in output order, then meta. */\nconst TRACK_ORDER = [\n 'segments',\n 'frame',\n 'camera',\n 'cursor',\n 'cam',\n 'zoom',\n 'tilt',\n 'speed',\n 'overlay',\n 'object',\n 'audio',\n 'export',\n 'knob',\n 'look',\n 'data',\n 'code',\n 'config',\n]\n\nfunction compareOps(a: Op, b: Op): number {\n const ta = TRACK_ORDER.indexOf(a.track)\n const tb = TRACK_ORDER.indexOf(b.track)\n if (ta !== tb) return (ta === -1 ? 99 : ta) - (tb === -1 ? 99 : tb)\n const anchorA = timeAnchor(a)\n const anchorB = timeAnchor(b)\n if (anchorA !== anchorB) return anchorA - anchorB\n return a.id < b.id ? -1 : a.id > b.id ? 1 : 0\n}\n\nfunction timeAnchor(op: Op): number {\n for (const k of ['in', 'at', 'start']) {\n const pair = op.props?.[k]\n if (pair) {\n const v = pair[1] ?? pair[0]\n if (typeof v === 'number') return v\n }\n }\n return Number.MAX_SAFE_INTEGER\n}\n","import type { Op } from './types'\n\n/**\n * Deterministic prose over an op list — the line that lands in the agent's\n * context (\"zoom z3: end 4.2s→4.6s, level 2.2→1.8; tilt t1 removed\").\n * Same ops ⇒ byte-identical string; unit-pinned like the geometry mirrors.\n */\nexport function summarize(\n ops: readonly Op[],\n options: { maxItems?: number } = {},\n): string {\n const max = options.maxItems ?? 12\n if (ops.length === 0) return 'no changes'\n const shown = ops.slice(0, max)\n const parts = shown.map(phrase)\n const hidden = ops.length - shown.length\n if (hidden > 0)\n parts.push(`and ${hidden} smaller change${hidden === 1 ? '' : 's'}`)\n return parts.join('; ')\n}\n\nfunction phrase(op: Op): string {\n if (op.track === 'code') {\n return `code ${op.id} changed (${op.lines ?? 0} line${op.lines === 1 ? '' : 's'})`\n }\n if (op.track === 'knob') return knobPhrase(op)\n if (op.track === 'config') {\n if (op.id === 'duration' && op.props?.value) {\n const [from, to] = op.props.value\n return `duration ${fmt(from, true)}→${fmt(to, true)}`\n }\n return `${op.id} changed`\n }\n if (op.track === 'data') {\n const pair = op.props?.value\n if (op.op === 'add') return `data ${op.id} set to ${fmt(pair?.[1])}`\n if (op.op === 'remove') return `data ${op.id} removed`\n return `data ${op.id}: ${fmt(pair?.[0])}→${fmt(pair?.[1])}`\n }\n if (op.track === 'look') {\n if (op.op === 'add') return `look \"${op.id}\" added`\n if (op.op === 'remove') return `look \"${op.id}\" removed`\n return `look \"${op.id}\" changed`\n }\n if (op.track === 'segments') {\n const d = op.props?.duration\n if (d) return `trim: kept footage ${fmt(d[0], true)}→${fmt(d[1], true)}`\n return 'segments edited'\n }\n if (SCOPED_TRACKS.has(op.track)) {\n return `${op.track} ${propList(op.props)}`\n }\n\n // Id-keyed span/clip tracks.\n const name = `${op.track} ${op.id}`\n // The output-duration pair a speed edit carries (what the cut gained\n // or lost in screen seconds) rides add/remove phrases too — a new 2× span\n // shortens the video without modifying anything.\n const output = op.props?.output\n const outputNote = output\n ? `output ${fmt(output[0], true)}→${fmt(output[1], true)}`\n : null\n if (op.op === 'add') {\n const range = rangeOf(op)\n const bits = [range, outputNote].filter(Boolean).join(', ')\n return bits ? `${name} added (${bits})` : `${name} added`\n }\n if (op.op === 'remove')\n return outputNote ? `${name} removed (${outputNote})` : `${name} removed`\n return `${name}: ${propList(op.props)}`\n}\n\nfunction knobPhrase(op: Op): string {\n const rename = op.props?.key\n if (rename) return `replaced knob ${fmt(rename[0])} with ${fmt(rename[1])}`\n if (op.op === 'add') {\n const kind = (op.props?.kind ?? [])[1]\n return typeof kind === 'string'\n ? `knob ${op.id} added (${kind})`\n : `knob ${op.id} added`\n }\n if (op.op === 'remove') return `knob ${op.id} removed`\n const value = op.props?.value\n const parts: string[] = []\n if (value) parts.push(`${fmt(value[0])}→${fmt(value[1])}`)\n for (const [k, [from, to]] of Object.entries(op.props ?? {})) {\n if (k === 'value') continue\n parts.push(`${k} ${fmt(from)}→${fmt(to)}`)\n }\n return `knob ${op.id}: ${parts.join(', ')}`\n}\n\nconst SCOPED_TRACKS = new Set(['frame', 'cursor', 'cam', 'export', 'camera'])\n\n/** Humanized property names for the take vocabulary. */\nconst PROP_LABELS: Record<string, string> = {\n in: 'start',\n out: 'end',\n cx: 'focus x',\n cy: 'focus y',\n}\n\n/** Properties whose numbers are seconds. */\nconst TIME_PROPS = new Set([\n 'in',\n 'out',\n 'at',\n 'start',\n 'end',\n 'duration',\n 'output',\n])\n\nfunction propList(props: Op['props']): string {\n if (!props) return 'changed'\n return Object.entries(props)\n .map(([k, [from, to]]) => {\n const time = TIME_PROPS.has(k)\n return `${PROP_LABELS[k] ?? k} ${fmt(from, time)}→${fmt(to, time)}`\n })\n .join(', ')\n}\n\nfunction rangeOf(op: Op): string | null {\n const startPair = anchorPair(op, ['in', 'at', 'start'])\n const start = startPair ? startPair[1] : undefined\n if (typeof start !== 'number') return null\n const endPair = anchorPair(op, ['out'])\n const end = endPair ? endPair[1] : undefined\n return typeof end === 'number'\n ? `${fmt(start, true)}–${fmt(end, true)}`\n : `at ${fmt(start, true)}`\n}\n\nfunction anchorPair(\n op: Op,\n keys: readonly string[],\n): [unknown, unknown] | undefined {\n for (const k of keys) {\n const pair = op.props?.[k]\n if (pair) return pair\n }\n return undefined\n}\n\n/**\n * Long strings (content knobs carry whole headlines) collapse to\n * one line and truncate — the summary is a changelog sentence, and it also\n * rides 409 stale_base bodies, so a multiline value must never spill raw.\n */\nconst FMT_STRING_MAX = 40\n\nfunction fmt(v: unknown, time = false): string {\n if (v === undefined) return '∅'\n if (v === null) return 'null'\n if (typeof v === 'number') {\n const rounded = Math.round(v * 100) / 100\n return time ? `${rounded}s` : String(rounded)\n }\n if (typeof v === 'boolean') return v ? 'on' : 'off'\n if (typeof v === 'string') {\n if (v === '') return '\"\"'\n const flat = v.replace(/\\s*\\n\\s*/g, ' ')\n const clipped =\n flat.length > FMT_STRING_MAX\n ? `${flat.slice(0, FMT_STRING_MAX - 1)}…`\n : flat\n return clipped === v ? quoteIfNeeded(clipped) : `\"${clipped}\"`\n }\n return 'changed'\n}\n\nfunction quoteIfNeeded(s: string): string {\n return /^[\\w.#-]+$/.test(s) ? s : `\"${s}\"`\n}\n"],"mappings":"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","names":[]}
package/dist/limits.d.ts DELETED
@@ -1,76 +0,0 @@
1
- /**
2
- * The plan limits table.
3
- *
4
- * ONE table, keyed by `user.plan`, read by every quota verdict on the API
5
- * and by every client that states a limit before the user hits it (the
6
- * recorder's countdown, the CLI's `record`, the settings usage strip).
7
- * 'free' is the only row today and nothing writes `user.plan`; a paid plan
8
- * is a second row here plus a server-side writer, never a rewrite of the
9
- * guards. An unknown or absent plan resolves to free — never to a grant.
10
- *
11
- * Numbers are proposals that can be retuned as data: every
12
- * refusal prints its number from here, so the copy can never disagree.
13
- */
14
- type Plan = 'free';
15
- declare const PLANS: readonly Plan[];
16
- interface PlanLimits {
17
- /** Session vos creates per rolling 24h. */
18
- dailySaves: number;
19
- /** Private voses held at once (counted at create). */
20
- privateVoses: number;
21
- /** Key-authed .glb/.gltf uploads per 24h, per owning user. */
22
- keyModelUploads: number;
23
- /** Key-authed recording uploads per 24h, per owning user. */
24
- keyRecordingUploads: number;
25
- /** Key-authed image uploads per 24h (posters/stills an agent files into a project). */
26
- keyImageUploads: number;
27
- /** Session recording uploads per 24h (uploads count as uploads). */
28
- recordingUploads: number;
29
- /** Session image / HDR / audio uploads per 24h. */
30
- otherUploads: number;
31
- /** Longest hosted recording, seconds (stated before you record). */
32
- recordingMaxSeconds: number;
33
- /** Largest single recording upload, bytes (the ingest ceiling). */
34
- recordingMaxBytes: number;
35
- /** Total asset bytes an account may hold (said in hours in the UI). */
36
- storageBytes: number;
37
- /** Session versions per vos per 24h. */
38
- versionsPerVosDay: number;
39
- /** Key-authed version pushes per 24h across every vos. */
40
- keyVersionsPerDay: number;
41
- /** Voses + assets + recipes one folder may hold. */
42
- folderItems: number;
43
- /** Subfolders one folder may hold. */
44
- subfolders: number;
45
- /** Plumbing media enqueues (preview + thumbnail) per vos per hour. */
46
- mediaPerVosHour: number;
47
- /** Plumbing media enqueues per account per 24h. Past it, media DEFERS. */
48
- mediaPerAccountDay: number;
49
- /** Artifact render output-minutes per account per 24h (armed with cloud export). */
50
- artifactRenderMinutesPerDay: number;
51
- /** Own-vos backdrop bakes per account per 24h (cache hits never count). */
52
- backdropBakesPerDay: number;
53
- /** Folders per user, across every nesting level. */
54
- folders: number;
55
- /** Recipe files (.md assets) per user. */
56
- recipes: number;
57
- /** Key-authed recipe creates per 24h. */
58
- keyRecipeCreates: number;
59
- /** Recipe body byte cap (upload + in-place replace). */
60
- recipeBodyBytes: number;
61
- }
62
- /** Unknown or absent ⇒ free. A plan the table does not know is never a grant. */
63
- declare function planLimits(plan?: string | null): PlanLimits;
64
- /**
65
- * The storage ceiling in the unit a human can picture. Footage at ~5 Mbps
66
- * is ~0.375 GB per 10 minutes, so 5 GB reads as "about 2 hours"; the guard
67
- * counts bytes, the sentence says hours. Rounded to a half hour.
68
- */
69
- declare const FOOTAGE_BYTES_PER_SECOND: number;
70
- declare function bytesAsFootageHours(bytes: number): number;
71
- /** `1.2 GB`, `640 MB`, `12 KB` — for the usage strip and refusals. */
72
- declare function formatBytes(bytes: number): string;
73
- /** `30 min`, `1 h 30 min`, `45 s` — the duration cap in words. */
74
- declare function formatDurationCap(seconds: number): string;
75
-
76
- export { FOOTAGE_BYTES_PER_SECOND, PLANS, type Plan, type PlanLimits, bytesAsFootageHours, formatBytes, formatDurationCap, planLimits };
package/dist/limits.js DELETED
@@ -1,59 +0,0 @@
1
- // src/limits.ts
2
- var PLANS = ["free"];
3
- var FREE = {
4
- dailySaves: 10,
5
- privateVoses: 20,
6
- keyModelUploads: 10,
7
- keyRecordingUploads: 20,
8
- keyImageUploads: 40,
9
- recordingUploads: 20,
10
- otherUploads: 50,
11
- recordingMaxSeconds: 30 * 60,
12
- recordingMaxBytes: 512 * 1024 * 1024,
13
- storageBytes: 5 * 1024 * 1024 * 1024,
14
- versionsPerVosDay: 100,
15
- keyVersionsPerDay: 200,
16
- folderItems: 500,
17
- subfolders: 50,
18
- mediaPerVosHour: 20,
19
- mediaPerAccountDay: 300,
20
- artifactRenderMinutesPerDay: 60,
21
- backdropBakesPerDay: 20,
22
- // Folders & recipes: sanity caps, not a paywall — organization
23
- // stays free at every size that isn't abuse.
24
- folders: 50,
25
- recipes: 200,
26
- keyRecipeCreates: 20,
27
- recipeBodyBytes: 64 * 1024
28
- };
29
- var LIMITS = { free: FREE };
30
- function planLimits(plan) {
31
- return LIMITS[plan ?? "free"] ?? FREE;
32
- }
33
- var FOOTAGE_BYTES_PER_SECOND = 5e6 / 8 * 1;
34
- function bytesAsFootageHours(bytes) {
35
- return Math.round(bytes / FOOTAGE_BYTES_PER_SECOND / 3600 * 2) / 2;
36
- }
37
- function formatBytes(bytes) {
38
- if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
39
- if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
40
- if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`;
41
- return `${bytes} B`;
42
- }
43
- function formatDurationCap(seconds) {
44
- if (seconds < 60) return `${seconds} s`;
45
- const m = Math.round(seconds / 60);
46
- if (m < 60) return `${m} min`;
47
- const h = Math.floor(m / 60);
48
- const rest = m % 60;
49
- return rest ? `${h} h ${rest} min` : `${h} h`;
50
- }
51
- export {
52
- FOOTAGE_BYTES_PER_SECOND,
53
- PLANS,
54
- bytesAsFootageHours,
55
- formatBytes,
56
- formatDurationCap,
57
- planLimits
58
- };
59
- //# sourceMappingURL=limits.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/limits.ts"],"sourcesContent":["/**\n * The plan limits table.\n *\n * ONE table, keyed by `user.plan`, read by every quota verdict on the API\n * and by every client that states a limit before the user hits it (the\n * recorder's countdown, the CLI's `record`, the settings usage strip).\n * 'free' is the only row today and nothing writes `user.plan`; a paid plan\n * is a second row here plus a server-side writer, never a rewrite of the\n * guards. An unknown or absent plan resolves to free — never to a grant.\n *\n * Numbers are proposals that can be retuned as data: every\n * refusal prints its number from here, so the copy can never disagree.\n */\n\nexport type Plan = 'free'\n\nexport const PLANS: readonly Plan[] = ['free']\n\nexport interface PlanLimits {\n /** Session vos creates per rolling 24h. */\n dailySaves: number\n /** Private voses held at once (counted at create). */\n privateVoses: number\n /** Key-authed .glb/.gltf uploads per 24h, per owning user. */\n keyModelUploads: number\n /** Key-authed recording uploads per 24h, per owning user. */\n keyRecordingUploads: number\n /** Key-authed image uploads per 24h (posters/stills an agent files into a project). */\n keyImageUploads: number\n /** Session recording uploads per 24h (uploads count as uploads). */\n recordingUploads: number\n /** Session image / HDR / audio uploads per 24h. */\n otherUploads: number\n /** Longest hosted recording, seconds (stated before you record). */\n recordingMaxSeconds: number\n /** Largest single recording upload, bytes (the ingest ceiling). */\n recordingMaxBytes: number\n /** Total asset bytes an account may hold (said in hours in the UI). */\n storageBytes: number\n /** Session versions per vos per 24h. */\n versionsPerVosDay: number\n /** Key-authed version pushes per 24h across every vos. */\n keyVersionsPerDay: number\n /** Voses + assets + recipes one folder may hold. */\n folderItems: number\n /** Subfolders one folder may hold. */\n subfolders: number\n /** Plumbing media enqueues (preview + thumbnail) per vos per hour. */\n mediaPerVosHour: number\n /** Plumbing media enqueues per account per 24h. Past it, media DEFERS. */\n mediaPerAccountDay: number\n /** Artifact render output-minutes per account per 24h (armed with cloud export). */\n artifactRenderMinutesPerDay: number\n /** Own-vos backdrop bakes per account per 24h (cache hits never count). */\n backdropBakesPerDay: number\n /** Folders per user, across every nesting level. */\n folders: number\n /** Recipe files (.md assets) per user. */\n recipes: number\n /** Key-authed recipe creates per 24h. */\n keyRecipeCreates: number\n /** Recipe body byte cap (upload + in-place replace). */\n recipeBodyBytes: number\n}\n\nconst FREE: PlanLimits = {\n dailySaves: 10,\n privateVoses: 20,\n keyModelUploads: 10,\n keyRecordingUploads: 20,\n keyImageUploads: 40,\n recordingUploads: 20,\n otherUploads: 50,\n recordingMaxSeconds: 30 * 60,\n recordingMaxBytes: 512 * 1024 * 1024,\n storageBytes: 5 * 1024 * 1024 * 1024,\n versionsPerVosDay: 100,\n keyVersionsPerDay: 200,\n folderItems: 500,\n subfolders: 50,\n mediaPerVosHour: 20,\n mediaPerAccountDay: 300,\n artifactRenderMinutesPerDay: 60,\n backdropBakesPerDay: 20,\n // Folders & recipes: sanity caps, not a paywall — organization\n // stays free at every size that isn't abuse.\n folders: 50,\n recipes: 200,\n keyRecipeCreates: 20,\n recipeBodyBytes: 64 * 1024,\n}\n\n// Keyed by string on purpose: the column is free text at the wire, so a\n// value the table does not know must fall through to free, never throw.\nconst LIMITS: Partial<Record<string, PlanLimits>> = { free: FREE }\n\n/** Unknown or absent ⇒ free. A plan the table does not know is never a grant. */\nexport function planLimits(plan?: string | null): PlanLimits {\n return LIMITS[plan ?? 'free'] ?? FREE\n}\n\n/**\n * The storage ceiling in the unit a human can picture. Footage at ~5 Mbps\n * is ~0.375 GB per 10 minutes, so 5 GB reads as \"about 2 hours\"; the guard\n * counts bytes, the sentence says hours. Rounded to a half hour.\n */\nexport const FOOTAGE_BYTES_PER_SECOND = (5_000_000 / 8) * 1\nexport function bytesAsFootageHours(bytes: number): number {\n return Math.round((bytes / FOOTAGE_BYTES_PER_SECOND / 3600) * 2) / 2\n}\n\n/** `1.2 GB`, `640 MB`, `12 KB` — for the usage strip and refusals. */\nexport function formatBytes(bytes: number): string {\n if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`\n if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`\n if (bytes >= 1024) return `${Math.round(bytes / 1024)} KB`\n return `${bytes} B`\n}\n\n/** `30 min`, `1 h 30 min`, `45 s` — the duration cap in words. */\nexport function formatDurationCap(seconds: number): string {\n if (seconds < 60) return `${seconds} s`\n const m = Math.round(seconds / 60)\n if (m < 60) return `${m} min`\n const h = Math.floor(m / 60)\n const rest = m % 60\n return rest ? `${h} h ${rest} min` : `${h} h`\n}\n"],"mappings":";AAgBO,IAAM,QAAyB,CAAC,MAAM;AAiD7C,IAAM,OAAmB;AAAA,EACvB,YAAY;AAAA,EACZ,cAAc;AAAA,EACd,iBAAiB;AAAA,EACjB,qBAAqB;AAAA,EACrB,iBAAiB;AAAA,EACjB,kBAAkB;AAAA,EAClB,cAAc;AAAA,EACd,qBAAqB,KAAK;AAAA,EAC1B,mBAAmB,MAAM,OAAO;AAAA,EAChC,cAAc,IAAI,OAAO,OAAO;AAAA,EAChC,mBAAmB;AAAA,EACnB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EACpB,6BAA6B;AAAA,EAC7B,qBAAqB;AAAA;AAAA;AAAA,EAGrB,SAAS;AAAA,EACT,SAAS;AAAA,EACT,kBAAkB;AAAA,EAClB,iBAAiB,KAAK;AACxB;AAIA,IAAM,SAA8C,EAAE,MAAM,KAAK;AAG1D,SAAS,WAAW,MAAkC;AAC3D,SAAO,OAAO,QAAQ,MAAM,KAAK;AACnC;AAOO,IAAM,2BAA4B,MAAY,IAAK;AACnD,SAAS,oBAAoB,OAAuB;AACzD,SAAO,KAAK,MAAO,QAAQ,2BAA2B,OAAQ,CAAC,IAAI;AACrE;AAGO,SAAS,YAAY,OAAuB;AACjD,MAAI,SAAS,QAAQ,EAAG,QAAO,IAAI,QAAQ,QAAQ,GAAG,QAAQ,CAAC,CAAC;AAChE,MAAI,SAAS,QAAQ,EAAG,QAAO,GAAG,KAAK,MAAM,QAAQ,QAAQ,CAAC,CAAC;AAC/D,MAAI,SAAS,KAAM,QAAO,GAAG,KAAK,MAAM,QAAQ,IAAI,CAAC;AACrD,SAAO,GAAG,KAAK;AACjB;AAGO,SAAS,kBAAkB,SAAyB;AACzD,MAAI,UAAU,GAAI,QAAO,GAAG,OAAO;AACnC,QAAM,IAAI,KAAK,MAAM,UAAU,EAAE;AACjC,MAAI,IAAI,GAAI,QAAO,GAAG,CAAC;AACvB,QAAM,IAAI,KAAK,MAAM,IAAI,EAAE;AAC3B,QAAM,OAAO,IAAI;AACjB,SAAO,OAAO,GAAG,CAAC,MAAM,IAAI,SAAS,GAAG,CAAC;AAC3C;","names":[]}
@@ -1,2 +0,0 @@
1
-
2
- export { }
@@ -1,2 +0,0 @@
1
- import "../chunk-K7EIJSYQ.js";
2
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,23 +0,0 @@
1
- /**
2
- * Shared Utilities
3
- * Common utility functions used across frontend and backend
4
- */
5
- /**
6
- * Format a filename to a human-readable label
7
- * @example formatLabel('basic-fade') => 'Basic Fade'
8
- */
9
- declare function formatLabel(filename: string): string;
10
- /**
11
- * Generate a unique ID
12
- */
13
- declare function generateId(): string;
14
- /**
15
- * Sleep for a specified duration
16
- */
17
- declare function sleep(ms: number): Promise<void>;
18
- /**
19
- * Safely parse JSON with a fallback
20
- */
21
- declare function safeJsonParse<T>(json: string, fallback: T): T;
22
-
23
- export { formatLabel, generateId, safeJsonParse, sleep };
@@ -1,13 +0,0 @@
1
- import {
2
- formatLabel,
3
- generateId,
4
- safeJsonParse,
5
- sleep
6
- } from "../chunk-RDD5SU6G.js";
7
- export {
8
- formatLabel,
9
- generateId,
10
- safeJsonParse,
11
- sleep
12
- };
13
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}