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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* MotionSpec-Compiler — v0.2 (Bibliothek)
|
|
4
|
+
* ----------------------------------------------------------------------
|
|
5
|
+
* Uebersetzt eine validierte MotionSpec deterministisch in Code.
|
|
6
|
+
* Kein Modell beteiligt: dieselbe Spec ergibt immer denselben Code.
|
|
7
|
+
*
|
|
8
|
+
* Sichere Interpolation (v0.2):
|
|
9
|
+
* {{pfad}} -> JS-Literal: Strings/Objekte via JSON.stringify,
|
|
10
|
+
* Zahlen/Booleans direkt. Ein Wert kann den String-/
|
|
11
|
+
* Ausdruckskontext NIE verlassen.
|
|
12
|
+
* {{css pfad}} -> Roh-Einfuegung fuer CSS-Kontexte, aber nur wenn der
|
|
13
|
+
* Wert die CSS-Allow-List besteht (sonst Abbruch).
|
|
14
|
+
*
|
|
15
|
+
* Ablauf: Trust Boundary -> Defaults -> Templates -> Gates -> Report.
|
|
16
|
+
*/
|
|
17
|
+
const { validateSpec, unsafeToken } = require("./validate.js");
|
|
18
|
+
const VERSION = require("../../package.json").version;
|
|
19
|
+
|
|
20
|
+
const BUDGET = 10; /* Performance-Budget: Summe der Primitiv-Kosten */
|
|
21
|
+
const CSS_SAFE_RE = /^[A-Za-z0-9 _\-#.:,()>+~*=%[\]"|^$]{0,200}$/;
|
|
22
|
+
|
|
23
|
+
function resolvePath(ctx, expr) {
|
|
24
|
+
return expr.split(".").reduce(
|
|
25
|
+
(o, k) => (o === undefined || o === null ? undefined : o[k]),
|
|
26
|
+
ctx
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function jsLiteral(v) {
|
|
31
|
+
if (v === undefined || v === null) return "null";
|
|
32
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
33
|
+
return JSON.stringify(v); /* Strings UND Objekte: immer korrekt escaped */
|
|
34
|
+
}
|
|
35
|
+
|
|
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
|
+
function fill(tpl, ctx) {
|
|
45
|
+
return tpl.replace(/\{\{([^}]+)\}\}/g, (_, raw) => {
|
|
46
|
+
const expr = raw.trim();
|
|
47
|
+
if (expr.startsWith("css ")) {
|
|
48
|
+
const v = resolvePath(ctx, expr.slice(4).trim());
|
|
49
|
+
return cssRaw(v === undefined ? "" : v, expr);
|
|
50
|
+
}
|
|
51
|
+
return jsLiteral(resolvePath(ctx, expr));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
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 } }
|
|
69
|
+
*/
|
|
70
|
+
function compileSpec(spec, catalog, opts) {
|
|
71
|
+
const o = opts || {};
|
|
72
|
+
const budget = typeof o.budget === "number" ? o.budget : BUDGET; /* Audit #16: konfigurierbar */
|
|
73
|
+
const v = validateSpec(spec, catalog);
|
|
74
|
+
if (!v.ok) return { ok: false, errors: v.errors };
|
|
75
|
+
|
|
76
|
+
/* ADR-0001 D4: deprecation note comes from the validator (single source),
|
|
77
|
+
* so validate-only callers and the compile report agree. */
|
|
78
|
+
const deprecations = v.deprecations || [];
|
|
79
|
+
|
|
80
|
+
const respectRM = !!(spec.globals && spec.globals.respectReducedMotion);
|
|
81
|
+
const js = [], css = [];
|
|
82
|
+
let cost = 0, nJs = 0, nCss = 0;
|
|
83
|
+
|
|
84
|
+
for (const m of spec.motions) {
|
|
85
|
+
const prim = catalog[m.primitive];
|
|
86
|
+
const params = withDefaults(prim.paramSchema || {}, m.params);
|
|
87
|
+
const trigger = Object.assign({}, prim.triggerDefaults || {}, m.trigger || {});
|
|
88
|
+
const ctx = { id: m.id, target: m.target, params, trigger, globals: spec.globals || {} };
|
|
89
|
+
let code;
|
|
90
|
+
try { code = fill(prim.template, ctx); }
|
|
91
|
+
catch (e) { return { ok: false, errors: ["[MS-COMPILE-CSS] Compile-Abbruch bei motion '" + m.id + "': " + e.message] }; }
|
|
92
|
+
cost += (prim.performance && prim.performance.cost) || 0;
|
|
93
|
+
if (prim.output === "css") {
|
|
94
|
+
css.push(" /* " + m.id + " (" + m.primitive + ") */");
|
|
95
|
+
css.push(" " + code.split("\n").join("\n "));
|
|
96
|
+
nCss++;
|
|
97
|
+
} else {
|
|
98
|
+
js.push(" /* " + m.id + " (" + m.primitive + ") */");
|
|
99
|
+
js.push(" " + code);
|
|
100
|
+
nJs++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Audit-Befund #1: Kommentar-Injection via Dateiname verhindern. */
|
|
105
|
+
const safeName = String(o.specName || "spec").replace(/\*\/|\/\*/g, "_").slice(0, 80);
|
|
106
|
+
const specLabel = safeName + " Ziel: " + spec.meta.target;
|
|
107
|
+
let jsOut = null, cssOut = null;
|
|
108
|
+
|
|
109
|
+
if (nJs > 0) {
|
|
110
|
+
const head = [
|
|
111
|
+
"/* MotionSpec-Compiler v" + VERSION + " - generiertes Artefakt. NICHT von Hand bearbeiten; die Spec ist die Quelle. */",
|
|
112
|
+
"/* Spec: " + specLabel + " */",
|
|
113
|
+
"(function () {",
|
|
114
|
+
" if (typeof gsap === 'undefined') { console.warn('[motion] GSAP nicht geladen.'); return; }",
|
|
115
|
+
" gsap.registerPlugin(ScrollTrigger);",
|
|
116
|
+
];
|
|
117
|
+
if (respectRM) {
|
|
118
|
+
head.push(" var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;");
|
|
119
|
+
head.push(" if (reduceMotion) return; /* a11y: respectReducedMotion */");
|
|
120
|
+
}
|
|
121
|
+
jsOut = head.concat(js, ["})();", ""]).join("\n");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (nCss > 0) {
|
|
125
|
+
let body = "/* MotionSpec-Compiler v" + VERSION + " - generiertes Artefakt. */\n";
|
|
126
|
+
if (respectRM) body += "@media (prefers-reduced-motion: no-preference) {\n" + css.join("\n") + "\n}\n";
|
|
127
|
+
else body += css.join("\n") + "\n";
|
|
128
|
+
cssOut = body;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
js: jsOut,
|
|
134
|
+
css: cssOut,
|
|
135
|
+
report: {
|
|
136
|
+
motions: spec.motions.length,
|
|
137
|
+
jsCount: nJs,
|
|
138
|
+
cssCount: nCss,
|
|
139
|
+
cost,
|
|
140
|
+
budget,
|
|
141
|
+
budgetOk: cost <= budget,
|
|
142
|
+
reducedMotion: respectRM,
|
|
143
|
+
specVersion: spec.specVersion,
|
|
144
|
+
deprecations,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { compileSpec, fill, withDefaults, BUDGET };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* MotionSpec — Trust Boundary (v0.3, Phase 2 / Sprint 2)
|
|
4
|
+
* ----------------------------------------------------------------------
|
|
5
|
+
* Anti-Corruption Layer zwischen dem Autor einer Spec (Modell oder Mensch)
|
|
6
|
+
* und dem Compiler. Prinzip: FAIL-CLOSED. Eine Spec passiert nur, wenn JEDE
|
|
7
|
+
* Pruefung besteht. Es wird niemals eine Teil-Ausgabe erzeugt.
|
|
8
|
+
*
|
|
9
|
+
* STABILE ERROR-CODES (Audit #15 / Sprint-2 S2-04):
|
|
10
|
+
* Jeder Fehler traegt ein sprachneutrales Praefix [MS-XXX]. Integratoren
|
|
11
|
+
* matchen auf den Code, nicht auf den (deutschen) Klartext. validateSpec
|
|
12
|
+
* liefert zusaetzlich errorCodes[] fuer rein programmatische Nutzung.
|
|
13
|
+
* Die Code-Liste ist API: Codes werden nie wiederverwendet/umdefiniert.
|
|
14
|
+
*
|
|
15
|
+
* Code-Registry:
|
|
16
|
+
* MS-SPEC-OBJ Spec ist kein Objekt
|
|
17
|
+
* MS-SPEC-KEY Unbekannter Top-Level-Schluessel
|
|
18
|
+
* MS-SPEC-VER specVersion fehlt/unbekannt
|
|
19
|
+
* MS-SPEC-MOTIONS motions fehlt/leer
|
|
20
|
+
* MS-META-MISSING meta fehlt
|
|
21
|
+
* MS-META-KEY Unbekannter meta-Schluessel
|
|
22
|
+
* MS-META-TARGET meta.target nicht unterstuetzt
|
|
23
|
+
* MS-GLOBALS-OBJ globals kein Objekt
|
|
24
|
+
* MS-GLOBALS-KEY unbekannter globals-Schluessel
|
|
25
|
+
* MS-MOTION-OBJ motion ist kein Objekt
|
|
26
|
+
* MS-MOTION-KEY Unbekannter motion-Schluessel
|
|
27
|
+
* MS-ID-FORMAT id fehlt/Format verletzt
|
|
28
|
+
* MS-ID-DUP id nicht eindeutig
|
|
29
|
+
* MS-TARGET-UNSAFE target fehlt/unsicherer Selektor
|
|
30
|
+
* MS-PRIM-MISSING primitive fehlt
|
|
31
|
+
* MS-PRIM-UNKNOWN primitive nicht im Katalog (Allow-List)
|
|
32
|
+
* MS-PARAM-UNKNOWN unbekannter Parameter
|
|
33
|
+
* MS-PARAM-REQ Pflichtparameter fehlt
|
|
34
|
+
* MS-PARAM-TYPE falscher Parameter-Typ
|
|
35
|
+
* MS-PARAM-MIN Parameter unter Minimum
|
|
36
|
+
* MS-PARAM-MAX Parameter ueber Maximum
|
|
37
|
+
* MS-PARAM-CHARSET String-Parameter enthaelt unzulaessige Zeichen
|
|
38
|
+
* MS-PARAM-UNSAFE String-Parameter enthaelt gefaehrliches Token (javascript:, expression(, url(, ...)
|
|
39
|
+
* MS-PARAM-PATTERN String-Parameter verletzt erlaubtes Muster
|
|
40
|
+
* MS-PARAM-PATTERN-DEF paramSchema.pattern ist selbst ungueltig
|
|
41
|
+
* MS-TRANSFORM-KEY unerlaubter Transform-Schluessel
|
|
42
|
+
* MS-TRANSFORM-TYPE Transform-Wert keine Zahl
|
|
43
|
+
* MS-TRIGGER-OBJ trigger kein Objekt
|
|
44
|
+
* MS-TRIGGER-KEY unbekannter trigger-Schluessel
|
|
45
|
+
* MS-TRIGGER-VAL trigger-Positionswert ungueltig
|
|
46
|
+
* MS-CATALOG-PIN catalogVersion-Pin hat falsches Format
|
|
47
|
+
* MS-CATALOG-PIN-MISMATCH catalogVersion-Pin passt nicht zum geladenen Katalog
|
|
48
|
+
* MS-RESP-UNSUPPORTED responsive noch nicht unterstuetzt (-> specVersion 0.2)
|
|
49
|
+
* MS-DEPRECATED-VERSION specVersion ist deprecated. KEIN harter Fehler:
|
|
50
|
+
* wird in validateSpec().deprecations[] UND im Compile-Report
|
|
51
|
+
* gemeldet (gemeinsame Quelle deprecationsFor; ADR-0001 D4).
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/* ADR-0001 (signed 2026-06-15): v1 carries "1.0". "0.1" stays accepted for
|
|
55
|
+
* one minor cycle (deprecated, compile report emits a note; removed in v1.2). */
|
|
56
|
+
const SPEC_VERSIONS = ["1.0", "0.1"];
|
|
57
|
+
const DEPRECATED_VERSIONS = ["0.1"];
|
|
58
|
+
|
|
59
|
+
/* ADR-0001 D4: single source of the deprecation signal. Surfaced by BOTH
|
|
60
|
+
* validateSpec (so validate-only callers see it) and the compile report. */
|
|
61
|
+
function deprecationsFor(specVersion) {
|
|
62
|
+
if (DEPRECATED_VERSIONS.indexOf(specVersion) === -1) return [];
|
|
63
|
+
return [{
|
|
64
|
+
code: "MS-DEPRECATED-VERSION",
|
|
65
|
+
message: 'specVersion "' + specVersion + '" is deprecated; migrate to "1.0". Accepted until v1.2.',
|
|
66
|
+
}];
|
|
67
|
+
}
|
|
68
|
+
const TARGETS = ["vanilla-gsap"];
|
|
69
|
+
const TRANSFORM_KEYS = ["opacity", "x", "y", "scale", "rotation", "xPercent", "yPercent"];
|
|
70
|
+
const TOP_KEYS = ["specVersion", "catalogVersion", "meta", "globals", "motions"];
|
|
71
|
+
const CATALOG_PIN_RE = /^[0-9a-f]{16}$/;
|
|
72
|
+
const META_KEYS = ["project", "target", "createdWith"];
|
|
73
|
+
const GLOBALS_KEYS = ["respectReducedMotion", "defaultEase"];
|
|
74
|
+
const MOTION_KEYS = ["id", "primitive", "target", "params", "trigger", "responsive"];
|
|
75
|
+
|
|
76
|
+
const { catalogVersion } = require("./catalog.js");
|
|
77
|
+
|
|
78
|
+
const ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
|
79
|
+
const SELECTOR_RE = /^[A-Za-z0-9 _\-#.:,()>+~*=[\]"|^$]{1,200}$/;
|
|
80
|
+
const STRING_PARAM_RE = /^[^\x00-\x1F\x7F`\\]{0,200}$/;
|
|
81
|
+
|
|
82
|
+
/* Dangerous substrings that must never reach emitted JS/CSS, even when the
|
|
83
|
+
* charset gate would otherwise allow them (e.g. "url(javascript:…)",
|
|
84
|
+
* "expression(…)"). Checked by BOTH the validator (so motion_validate ok=true
|
|
85
|
+
* predicts compile success — closes the validate↔compile asymmetry) and the
|
|
86
|
+
* compiler's cssRaw (defense in depth). Lower-cased comparison. */
|
|
87
|
+
const UNSAFE_TOKENS = [
|
|
88
|
+
"javascript:", "vbscript:", "expression(", "url(", "@import",
|
|
89
|
+
"behavior:", "-moz-binding", "</", "<script",
|
|
90
|
+
];
|
|
91
|
+
function unsafeToken(s) {
|
|
92
|
+
const l = String(s).toLowerCase();
|
|
93
|
+
for (const t of UNSAFE_TOKENS) if (l.indexOf(t) !== -1) return t;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function safeSelector(s) {
|
|
98
|
+
return (
|
|
99
|
+
typeof s === "string" &&
|
|
100
|
+
SELECTOR_RE.test(s) &&
|
|
101
|
+
s.indexOf("/*") === -1 &&
|
|
102
|
+
s.indexOf("*/") === -1
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function validateParams(prim, params, at, push, partial) {
|
|
107
|
+
const schema = prim.paramSchema || {};
|
|
108
|
+
Object.keys(params).forEach((k) => {
|
|
109
|
+
if (!schema[k]) push("MS-PARAM-UNKNOWN", at + ': unbekannter Parameter "' + k + '" fuer Primitiv "' + prim.name + '".');
|
|
110
|
+
});
|
|
111
|
+
Object.keys(schema).forEach((k) => {
|
|
112
|
+
const def = schema[k];
|
|
113
|
+
const has = Object.prototype.hasOwnProperty.call(params, k);
|
|
114
|
+
if (!has) {
|
|
115
|
+
if (def.required && !partial) push("MS-PARAM-REQ", at + ': Pflichtparameter "' + k + '" fehlt.');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const v = params[k];
|
|
119
|
+
if (def.type === "number") {
|
|
120
|
+
if (typeof v !== "number" || isNaN(v)) { push("MS-PARAM-TYPE", at + ': "' + k + '" muss eine Zahl sein.'); return; }
|
|
121
|
+
if (typeof def.min === "number" && v < def.min) push("MS-PARAM-MIN", at + ': "' + k + '" = ' + v + " liegt unter dem Minimum " + def.min + ".");
|
|
122
|
+
if (typeof def.max === "number" && v > def.max) push("MS-PARAM-MAX", at + ': "' + k + '" = ' + v + " liegt ueber dem Maximum " + def.max + ".");
|
|
123
|
+
} else if (def.type === "string") {
|
|
124
|
+
if (typeof v !== "string") push("MS-PARAM-TYPE", at + ': "' + k + '" muss ein String sein.');
|
|
125
|
+
else if (!STRING_PARAM_RE.test(v)) push("MS-PARAM-CHARSET", at + ': "' + k + '" enthaelt unzulaessige Zeichen (Steuerzeichen, Backslash, Backtick) oder ist zu lang.');
|
|
126
|
+
else if (unsafeToken(v)) push("MS-PARAM-UNSAFE", at + ': "' + k + '" enthaelt ein unzulaessiges Token "' + unsafeToken(v) + '" (z.B. javascript:, expression(, url(). Abgelehnt.');
|
|
127
|
+
else if (def.pattern) {
|
|
128
|
+
let re = null;
|
|
129
|
+
try { re = new RegExp(def.pattern); }
|
|
130
|
+
catch { push("MS-PARAM-PATTERN-DEF", at + ': paramSchema.pattern fuer "' + k + '" ist kein gueltiger regulaerer Ausdruck.'); }
|
|
131
|
+
if (re && !re.test(v))
|
|
132
|
+
push("MS-PARAM-PATTERN", at + ': "' + k + '" = "' + v + '" entspricht nicht dem erlaubten Muster ' + def.pattern + ".");
|
|
133
|
+
}
|
|
134
|
+
} else if (def.type === "boolean") {
|
|
135
|
+
if (typeof v !== "boolean") push("MS-PARAM-TYPE", at + ': "' + k + '" muss true oder false sein.');
|
|
136
|
+
} else if (def.type === "transform") {
|
|
137
|
+
if (typeof v !== "object" || v === null || Array.isArray(v)) {
|
|
138
|
+
push("MS-PARAM-TYPE", at + ': "' + k + '" muss ein Transform-Objekt sein.'); return;
|
|
139
|
+
}
|
|
140
|
+
Object.keys(v).forEach((tk) => {
|
|
141
|
+
if (TRANSFORM_KEYS.indexOf(tk) === -1)
|
|
142
|
+
push("MS-TRANSFORM-KEY", at + ': "' + k + "." + tk + '" ist kein erlaubter Transform-Schluessel (erlaubt: ' + TRANSFORM_KEYS.join(", ") + ").");
|
|
143
|
+
else if (typeof v[tk] !== "number")
|
|
144
|
+
push("MS-TRANSFORM-TYPE", at + ': "' + k + "." + tk + '" muss eine Zahl sein.');
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function validateTrigger(trigger, at, push) {
|
|
151
|
+
if (typeof trigger !== "object" || trigger === null || Array.isArray(trigger)) {
|
|
152
|
+
push("MS-TRIGGER-OBJ", at + ": trigger muss ein Objekt sein."); return;
|
|
153
|
+
}
|
|
154
|
+
const KEYS = ["start", "end", "once"];
|
|
155
|
+
Object.keys(trigger).forEach((k) => {
|
|
156
|
+
if (KEYS.indexOf(k) === -1) push("MS-TRIGGER-KEY", at + ': trigger: unbekannter Schluessel "' + k + '".');
|
|
157
|
+
});
|
|
158
|
+
["start", "end"].forEach((k) => {
|
|
159
|
+
if (trigger[k] !== undefined) {
|
|
160
|
+
if (typeof trigger[k] !== "string" || !/^[A-Za-z0-9 %+=.-]{1,40}$/.test(trigger[k]))
|
|
161
|
+
push("MS-TRIGGER-VAL", at + ": trigger." + k + ' muss ein einfacher Positions-String sein (z.B. "top 80%").');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (trigger.once !== undefined && typeof trigger.once !== "boolean")
|
|
165
|
+
push("MS-TRIGGER-VAL", at + ": trigger.once muss true oder false sein.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validateSpec(spec, catalog) {
|
|
169
|
+
const errors = [];
|
|
170
|
+
const errorCodes = [];
|
|
171
|
+
/* push(code, msg) -> errors bekommt "[code] msg", errorCodes den Code. */
|
|
172
|
+
const push = (code, m) => { errors.push("[" + code + "] " + m); errorCodes.push(code); };
|
|
173
|
+
|
|
174
|
+
if (typeof spec !== "object" || spec === null || Array.isArray(spec))
|
|
175
|
+
return { ok: false, errors: ["[MS-SPEC-OBJ] Spec ist kein Objekt."], errorCodes: ["MS-SPEC-OBJ"], deprecations: [] };
|
|
176
|
+
|
|
177
|
+
const deprecations = deprecationsFor(spec.specVersion);
|
|
178
|
+
|
|
179
|
+
Object.keys(spec).forEach((k) => {
|
|
180
|
+
if (TOP_KEYS.indexOf(k) === -1) push("MS-SPEC-KEY", 'Unbekannter Schluessel auf oberster Ebene: "' + k + '".');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (SPEC_VERSIONS.indexOf(spec.specVersion) === -1)
|
|
184
|
+
push("MS-SPEC-VER", "specVersion fehlt oder ist unbekannt (erlaubt: " + SPEC_VERSIONS.join(", ") + ").");
|
|
185
|
+
|
|
186
|
+
/* ADR-0001 D2: optional catalogVersion pin for reproducibility. If a spec
|
|
187
|
+
* pins a catalog hash, it MUST match the loaded catalog — fail-closed, so a
|
|
188
|
+
* spec written against a different catalog cannot silently compile differently. */
|
|
189
|
+
if (spec.catalogVersion !== undefined) {
|
|
190
|
+
if (typeof spec.catalogVersion !== "string" || !CATALOG_PIN_RE.test(spec.catalogVersion)) {
|
|
191
|
+
push("MS-CATALOG-PIN", "catalogVersion muss ein 16-stelliger Hex-Hash sein (oder weglassen).");
|
|
192
|
+
} else {
|
|
193
|
+
const actual = catalogVersion(catalog);
|
|
194
|
+
if (spec.catalogVersion !== actual)
|
|
195
|
+
push("MS-CATALOG-PIN-MISMATCH", "catalogVersion-Pin (" + spec.catalogVersion + ") passt nicht zum geladenen Katalog (" + actual + "). Spec wurde gegen einen anderen Katalog geschrieben.");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!spec.meta || typeof spec.meta !== "object") {
|
|
200
|
+
push("MS-META-MISSING", "meta fehlt.");
|
|
201
|
+
} else {
|
|
202
|
+
Object.keys(spec.meta).forEach((k) => {
|
|
203
|
+
if (META_KEYS.indexOf(k) === -1) push("MS-META-KEY", 'meta: unbekannter Schluessel "' + k + '".');
|
|
204
|
+
});
|
|
205
|
+
if (TARGETS.indexOf(spec.meta.target) === -1)
|
|
206
|
+
push("MS-META-TARGET", "meta.target fehlt oder wird nicht unterstuetzt (erlaubt: " + TARGETS.join(", ") + ").");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (spec.globals !== undefined) {
|
|
210
|
+
if (typeof spec.globals !== "object" || spec.globals === null || Array.isArray(spec.globals)) {
|
|
211
|
+
push("MS-GLOBALS-OBJ", "globals muss ein Objekt sein.");
|
|
212
|
+
} else {
|
|
213
|
+
Object.keys(spec.globals).forEach((k) => {
|
|
214
|
+
if (GLOBALS_KEYS.indexOf(k) === -1) push("MS-GLOBALS-KEY", 'globals: unbekannter Schluessel "' + k + '" (erlaubt: ' + GLOBALS_KEYS.join(", ") + ").");
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!Array.isArray(spec.motions) || spec.motions.length === 0) {
|
|
220
|
+
push("MS-SPEC-MOTIONS", "motions fehlt oder ist leer.");
|
|
221
|
+
return { ok: errors.length === 0, errors, errorCodes, deprecations };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const seen = {};
|
|
225
|
+
spec.motions.forEach((m, i) => {
|
|
226
|
+
const at = "motions[" + i + "]";
|
|
227
|
+
if (typeof m !== "object" || m === null) { push("MS-MOTION-OBJ", at + " ist kein Objekt."); return; }
|
|
228
|
+
|
|
229
|
+
Object.keys(m).forEach((k) => {
|
|
230
|
+
if (MOTION_KEYS.indexOf(k) === -1) push("MS-MOTION-KEY", at + ': unbekannter Schluessel "' + k + '".');
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (typeof m.id !== "string" || !ID_RE.test(m.id))
|
|
234
|
+
push("MS-ID-FORMAT", at + ": id fehlt oder verletzt das Format [A-Za-z0-9_-]{1,64}.");
|
|
235
|
+
else if (seen[m.id]) push("MS-ID-DUP", at + ': id "' + m.id + '" ist nicht eindeutig.');
|
|
236
|
+
else seen[m.id] = true;
|
|
237
|
+
|
|
238
|
+
if (!safeSelector(m.target))
|
|
239
|
+
push("MS-TARGET-UNSAFE", at + ": target fehlt oder ist kein sicherer CSS-Selektor (verboten: Quotes, Backslash, {, }, ;, @, Steuerzeichen; max. 200 Zeichen).");
|
|
240
|
+
|
|
241
|
+
/* --- Kern-Schutz: Primitive-Allow-List --- */
|
|
242
|
+
if (typeof m.primitive !== "string" || !m.primitive) {
|
|
243
|
+
push("MS-PRIM-MISSING", at + ": primitive fehlt.");
|
|
244
|
+
} else if (!catalog[m.primitive]) {
|
|
245
|
+
push("MS-PRIM-UNKNOWN", at + ': primitive "' + m.primitive + '" existiert nicht im Katalog. '
|
|
246
|
+
+ "Erlaubt sind ausschliesslich: " + Object.keys(catalog).sort().join(", ") + ".");
|
|
247
|
+
} else {
|
|
248
|
+
const prim = catalog[m.primitive];
|
|
249
|
+
validateParams(prim, m.params || {}, at, push, false);
|
|
250
|
+
if (m.trigger !== undefined) validateTrigger(m.trigger, at, push);
|
|
251
|
+
if (m.responsive !== undefined) {
|
|
252
|
+
push("MS-RESP-UNSUPPORTED", at + ": responsive wird in dieser specVersion noch nicht unterstuetzt (geplant fuer 0.2). Feld entfernen.");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
return { ok: errors.length === 0, errors, errorCodes, deprecations };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
module.exports = {
|
|
261
|
+
validateSpec, safeSelector, ID_RE, deprecationsFor, unsafeToken,
|
|
262
|
+
SPEC_VERSIONS, DEPRECATED_VERSIONS, TARGETS,
|
|
263
|
+
TOP_KEYS, META_KEYS, GLOBALS_KEYS, MOTION_KEYS,
|
|
264
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Demo-Page-Generator (MS-01 / T01) — Geraete-Verifikation der Primitive.
|
|
4
|
+
*
|
|
5
|
+
* node bin/motion.js demo -> out/demo/index.html
|
|
6
|
+
*
|
|
7
|
+
* Eine scrollbare Seite mit einer Sektion je Primitiv. Der Motion-Code ist
|
|
8
|
+
* ECHTER Compiler-Output (compileSpec) — verifiziert wird also exakt das,
|
|
9
|
+
* was Kunden bekommen, nicht eine Nachbildung.
|
|
10
|
+
*
|
|
11
|
+
* Reduced-Motion-Simulation: ?rm=1 ueberschreibt window.matchMedia VOR dem
|
|
12
|
+
* Motion-Skript — getestet wird der echte Gate-Pfad im kompilierten Code.
|
|
13
|
+
*
|
|
14
|
+
* Demo-Bloecke: jedes Primitiv KANN ein "demo"-Feld tragen
|
|
15
|
+
* { html, params, trigger? } — sonst greift der generische Block.
|
|
16
|
+
*/
|
|
17
|
+
const fs = require("fs");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
const { loadCatalog, catalogVersion } = require("../compiler/catalog.js");
|
|
20
|
+
const { compileSpec } = require("../compiler/compile.js");
|
|
21
|
+
|
|
22
|
+
/* Fallback-Demos fuer den Bestand (Katalog-Definitionen ohne demo-Feld) */
|
|
23
|
+
const BUILTIN_DEMOS = {
|
|
24
|
+
scrollReveal: {
|
|
25
|
+
html: '<h2 class="d-scrollReveal">scrollReveal — gleitet beim Eintritt herein</h2>',
|
|
26
|
+
params: { from: { opacity: 0, y: 48 }, duration: 0.8 },
|
|
27
|
+
},
|
|
28
|
+
staggerReveal: {
|
|
29
|
+
html: '<div class="grid"><div class="card d-staggerReveal">Karte 1</div><div class="card d-staggerReveal">Karte 2</div><div class="card d-staggerReveal">Karte 3</div></div>',
|
|
30
|
+
params: { from: { opacity: 0, y: 32 }, stagger: 0.15 },
|
|
31
|
+
},
|
|
32
|
+
parallaxLayer: {
|
|
33
|
+
html: '<div class="parallax-frame"><div class="bg d-parallaxLayer">PARALLAX</div><p>Vordergrund scrollt normal, Hintergrund versetzt.</p></div>',
|
|
34
|
+
params: { yPercent: -25, scrub: 1 },
|
|
35
|
+
},
|
|
36
|
+
pinnedSection: {
|
|
37
|
+
html: '<div class="pin-stage d-pinnedSection"><h2>pinnedSection</h2><p>Diese Sektion bleibt stehen, waehrend du weiterscrollst.</p></div>',
|
|
38
|
+
params: { distance: "+=80%" },
|
|
39
|
+
},
|
|
40
|
+
cssTransition: {
|
|
41
|
+
html: '<button class="cta d-cssTransition">Hover mich — cssTransition</button>',
|
|
42
|
+
params: { hoverValue: "translateY(-4px) scale(1.04)", duration: 0.25 },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const CHECKLIST = [
|
|
47
|
+
"Laeuft die Bewegung fluessig (kein Ruckeln) beim langsamen UND schnellen Scrollen?",
|
|
48
|
+
"Mobile: gleiche Pruefung auf einem echten Telefon (nicht nur DevTools).",
|
|
49
|
+
"?rm=1 anhaengen: Inhalte muessen sofort sichtbar sein, keine Bewegung.",
|
|
50
|
+
"Layout-Shift? Elemente duerfen vor Animation keinen Sprung verursachen.",
|
|
51
|
+
"Nach bestandener Pruefung: performance.verifiedAt im Primitiv setzen.",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function buildDemo(outDir) {
|
|
55
|
+
const catalog = loadCatalog();
|
|
56
|
+
const names = Object.keys(catalog).sort();
|
|
57
|
+
const motions = [];
|
|
58
|
+
const sections = [];
|
|
59
|
+
|
|
60
|
+
names.forEach((name) => {
|
|
61
|
+
const prim = catalog[name];
|
|
62
|
+
const demo = prim.demo || BUILTIN_DEMOS[name];
|
|
63
|
+
if (!demo) {
|
|
64
|
+
sections.push('<section class="prim"><h2>' + name + "</h2><p class='miss'>KEIN DEMO-BLOCK — Primitiv darf nicht verifiziert werden.</p></section>");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const target = "." + "d-" + name;
|
|
68
|
+
motions.push(Object.assign(
|
|
69
|
+
{ id: "demo-" + name, primitive: name, target, params: demo.params || {} },
|
|
70
|
+
demo.trigger ? { trigger: demo.trigger } : {}
|
|
71
|
+
));
|
|
72
|
+
sections.push(
|
|
73
|
+
'<section class="prim" id="' + name + '">' +
|
|
74
|
+
"<h3>" + name + ' <span class="v">' + (prim.performance && prim.performance.verifiedAt ? "verifiziert " + prim.performance.verifiedAt : "UNVERIFIZIERT") + "</span></h3>" +
|
|
75
|
+
'<p class="purpose">' + (prim.purpose || "") + "</p>" +
|
|
76
|
+
'<div class="stage">' + demo.html + "</div>" +
|
|
77
|
+
"</section>"
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const spec = {
|
|
82
|
+
specVersion: "1.0",
|
|
83
|
+
meta: { project: "demo", target: "vanilla-gsap", createdWith: "demo-generator" },
|
|
84
|
+
globals: { respectReducedMotion: true },
|
|
85
|
+
motions,
|
|
86
|
+
};
|
|
87
|
+
const res = compileSpec(spec, catalog, { specName: "demo" });
|
|
88
|
+
if (!res.ok) throw new Error("Demo-Spec abgewiesen:\n " + res.errors.join("\n "));
|
|
89
|
+
|
|
90
|
+
const html = `<!doctype html>
|
|
91
|
+
<html lang="de">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="utf-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
95
|
+
<title>MotionSpec — Verifikations-Demo (Katalog ${catalogVersion(catalog)})</title>
|
|
96
|
+
<style>
|
|
97
|
+
:root { color-scheme: light dark; }
|
|
98
|
+
body { font-family: system-ui, sans-serif; margin: 0; line-height: 1.5; }
|
|
99
|
+
header, footer { padding: 3rem 1.5rem; max-width: 52rem; margin: 0 auto; }
|
|
100
|
+
.prim { min-height: 90vh; padding: 4rem 1.5rem; max-width: 52rem; margin: 0 auto; border-top: 1px solid #8884; }
|
|
101
|
+
.stage { margin-top: 2rem; }
|
|
102
|
+
.grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 1rem; }
|
|
103
|
+
.card { padding: 2rem 1rem; border: 1px solid #8886; border-radius: 10px; text-align: center; }
|
|
104
|
+
.parallax-frame { position: relative; overflow: hidden; border-radius: 10px; padding: 4rem 1rem; border: 1px solid #8886; }
|
|
105
|
+
.parallax-frame .bg { position: absolute; inset: -30% 0; display: flex; align-items: center; justify-content: center; font-size: 4rem; opacity: .12; pointer-events: none; }
|
|
106
|
+
.pin-stage { padding: 3rem 1rem; border: 1px solid #8886; border-radius: 10px; }
|
|
107
|
+
.cta { font-size: 1rem; padding: .8rem 1.6rem; border-radius: 8px; border: 1px solid #8886; cursor: pointer; background: none; }
|
|
108
|
+
.counter { font-size: 3rem; font-weight: 700; }
|
|
109
|
+
.v { font-size: .65rem; padding: .15rem .5rem; border: 1px solid #8886; border-radius: 99px; vertical-align: middle; }
|
|
110
|
+
.miss { color: #c00; font-weight: 600; }
|
|
111
|
+
.purpose { opacity: .7; }
|
|
112
|
+
.rm-note { padding: .6rem 1rem; border: 1px dashed #8888; border-radius: 8px; font-size: .85rem; }
|
|
113
|
+
ol li { margin-bottom: .4rem; }
|
|
114
|
+
.marquee-track { overflow: hidden; white-space: nowrap; border: 1px solid #8886; border-radius: 10px; padding: 1rem 0; }
|
|
115
|
+
</style>
|
|
116
|
+
${res.css ? "<style>\n" + res.css + "</style>" : ""}
|
|
117
|
+
</head>
|
|
118
|
+
<body>
|
|
119
|
+
<header>
|
|
120
|
+
<h1>MotionSpec — Verifikations-Demo</h1>
|
|
121
|
+
<p>Katalog <code>${catalogVersion(catalog)}</code> · ${names.length} Primitive · Compiler-Output 1:1 wie in Produktion.</p>
|
|
122
|
+
<p class="rm-note">Reduced-Motion-Pfad testen: <a href="?rm=1">?rm=1 anhaengen</a> — alles muss sofort sichtbar sein, ohne Bewegung.</p>
|
|
123
|
+
<details><summary><strong>Geraete-Checkliste (MS-02)</strong></summary><ol>${CHECKLIST.map((c) => "<li>" + c + "</li>").join("")}</ol></details>
|
|
124
|
+
</header>
|
|
125
|
+
${sections.join("\n")}
|
|
126
|
+
<footer><p>Ende. Wenn alles oben fluessig lief: verifiedAt setzen, Kandidat befoerdern.</p></footer>
|
|
127
|
+
<script>
|
|
128
|
+
if (new URLSearchParams(location.search).get("rm") === "1") {
|
|
129
|
+
const orig = window.matchMedia.bind(window);
|
|
130
|
+
window.matchMedia = (q) => q.includes("prefers-reduced-motion")
|
|
131
|
+
? { matches: true, media: q, addEventListener(){}, removeEventListener(){} }
|
|
132
|
+
: orig(q);
|
|
133
|
+
document.title += " [reduced-motion simuliert]";
|
|
134
|
+
}
|
|
135
|
+
</script>
|
|
136
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
|
137
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
|
|
138
|
+
<script>
|
|
139
|
+
${res.js || ""}
|
|
140
|
+
</script>
|
|
141
|
+
</body>
|
|
142
|
+
</html>`;
|
|
143
|
+
|
|
144
|
+
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
|
145
|
+
const file = path.join(outDir, "index.html");
|
|
146
|
+
fs.writeFileSync(file, html);
|
|
147
|
+
return { file, primitives: names.length, motions: motions.length, report: res.report };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = { buildDemo, BUILTIN_DEMOS };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Client-Discovery (Sprint-2 S2-01) — Werkzeug, nicht der Lauf selbst.
|
|
4
|
+
*
|
|
5
|
+
* Eingabe: eine Brief-Datei (JSON), die beschreibt, welche Motions eine
|
|
6
|
+
* Seite WILL — in natuerlicher Sprache plus Ziel-Selektor:
|
|
7
|
+
* { "project": "kunde-x", "intents": [
|
|
8
|
+
* { "what": "Hero-Headline gleitet beim Scrollen herein", "target": ".hero h1" },
|
|
9
|
+
* { "what": "Zahlen im Stats-Block zaehlen hoch", "target": ".stats .num" },
|
|
10
|
+
* ... ] }
|
|
11
|
+
*
|
|
12
|
+
* Fuer jeden Intent versucht das Tool, ihn mit dem AKTUELLEN Katalog
|
|
13
|
+
* auszudruecken (heuristisches Mapping, gleiche Keyword-Logik wie der
|
|
14
|
+
* Mock-Router) und kompiliert. Ergebnis: ein Gap-Report (Markdown) mit
|
|
15
|
+
* - abgedeckt: Intent -> Primitiv, kompiliert
|
|
16
|
+
* - LUECKE: kein Primitiv passt -> benannte Katalog-Luecke
|
|
17
|
+
* Das Tool ENTSCHEIDET nichts und baut keine Primitive — es liefert die
|
|
18
|
+
* Daten, die Phase B (Freeze) und Phase C (Katalog) informieren.
|
|
19
|
+
*
|
|
20
|
+
* Anti-Goal-konform: Luecken werden GELOGGT, nie still gepatcht.
|
|
21
|
+
*/
|
|
22
|
+
const { loadCatalog } = require("../compiler/catalog.js");
|
|
23
|
+
const { compileSpec } = require("../compiler/compile.js");
|
|
24
|
+
|
|
25
|
+
/* Heuristisches Intent->Primitiv-Mapping (bewusst konservativ; ein echtes
|
|
26
|
+
* Modell kann das spaeter ersetzen — hier geht es nur um Gap-Findung). */
|
|
27
|
+
const RULES = [
|
|
28
|
+
{ re: /(stagger|nacheinander|gestaffelt|one after|cards?|karten|liste)/i, primitive: "staggerReveal", params: { from: { opacity: 0, y: 32 } } },
|
|
29
|
+
{ re: /(z(ä|ae)hl|counter|hochz(ä|ae)hl|count up|nummer|zahl)/i, primitive: "counterUp", params: {} },
|
|
30
|
+
{ re: /(parallax|tiefe|depth|layer|ebene)/i, primitive: "parallaxLayer", params: {} },
|
|
31
|
+
{ re: /(pin|fixier|sticky|festhalten|haften)/i, primitive: "pinnedSection", params: {} },
|
|
32
|
+
{ re: /(marquee|laufband|logo.?band|ticker|endlos)/i, primitive: "marquee", params: {} },
|
|
33
|
+
{ re: /(skalier|scale|zoom|gr(ö|oe)(ß|ss)er werden)/i, primitive: "scaleOnScroll", params: {} },
|
|
34
|
+
{ re: /(hover|maus|cursor|button)/i, primitive: "cssTransition", params: {} },
|
|
35
|
+
{ re: /(reveal|einblend|fade|erscheinen|appear|headline|(ü|ue)berschrift|gleitet|scroll)/i, primitive: "scrollReveal", params: { from: { opacity: 0, y: 48 } } },
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function mapIntent(what) {
|
|
39
|
+
for (const r of RULES) if (r.re.test(what)) return r;
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function discover(brief) {
|
|
44
|
+
const catalog = loadCatalog();
|
|
45
|
+
const covered = [];
|
|
46
|
+
const gaps = [];
|
|
47
|
+
(brief.intents || []).forEach((intent, i) => {
|
|
48
|
+
const rule = mapIntent(intent.what || "");
|
|
49
|
+
if (!rule) {
|
|
50
|
+
gaps.push({ i, what: intent.what, target: intent.target, reason: "kein Katalog-Primitiv passt zur Absicht" });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const spec = {
|
|
54
|
+
specVersion: "1.0",
|
|
55
|
+
meta: { project: brief.project || "discovery", target: "vanilla-gsap", createdWith: "discover" },
|
|
56
|
+
globals: { respectReducedMotion: true },
|
|
57
|
+
motions: [{ id: "intent-" + i, primitive: rule.primitive, target: intent.target || ".target", params: rule.params }],
|
|
58
|
+
};
|
|
59
|
+
const res = compileSpec(spec, catalog);
|
|
60
|
+
if (res.ok) covered.push({ i, what: intent.what, primitive: rule.primitive, budget: res.report.cost });
|
|
61
|
+
else gaps.push({ i, what: intent.what, target: intent.target, reason: "Mapping vorhanden, aber Compile/Validate scheiterte: " + (res.errors || []).join(" | ") });
|
|
62
|
+
});
|
|
63
|
+
return { project: brief.project || "discovery", total: (brief.intents || []).length, covered, gaps, catalogPrimitives: Object.keys(catalog).sort() };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toMarkdown(r) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push("# Gap-Report — " + r.project);
|
|
69
|
+
lines.push("");
|
|
70
|
+
lines.push("Erzeugt: " + new Date().toISOString().slice(0, 10) + " · Tool: `motion discover` (S2-01)");
|
|
71
|
+
lines.push("Katalog: " + r.catalogPrimitives.join(", "));
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("**" + r.covered.length + " / " + r.total + " Absichten abgedeckt · " + r.gaps.length + " Luecke(n).**");
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("> Heuristik errt bewusst Richtung „abgedeckt“ (Keyword-Mapping). „Abgedeckt“ ist daher EIN VORSCHLAG, der vom Menschen am echten Design bestaetigt werden muss (Anti-Goal A2: keine Schein-Abdeckung). Gemeldete LUECKEN sind dagegen hochsicher.");
|
|
76
|
+
lines.push("");
|
|
77
|
+
lines.push("## Abgedeckt");
|
|
78
|
+
if (r.covered.length === 0) lines.push("_keine_");
|
|
79
|
+
r.covered.forEach((c) => lines.push("- ✓ „" + c.what + "“ → `" + c.primitive + "` (cost " + c.budget + ")"));
|
|
80
|
+
lines.push("");
|
|
81
|
+
lines.push("## LUECKEN (Signal fuer Phase B + C — nichts wird still gepatcht)");
|
|
82
|
+
if (r.gaps.length === 0) lines.push("_keine — der aktuelle Katalog deckt die Seite vollstaendig._");
|
|
83
|
+
r.gaps.forEach((g) => lines.push("- ✗ „" + g.what + "“ (target: " + (g.target || "—") + ")\n - " + g.reason));
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push("## Naechster Schritt");
|
|
86
|
+
lines.push(r.gaps.length === 0
|
|
87
|
+
? "Katalog reicht. Specs schreiben, kompilieren, Geraete-Check, ausliefern (S2-06)."
|
|
88
|
+
: "Luecken in S2-02 (Freeze: deckt v1 sie ab oder 0.2?) und S2-05 (neue Primitive nur fuer diese Luecken) einspeisen.");
|
|
89
|
+
return lines.join("\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { discover, toMarkdown, mapIntent };
|