@hyperframes/inspector 0.8.180 → 0.8.183

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/README.md CHANGED
@@ -42,7 +42,7 @@ The package ships the capability, not agent tools: a harness defines its own too
42
42
 
43
43
  ## Output
44
44
 
45
- The default JSON is capped at 12,000 UTF-8 bytes, including metadata. `snippets` contain file, start/end line, kinds, associated input target indices, **unmodified full source lines**, and whether that exact block occurs only once in the file. No pretty-printing or ellipses are inserted into code. Adjacent/overlapping blocks merge across targets.
45
+ The default JSON is capped at 12,000 UTF-8 bytes, including metadata. Each snippet's `id` names its file and its start and end lines (`file:start-end`); a snippet also carries its kinds, **unmodified full source lines**, and whether that exact block occurs only once in the file. When a query names more than one target, `targets` gives the input target indices it serves. No pretty-printing or ellipses are inserted into code. Adjacent/overlapping blocks merge across targets.
46
46
 
47
47
  ```json
48
48
  {
@@ -51,9 +51,6 @@ The default JSON is capped at 12,000 UTF-8 bytes, including metadata. `snippets`
51
51
  "snippets": [
52
52
  {
53
53
  "id": "scene.html:20-24",
54
- "file": "scene.html",
55
- "startLine": 20,
56
- "endLine": 24,
57
54
  "kind": ["animation creation"],
58
55
  "targets": [0, 1],
59
56
  "code": "the exact original source lines, including whitespace",
@@ -65,7 +62,7 @@ The default JSON is capped at 12,000 UTF-8 bytes, including metadata. `snippets`
65
62
 
66
63
  This illustration omits metadata. Actual files in the implementation report show the full output.
67
64
 
68
- Shared helpers appear once with all **queried** targets using them. `coTargets` names observed additional DOM targets of the same tween; it does not claim to enumerate all separate calls to a helper. Creation statements and authored caller statements are mapped; small named helper functions are returned whole. Variable assignments are not a stack/data-flow history. A missing source origin increments `unmappedAnimations`, rather than guessing an editable location.
65
+ Shared helpers appear once with all **queried** targets using them. `coTargets` names up to three observed additional DOM targets of the same tween, prioritizing elements outside the query, and `coTargetsOmitted` counts the rest; it does not claim to enumerate all separate calls to a helper. Creation statements and authored caller statements are mapped; small named helper functions are returned whole. Variable assignments are not a stack/data-flow history. A missing source origin increments `unmappedAnimations`, rather than guessing an editable location.
69
66
 
70
67
  HTML snippets are uniquely identified authored opening tags (their full source lines); dynamically generated markup may have no authored tag match. CSS is an authored-selector candidate scan, not proof of active cascade, media-query state, or inherited styles. In particular, a class reused in different scoped scene styles can have multiple candidate source rules.
71
68
 
@@ -1394,7 +1394,7 @@
1394
1394
  return timeAtSourceTime(playbackRate, Math.max(0, sourceDuration - mediaStart));
1395
1395
  }
1396
1396
 
1397
- // ../core/src/mediaTiming.ts
1397
+ // ../parsers/src/mediaTiming.ts
1398
1398
  var MEDIA_START_BASIS_ATTR = "data-hf-media-start-basis";
1399
1399
  function readMediaStartBasis(value) {
1400
1400
  return value?.trim().toLowerCase() === "global" ? "global" : "local";
package/dist/live.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire } from "node:module"; const require = createRequire(impor
2
2
 
3
3
  // src/live.ts
4
4
  import { randomUUID } from "node:crypto";
5
- import { readFileSync as readFileSync2, realpathSync } from "node:fs";
5
+ import { readFileSync as readFileSync2, realpathSync as realpathSync2 } from "node:fs";
6
6
 
7
7
  // src/authoredState.ts
8
8
  var PLAIN_CLASS = /^[a-zA-Z_-][\w-]*$/;
@@ -459,10 +459,11 @@ function formatElementMap(snapshot, { file = "index.html" } = {}) {
459
459
  }
460
460
 
461
461
  // src/sources.ts
462
- import { closeSync, fstatSync, openSync, readFileSync, readdirSync } from "node:fs";
463
- import { extname, join, relative } from "node:path";
462
+ import { closeSync, fstatSync, openSync, readFileSync, readdirSync, realpathSync } from "node:fs";
463
+ import { extname, join, posix, relative, resolve } from "node:path";
464
464
  import { createHash as createHash2 } from "node:crypto";
465
465
  import { parse as parse2 } from "acorn";
466
+ import { Parser } from "htmlparser2";
466
467
  import postcss from "postcss";
467
468
  import { TraceMap, originalPositionFor } from "@jridgewell/trace-mapping";
468
469
 
@@ -556,6 +557,14 @@ function occurrences(text, needle) {
556
557
  }
557
558
  return hits;
558
559
  }
560
+ function realPath(path) {
561
+ try {
562
+ return realpathSync(path);
563
+ } catch {
564
+ return resolve(path);
565
+ }
566
+ }
567
+ var SHOWN_WHOLE = 2e3;
559
568
  var excluded = /* @__PURE__ */ new Set([...PREVIEW_IGNORED_DIRS, ".agents", ".claude", "evidence", "snapshots"]);
560
569
  function sourceFiles(project) {
561
570
  const files = [];
@@ -649,7 +658,24 @@ function rawTextElements(text, tag) {
649
658
  }
650
659
  return elements;
651
660
  }
661
+ function nesting(text) {
662
+ const parents = /* @__PURE__ */ new Map();
663
+ const open = [];
664
+ const parser = new Parser({
665
+ onopentag() {
666
+ parents.set(parser.startIndex, open.at(-1));
667
+ open.push(parser.startIndex);
668
+ },
669
+ onclosetag() {
670
+ open.pop();
671
+ }
672
+ });
673
+ parser.write(text);
674
+ parser.end();
675
+ return parents;
676
+ }
652
677
  function htmlTagsOf(file, text) {
678
+ const parents = nesting(text);
653
679
  return [...text.matchAll(/<([a-z][\w-]*)\b[^>]*>/gi)].map((m) => {
654
680
  const attr = (name) => {
655
681
  const quoted = new RegExp(`\\s${name}\\s*=\\s*["']([^"']*)["']`, "i").exec(m[0]);
@@ -663,7 +689,11 @@ function htmlTagsOf(file, text) {
663
689
  tag: m[1].toLowerCase(),
664
690
  id: attr("id"),
665
691
  hfId: attr("data-hf-id"),
666
- classes: (attr("class") ?? "").split(/\s+/).filter(Boolean)
692
+ classes: (attr("class") ?? "").split(/\s+/).filter(Boolean),
693
+ compositionId: attr("data-composition-id"),
694
+ compositionSrc: attr("data-composition-src"),
695
+ inMarkup: parents.has(m.index),
696
+ parent: parents.get(m.index)
667
697
  };
668
698
  });
669
699
  }
@@ -680,11 +710,14 @@ function authoredHtml(project) {
680
710
  return html;
681
711
  }
682
712
  var Sources = class {
683
- constructor(project) {
713
+ constructor(project, served) {
684
714
  this.project = project;
715
+ const root = realPath(project);
716
+ const bundled = served && new Set([...served].map((path) => relative(root, realPath(path))));
685
717
  for (const path of sourceFiles(project)) {
686
718
  const file = relative(project, path);
687
719
  if (/(?:^|\/)(?:vendor|lib)\//.test(file)) continue;
720
+ if (bundled && !bundled.has(file)) continue;
688
721
  const fd = openSync(path, "r");
689
722
  let text;
690
723
  try {
@@ -767,12 +800,69 @@ var Sources = class {
767
800
  };
768
801
  }
769
802
  markup(element, target) {
803
+ const tag = this.markupTag(element);
804
+ return tag ? this.slice(tag.file, tag.start, tag.end, "html opening tag", target) : null;
805
+ }
806
+ markupTag(element) {
770
807
  const sameTag = this.htmlTags.filter((tag) => tag.tag === element.tag);
771
808
  const byHfId = element.hfId ? sameTag.filter((tag) => tag.hfId === element.hfId) : [];
772
809
  const tags = byHfId.length ? byHfId : sameTag.filter(
773
810
  (tag) => element.id ? tag.id === element.id : element.classes.length > 0 && element.classes.every((c) => tag.classes.includes(c))
774
811
  );
775
- return tags.length === 1 ? this.slice(tags[0].file, tags[0].start, tags[0].end, "html opening tag", target) : null;
812
+ return tags.length === 1 ? tags[0] : null;
813
+ }
814
+ nestingIndex;
815
+ get nested() {
816
+ if (this.nestingIndex) return this.nestingIndex;
817
+ const tagAt = new Map(this.htmlTags.map((t) => [`${t.file}:${t.start}`, t]));
818
+ const hostsOf = /* @__PURE__ */ new Map();
819
+ for (const host of this.htmlTags.filter((t) => t.inMarkup && t.compositionSrc)) {
820
+ const file = this.mountedFile(host);
821
+ if (file) hostsOf.set(file, [...hostsOf.get(file) ?? [], host]);
822
+ }
823
+ return this.nestingIndex = { tagAt, hostsOf };
824
+ }
825
+ fileByPath;
826
+ mountedFile(host) {
827
+ const files = this.fileByPath ??= new Map(
828
+ [...this.files.keys()].map((file) => [file.replace(/\\/g, "/"), file])
829
+ );
830
+ const src = (host.compositionSrc ?? "").split(/[?#]/)[0].replace(/\\/g, "/");
831
+ const path = [
832
+ posix.normalize(src.replace(/^\.?\//, "")),
833
+ posix.join(posix.dirname(host.file.replace(/\\/g, "/")), src)
834
+ ].find((file) => files.has(file));
835
+ return path === void 0 ? void 0 : files.get(path);
836
+ }
837
+ compositions(tag, target) {
838
+ const { tagAt, hostsOf } = this.nested;
839
+ const shown2 = /* @__PURE__ */ new Set([tag]);
840
+ const result = [];
841
+ const show = (t, kind) => {
842
+ if (shown2.has(t)) return false;
843
+ shown2.add(t);
844
+ const snippet = this.slice(t.file, t.start, t.end, kind, target);
845
+ if (snippet) result.push(snippet);
846
+ return true;
847
+ };
848
+ const mounted = tag.compositionSrc && this.mountedFile(tag);
849
+ const root = mounted && this.htmlTags.find(
850
+ (t) => t.file === mounted && t.inMarkup && t.compositionId && (!tag.compositionId || t.compositionId === tag.compositionId)
851
+ );
852
+ if (root) show(root, "composition root");
853
+ const parentOf = (t) => t.parent === void 0 ? void 0 : tagAt.get(`${t.file}:${t.parent}`);
854
+ const climb = [{ tag, candidate: false }];
855
+ for (let from = climb.shift(); from; from = climb.shift()) {
856
+ for (let at2 = parentOf(from.tag); at2; at2 = parentOf(at2))
857
+ if (at2.compositionId)
858
+ show(at2, from.candidate ? "composition root candidate" : "composition root");
859
+ const hosts = hostsOf.get(from.tag.file) ?? [];
860
+ const candidate = from.candidate || hosts.length > 1;
861
+ for (const host of hosts)
862
+ if (show(host, candidate ? "composition host candidate" : "composition host"))
863
+ climb.push({ tag: host, candidate });
864
+ }
865
+ return result;
776
866
  }
777
867
  mapper(html, provenance) {
778
868
  const htmlLines = starts(html);
@@ -822,17 +912,20 @@ var Sources = class {
822
912
  const enclosing = hit.chunk.statements.filter((s) => hit.local >= s.start && hit.local < s.end).sort((a, b) => a.end - a.start - (b.end - b.start))[0];
823
913
  if (!enclosing) continue;
824
914
  const helper = hit.chunk.statements.filter(
825
- (s) => s.type === "FunctionDeclaration" && hit.local >= s.start && hit.local < s.end && s.end - s.start <= 2e3
915
+ (s) => s.type === "FunctionDeclaration" && hit.local >= s.start && hit.local < s.end && s.end - s.start <= SHOWN_WHOLE
826
916
  ).sort((a, b) => a.end - a.start - (b.end - b.start))[0];
827
- const range = helper ?? enclosing;
917
+ const caller = result.length > 0;
918
+ const callLine = caller && !helper && enclosing.end - enclosing.start > SHOWN_WHOLE;
919
+ const range = callLine ? { start: hit.local, end: hit.local + 1 } : helper ?? enclosing;
828
920
  const snippet = this.slice(
829
921
  hit.chunk.file,
830
922
  hit.chunk.offset + range.start,
831
923
  hit.chunk.offset + range.end,
832
- result.length ? "animation caller" : "animation creation",
924
+ caller ? "animation caller" : "animation creation",
833
925
  target
834
926
  );
835
927
  if (snippet) result.push(snippet);
928
+ if (callLine) continue;
836
929
  for (const dependency of hit.chunk.dependencies(range)) {
837
930
  const definition = this.slice(
838
931
  hit.chunk.file,
@@ -849,9 +942,6 @@ var Sources = class {
849
942
  };
850
943
  }
851
944
  };
852
- function markupSnippet(sources, element, target) {
853
- return sources.markup(element, target);
854
- }
855
945
  function mergeSnippets(snippets, sources) {
856
946
  const sorted = snippets.sort((a, b) => a.file.localeCompare(b.file) || a.startLine - b.startLine);
857
947
  const output = [];
@@ -874,16 +964,31 @@ function mergeSnippets(snippets, sources) {
874
964
  }
875
965
 
876
966
  // src/mapInspection.ts
877
- var coverage = [
878
- "GSAP associations observed since page initialization; callback-created animations may require an explicit time query.",
879
- "Animation source dependencies include lexically resolved declarations in the same script; later mutations, imports, and cross-script bindings are not traced.",
880
- "CSS selector matches are candidates, not a full cascade or dependency graph. HTML is the authored opening tag when uniquely located.",
881
- "Direct constructors, other animation systems, iframe contents, removed animations and unexecuted paths may be absent; missing origins are reported."
967
+ var KEPT_FIRST_UNDER_BUDGET = [
968
+ "animation creation",
969
+ "animation caller",
970
+ "timeline attachment",
971
+ "html opening tag",
972
+ "animation source dependency",
973
+ "matched CSS candidate"
882
974
  ];
975
+ var rank = (kind) => {
976
+ const at2 = KEPT_FIRST_UNDER_BUDGET.indexOf(kind);
977
+ return at2 === -1 ? KEPT_FIRST_UNDER_BUDGET.length : at2;
978
+ };
979
+ var priority = (snippet) => Math.min(...snippet.kind.map(rank));
980
+ var byPosition = (a, b) => a.file.localeCompare(b.file) || a.startLine - b.startLine;
981
+ var CO_TARGETS_SHOWN = 3;
982
+ function shown(snippet, targetCount) {
983
+ const { id, kind, code, uniqueInFile, targets } = snippet;
984
+ return targetCount === 1 ? { id, kind, code, uniqueInFile } : { id, kind, targets, code, uniqueInFile };
985
+ }
883
986
  function mapInspection(raw, request, context) {
884
987
  const { sources, map, catalog, generation, sourceVersion, reused, start, visualState } = context;
885
988
  const maxBytes = request.maxBytes ?? 12e3;
886
989
  const collected = [];
990
+ const compositions = [];
991
+ const queriedRefs = new Set(raw.results.flatMap((result) => result.elements.map((el) => el.ref)));
887
992
  const results = raw.results.map((result, index) => {
888
993
  let unmappedAnimations = 0;
889
994
  for (const animation of result.animations) {
@@ -896,8 +1001,11 @@ function mapInspection(raw, request, context) {
896
1001
  );
897
1002
  }
898
1003
  for (const el of result.elements) {
899
- const snippet = markupSnippet(sources, el, index);
1004
+ const tag = sources.markupTag(el);
1005
+ if (!tag) continue;
1006
+ const snippet = sources.slice(tag.file, tag.start, tag.end, "html opening tag", index);
900
1007
  if (snippet) collected.push(snippet);
1008
+ compositions.push(...sources.compositions(tag, index));
901
1009
  }
902
1010
  for (const id of result.css) {
903
1011
  const rule = sources.cssRules.find((rule2) => rule2.id === id);
@@ -912,30 +1020,35 @@ function mapInspection(raw, request, context) {
912
1020
  if (snippet) collected.push(snippet);
913
1021
  }
914
1022
  }
1023
+ const coTargets = [
1024
+ ...new Map(
1025
+ result.animations.flatMap((animation) => animation.targets).filter((element) => !result.elements.some((own) => own.ref === element.ref)).map((element) => [element.ref, element])
1026
+ ).values()
1027
+ ].sort((a, b) => Number(queriedRefs.has(a.ref)) - Number(queriedRefs.has(b.ref)));
915
1028
  return {
916
1029
  target: index,
917
1030
  status: result.status,
918
1031
  candidates: result.candidates,
919
1032
  candidatesOmitted: 0,
920
1033
  animations: result.animations.length,
921
- coTargets: [
922
- ...new Map(
923
- result.animations.flatMap((animation) => animation.targets).filter((element) => !result.elements.some((own) => own.ref === element.ref)).map((element) => [element.ref, element])
924
- ).values()
925
- ].slice(0, 10),
1034
+ coTargets: coTargets.slice(0, CO_TARGETS_SHOWN),
1035
+ ...coTargets.length > CO_TARGETS_SHOWN && {
1036
+ coTargetsOmitted: coTargets.length - CO_TARGETS_SHOWN
1037
+ },
926
1038
  unmappedAnimations,
927
1039
  scopeTruncated: result.truncated
928
1040
  };
929
1041
  });
930
- const merged = mergeSnippets(collected, sources);
1042
+ const merged = withCompositionsLast(mergeSnippets(collected, sources), compositions, sources);
931
1043
  for (const snippet of merged) catalog.set(snippet.id, snippet);
932
- let selected = merged;
1044
+ let selected = [...merged].sort((a, b) => priority(a) - priority(b));
933
1045
  if (request.snippets)
934
1046
  selected = request.snippets.map((id) => {
935
1047
  const snippet = catalog.get(id);
936
1048
  if (!snippet) throw new Error(`unknown_snippet: ${id}`);
937
1049
  return snippet;
938
1050
  });
1051
+ const candidateContext = {};
939
1052
  const response = {
940
1053
  generation,
941
1054
  visualState,
@@ -946,7 +1059,7 @@ function mapInspection(raw, request, context) {
946
1059
  omitted: [],
947
1060
  omittedCount: 0,
948
1061
  nextOffset: null,
949
- coverage,
1062
+ ...candidateContext,
950
1063
  recordLimitReached: raw.recordLimitReached,
951
1064
  hooked: raw.hooked,
952
1065
  debugAvailable: true,
@@ -967,23 +1080,46 @@ function mapInspection(raw, request, context) {
967
1080
  }
968
1081
  };
969
1082
  trimCandidates(maxBytes / 2, 5);
1083
+ const targetCount = request.targets.length;
1084
+ const kept = [];
970
1085
  for (let i = offset; i < selected.length; i++) {
971
- response.snippets.push(selected[i]);
1086
+ response.snippets.push(shown(selected[i], targetCount));
1087
+ const priorCoverage = response.coverage;
1088
+ if (response.coverage === void 0 && selected[i].kind.some(
1089
+ (kind) => kind.startsWith("composition ") && kind.endsWith(" candidate")
1090
+ ))
1091
+ response.coverage = [
1092
+ "Composition candidates are alternative source mounts and their enclosing roots, not a verified runtime instance chain; all alternatives remain available within the snippet budget and pagination."
1093
+ ];
972
1094
  if (size() > maxBytes - 700) {
973
1095
  response.snippets.pop();
1096
+ response.coverage = priorCoverage;
974
1097
  response.omittedCount = selected.length - i;
975
1098
  response.omitted = selected.slice(i, i + 8).map((item) => ({ id: item.id, bytes: Buffer.byteLength(item.code) }));
976
1099
  const nothingFit = response.snippets.length === 0;
977
1100
  response.nextOffset = nothingFit ? i + 1 < selected.length ? i + 1 : null : i;
978
1101
  break;
979
1102
  }
1103
+ kept.push(selected[i]);
980
1104
  }
1105
+ if (!request.snippets)
1106
+ response.snippets = kept.sort(byPosition).map((snippet) => shown(snippet, targetCount));
981
1107
  trimCandidates(maxBytes, 0);
982
1108
  if (Buffer.byteLength(JSON.stringify(response)) > maxBytes)
983
1109
  throw new Error("metadata_exceeds_budget: use fewer targets or increase maxBytes");
984
1110
  if (request.debug) return { ...response, debug: raw };
985
1111
  return response;
986
1112
  }
1113
+ function withCompositionsLast(merged, compositions, sources) {
1114
+ const covered = (s) => merged.some((m) => m.file === s.file && m.startLine <= s.startLine && m.endLine >= s.endLine);
1115
+ return [
1116
+ ...merged,
1117
+ ...mergeSnippets(
1118
+ compositions.filter((s) => !covered(s)),
1119
+ sources
1120
+ )
1121
+ ];
1122
+ }
987
1123
 
988
1124
  // src/pageQueries.ts
989
1125
  function queryPage(query) {
@@ -1118,8 +1254,8 @@ var located = (sources) => onePerSourceLine(sources).map((s) => `${s.file}:${s.s
1118
1254
  function describeValue(value) {
1119
1255
  if (value.kind === "unknown") return "?";
1120
1256
  if (value.kind === "expression" && value.value === "custom function") return "custom function";
1121
- const shown = typeof value.value === "number" ? Number(value.value.toFixed(4)) : value.value;
1122
- return `${shown}${value.kind === "literal" ? "" : ` (${value.kind})`}`;
1257
+ const shown2 = typeof value.value === "number" ? Number(value.value.toFixed(4)) : value.value;
1258
+ return `${shown2}${value.kind === "literal" ? "" : ` (${value.kind})`}`;
1123
1259
  }
1124
1260
  function describeValues(values) {
1125
1261
  return ` authored: ${values.properties.map((p) => `${p.property}: ${describeValue(p.from)} \u2192 ${describeValue(p.to)}`).join("; ")}; ease: ${describeValue(values.ease)}`;
@@ -1334,8 +1470,8 @@ function assertGeneration(requested, current) {
1334
1470
 
1335
1471
  // src/live.ts
1336
1472
  var runtime = () => readFileSync2(new URL(`./inspection.${"js"}`, import.meta.url), "utf8");
1337
- function prepareLiveInspection(projectDir, html, provenance, sourceVersion) {
1338
- const project = realpathSync(projectDir);
1473
+ function prepareLiveInspection(projectDir, html, provenance, sourceVersion, reads) {
1474
+ const project = realpathSync2(projectDir);
1339
1475
  if (fingerprint2(project) !== sourceVersion) throw new Error("source_changed_during_load");
1340
1476
  if (!/<head\b[^>]*>/i.test(html)) throw new Error("preview_head_missing");
1341
1477
  const generation = randomUUID();
@@ -1353,7 +1489,7 @@ ${runtime()}`.replace(/<\/script/gi, "<\\/script");
1353
1489
  pageGeneration: generation,
1354
1490
  sourceContext() {
1355
1491
  if (!context) {
1356
- const sources = new Sources(project);
1492
+ const sources = new Sources(project, reads);
1357
1493
  context = { sources, map: sources.mapper(document2, [...provenance]) };
1358
1494
  }
1359
1495
  return context;