motionspec 1.0.4 → 1.0.5

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.
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * worker-smoke.js — Post-Deploy-Smoke fuer den motionspec-mcp Worker.
5
+ * ------------------------------------------------------------------
6
+ * Der Worker ist "all private": ALLE Routen ausser /dashboard sitzen hinter dem
7
+ * Key-Gate (worker/index.mjs). Daher zwei Modi:
8
+ *
9
+ * - Mit Key (env MOTIONSPEC_KEY oder MCP_SHARED_SECRET): GET /health mit
10
+ * Header x-motionspec-key. Verlangt 200 + ok:true und prueft catalogVersion
11
+ * gegen den lokalen Pin (Byte-Identitaet local==hosted, vgl. CLAUDE.md).
12
+ *
13
+ * - Ohne Key: ungated Liveness gegen /dashboard (HTML 200). Reicht als
14
+ * Post-Deploy-Lebenszeichen; der Catalog-Pin wird dann NICHT geprueft und
15
+ * das ausdruecklich vermerkt.
16
+ *
17
+ * Idempotent: nur read-only GETs, kein State. Exit 1 bei jedem Mismatch.
18
+ * Ziel-URL ueberschreibbar via WORKER_URL (Default = Hosted Prod).
19
+ */
20
+ const SECRET_HEADER = "x-motionspec-key";
21
+ const BASE = (process.env.WORKER_URL || "https://motionspec-mcp.froeba-kevin.workers.dev")
22
+ .replace(/\/+$/, "");
23
+ const KEY = process.env.MOTIONSPEC_KEY || process.env.MCP_SHARED_SECRET || "";
24
+
25
+ function localCatalogPin() {
26
+ try {
27
+ // Lokaler Pin aus den primitives/ (loadCatalog) — derselbe Hash, den der
28
+ // Worker aus dem gebundelten Katalog rechnet (vgl. worker/index.mjs).
29
+ const { loadCatalog, catalogVersion } = require("../src/compiler/catalog.js");
30
+ return catalogVersion(loadCatalog());
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ function fail(label, problems) {
37
+ console.error("Smoke FAIL — " + label + ":");
38
+ problems.forEach((p) => console.error(" - " + p));
39
+ process.exit(1);
40
+ }
41
+
42
+ async function gatedHealth() {
43
+ const url = BASE + "/health";
44
+ let res, body;
45
+ try {
46
+ res = await fetch(url, {
47
+ headers: { "user-agent": "motionspec-smoke", [SECRET_HEADER]: KEY },
48
+ });
49
+ body = await res.json();
50
+ } catch (e) {
51
+ fail(url, ["nicht erreichbar (" + e.message + ")"]);
52
+ }
53
+
54
+ const problems = [];
55
+ if (res.status !== 200) problems.push("HTTP " + res.status + " (erwartet 200)");
56
+ if (!body || body.ok !== true) problems.push("ok !== true im Body: " + JSON.stringify(body));
57
+
58
+ const pin = localCatalogPin();
59
+ if (pin && body && body.catalogVersion && body.catalogVersion !== pin) {
60
+ problems.push(
61
+ "catalogVersion hosted=" + body.catalogVersion + " != local=" + pin +
62
+ " (Engine NICHT byte-identisch)"
63
+ );
64
+ }
65
+ if (problems.length) fail(url, problems);
66
+
67
+ console.error(
68
+ "Smoke OK (gated /health) — " + url + " | version=" + body.version +
69
+ " catalogVersion=" + body.catalogVersion + " primitives=" + body.primitives
70
+ );
71
+ }
72
+
73
+ async function ungatedLiveness() {
74
+ const url = BASE + "/dashboard";
75
+ let res, text;
76
+ try {
77
+ res = await fetch(url, { headers: { "user-agent": "motionspec-smoke" } });
78
+ text = await res.text();
79
+ } catch (e) {
80
+ fail(url, ["nicht erreichbar (" + e.message + ")"]);
81
+ }
82
+
83
+ const problems = [];
84
+ if (res.status !== 200) problems.push("HTTP " + res.status + " (erwartet 200)");
85
+ const ct = res.headers.get("content-type") || "";
86
+ if (!/text\/html/i.test(ct)) problems.push("content-type nicht text/html: " + ct);
87
+ if (!text || !/<!doctype html|<html/i.test(text)) problems.push("kein HTML-Body");
88
+ if (problems.length) fail(url, problems);
89
+
90
+ console.error(
91
+ "Smoke OK (ungated /dashboard liveness) — " + url +
92
+ "\n Hinweis: kein MOTIONSPEC_KEY/MCP_SHARED_SECRET gesetzt -> Catalog-Pin NICHT geprueft."
93
+ );
94
+ }
95
+
96
+ async function main() {
97
+ if (KEY) await gatedHealth();
98
+ else await ungatedLiveness();
99
+ }
100
+
101
+ main();
package/catalog.lock.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "generatedAt": "2026-06-15",
2
+ "generatedAt": "2026-06-29",
3
3
  "note": "Released catalog baseline for ADR-0001 D2 SemVer gate. Relock at release time.",
4
4
  "primitives": {
5
5
  "counterUp": {
@@ -70,6 +70,33 @@
70
70
  }
71
71
  }
72
72
  },
73
+ "floatLoop": {
74
+ "version": "1.0.0",
75
+ "output": "css",
76
+ "params": {
77
+ "axis": {
78
+ "type": "string",
79
+ "required": false,
80
+ "min": null,
81
+ "max": null,
82
+ "pattern": "^(x|y)$"
83
+ },
84
+ "distance": {
85
+ "type": "string",
86
+ "required": false,
87
+ "min": null,
88
+ "max": null,
89
+ "pattern": "^[0-9]*\\.?[0-9]+(px|rem|%)$"
90
+ },
91
+ "duration": {
92
+ "type": "number",
93
+ "required": false,
94
+ "min": 1,
95
+ "max": 10,
96
+ "pattern": null
97
+ }
98
+ }
99
+ },
73
100
  "marquee": {
74
101
  "version": "1.0.0",
75
102
  "output": "css",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "motionspec",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "mcpName": "io.github.MasterPlayspots/motionspec",
5
5
  "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.",
6
6
  "license": "MIT",
@@ -21,7 +21,7 @@
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/MasterPlayspots/motionspec.git"
23
23
  },
24
- "homepage": "https://github.com/MasterPlayspots/motionspec#readme",
24
+ "homepage": "https://www.npmjs.com/package/motionspec",
25
25
  "bugs": {
26
26
  "url": "https://github.com/MasterPlayspots/motionspec/issues"
27
27
  },
@@ -38,7 +38,7 @@
38
38
  "catalog.lock.json"
39
39
  ],
40
40
  "scripts": {
41
- "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 test/cli-output.test.mjs",
41
+ "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/csstransition-validation.test.js test/safety-parity.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/worker-keys.test.mjs test/worker-abuse-alert.test.mjs test/worker-preauth-ratelimit.test.mjs test/analytics-engine-sink.test.mjs test/canary.test.mjs test/dashboard.test.mjs test/mcp.test.mjs test/cli-output.test.mjs test/waapi-lowering.test.js test/forge-prioritize.test.js test/forge-promote-gate.test.js test/forge-generate.test.js test/forge-workflow-guard.test.js test/canonical-guard.test.js",
42
42
  "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",
43
43
  "e2e": "playwright test",
44
44
  "sbom": "npm sbom --sbom-format cyclonedx | node bin/sbom-check.js --filter > sbom.cdx.json",
@@ -52,7 +52,13 @@
52
52
  "pipeline": "node bin/motion.js pipeline",
53
53
  "catalog": "node bin/motion.js catalog",
54
54
  "stats": "node bin/motion.js stats",
55
- "prepublishOnly": "npm test && npm run catalog-lock:check && npm run sbom && npm run sbom:check && node bin/license-check.js && node -e \"if(!require('fs').existsSync('CHANGELOG.md'))throw new Error('CHANGELOG.md fehlt')\"",
55
+ "assert-canonical": "node bin/assert-canonical.js",
56
+ "prepublishOnly": "npm run assert-canonical && npm test && npm run catalog-lock:check && npm run sbom && npm run sbom:check && node bin/license-check.js && node -e \"if(!require('fs').existsSync('CHANGELOG.md'))throw new Error('CHANGELOG.md fehlt')\"",
57
+ "predeploy:worker": "npm run assert-canonical",
58
+ "deploy:worker": "wrangler deploy",
59
+ "smoke:worker": "node bin/worker-smoke.js",
60
+ "release": "npm run assert-canonical && npm test && npm run catalog-lock:check && npm run sbom && npm run sbom:check && node bin/license-check.js && npm publish",
61
+ "release:worker": "npm run assert-canonical && npm run deploy:worker && npm run smoke:worker",
56
62
  "lint": "eslint .",
57
63
  "prepare": "husky"
58
64
  },
@@ -71,6 +77,6 @@
71
77
  "eslint": "^10.5.0",
72
78
  "globals": "^17.6.0",
73
79
  "husky": "^9.1.7",
74
- "wrangler": "4.103.0"
80
+ "wrangler": "4.105.0"
75
81
  }
76
82
  }
@@ -1,17 +1,19 @@
1
1
  {
2
2
  "name": "cssTransition",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "purpose": "Einfache Hover-Mikrointeraktion ganz ohne JavaScript.",
5
5
  "output": "css",
6
6
  "engine": "native-css",
7
7
  "paramSchema": {
8
8
  "property": {
9
9
  "type": "string",
10
- "default": "transform"
10
+ "default": "transform",
11
+ "pattern": "^(transform|opacity|color|background-color|border-color|filter|width|height)$"
11
12
  },
12
13
  "hoverValue": {
13
14
  "type": "string",
14
- "default": "translateY(-4px)"
15
+ "default": "translateY(-4px)",
16
+ "pattern": "^[A-Za-z0-9#%.,()-]+( [A-Za-z0-9#%.,()-]+)?( [A-Za-z0-9#%.,()-]+)?$"
15
17
  },
16
18
  "duration": {
17
19
  "type": "number",
@@ -21,7 +23,8 @@
21
23
  },
22
24
  "easing": {
23
25
  "type": "string",
24
- "default": "ease-out"
26
+ "default": "ease-out",
27
+ "pattern": "^(ease|ease-in|ease-out|ease-in-out|linear|step-start|step-end|cubic-bezier\\([-0-9., ]{1,40}\\))$"
25
28
  }
26
29
  },
27
30
  "triggerDefaults": {},
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "floatLoop",
3
+ "version": "1.0.0",
4
+ "purpose": "Sanftes, endloses Schweben dekorativer Elemente — kontinuierliche Loop-Bewegung entlang einer Achse (compositor-guenstig).",
5
+ "output": "css",
6
+ "engine": "native-css",
7
+ "paramSchema": {
8
+ "distance": {
9
+ "type": "string",
10
+ "default": "8px",
11
+ "pattern": "^[0-9]*\\.?[0-9]+(px|rem|%)$"
12
+ },
13
+ "duration": {
14
+ "type": "number",
15
+ "default": 3,
16
+ "min": 1,
17
+ "max": 10
18
+ },
19
+ "axis": {
20
+ "type": "string",
21
+ "default": "y",
22
+ "pattern": "^(x|y)$"
23
+ }
24
+ },
25
+ "triggerDefaults": {},
26
+ "performance": {
27
+ "verified": true,
28
+ "lcpSafe": true,
29
+ "cost": 1,
30
+ "verifiedAt": "2026-06-29"
31
+ },
32
+ "a11y": {
33
+ "reducedMotionFallback": "static"
34
+ },
35
+ "template": "{{css target}} { animation: motion-floatLoop-{{css id}} {{css params.duration}}s ease-in-out infinite alternate; }\n@keyframes motion-floatLoop-{{css id}} { from { transform: translate{{css params.axis}}(0) } to { transform: translate{{css params.axis}}(-{{css params.distance}}) } }",
36
+ "demo": {
37
+ "html": "<div class=\"float-box d-floatLoop\" style=\"display:inline-flex;align-items:center;justify-content:center;width:6rem;height:6rem;border:1px solid #8886;border-radius:14px\">FLOAT</div>",
38
+ "params": {
39
+ "distance": "10px",
40
+ "duration": 3,
41
+ "axis": "y"
42
+ }
43
+ }
44
+ }
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "pinnedSection",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "purpose": "Sektion fixieren, waehrend der Inhalt hindurchscrollt.",
5
5
  "output": "js",
6
6
  "engine": "gsap.ScrollTrigger",
7
7
  "paramSchema": {
8
8
  "distance": {
9
9
  "type": "string",
10
- "default": "+=100%"
10
+ "default": "+=100%",
11
+ "pattern": "^\\+=[0-9]{1,6}(\\.[0-9]{1,3})?(px|%|vh|vw|em|rem)?$"
11
12
  },
12
13
  "pinSpacing": {
13
14
  "type": "boolean",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scrollReveal",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "purpose": "Element blendet beim Eintritt in den Viewport ein.",
5
5
  "output": "js",
6
6
  "engine": "gsap.ScrollTrigger",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "ease": {
19
19
  "type": "string",
20
- "default": "power3.out"
20
+ "default": "power3.out",
21
+ "pattern": "^[A-Za-z0-9.()]{1,40}$"
21
22
  },
22
23
  "stagger": {
23
24
  "type": "number",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "staggerReveal",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "purpose": "Mehrere Kind-Elemente gestaffelt nacheinander einblenden.",
5
5
  "output": "js",
6
6
  "engine": "gsap.ScrollTrigger",
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "ease": {
19
19
  "type": "string",
20
- "default": "power2.out"
20
+ "default": "power2.out",
21
+ "pattern": "^[A-Za-z0-9.()]{1,40}$"
21
22
  },
22
23
  "stagger": {
23
24
  "type": "number",
@@ -117,4 +117,4 @@ function catalogSummary(catalog, fields) {
117
117
  });
118
118
  }
119
119
 
120
- module.exports = { loadCatalog, buildCatalog, catalogVersion, checkPrimitive, catalogSummary };
120
+ module.exports = { loadCatalog, buildCatalog, catalogVersion, checkPrimitive, catalogSummary, screenPattern };
@@ -14,11 +14,13 @@
14
14
  *
15
15
  * Ablauf: Trust Boundary -> Defaults -> Templates -> Gates -> Report.
16
16
  */
17
- const { validateSpec, unsafeToken } = require("./validate.js");
17
+ const { validateSpec } = require("./validate.js");
18
+ /* CSS-Rohtext-Screening + Default-Fuellung kommen aus der geteilten Quelle
19
+ * (safety.js) — byte-identisch genutzt vom WAAPI-Lowering (lower-waapi.js). */
20
+ const { cssRaw, withDefaults } = require("./safety.js");
18
21
  const VERSION = require("../../package.json").version;
19
22
 
20
23
  const BUDGET = 10; /* Performance-Budget: Summe der Primitiv-Kosten */
21
- const CSS_SAFE_RE = /^[A-Za-z0-9 _\-#.:,()>+~*=%[\]"|^$]{0,200}$/;
22
24
 
23
25
  function resolvePath(ctx, expr) {
24
26
  return expr.split(".").reduce(
@@ -33,14 +35,6 @@ function jsLiteral(v) {
33
35
  return JSON.stringify(v); /* Strings UND Objekte: immer korrekt escaped */
34
36
  }
35
37
 
36
- function cssRaw(v, expr) {
37
- const s = String(v);
38
- const bad = unsafeToken(s); /* defense in depth: never emit javascript:/expression(/url( ... */
39
- if (bad || !CSS_SAFE_RE.test(s) || s.includes("/*") || s.includes("*/"))
40
- throw new Error('CSS-Interpolation abgelehnt fuer "' + expr + '": Wert "' + s + '"' + (bad ? ' enthaelt gefaehrliches Token "' + bad + '"' : " verletzt die CSS-Allow-List") + ".");
41
- return s;
42
- }
43
-
44
38
  function fill(tpl, ctx) {
45
39
  return tpl.replace(/\{\{([^}]+)\}\}/g, (_, raw) => {
46
40
  const expr = raw.trim();
@@ -52,16 +46,6 @@ function fill(tpl, ctx) {
52
46
  });
53
47
  }
54
48
 
55
- function withDefaults(schema, given) {
56
- const out = {};
57
- Object.keys(schema).forEach((k) => {
58
- out[k] = given && Object.prototype.hasOwnProperty.call(given, k)
59
- ? given[k]
60
- : schema[k].default;
61
- });
62
- return out;
63
- }
64
-
65
49
  /**
66
50
  * @typedef {Object} CompileReport
67
51
  * @property {number} motions - Anzahl kompilierter Motions
@@ -172,4 +156,6 @@ function compileSpec(spec, catalog, opts) {
172
156
  };
173
157
  }
174
158
 
175
- module.exports = { compileSpec, fill, withDefaults, BUDGET };
159
+ /* withDefaults/cssRaw werden aus safety.js re-exportiert, damit der Parity-Test
160
+ * die Referenz-Identitaet beider Paesse gegen die EINE Quelle pruefen kann. */
161
+ module.exports = { compileSpec, fill, withDefaults, cssRaw, BUDGET };
@@ -18,6 +18,7 @@ const KEYWORD_MAP = [
18
18
  { re: /(parallax|tiefe|depth|layer|ebene)/i, primitive: "parallaxLayer", target: ".hero .bg", params: { yPercent: -25 } },
19
19
  { re: /(pin|fixier|sticky|festhalten|haften)/i, primitive: "pinnedSection", target: ".showcase", params: { distance: "+=100%" } },
20
20
  { re: /(marquee|laufband|logo.?band|ticker|endlos)/i, primitive: "marquee", target: ".marquee-track", params: {} },
21
+ { re: /(schweb|schwebe|schweben|float|floating|bob|gleitet sanft|auf und ab)/i, primitive: "floatLoop", target: ".float", params: { distance: "8px", duration: 3, axis: "y" } },
21
22
  { re: /(skalier|scale|zoom|gr(ö|oe)(ß|ss)er werden)/i, primitive: "scaleOnScroll", target: ".section", params: {} },
22
23
  { re: /(hover|maus|cursor|button)/i, primitive: "cssTransition", target: ".cta-button", params: { hoverValue: "translateY(-4px)" } },
23
24
  { re: /(reveal|einblend|fade|erscheinen|appear|headline|(ü|ue)berschrift|gleitet|scroll)/i, primitive: "scrollReveal", target: ".hero h1", params: { from: { opacity: 0, y: 48 } } },