motionspec 1.0.3 → 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,90 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /*
4
+ * SBOM-Werkzeug (TASK-021, Audit-Befund #13). Zero-dep, CJS.
5
+ *
6
+ * Hintergrund: `npm sbom --omit dev` laesst hier 9 Laufzeit-Pakete aus
7
+ * (cross-spawn, debug, ms, which, isexe, path-key, shebang-command,
8
+ * shebang-regex, fast-deep-equal) — sie haengen am Prod-Baum von
9
+ * @modelcontextprotocol/sdk, werden aber, weil sie ZUSAETZLICH ueber einen
10
+ * dev-Pfad (eslint) erreichbar sind, von npm faelschlich weggeschnitten. Das
11
+ * Lizenz-Gate war dadurch lueckenhaft. Gleichzeitig darf die SBOM keine reinen
12
+ * dev-Pakete (wrangler/eslint/…) enthalten, weil deren Lizenzen das Gate sonst
13
+ * (zu Recht) rot faerben und nicht ausgeliefert werden.
14
+ *
15
+ * Loesung: die VOLLE SBOM erzeugen und auf die LAUFZEIT-Pakete filtern
16
+ * (Quelle der Wahrheit: package-lock.json, top-level node_modules/* mit
17
+ * dev!==true). Das ergibt eine vollstaendige UND ausschliesslich-Laufzeit-SBOM.
18
+ *
19
+ * Modi:
20
+ * --filter liest eine volle CycloneDX-SBOM von stdin, schreibt die
21
+ * Laufzeit-gefilterte SBOM nach stdout (genutzt vom `sbom`-Script).
22
+ * (default) prueft sbom.cdx.json: jedes Laufzeit-Paket MUSS als Komponente
23
+ * vorhanden sein; fehlt eines -> Liste + exit 1, sonst exit 0.
24
+ */
25
+ const fs = require("fs");
26
+ const path = require("path");
27
+
28
+ const ROOT = path.join(__dirname, "..");
29
+ const LOCK = path.join(ROOT, "package-lock.json");
30
+ const SBOM = path.join(ROOT, "sbom.cdx.json");
31
+
32
+ /* Laufzeit-Pakete laut Lockfile: top-level node_modules/<name>, dev !== true.
33
+ * Liefert ein Set voller Namen (inkl. @scope). */
34
+ function runtimeNames() {
35
+ const lock = JSON.parse(fs.readFileSync(LOCK, "utf8"));
36
+ const names = new Set();
37
+ for (const key of Object.keys(lock.packages || {})) {
38
+ const m = /^node_modules\/(@[^/]+\/[^/]+|[^/]+)$/.exec(key);
39
+ if (!m) continue; // nur top-level (keine verschachtelten Dubletten)
40
+ if (lock.packages[key].dev === true) continue; // reine dev-Pakete weglassen
41
+ names.add(m[1]);
42
+ }
43
+ return names;
44
+ }
45
+
46
+ /* Voller Name einer CycloneDX-Komponente (npm legt @scope direkt in name, aber
47
+ * group wird defensiv mitgenommen, falls ein Generator es doch trennt). */
48
+ function compName(c) {
49
+ return c.group ? c.group + "/" + c.name : c.name;
50
+ }
51
+
52
+ function readStdin() {
53
+ return fs.readFileSync(0, "utf8");
54
+ }
55
+
56
+ function filterMode() {
57
+ const full = JSON.parse(readStdin());
58
+ const runtime = runtimeNames();
59
+ const keep = (full.components || []).filter((c) => runtime.has(compName(c)));
60
+ const keptRefs = new Set(keep.map((c) => c["bom-ref"]).filter(Boolean));
61
+ full.components = keep;
62
+ /* Abhaengigkeits-Graph auf behaltene Komponenten beschneiden, damit keine
63
+ * baumelnden Referenzen zurueckbleiben (die Wurzel-Komponente bleibt). */
64
+ if (Array.isArray(full.dependencies)) {
65
+ const rootRef = full.metadata && full.metadata.component && full.metadata.component["bom-ref"];
66
+ full.dependencies = full.dependencies
67
+ .filter((d) => d.ref === rootRef || keptRefs.has(d.ref))
68
+ .map((d) => Array.isArray(d.dependsOn)
69
+ ? Object.assign({}, d, { dependsOn: d.dependsOn.filter((r) => r === rootRef || keptRefs.has(r)) })
70
+ : d);
71
+ }
72
+ process.stdout.write(JSON.stringify(full, null, 2) + "\n");
73
+ }
74
+
75
+ function checkMode() {
76
+ if (!fs.existsSync(SBOM)) { console.error("Keine sbom.cdx.json — erst `npm run sbom`."); process.exit(1); }
77
+ const sbom = JSON.parse(fs.readFileSync(SBOM, "utf8"));
78
+ const present = new Set((sbom.components || []).map(compName));
79
+ const missing = [...runtimeNames()].filter((n) => !present.has(n)).sort();
80
+ if (missing.length) {
81
+ console.error("SBOM unvollstaendig — " + missing.length + " Laufzeit-Paket(e) fehlen:");
82
+ missing.forEach((n) => console.error(" - " + n));
83
+ console.error("Fix: `npm run sbom` neu erzeugen (vollstaendige Laufzeit-SBOM).");
84
+ process.exit(1);
85
+ }
86
+ console.error("SBOM OK — alle " + present.size + " Komponenten decken die Laufzeit-Pakete ab.");
87
+ }
88
+
89
+ if (process.argv.includes("--filter")) filterMode();
90
+ else checkMode();
@@ -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.3",
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,10 +38,11 @@
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
- "sbom": "npm sbom --sbom-format cyclonedx --omit dev > sbom.cdx.json",
44
+ "sbom": "npm sbom --sbom-format cyclonedx | node bin/sbom-check.js --filter > sbom.cdx.json",
45
+ "sbom:check": "node bin/sbom-check.js",
45
46
  "catalog-lock": "node bin/catalog-lock.js",
46
47
  "catalog-lock:check": "node bin/catalog-lock.js --check",
47
48
  "mcp": "node src/mcp/server.mjs",
@@ -51,20 +52,31 @@
51
52
  "pipeline": "node bin/motion.js pipeline",
52
53
  "catalog": "node bin/motion.js catalog",
53
54
  "stats": "node bin/motion.js stats",
54
- "prepublishOnly": "npm test && npm run catalog-lock:check && npm run sbom && node bin/license-check.js",
55
- "lint": "eslint ."
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",
62
+ "lint": "eslint .",
63
+ "prepare": "husky"
56
64
  },
57
65
  "engines": {
58
66
  "node": ">=18"
59
67
  },
60
68
  "dependencies": {
61
69
  "@modelcontextprotocol/sdk": "1.29.0",
62
- "zod": "^4.4.3"
70
+ "zod": "4.4.3"
63
71
  },
64
72
  "devDependencies": {
73
+ "@commitlint/cli": "^21.0.2",
74
+ "@commitlint/config-conventional": "^21.0.2",
65
75
  "@eslint/js": "^10.0.1",
66
76
  "@playwright/test": "^1.50.0",
67
77
  "eslint": "^10.5.0",
68
- "globals": "^17.6.0"
78
+ "globals": "^17.6.0",
79
+ "husky": "^9.1.7",
80
+ "wrangler": "4.105.0"
69
81
  }
70
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",
@@ -35,9 +35,9 @@
35
35
  "additionalProperties": false,
36
36
  "required": ["id", "primitive", "target"],
37
37
  "properties": {
38
- "id": { "type": "string", "minLength": 1 },
38
+ "id": { "type": "string", "minLength": 1, "maxLength": 64, "pattern": "^[A-Za-z0-9_-]{1,64}$" },
39
39
  "primitive": { "type": "string", "description": "Muss ein Name aus dem Primitive-Katalog sein. Allow-List, zur Laufzeit geprueft." },
40
- "target": { "type": "string", "minLength": 1 },
40
+ "target": { "type": "string", "minLength": 1, "maxLength": 200 },
41
41
  "params": { "type": "object" },
42
42
  "trigger": {
43
43
  "type": "object",
@@ -91,4 +91,30 @@ function catalogVersion(catalog) {
91
91
  return crypto.createHash("sha256").update(stable).digest("hex").slice(0, 16);
92
92
  }
93
93
 
94
- module.exports = { loadCatalog, buildCatalog, catalogVersion, checkPrimitive };
94
+ /* Gemeinsame Katalog-Zusammenfassung (TASK-026, Audit #23). Vorher gab es zwei
95
+ * divergierende Implementierungen (prompt.js 4 Felder, register-tools.js 8). Eine
96
+ * Quelle: Default sind alle 8 Felder; ein optionales fields-Subset liefert weniger
97
+ * (z.B. fuer einen schlankeren Prompt). Primitive nach Namen sortiert. */
98
+ function catalogSummary(catalog, fields) {
99
+ const ALL = ["name", "version", "purpose", "engine", "cost", "paramSchema", "triggerDefaults", "reducedMotionFallback"];
100
+ const pick = Array.isArray(fields) ? fields : ALL;
101
+ return Object.keys(catalog).sort().map((name) => {
102
+ const p = catalog[name];
103
+ const full = {
104
+ name,
105
+ version: p.version,
106
+ purpose: p.purpose,
107
+ engine: p.engine,
108
+ cost: (p.performance && p.performance.cost) || 0,
109
+ paramSchema: p.paramSchema || {},
110
+ triggerDefaults: p.triggerDefaults || {},
111
+ reducedMotionFallback: (p.a11y && p.a11y.reducedMotionFallback) || null,
112
+ };
113
+ if (pick === ALL) return full;
114
+ const o = {};
115
+ pick.forEach((f) => { o[f] = full[f]; });
116
+ return o;
117
+ });
118
+ }
119
+
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,20 +46,35 @@ 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
- /*
66
- * compileSpec(spec, catalog, opts) -> {
67
- * ok, errors?, js?, css?, report: { motions, jsCount, cssCount,
68
- * cost, budget, budgetOk, reducedMotion } }
49
+ /**
50
+ * @typedef {Object} CompileReport
51
+ * @property {number} motions - Anzahl kompilierter Motions
52
+ * @property {number} jsCount - Anzahl JS-Ausgaben
53
+ * @property {number} cssCount - Anzahl CSS-Ausgaben
54
+ * @property {number} cost - Summe der Primitiv-Kosten
55
+ * @property {number} budget - Performance-Budget (Default BUDGET)
56
+ * @property {boolean} budgetOk - true wenn cost <= budget
57
+ * @property {boolean} reducedMotion - ob ein prefers-reduced-motion-Guard emittiert wird
58
+ * @property {boolean} reducedMotionOverriddenOff - true wenn die Spec den Guard explizit abschaltet
59
+ * @property {string} specVersion - specVersion der Quelle
60
+ * @property {Array<{code:string,message:string}>} deprecations - Deprecation-Hinweise (ADR-0001 D4)
61
+ */
62
+ /**
63
+ * @typedef {Object} CompileResult
64
+ * @property {boolean} ok - true bei Erfolg, false fail-closed (keine Teil-Ausgabe)
65
+ * @property {string[]} [errors] - Fehler mit [MS-XXX]-Praefix, nur wenn ok=false
66
+ * @property {string|null} [js] - generiertes JS (null wenn keine JS-Motions)
67
+ * @property {string|null} [css] - generiertes CSS (null wenn keine CSS-Motions)
68
+ * @property {CompileReport} [report] - Report, nur wenn ok=true
69
+ */
70
+ /**
71
+ * Validiert (fail-closed) und kompiliert eine MotionSpec deterministisch zu GSAP/CSS.
72
+ * Dieselbe Spec ergibt immer denselben Code; bei Validierungs- oder CSS-Fehlern
73
+ * wird nichts emittiert.
74
+ * @param {Object} spec - die MotionSpec
75
+ * @param {Object} catalog - geladener Primitiv-Katalog
76
+ * @param {{specName?: string, budget?: number}} [opts] - optionaler Artefakt-Name + Budget
77
+ * @returns {CompileResult}
69
78
  */
70
79
  function compileSpec(spec, catalog, opts) {
71
80
  const o = opts || {};
@@ -77,7 +86,7 @@ function compileSpec(spec, catalog, opts) {
77
86
  * so validate-only callers and the compile report agree. */
78
87
  const deprecations = v.deprecations || [];
79
88
 
80
- const respectRM = !!(spec.globals && spec.globals.respectReducedMotion);
89
+ const respectRM = !(spec.globals && spec.globals.respectReducedMotion === false);
81
90
  const js = [], css = [];
82
91
  let cost = 0, nJs = 0, nCss = 0;
83
92
 
@@ -140,10 +149,13 @@ function compileSpec(spec, catalog, opts) {
140
149
  budget,
141
150
  budgetOk: cost <= budget,
142
151
  reducedMotion: respectRM,
152
+ reducedMotionOverriddenOff: !!(spec.globals && spec.globals.respectReducedMotion === false),
143
153
  specVersion: spec.specVersion,
144
154
  deprecations,
145
155
  },
146
156
  };
147
157
  }
148
158
 
149
- 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 };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ /*
3
+ * Gemeinsame Keyword->Primitiv-Heuristik (TASK-025, Audit #22).
4
+ *
5
+ * Vorher gab es ZWEI divergierende Tabellen: clients.js (mockClient, 5 Eintraege,
6
+ * mit target/params) und discover.js (Gap-Report, 8 Eintraege, ohne target). Dadurch
7
+ * fehlten counterUp/marquee/scaleOnScroll im Mock — der Repair-Loop lief gegen ein
8
+ * kuenstlich unvollstaendiges Modell. Eine Quelle der Wahrheit schliesst die Luecke.
9
+ *
10
+ * Quelle der Regex/Reihenfolge ist discover.js (der vollstaendigere 8er-Satz);
11
+ * target/params der fuenf bekannten Primitive stammen aus clients.js, fuer die drei
12
+ * neuen sind sinnvolle Default-Selektoren gesetzt. WICHTIG: Reihenfolge ist API —
13
+ * discover.mapIntent() liefert den ERSTEN Treffer, daher Eintraege nicht umsortieren.
14
+ */
15
+ const KEYWORD_MAP = [
16
+ { re: /(stagger|nacheinander|gestaffelt|one after|cards?|karten|liste)/i, primitive: "staggerReveal", target: ".features .card", params: { from: { opacity: 0, y: 32 } } },
17
+ { re: /(z(ä|ae)hl|counter|hochz(ä|ae)hl|count up|nummer|zahl)/i, primitive: "counterUp", target: ".stats .num", params: {} },
18
+ { re: /(parallax|tiefe|depth|layer|ebene)/i, primitive: "parallaxLayer", target: ".hero .bg", params: { yPercent: -25 } },
19
+ { re: /(pin|fixier|sticky|festhalten|haften)/i, primitive: "pinnedSection", target: ".showcase", params: { distance: "+=100%" } },
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" } },
22
+ { re: /(skalier|scale|zoom|gr(ö|oe)(ß|ss)er werden)/i, primitive: "scaleOnScroll", target: ".section", params: {} },
23
+ { re: /(hover|maus|cursor|button)/i, primitive: "cssTransition", target: ".cta-button", params: { hoverValue: "translateY(-4px)" } },
24
+ { re: /(reveal|einblend|fade|erscheinen|appear|headline|(ü|ue)berschrift|gleitet|scroll)/i, primitive: "scrollReveal", target: ".hero h1", params: { from: { opacity: 0, y: 48 } } },
25
+ ];
26
+
27
+ module.exports = { KEYWORD_MAP };