@hyperframes/lint 0.7.70 → 0.7.72

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/dist/browser.js CHANGED
@@ -861,6 +861,7 @@ var coreRules = [
861
861
  ];
862
862
 
863
863
  // src/rules/media.ts
864
+ import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
864
865
  function escapeRegExp2(value) {
865
866
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
866
867
  }
@@ -1063,6 +1064,56 @@ var mediaRules = [
1063
1064
  }
1064
1065
  return findings;
1065
1066
  },
1067
+ // color_grading_* — grading is a structured media-only contract. Unknown
1068
+ // keys are ignored by the runtime, so catch them before an agent can report
1069
+ // controls that never actually rendered.
1070
+ ({ tags }) => {
1071
+ const findings = [];
1072
+ for (const tag of tags) {
1073
+ const raw = readDecodedAttr(tag.raw, "data-color-grading");
1074
+ if (raw === null) continue;
1075
+ const elementId = readAttr(tag.raw, "id") || void 0;
1076
+ const report = (code, message, fixHint) => {
1077
+ findings.push({
1078
+ code,
1079
+ severity: "error",
1080
+ message,
1081
+ elementId,
1082
+ fixHint,
1083
+ snippet: truncateSnippet(tag.raw)
1084
+ });
1085
+ };
1086
+ if (tag.name !== "video" && tag.name !== "img") {
1087
+ report(
1088
+ "color_grading_non_media",
1089
+ `data-color-grading on <${tag.name}> has no effect. The shader runtime only grades real <video> and <img> elements.`,
1090
+ "Move the grading attribute to the real <video> or <img> media element. Do not attach it to a wrapper or CSS background."
1091
+ );
1092
+ continue;
1093
+ }
1094
+ const trimmed = raw.trim();
1095
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) continue;
1096
+ let parsed;
1097
+ try {
1098
+ parsed = JSON.parse(trimmed);
1099
+ } catch {
1100
+ report(
1101
+ "color_grading_invalid_json",
1102
+ "data-color-grading contains malformed JSON and will not render.",
1103
+ 'Use valid JSON, for example {"preset":"skin-soft","intensity":0.6,"adjust":{"highlights":-0.08}}.'
1104
+ );
1105
+ continue;
1106
+ }
1107
+ for (const issue of validateColorGradingContract(parsed)) {
1108
+ report(
1109
+ "color_grading_invalid_structure",
1110
+ `data-color-grading ${issue.path} ${issue.message}.`,
1111
+ issue.hint ?? "Use the documented media-treatment contract and correct or remove the invalid value."
1112
+ );
1113
+ }
1114
+ }
1115
+ return findings;
1116
+ },
1066
1117
  // video_missing_muted
1067
1118
  ({ tags }) => {
1068
1119
  const findings = [];
@@ -1149,27 +1200,6 @@ var mediaRules = [
1149
1200
  }
1150
1201
  return findings;
1151
1202
  },
1152
- // media_in_subcomposition — <video>/<audio> only render as a DIRECT child of the host
1153
- // root (index.html). Inside a sub-composition <template> the runtime never seeks/decodes
1154
- // them, so they render BLANK/black in preview and renders — and the other lint/validate
1155
- // passes otherwise miss it (only a per-frame snapshot reveals the blank panel).
1156
- ({ tags, options }) => {
1157
- const findings = [];
1158
- if (!options.isSubComposition) return findings;
1159
- for (const tag of tags) {
1160
- if (tag.name !== "video" && tag.name !== "audio") continue;
1161
- const elementId = readAttr(tag.raw, "id") || void 0;
1162
- findings.push({
1163
- code: "media_in_subcomposition",
1164
- severity: "error",
1165
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> is inside a sub-composition. The runtime only drives media that is a DIRECT child of the host root (index.html); media inside a sub-comp <template> is never seeked/decoded and renders BLANK/black in preview and renders.`,
1166
- elementId,
1167
- fixHint: "Move the media OUT of the sub-composition: place the <video>/<audio> as a direct child of #root in index.html, positioned over the scene, and drive any per-scene motion on the MAIN timeline at global time (a sub-comp timeline cannot reach host elements). See composition-patterns.md archetype B.",
1168
- snippet: truncateSnippet(tag.raw)
1169
- });
1170
- }
1171
- return findings;
1172
- },
1173
1203
  // self_closing_media_tag
1174
1204
  ({ source }) => {
1175
1205
  const findings = [];
@@ -2426,8 +2456,12 @@ ${right.raw}`)
2426
2456
  return findings;
2427
2457
  },
2428
2458
  // gsap_infinite_repeat
2429
- ({ scripts }) => {
2459
+ ({ scripts, rootTag }) => {
2430
2460
  const findings = [];
2461
+ const declaredDuration = Number.parseFloat(
2462
+ rootTag ? readAttr(rootTag.raw, "data-duration") ?? "" : ""
2463
+ );
2464
+ const hasFiniteCompositionWindow = Number.isFinite(declaredDuration) && declaredDuration > 0;
2431
2465
  const pattern = /repeat\s*:\s*-1(?!\d)/g;
2432
2466
  for (const { snippet } of scanScriptsForRegexMatches(scripts, pattern, {
2433
2467
  stripComments: true,
@@ -2436,9 +2470,9 @@ ${right.raw}`)
2436
2470
  })) {
2437
2471
  findings.push({
2438
2472
  code: "gsap_infinite_repeat",
2439
- severity: "error",
2440
- message: "GSAP tween uses `repeat: -1` (infinite). Infinite repeats break the deterministic capture engine which seeks to exact frame times. Use a finite repeat count calculated from the composition duration: `repeat: Math.max(0, Math.floor(duration / cycleDuration) - 1)`.",
2441
- fixHint: "Replace `repeat: -1` with a finite count, e.g. `repeat: Math.max(0, Math.floor(totalDuration / singleCycleDuration) - 1)`. Use Math.floor (not Math.ceil) so the animation fits, and clamp at zero so a short composition cannot evaluate to -1.",
2473
+ severity: hasFiniteCompositionWindow ? "warning" : "error",
2474
+ message: hasFiniteCompositionWindow ? `GSAP tween uses \`repeat: -1\` (infinite), but the composition declares a finite ${declaredDuration}s window. HyperFrames clips deterministic seeking and export to that explicit duration.` : "GSAP tween uses `repeat: -1` (infinite) without a finite composition `data-duration`. The timeline can report an unbounded duration and make render planning fail.",
2475
+ fixHint: hasFiniteCompositionWindow ? "Keep the explicit finite composition `data-duration`. Use a finite repeat count only when the loop itself must end before the composition does." : "Add a finite composition `data-duration`, or replace `repeat: -1` with `repeat: Math.max(0, Math.floor(totalDuration / singleCycleDuration) - 1)`.",
2442
2476
  snippet: truncateSnippet(snippet)
2443
2477
  });
2444
2478
  }