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 +21 -0
- package/README.md +102 -0
- package/bin/catalog-lock.js +46 -0
- package/bin/license-check.js +32 -0
- package/bin/motion.js +136 -0
- package/catalog.lock.json +243 -0
- package/package.json +68 -0
- package/primitives/counterUp.json +54 -0
- package/primitives/cssTransition.json +38 -0
- package/primitives/marquee.json +44 -0
- package/primitives/parallaxLayer.json +35 -0
- package/primitives/pinnedSection.json +30 -0
- package/primitives/scaleOnScroll.json +55 -0
- package/primitives/scrollReveal.json +43 -0
- package/primitives/staggerReveal.json +43 -0
- package/schema/motionspec.schema.json +56 -0
- package/src/compiler/catalog-semver.js +133 -0
- package/src/compiler/catalog.js +94 -0
- package/src/compiler/compile.js +149 -0
- package/src/compiler/validate.js +264 -0
- package/src/demo/build-demo.js +150 -0
- package/src/discover/discover.js +92 -0
- package/src/mcp/register-tools.js +163 -0
- package/src/mcp/server.mjs +46 -0
- package/src/router/cache.js +81 -0
- package/src/router/clients.js +98 -0
- package/src/router/prompt.js +99 -0
- package/src/router/route.js +108 -0
- package/src/router/telemetry-sink.js +53 -0
- package/src/router/telemetry.js +68 -0
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
|
+
}
|