motionspec 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kevin Froeba
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # MotionSpec
2
+
3
+ A formal intermediate language for scroll-driven web motion. A small model translates a request into a **schema-validated JSON spec**; a **deterministic compiler** turns that spec into GSAP/CSS — hallucination-proof by construction, with an enforced `prefers-reduced-motion` fallback and a performance budget.
4
+
5
+ The thesis: **capability lives in the catalog, not the model.** A bigger model can write more elaborate specs, but it can never emit a primitive, parameter, or selector the Trust Boundary hasn't approved. The compiler trusts only what passes.
6
+
7
+ ```
8
+ request ──> Routing (small model, Stage A) ──> MotionSpec (JSON)
9
+ │ cache · 1 repair-retry · escalation │
10
+ ▼ ▼
11
+ telemetry TRUST BOUNDARY (fail-closed)
12
+
13
+
14
+ Compiler (no model, Stage B)
15
+
16
+ ▼ out/*.motion.js + .css
17
+ ```
18
+
19
+ ## Status
20
+
21
+ | | |
22
+ |---|---|
23
+ | Version | **v1.0.0** · schema frozen at spec v1 (ADR-0001, signed) |
24
+ | Tests | **151** green · CI on Node 18/20/22 |
25
+ | Catalog | **8** primitives, all device-verified |
26
+ | Dependencies | **0 vulnerabilities** · SBOM committed · all permissive licenses |
27
+ | Coverage | **98.4% lines / 95.9% functions** of `src/` + `worker/` (CI gate fails under 90%) |
28
+ | Machine audit | 7.2/10 (Production-Ready), independently re-audited |
29
+ | First client | CHS Computer — live on Vercel |
30
+ | Hosted MCP | **live** — private, secret-gated Cloudflare Worker · per-minute cron canary + external heartbeat (synthetic error → email in <5 min, proven) · gated `/dashboard` |
31
+
32
+ Schema v1 is frozen: `specVersion "1.0"` is the stable public contract; `"0.1"` is deprecated and accepted until v1.2. The `[MS-XXX]` error-code registry is public API. Phase B (test & security) is closed — CI is green on the x86 runner incl. Playwright `e2e` for every primitive (all jobs pass on every push to `main` — see the repo Actions tab; the x86 runner is the source of truth). **Phase C (observability + hosted MCP) is live and gate-proven**: the MCP server runs as a private, secret-gated Cloudflare Worker; a per-minute cron canary runs `validate→compile` and pings an external heartbeat, so a failure alerts by email within 5 minutes (verified on real infra). A gated `/dashboard` renders live telemetry.
33
+
34
+ ## Quickstart
35
+
36
+ ```bash
37
+ npm ci # install (0 runtime deps beyond MCP SDK + zod)
38
+ npm test # 151 tests: validator, compiler-golden, router, fuzz, schema parity, worker contract
39
+ node bin/motion.js catalog # primitives + catalog version (16-char hash)
40
+ node bin/motion.js compile examples/hero.motionspec.json
41
+ node bin/motion.js pipeline "Hero headline fades in, cards staggered" --mock
42
+ node bin/motion.js stats # telemetry (model / repaired / cache-hit / escalate)
43
+ ```
44
+
45
+ Live model instead of `--mock`: set `MOTION_API_KEY` (or `OPENROUTER_API_KEY`); optional `MOTION_MODEL` (default `anthropic/claude-haiku-4.5`) and `MOTION_BASE_URL` (any OpenAI-compatible endpoint). See `.env.example`.
46
+
47
+ ## Gates (run these — they are the contract)
48
+
49
+ ```bash
50
+ npm test # full suite, fail-closed trust boundary + golden determinism
51
+ npm run coverage # FAILS under 90% lines/functions (src/ + worker/)
52
+ npm run catalog-lock:check # ADR-0001 D2: a tightened bound shipped as a "patch" fails here
53
+ npm run sbom # regenerate CycloneDX SBOM; then `node bin/license-check.js`
54
+ npm run e2e # real-browser Playwright (CI x86 only — the sandbox/ARM cannot run Chromium)
55
+ ```
56
+
57
+ ## MCP server (distribution)
58
+
59
+ Any MCP-capable agent (Claude Code, Cowork, Cursor, …) can use MotionSpec directly — the host LLM is the spec author, the Trust Boundary stays enforced:
60
+
61
+ ```bash
62
+ npm run mcp # start stdio server
63
+ claude mcp add motionspec -- node <repo>/src/mcp/server.mjs
64
+ ```
65
+
66
+ Tools: `motion_catalog` (primitives + authoring rules) · `motion_validate` (fail-closed, surfaces deprecations) · `motion_compile` (deterministic) · `motion_stats`. Input is size-capped (`MS-INPUT-TOO-LARGE`, 64 KB). Tested in `test/mcp.test.mjs`.
67
+
68
+ ## Guarantees
69
+
70
+ 1. **Allow-list** — a primitive not in the catalog never reaches the compiler.
71
+ 2. **Injection-proof** — ids, selectors, string params and triggers are charset-validated; every interpolation is a JS literal (`JSON.stringify`) or a CSS-screened raw value. Malicious model output is rejected fail-closed (tested + fuzzed over 6000 random specs).
72
+ 3. **a11y** — `respectReducedMotion` gates JS and CSS; the model cannot switch it off.
73
+ 4. **Determinism** — same spec ⇒ identical code (golden-file tests).
74
+ 5. **Versioned** — schema frozen v1; catalog SemVer enforced by a diff-gate; specs may pin `catalogVersion` for reproducibility (`MS-CATALOG-PIN-MISMATCH` fail-closed).
75
+ 6. **Observability** — every request logs `model | model-repaired | cache-hit | escalate-*` to `telemetry/events.jsonl`; escalation clusters are the signal for new primitives (concept §3.3).
76
+
77
+ ## Layout
78
+
79
+ ```
80
+ schema/ MotionSpec JSON schema (static contract, parity-tested vs validator)
81
+ primitives/ catalog: 8 verified primitives (safe templates)
82
+ catalog.lock.json released catalog baseline (SemVer diff-gate)
83
+ src/compiler/ catalog.js · catalog-semver.js · validate.js (Trust Boundary) · compile.js
84
+ src/router/ prompt.js · clients.js (openai-compat + mock) · route.js · cache.js · telemetry.js
85
+ src/mcp/ server.mjs (4 tools, fail-closed, input-capped)
86
+ bin/ motion.js (CLI) · catalog-lock.js · license-check.js
87
+ test/ 151 tests incl. injection attacks, fuzz, golden files, schema parity, worker contract; test/e2e (Playwright)
88
+ docs/ ADR-0001 (schema freeze) · ROADMAP_TO_10 · PHASE_A_REAUDIT · CLAUDE_CODE_HANDOFF
89
+ ```
90
+
91
+ Concept, research, pitch, business docs and the CHS client site live in the sibling `B_MotionSpec/` folder.
92
+
93
+ ## Docs
94
+
95
+ - `docs/CLAUDE_CODE_HANDOFF.md` — **start here when continuing in Claude Code (terminal).**
96
+ - `docs/ROADMAP_TO_10_2026-06-15.md` — the production path to a proven 10/10 (Phases A–G, anti-goals, machine gates).
97
+ - `docs/adr/0001-schema-freeze-v1.md` — the frozen v1 contract and why.
98
+ - `docs/PHASE_A_REAUDIT_2026-06-15.md` — independent re-audit findings + fixes.
99
+
100
+ ## License
101
+
102
+ MIT.
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * Relock the catalog baseline (ADR-0001 D2).
5
+ * node bin/catalog-lock.js -> write catalog.lock.json from current catalog
6
+ * node bin/catalog-lock.js --check -> exit non-zero if current catalog violates
7
+ * SemVer bump rules vs the committed lock
8
+ *
9
+ * Run --check in CI; run without flags at release time to advance the baseline.
10
+ * Refuses to relock while there are unacknowledged bump violations.
11
+ */
12
+ const fs = require("fs");
13
+ const path = require("path");
14
+ const { loadCatalog } = require("../src/compiler/catalog.js");
15
+ const { snapshotCatalog, diffCatalog } = require("../src/compiler/catalog-semver.js");
16
+
17
+ const LOCK = path.join(__dirname, "..", "catalog.lock.json");
18
+ const check = process.argv.includes("--check");
19
+ const current = snapshotCatalog(loadCatalog());
20
+
21
+ if (check) {
22
+ if (!fs.existsSync(LOCK)) { console.error("No catalog.lock.json — run `npm run catalog-lock`."); process.exit(1); }
23
+ const lock = JSON.parse(fs.readFileSync(LOCK, "utf8")).primitives;
24
+ const { violations, added, removed } = diffCatalog(lock, current);
25
+ if (violations.length) {
26
+ console.error("Catalog SemVer violations:");
27
+ violations.forEach((v) => console.error(" - " + v.msg));
28
+ process.exit(1);
29
+ }
30
+ console.error("Catalog SemVer OK (added: " + (added.join(", ") || "none") + "; removed: " + (removed.join(", ") || "none") + ").");
31
+ process.exit(0);
32
+ }
33
+
34
+ // Relock: validate first (refuse on violation), then advance the baseline.
35
+ if (fs.existsSync(LOCK)) {
36
+ const lock = JSON.parse(fs.readFileSync(LOCK, "utf8")).primitives;
37
+ const { violations } = diffCatalog(lock, current);
38
+ if (violations.length) {
39
+ console.error("Refusing to relock — fix these bumps first:");
40
+ violations.forEach((v) => console.error(" - " + v.msg));
41
+ process.exit(1);
42
+ }
43
+ }
44
+ const out = { generatedAt: new Date().toISOString().slice(0, 10), note: "Released catalog baseline for ADR-0001 D2 SemVer gate. Relock at release time.", primitives: current };
45
+ fs.writeFileSync(LOCK, JSON.stringify(out, null, 2) + "\n");
46
+ console.error("Wrote catalog.lock.json (" + Object.keys(current).length + " primitives).");
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /* Phase B security (roadmap anti-goal #5): block any dependency whose license
4
+ * is not on the permissive allow-list. Reads the CycloneDX SBOM. Run after
5
+ * `npm run sbom`. Exits non-zero on a disallowed or missing license. */
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const ALLOW = new Set([
10
+ "MIT", "ISC", "0BSD", "BSD-2-Clause", "BSD-3-Clause",
11
+ "Apache-2.0", "CC0-1.0", "Unlicense", "BlueOak-1.0.0",
12
+ ]);
13
+
14
+ const file = path.join(__dirname, "..", "sbom.cdx.json");
15
+ if (!fs.existsSync(file)) { console.error("No sbom.cdx.json — run `npm run sbom` first."); process.exit(1); }
16
+ const sbom = JSON.parse(fs.readFileSync(file, "utf8"));
17
+ const comps = sbom.components || [];
18
+
19
+ const bad = [];
20
+ for (const c of comps) {
21
+ const ids = (c.licenses || []).map((l) => (l.license && (l.license.id || l.license.name)) || l.expression).filter(Boolean);
22
+ if (!ids.length) { bad.push(c.name + "@" + c.version + " (no license declared)"); continue; }
23
+ const ok = ids.some((id) => ALLOW.has(id));
24
+ if (!ok) bad.push(c.name + "@" + c.version + " (" + ids.join(", ") + ")");
25
+ }
26
+
27
+ if (bad.length) {
28
+ console.error("Disallowed / missing licenses (" + bad.length + "):");
29
+ bad.forEach((b) => console.error(" - " + b));
30
+ process.exit(1);
31
+ }
32
+ console.error("License check OK — " + comps.length + " components, all permissive.");
package/bin/motion.js ADDED
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * MotionSpec CLI
5
+ *
6
+ * node bin/motion.js compile <spec.json> Spec -> Code (out/)
7
+ * node bin/motion.js route "<anfrage>" [--mock] Anfrage -> Spec (out/)
8
+ * node bin/motion.js pipeline "<anfrage>" [--mock] Anfrage -> Spec -> Code
9
+ * node bin/motion.js catalog Primitive anzeigen
10
+ * node bin/motion.js stats Telemetrie-Zusammenfassung
11
+ *
12
+ * Ohne --mock nutzt route/pipeline einen OpenAI-kompatiblen Endpunkt
13
+ * (MOTION_BASE_URL, MOTION_MODEL, MOTION_API_KEY/OPENROUTER_API_KEY).
14
+ */
15
+ const fs = require("fs");
16
+ const path = require("path");
17
+ const { loadCatalog, catalogVersion } = require("../src/compiler/catalog.js");
18
+ const { compileSpec } = require("../src/compiler/compile.js");
19
+ const { route } = require("../src/router/route.js");
20
+ const { mockClient, openAICompatClient } = require("../src/router/clients.js");
21
+ const telemetry = require("../src/router/telemetry.js");
22
+
23
+ const ROOT = path.join(__dirname, "..");
24
+ const OUT = path.join(ROOT, "out");
25
+
26
+ function writeOut(name, result) {
27
+ if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
28
+ const written = [];
29
+ if (result.js) { fs.writeFileSync(path.join(OUT, name + ".motion.js"), result.js); written.push("out/" + name + ".motion.js"); }
30
+ if (result.css) { fs.writeFileSync(path.join(OUT, name + ".motion.css"), result.css); written.push("out/" + name + ".motion.css"); }
31
+ return written;
32
+ }
33
+
34
+ function printReport(r, written) {
35
+ console.log(" Kompiliert : " + r.motions + " Motions (js: " + r.jsCount + ", css: " + r.cssCount + ")");
36
+ console.log(" Reduced-Motion-Schutz : " + (r.reducedMotion ? "aktiv" : "AUS"));
37
+ console.log(" Performance-Budget : " + r.cost + " / " + r.budget + " - " + (r.budgetOk ? "OK" : "UEBERSCHRITTEN"));
38
+ if (written.length) console.log(" Ausgabe : " + written.join(", "));
39
+ }
40
+
41
+ async function main() {
42
+ /* Audit-Befund #4: Flags positionsunabhaengig parsen. */
43
+ const argv = process.argv.slice(2);
44
+ const cmd = argv[0];
45
+ const flags = new Set(argv.filter((a) => a.startsWith("--")));
46
+ const arg = argv.slice(1).find((a) => !a.startsWith("--"));
47
+ const catalog = loadCatalog();
48
+
49
+ if (cmd === "catalog") {
50
+ console.log("Katalog-Version: " + catalogVersion(catalog));
51
+ Object.keys(catalog).sort().forEach((n) => {
52
+ const p = catalog[n];
53
+ console.log(" " + n + " v" + p.version + " [" + p.engine + ", cost " + ((p.performance && p.performance.cost) || 0) + "] " + p.purpose);
54
+ });
55
+ return;
56
+ }
57
+
58
+ if (cmd === "demo") {
59
+ const { buildDemo } = require("../src/demo/build-demo.js");
60
+ const r = buildDemo(path.join(OUT, "demo"));
61
+ console.log("\n Demo-Seite: out/demo/index.html (" + r.primitives + " Primitive, " + r.motions + " Motions, Budget " + r.report.cost + "/" + r.report.budget + ")\n");
62
+ return;
63
+ }
64
+
65
+ if (cmd === "discover") {
66
+ if (!arg) { console.error('Aufruf: motion discover <brief.json> (S2-01 Gap-Report)'); process.exit(2); }
67
+ const { discover, toMarkdown } = require("../src/discover/discover.js");
68
+ let brief;
69
+ try { brief = JSON.parse(fs.readFileSync(arg, "utf8")); }
70
+ catch (e) { console.error("Brief nicht lesbar oder kein gueltiges JSON: " + e.message); process.exit(2); }
71
+ const r = discover(brief);
72
+ if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
73
+ const md = path.join(OUT, "gap-report-" + (r.project || "discovery") + ".md");
74
+ fs.writeFileSync(md, toMarkdown(r));
75
+ console.log("\n Discovery: " + r.covered.length + "/" + r.total + " abgedeckt, " + r.gaps.length + " Luecke(n) -> " + path.relative(ROOT, md) + "\n");
76
+ return;
77
+ }
78
+
79
+ if (cmd === "stats") {
80
+ const s = telemetry.summary();
81
+ console.log("Telemetrie: " + s.total + " Ereignisse");
82
+ Object.keys(s.byOutcome).forEach((k) => console.log(" " + k + ": " + s.byOutcome[k]));
83
+ return;
84
+ }
85
+
86
+ if (cmd === "compile") {
87
+ if (!arg) { console.error("Aufruf: motion compile <spec.json>"); process.exit(2); }
88
+ let spec;
89
+ try { spec = JSON.parse(fs.readFileSync(arg, "utf8")); }
90
+ catch (e) { console.error("Spec nicht lesbar oder kein gueltiges JSON: " + e.message); process.exit(2); }
91
+ const name = path.basename(arg).replace(/\.(motionspec\.)?json$/, "");
92
+ const res = compileSpec(spec, catalog, { specName: path.basename(arg) });
93
+ console.log("\n MotionSpec-Compiler v" + require("../package.json").version + " | Katalog: " + Object.keys(catalog).length + " Primitive\n");
94
+ if (!res.ok) {
95
+ console.log(" ABGEWIESEN durch die Trust Boundary - keine Ausgabe erzeugt:\n");
96
+ res.errors.forEach((e) => console.log(" x " + e));
97
+ console.log("");
98
+ process.exit(1);
99
+ }
100
+ console.log(" Trust Boundary : passiert - Spec ist gueltig.\n");
101
+ printReport(res.report, writeOut(name, res));
102
+ console.log("");
103
+ return;
104
+ }
105
+
106
+ if (cmd === "route" || cmd === "pipeline") {
107
+ if (!arg) { console.error('Aufruf: motion ' + cmd + ' "<anfrage>" [--mock]'); process.exit(2); }
108
+ const client = flags.has("--mock") ? mockClient() : openAICompatClient();
109
+ console.log("\n Routing (Stufe A) | Modell: " + client.name);
110
+ const r = await route(arg, { client, catalog, noCache: flags.has("--no-cache") });
111
+ if (!r.ok) {
112
+ console.log(" ESKALATION -> +1 (" + r.reason + ")");
113
+ if (r.errors) r.errors.forEach((e) => console.log(" x " + e));
114
+ console.log("");
115
+ process.exit(3);
116
+ }
117
+ console.log(" Spec erzeugt : Quelle " + r.source + ", " + r.attempts + " Versuch(e), " + r.ms + " ms");
118
+ const name = "request-" + Date.now();
119
+ if (!fs.existsSync(OUT)) fs.mkdirSync(OUT, { recursive: true });
120
+ const specFile = path.join(OUT, name + ".motionspec.json");
121
+ fs.writeFileSync(specFile, JSON.stringify(r.spec, null, 2));
122
+ console.log(" Spec : out/" + path.basename(specFile));
123
+ if (cmd === "pipeline") {
124
+ const res = compileSpec(r.spec, catalog, { specName: name });
125
+ if (!res.ok) { res.errors.forEach((e) => console.log(" x " + e)); process.exit(1); }
126
+ printReport(res.report, writeOut(name, res));
127
+ }
128
+ console.log("");
129
+ return;
130
+ }
131
+
132
+ console.error("Befehle: compile | route | pipeline | demo | catalog | stats");
133
+ process.exit(2);
134
+ }
135
+
136
+ main().catch((e) => { console.error("Fehler: " + e.message); process.exit(1); });
@@ -0,0 +1,243 @@
1
+ {
2
+ "generatedAt": "2026-06-15",
3
+ "note": "Released catalog baseline for ADR-0001 D2 SemVer gate. Relock at release time.",
4
+ "primitives": {
5
+ "counterUp": {
6
+ "version": "1.0.0",
7
+ "output": "js",
8
+ "params": {
9
+ "duration": {
10
+ "type": "number",
11
+ "required": false,
12
+ "min": 0.2,
13
+ "max": 5,
14
+ "pattern": null
15
+ },
16
+ "ease": {
17
+ "type": "string",
18
+ "required": false,
19
+ "min": null,
20
+ "max": null,
21
+ "pattern": "^[A-Za-z0-9.()]{1,40}$"
22
+ },
23
+ "locale": {
24
+ "type": "string",
25
+ "required": false,
26
+ "min": null,
27
+ "max": null,
28
+ "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$"
29
+ },
30
+ "step": {
31
+ "type": "number",
32
+ "required": false,
33
+ "min": 0.01,
34
+ "max": 1000,
35
+ "pattern": null
36
+ }
37
+ }
38
+ },
39
+ "cssTransition": {
40
+ "version": "1.1.0",
41
+ "output": "css",
42
+ "params": {
43
+ "duration": {
44
+ "type": "number",
45
+ "required": false,
46
+ "min": 0.05,
47
+ "max": 1,
48
+ "pattern": null
49
+ },
50
+ "easing": {
51
+ "type": "string",
52
+ "required": false,
53
+ "min": null,
54
+ "max": null,
55
+ "pattern": null
56
+ },
57
+ "hoverValue": {
58
+ "type": "string",
59
+ "required": false,
60
+ "min": null,
61
+ "max": null,
62
+ "pattern": null
63
+ },
64
+ "property": {
65
+ "type": "string",
66
+ "required": false,
67
+ "min": null,
68
+ "max": null,
69
+ "pattern": null
70
+ }
71
+ }
72
+ },
73
+ "marquee": {
74
+ "version": "1.0.0",
75
+ "output": "css",
76
+ "params": {
77
+ "direction": {
78
+ "type": "string",
79
+ "required": false,
80
+ "min": null,
81
+ "max": null,
82
+ "pattern": "^(normal|reverse)$"
83
+ },
84
+ "duration": {
85
+ "type": "number",
86
+ "required": false,
87
+ "min": 4,
88
+ "max": 120,
89
+ "pattern": null
90
+ },
91
+ "gap": {
92
+ "type": "string",
93
+ "required": false,
94
+ "min": null,
95
+ "max": null,
96
+ "pattern": "^[0-9]*\\.?[0-9]+(px|rem|em|vw|ch|%)$"
97
+ }
98
+ }
99
+ },
100
+ "parallaxLayer": {
101
+ "version": "1.1.0",
102
+ "output": "js",
103
+ "params": {
104
+ "scrub": {
105
+ "type": "number",
106
+ "required": false,
107
+ "min": 0,
108
+ "max": 4,
109
+ "pattern": null
110
+ },
111
+ "yPercent": {
112
+ "type": "number",
113
+ "required": false,
114
+ "min": -100,
115
+ "max": 100,
116
+ "pattern": null
117
+ }
118
+ }
119
+ },
120
+ "pinnedSection": {
121
+ "version": "1.1.0",
122
+ "output": "js",
123
+ "params": {
124
+ "distance": {
125
+ "type": "string",
126
+ "required": false,
127
+ "min": null,
128
+ "max": null,
129
+ "pattern": null
130
+ },
131
+ "pinSpacing": {
132
+ "type": "boolean",
133
+ "required": false,
134
+ "min": null,
135
+ "max": null,
136
+ "pattern": null
137
+ }
138
+ }
139
+ },
140
+ "scaleOnScroll": {
141
+ "version": "1.0.0",
142
+ "output": "js",
143
+ "params": {
144
+ "fromScale": {
145
+ "type": "number",
146
+ "required": false,
147
+ "min": 0.2,
148
+ "max": 1,
149
+ "pattern": null
150
+ },
151
+ "scrub": {
152
+ "type": "number",
153
+ "required": false,
154
+ "min": 0,
155
+ "max": 4,
156
+ "pattern": null
157
+ },
158
+ "toScale": {
159
+ "type": "number",
160
+ "required": false,
161
+ "min": 0.5,
162
+ "max": 2,
163
+ "pattern": null
164
+ },
165
+ "transformOrigin": {
166
+ "type": "string",
167
+ "required": false,
168
+ "min": null,
169
+ "max": null,
170
+ "pattern": "^[a-zA-Z0-9% ]{2,40}$"
171
+ }
172
+ }
173
+ },
174
+ "scrollReveal": {
175
+ "version": "1.1.0",
176
+ "output": "js",
177
+ "params": {
178
+ "duration": {
179
+ "type": "number",
180
+ "required": false,
181
+ "min": 0.1,
182
+ "max": 3,
183
+ "pattern": null
184
+ },
185
+ "ease": {
186
+ "type": "string",
187
+ "required": false,
188
+ "min": null,
189
+ "max": null,
190
+ "pattern": null
191
+ },
192
+ "from": {
193
+ "type": "transform",
194
+ "required": true,
195
+ "min": null,
196
+ "max": null,
197
+ "pattern": null
198
+ },
199
+ "stagger": {
200
+ "type": "number",
201
+ "required": false,
202
+ "min": 0,
203
+ "max": 0.5,
204
+ "pattern": null
205
+ }
206
+ }
207
+ },
208
+ "staggerReveal": {
209
+ "version": "1.1.0",
210
+ "output": "js",
211
+ "params": {
212
+ "duration": {
213
+ "type": "number",
214
+ "required": false,
215
+ "min": 0.1,
216
+ "max": 3,
217
+ "pattern": null
218
+ },
219
+ "ease": {
220
+ "type": "string",
221
+ "required": false,
222
+ "min": null,
223
+ "max": null,
224
+ "pattern": null
225
+ },
226
+ "from": {
227
+ "type": "transform",
228
+ "required": true,
229
+ "min": null,
230
+ "max": null,
231
+ "pattern": null
232
+ },
233
+ "stagger": {
234
+ "type": "number",
235
+ "required": false,
236
+ "min": 0.02,
237
+ "max": 0.6,
238
+ "pattern": null
239
+ }
240
+ }
241
+ }
242
+ }
243
+ }
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "motionspec",
3
+ "version": "1.0.0",
4
+ "description": "MotionSpec — a formal intermediate language for scroll-driven web motion. A small model writes schema-validated specs; a deterministic compiler emits GSAP/CSS, hallucination-proof by construction with enforced reduced-motion fallbacks.",
5
+ "license": "MIT",
6
+ "author": "Noah Froeba",
7
+ "keywords": [
8
+ "gsap",
9
+ "scrolltrigger",
10
+ "animation",
11
+ "motion",
12
+ "scroll",
13
+ "mcp",
14
+ "llm",
15
+ "accessibility",
16
+ "compiler",
17
+ "json-schema"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/MasterPlayspots/motionspec.git"
22
+ },
23
+ "homepage": "https://github.com/MasterPlayspots/motionspec#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/MasterPlayspots/motionspec/issues"
26
+ },
27
+ "bin": {
28
+ "motion": "bin/motion.js"
29
+ },
30
+ "main": "src/compiler/compile.js",
31
+ "files": [
32
+ "src",
33
+ "bin",
34
+ "primitives",
35
+ "schema",
36
+ "catalog.lock.json"
37
+ ],
38
+ "scripts": {
39
+ "test": "node --test test/validate.test.js test/compile.test.js test/specversion-v1.test.js test/route.test.js test/route-live.test.js test/catalog-wave1.test.js test/catalog-semver.test.js test/schema-parity.test.js test/catalog-pin.test.js test/css-safety.test.js test/fuzz.test.js test/hardening.test.js test/cache.test.js test/errorcodes.test.js test/catalog-integrity.test.js test/discover.test.js test/telemetry-sink.test.js test/worker-contract.test.mjs test/analytics-engine-sink.test.mjs test/canary.test.mjs test/dashboard.test.mjs test/mcp.test.mjs",
40
+ "coverage": "node --test --experimental-test-coverage --test-coverage-exclude=test/** --test-coverage-exclude=bin/** --test-coverage-lines=90 --test-coverage-functions=90 --test-coverage-branches=75 test/*.test.js test/*.test.mjs",
41
+ "e2e": "playwright test",
42
+ "sbom": "npm sbom --sbom-format cyclonedx --omit dev > sbom.cdx.json",
43
+ "catalog-lock": "node bin/catalog-lock.js",
44
+ "catalog-lock:check": "node bin/catalog-lock.js --check",
45
+ "mcp": "node src/mcp/server.mjs",
46
+ "test:golden-update": "UPDATE_GOLDEN=1 node --test test/compile.test.js",
47
+ "compile": "node bin/motion.js compile",
48
+ "route": "node bin/motion.js route",
49
+ "pipeline": "node bin/motion.js pipeline",
50
+ "catalog": "node bin/motion.js catalog",
51
+ "stats": "node bin/motion.js stats",
52
+ "prepublishOnly": "npm test && npm run catalog-lock:check && npm run sbom && node bin/license-check.js",
53
+ "lint": "eslint ."
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ },
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "1.29.0",
60
+ "zod": "^4.4.3"
61
+ },
62
+ "devDependencies": {
63
+ "@eslint/js": "^10.0.1",
64
+ "@playwright/test": "^1.50.0",
65
+ "eslint": "^10.5.0",
66
+ "globals": "^17.6.0"
67
+ }
68
+ }