ts-flp 1.0.5 → 1.1.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/dist/index.js CHANGED
@@ -3,6 +3,9 @@ var WORD = 64;
3
3
  var DWORD = 128;
4
4
  var TEXT = 192;
5
5
  var DATA = 208;
6
+ var FIXED_SIZE_OVERRIDES = {
7
+ 172: 3
8
+ };
6
9
  var NEW_TEXT_IDS = [
7
10
  TEXT + 49,
8
11
  // ArrangementID.Name
@@ -149,6 +152,7 @@ var EVENT_ID = {
149
152
  // 236
150
153
  };
151
154
  var EVENT_KIND = {
155
+ [172]: "unknown",
152
156
  // Project events
153
157
  [EVENT_ID.PROJECT_LOOP_ACTIVE]: "u8",
154
158
  [EVENT_ID.PROJECT_SHOW_INFO]: "u8",
@@ -257,6 +261,10 @@ function getEventKind(eventId) {
257
261
  }
258
262
  }
259
263
  function getEventFixedSize(eventId) {
264
+ const override = FIXED_SIZE_OVERRIDES[eventId];
265
+ if (override !== void 0) {
266
+ return override;
267
+ }
260
268
  if (eventId < WORD) {
261
269
  return 1;
262
270
  } else if (eventId < DWORD) {
@@ -533,62 +541,9 @@ function varIntSize(value) {
533
541
  }
534
542
 
535
543
  // src/parser/FlpParser.ts
536
- var KNOWN_DWORD_IDS = new Set(
537
- Object.keys(EVENT_KIND).map(Number).filter((id) => id >= DWORD && id < TEXT)
538
- );
539
544
  var FLP_HEADER_MAGIC = "FLhd";
540
545
  var FLP_DATA_MAGIC = "FLdt";
541
546
  var FLP_HEADER_SIZE = 6;
542
- var LOOKAHEAD_BYTES = 200;
543
- var LOOKAHEAD_VARINT_MAX = 1e5;
544
- function readVarIntRaw(buf, off, end) {
545
- let shift = 0;
546
- let size = 0;
547
- const start = off;
548
- while (true) {
549
- if (off >= end) return { size: -1, bytesRead: 0 };
550
- const byte = buf[off];
551
- off++;
552
- size |= (byte & 127) << shift;
553
- shift += 7;
554
- if (!(byte & 128)) break;
555
- }
556
- return { size, bytesRead: off - start };
557
- }
558
- function scoreAlignment(buf, off, end, bytesToCheck) {
559
- const limit = Math.min(off + bytesToCheck, end);
560
- let textDataCount = 0;
561
- let consecutiveSmallByte = 0;
562
- let maxConsecutiveSmallByte = 0;
563
- while (off < limit) {
564
- const id = buf[off];
565
- if (id < WORD) {
566
- off += 2;
567
- if (id < 32 && !KNOWN_DWORD_IDS.has(id)) {
568
- consecutiveSmallByte++;
569
- maxConsecutiveSmallByte = Math.max(maxConsecutiveSmallByte, consecutiveSmallByte);
570
- } else {
571
- consecutiveSmallByte = 0;
572
- }
573
- } else if (id < DWORD) {
574
- off += 3;
575
- consecutiveSmallByte = 0;
576
- } else if (id < TEXT) {
577
- off += 5;
578
- consecutiveSmallByte = 0;
579
- } else {
580
- const vi = readVarIntRaw(buf, off + 1, end);
581
- if (vi.size >= 0 && vi.size < LOOKAHEAD_VARINT_MAX && off + 1 + vi.bytesRead + vi.size <= end) {
582
- textDataCount++;
583
- off = off + 1 + vi.bytesRead + vi.size;
584
- } else {
585
- return -100;
586
- }
587
- consecutiveSmallByte = 0;
588
- }
589
- }
590
- return textDataCount * 10 - maxConsecutiveSmallByte * 3;
591
- }
592
547
  function parseFlp(buffer) {
593
548
  const reader = new BinaryReader(buffer);
594
549
  const headerMagic = reader.readBytes(4).toString("ascii");
@@ -600,8 +555,8 @@ function parseFlp(buffer) {
600
555
  throw new Error(`Invalid header size: expected ${FLP_HEADER_SIZE}, got ${headerSize}`);
601
556
  }
602
557
  const format = reader.readI16LE();
603
- const channelCount = reader.readU16LE();
604
- const ppq = reader.readU16LE();
558
+ reader.readU16LE();
559
+ reader.readU16LE();
605
560
  if (format < -1 || format > 80) {
606
561
  throw new Error(`Invalid file format: ${format}`);
607
562
  }
@@ -628,39 +583,10 @@ function parseFlp(buffer) {
628
583
  const eventId = reader.readU8();
629
584
  let payloadSize;
630
585
  let headerSize2;
631
- if (eventId < WORD) {
632
- payloadSize = 1;
633
- headerSize2 = 1;
634
- } else if (eventId < DWORD) {
635
- payloadSize = 2;
586
+ const fixedSize = getEventFixedSize(eventId);
587
+ if (fixedSize >= 0) {
588
+ payloadSize = fixedSize;
636
589
  headerSize2 = 1;
637
- } else if (eventId < TEXT) {
638
- if (KNOWN_DWORD_IDS.has(eventId)) {
639
- payloadSize = 4;
640
- headerSize2 = 1;
641
- } else {
642
- const afterDword = eventStart + 1 + 4;
643
- const vi = readVarIntRaw(buffer, eventStart + 1, eventsEnd);
644
- if (vi.size < 0 || vi.size > LOOKAHEAD_VARINT_MAX || eventStart + 1 + vi.bytesRead + vi.size > eventsEnd) {
645
- payloadSize = 4;
646
- headerSize2 = 1;
647
- } else if (vi.size === 3) {
648
- payloadSize = 4;
649
- headerSize2 = 1;
650
- } else {
651
- const afterVarInt = eventStart + 1 + vi.bytesRead + vi.size;
652
- const dwordScore = scoreAlignment(buffer, afterDword, eventsEnd, LOOKAHEAD_BYTES);
653
- const varIntScore = scoreAlignment(buffer, afterVarInt, eventsEnd, LOOKAHEAD_BYTES);
654
- if (varIntScore > dwordScore + 2) {
655
- payloadSize = vi.size;
656
- reader.seek(eventStart + 1 + vi.bytesRead);
657
- headerSize2 = 1 + vi.bytesRead;
658
- } else {
659
- payloadSize = 4;
660
- headerSize2 = 1;
661
- }
662
- }
663
- }
664
590
  } else {
665
591
  const sizeStart = reader.tell();
666
592
  payloadSize = reader.readVarInt();
@@ -843,6 +769,46 @@ function createNumberPayload(value, kind) {
843
769
  }
844
770
 
845
771
  // src/api/ProjectApi.ts
772
+ var PROJECT_EVENT_ORDER = [
773
+ EVENT_ID.PROJECT_TEMPO,
774
+ EVENT_ID.PROJECT_TITLE,
775
+ EVENT_ID.PROJECT_GENRE,
776
+ EVENT_ID.PROJECT_ARTISTS,
777
+ EVENT_ID.PROJECT_COMMENTS,
778
+ EVENT_ID.PROJECT_TIMESTAMP
779
+ ];
780
+ function insertProjectEvent(parsed, event) {
781
+ const orderIndex = PROJECT_EVENT_ORDER.indexOf(event.id);
782
+ let insertionIndex = -1;
783
+ if (orderIndex >= 0) {
784
+ const laterIds = new Set(PROJECT_EVENT_ORDER.slice(orderIndex + 1));
785
+ insertionIndex = parsed.events.findIndex((candidate) => laterIds.has(candidate.id));
786
+ if (insertionIndex < 0) {
787
+ const previousIds = new Set(PROJECT_EVENT_ORDER.slice(0, orderIndex + 1));
788
+ for (let index = parsed.events.length - 1; index >= 0; index--) {
789
+ if (previousIds.has(parsed.events[index].id)) {
790
+ insertionIndex = index + 1;
791
+ break;
792
+ }
793
+ }
794
+ }
795
+ }
796
+ if (insertionIndex < 0) {
797
+ insertionIndex = parsed.events.findIndex((candidate) => candidate.id === EVENT_ID.CHANNEL_NEW);
798
+ }
799
+ if (insertionIndex < 0) {
800
+ const versionIndex = parsed.events.findIndex(
801
+ (candidate) => candidate.id === EVENT_ID.PROJECT_FL_VERSION
802
+ );
803
+ insertionIndex = versionIndex >= 0 ? versionIndex + 1 : parsed.events.length;
804
+ }
805
+ const events = [...parsed.events];
806
+ events.splice(insertionIndex, 0, event);
807
+ return { ...parsed, events };
808
+ }
809
+ function replaceEventPayload(parsed, target, payload) {
810
+ return patchEvents(parsed, (event) => event === target ? { ...event, payload } : event);
811
+ }
846
812
  function readBpm(parsed) {
847
813
  const tempoEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
848
814
  if (tempoEvent) {
@@ -872,48 +838,97 @@ function readProjectMeta(parsed) {
872
838
  bpm: readBpm(parsed)
873
839
  };
874
840
  }
841
+ function compareVersion(version, expected) {
842
+ const actual = version.split(".").map((part) => Number.parseInt(part, 10));
843
+ const length = Math.max(actual.length, expected.length);
844
+ for (let index = 0; index < length; index++) {
845
+ const difference = (actual[index] ?? 0) - (expected[index] ?? 0);
846
+ if (difference !== 0) return Math.sign(difference);
847
+ }
848
+ return 0;
849
+ }
850
+ function validateBpm(parsed, bpm) {
851
+ if (!Number.isFinite(bpm)) {
852
+ throw new RangeError("BPM must be a finite number");
853
+ }
854
+ const legacyHighTempo = compareVersion(parsed.flVersion, [1, 4, 2]) >= 0 && compareVersion(parsed.flVersion, [11]) < 0;
855
+ const maximum = legacyHighTempo ? 999 : 522;
856
+ if (bpm < 10 || bpm > maximum) {
857
+ throw new RangeError(`BPM must be between 10 and ${maximum} for FL Studio ${parsed.flVersion}`);
858
+ }
859
+ if (parsed.flVersion !== "0.0.0" && compareVersion(parsed.flVersion, [3, 4]) < 0 && !Number.isInteger(bpm)) {
860
+ throw new TypeError(`FL Studio ${parsed.flVersion} does not support decimal BPM values`);
861
+ }
862
+ }
863
+ function writeTextProjectEvent(parsed, eventId, value) {
864
+ if (value === void 0) return parsed;
865
+ const payload = createTextPayload(value ?? "", parsed.useUnicode);
866
+ const existing = findFirstEvent(parsed, eventId);
867
+ return existing ? replaceEventPayload(parsed, existing, payload) : insertProjectEvent(parsed, createEvent(eventId, payload));
868
+ }
869
+ function writeProjectBpm(parsed, bpm) {
870
+ if (bpm === void 0 || bpm === null) return parsed;
871
+ validateBpm(parsed, bpm);
872
+ const scaledBpm = Math.round(bpm * 1e3);
873
+ const coarseBpm = Math.floor(scaledBpm / 1e3);
874
+ const fineBpm = scaledBpm % 1e3;
875
+ const modernEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TEMPO);
876
+ const coarseEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_COARSE);
877
+ const fineEvent = findFirstEvent(parsed, EVENT_ID.PROJECT__TEMPO_FINE);
878
+ let result = parsed;
879
+ if (modernEvent) {
880
+ result = replaceEventPayload(result, modernEvent, createNumberPayload(scaledBpm, "u32"));
881
+ }
882
+ if (coarseEvent) {
883
+ result = replaceEventPayload(result, coarseEvent, createNumberPayload(coarseBpm, "u16"));
884
+ }
885
+ if (fineEvent) {
886
+ result = replaceEventPayload(result, fineEvent, createNumberPayload(fineBpm, "u16"));
887
+ }
888
+ if (!modernEvent && !coarseEvent && !fineEvent) {
889
+ result = insertProjectEvent(
890
+ result,
891
+ createEvent(EVENT_ID.PROJECT_TEMPO, createNumberPayload(scaledBpm, "u32"))
892
+ );
893
+ } else if (!modernEvent && coarseEvent && !fineEvent && fineBpm > 0) {
894
+ const updatedCoarse = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_COARSE);
895
+ const index = result.events.indexOf(updatedCoarse) + 1;
896
+ const events = [...result.events];
897
+ events.splice(
898
+ index,
899
+ 0,
900
+ createEvent(EVENT_ID.PROJECT__TEMPO_FINE, createNumberPayload(fineBpm, "u16"))
901
+ );
902
+ result = { ...result, events };
903
+ } else if (!modernEvent && !coarseEvent && fineEvent) {
904
+ const updatedFine = findFirstEvent(result, EVENT_ID.PROJECT__TEMPO_FINE);
905
+ const index = result.events.indexOf(updatedFine);
906
+ const events = [...result.events];
907
+ events.splice(
908
+ index,
909
+ 0,
910
+ createEvent(EVENT_ID.PROJECT__TEMPO_COARSE, createNumberPayload(coarseBpm, "u16"))
911
+ );
912
+ result = { ...result, events };
913
+ }
914
+ return result;
915
+ }
875
916
  function writeProjectMeta(parsed, meta) {
876
- return patchEvents(parsed, (event) => {
877
- if (meta.name !== void 0 && event.id === EVENT_ID.PROJECT_TITLE) {
878
- return {
879
- ...event,
880
- payload: createTextPayload(meta.name ?? "", parsed.useUnicode)
881
- };
882
- }
883
- if (meta.description !== void 0 && event.id === EVENT_ID.PROJECT_COMMENTS) {
884
- return {
885
- ...event,
886
- payload: createTextPayload(meta.description ?? "", parsed.useUnicode)
887
- };
888
- }
889
- if (meta.artist !== void 0 && event.id === EVENT_ID.PROJECT_ARTISTS) {
890
- return {
891
- ...event,
892
- payload: createTextPayload(meta.artist ?? "", parsed.useUnicode)
893
- };
894
- }
895
- if (meta.genre !== void 0 && event.id === EVENT_ID.PROJECT_GENRE) {
896
- return {
897
- ...event,
898
- payload: createTextPayload(meta.genre ?? "", parsed.useUnicode)
899
- };
900
- }
901
- if (meta.bpm && Math.abs(meta.bpm) > 0 && event.id === EVENT_ID.PROJECT_TEMPO) {
902
- return {
903
- ...event,
904
- payload: createNumberPayload(Math.round(Math.abs(meta.bpm) * 1e3), "u32")
905
- };
906
- }
907
- return event;
908
- });
917
+ let result = parsed;
918
+ result = writeProjectBpm(result, meta.bpm);
919
+ result = writeTextProjectEvent(result, EVENT_ID.PROJECT_TITLE, meta.name);
920
+ result = writeTextProjectEvent(result, EVENT_ID.PROJECT_GENRE, meta.genre);
921
+ result = writeTextProjectEvent(result, EVENT_ID.PROJECT_ARTISTS, meta.artist);
922
+ result = writeTextProjectEvent(result, EVENT_ID.PROJECT_COMMENTS, meta.description);
923
+ return result;
909
924
  }
910
- var DELPHI_EPOCH = new Date(1899, 11, 30);
925
+ var DELPHI_EPOCH_MS = Date.UTC(1899, 11, 30);
911
926
  function delphiToDate(days) {
912
927
  const ms = days * 24 * 60 * 60 * 1e3;
913
- return new Date(DELPHI_EPOCH.getTime() + ms);
928
+ return new Date(DELPHI_EPOCH_MS + ms);
914
929
  }
915
930
  function dateToDelphiDays(date) {
916
- const ms = date.getTime() - DELPHI_EPOCH.getTime();
931
+ const ms = date.getTime() - DELPHI_EPOCH_MS;
917
932
  return ms / (24 * 60 * 60 * 1e3);
918
933
  }
919
934
  function readProjectTimeInfo(parsed) {
@@ -934,41 +949,33 @@ function readProjectTimeInfo(parsed) {
934
949
  };
935
950
  }
936
951
  function writeProjectTimeInfo(parsed, info) {
952
+ if (info.creationDate === void 0 && info.workTimeSeconds === void 0) {
953
+ return parsed;
954
+ }
937
955
  const current = readProjectTimeInfo(parsed);
938
- return patchEvents(parsed, (event) => {
939
- if (event.id !== EVENT_ID.PROJECT_TIMESTAMP) {
940
- return event;
941
- }
942
- const writer = new BinaryWriter();
943
- if (info.creationDate !== void 0 && info.creationDate !== null) {
944
- writer.writeF64LE(dateToDelphiDays(info.creationDate));
945
- } else if (current.creationDate !== null) {
946
- writer.writeF64LE(dateToDelphiDays(current.creationDate));
947
- } else {
948
- writer.writeF64LE(0);
949
- }
950
- if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null) {
951
- writer.writeF64LE(info.workTimeSeconds / (24 * 60 * 60));
952
- } else if (current.workTimeSeconds !== null) {
953
- writer.writeF64LE(current.workTimeSeconds / (24 * 60 * 60));
954
- } else {
955
- writer.writeF64LE(0);
956
- }
957
- return {
958
- ...event,
959
- payload: writer.toBuffer()
960
- };
961
- });
956
+ if (info.creationDate !== void 0 && info.creationDate !== null && Number.isNaN(info.creationDate.getTime())) {
957
+ throw new RangeError("creationDate must be a valid Date");
958
+ }
959
+ if (info.workTimeSeconds !== void 0 && info.workTimeSeconds !== null && (!Number.isFinite(info.workTimeSeconds) || info.workTimeSeconds < 0)) {
960
+ throw new RangeError("workTimeSeconds must be a finite, non-negative number");
961
+ }
962
+ const creationDateDays = info.creationDate === null ? 0 : info.creationDate !== void 0 ? dateToDelphiDays(info.creationDate) : current.creationDate !== null ? dateToDelphiDays(current.creationDate) : 0;
963
+ const workTimeDays = info.workTimeSeconds === null ? 0 : info.workTimeSeconds !== void 0 ? info.workTimeSeconds / (24 * 60 * 60) : current.workTimeSeconds !== null ? current.workTimeSeconds / (24 * 60 * 60) : 0;
964
+ const writer = new BinaryWriter();
965
+ writer.writeF64LE(creationDateDays);
966
+ writer.writeF64LE(workTimeDays);
967
+ const timestampEvent = findFirstEvent(parsed, EVENT_ID.PROJECT_TIMESTAMP);
968
+ return timestampEvent ? replaceEventPayload(parsed, timestampEvent, writer.toBuffer()) : insertProjectEvent(parsed, createEvent(EVENT_ID.PROJECT_TIMESTAMP, writer.toBuffer()));
962
969
  }
963
970
  function listSamples(parsed) {
964
971
  const samples = [];
965
972
  parsed.events.forEach((event, index) => {
966
973
  if (event.id === EVENT_ID.CHANNEL_SAMPLE_PATH) {
967
- const path = getEventString(event, parsed.useUnicode);
968
- if (path && path.length > 0) {
974
+ const path2 = getEventString(event, parsed.useUnicode);
975
+ if (path2 && path2.length > 0) {
969
976
  samples.push({
970
977
  eventIndex: index,
971
- path
978
+ path: path2
972
979
  });
973
980
  }
974
981
  }
@@ -1096,6 +1103,147 @@ function getPPQ(parsed) {
1096
1103
  reader.seek(12);
1097
1104
  return reader.readU16LE();
1098
1105
  }
1106
+
1107
+ // src/samples/inspectReferences.ts
1108
+ import path from "path";
1109
+ import * as fs from "fs/promises";
1110
+ function pathType(p) {
1111
+ if (!p || Array.from(p).some((c) => c.charCodeAt(0) < 32)) return "unknown";
1112
+ if (/^[A-Za-z]:/.test(p) || p.startsWith("\\")) return "windows";
1113
+ if (p.startsWith("/")) return "posix";
1114
+ if (/^[A-Za-z][\w+.-]*:/.test(p) || /[%$]/.test(p)) return "daw";
1115
+ return "relative";
1116
+ }
1117
+ function nativeAbsolute(p) {
1118
+ const kind = pathType(p);
1119
+ return process.platform === "win32" ? kind === "windows" && path.win32.isAbsolute(p) && !/^[\\/][^\\/]/.test(p) : kind === "posix";
1120
+ }
1121
+ function localRelative(p) {
1122
+ return pathType(p) === "relative";
1123
+ }
1124
+ function abort(signal) {
1125
+ if (signal?.aborted) {
1126
+ const e = new Error("Sample inspection aborted");
1127
+ e.name = "AbortError";
1128
+ throw e;
1129
+ }
1130
+ }
1131
+ async function inspectReferences(refs, options = {}) {
1132
+ const mode = options.mode ?? "resolve";
1133
+ if (mode !== "resolve" && mode !== "check") throw new TypeError("Unknown sample inspection mode");
1134
+ const concurrency = options.concurrency ?? 8;
1135
+ const maxEntries = options.maxSearchEntries ?? 1e5;
1136
+ if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 64) throw new RangeError("Concurrency must be between 1 and 64");
1137
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) throw new RangeError("Search limit must be a positive safe integer");
1138
+ for (const p of [options.projectPath, options.projectRoot, ...Object.values(options.relativeRoots ?? {}), ...options.searchRoots ?? []]) {
1139
+ if (p !== void 0 && !nativeAbsolute(p)) throw new TypeError("Inspection context paths must be native absolute paths");
1140
+ }
1141
+ abort(options.signal);
1142
+ const results = refs.map((ref) => {
1143
+ const kind = pathType(ref.path);
1144
+ const candidates = [];
1145
+ const add = (p, source) => {
1146
+ const normalized = path.normalize(p);
1147
+ if (!candidates.some((c) => c.path === normalized)) candidates.push({ path: normalized, source, confidence: "exact" });
1148
+ };
1149
+ if (ref.supported !== false) {
1150
+ if (nativeAbsolute(ref.path)) add(ref.path, "declared");
1151
+ const base = ref.relativeBase === "project-file" ? options.projectRoot ?? (options.projectPath ? path.dirname(options.projectPath) : void 0) : options.relativeRoots?.[ref.relativeType ?? ""] ?? (ref.relativeType === "3" ? options.projectRoot : void 0);
1152
+ const relative = ref.relativePath || (kind === "relative" ? ref.path : null);
1153
+ if (base && relative && localRelative(relative)) add(path.resolve(base, relative.replace(/\\/g, path.sep)), "relative");
1154
+ }
1155
+ return {
1156
+ referenceIndex: ref.referenceIndex,
1157
+ recordedPath: ref.path,
1158
+ pathType: ref.supported === false ? "unknown" : kind,
1159
+ relativePath: ref.relativePath ?? (kind === "relative" ? ref.path : null),
1160
+ candidates,
1161
+ resolvedPath: null,
1162
+ status: candidates.length ? "unchecked" : "unresolved",
1163
+ reason: candidates.length ? null : "No interpretable path in the supplied context"
1164
+ };
1165
+ });
1166
+ if (mode === "resolve") return results;
1167
+ const checks = /* @__PURE__ */ new Map();
1168
+ const check = (p) => {
1169
+ let pending = checks.get(p);
1170
+ if (!pending) {
1171
+ pending = fs.stat(p).then((s) => s.isFile() ? "file" : "absent", (e) => e.code === "ENOENT" || e.code === "ENOTDIR" ? "absent" : "inaccessible");
1172
+ checks.set(p, pending);
1173
+ }
1174
+ return pending;
1175
+ };
1176
+ let index = 0;
1177
+ await Promise.all(Array.from({ length: Math.min(concurrency, results.length) }, async () => {
1178
+ while (index < results.length) {
1179
+ abort(options.signal);
1180
+ const result = results[index++];
1181
+ const found = [];
1182
+ let inaccessible = false;
1183
+ for (const candidate of result.candidates) {
1184
+ abort(options.signal);
1185
+ const status = await check(candidate.path);
1186
+ if (status === "file") found.push(candidate.path);
1187
+ if (status === "inaccessible") inaccessible = true;
1188
+ }
1189
+ result.status = found.length > 1 ? "ambiguous" : found.length === 1 ? "found" : inaccessible ? "inaccessible" : result.candidates.length ? "missing" : "unresolved";
1190
+ result.resolvedPath = result.status === "found" ? found[0] : null;
1191
+ if (result.status === "inaccessible") result.reason = "A candidate could not be checked";
1192
+ }
1193
+ }));
1194
+ abort(options.signal);
1195
+ const unresolved = results.filter((r) => r.status !== "found");
1196
+ if (unresolved.length && options.searchRoots?.length) {
1197
+ const names = /* @__PURE__ */ new Map();
1198
+ const needed = new Set(unresolved.map((r) => path.posix.basename(r.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase()));
1199
+ const queue = [...new Set(options.searchRoots.map((r) => path.normalize(r)))];
1200
+ const visited = /* @__PURE__ */ new Set();
1201
+ let entries = 0;
1202
+ let incomplete = false;
1203
+ while (queue.length) {
1204
+ abort(options.signal);
1205
+ const directory = queue.pop();
1206
+ if (visited.has(directory)) continue;
1207
+ visited.add(directory);
1208
+ try {
1209
+ const handle = await fs.opendir(directory);
1210
+ for await (const entry of handle) {
1211
+ abort(options.signal);
1212
+ if (++entries > maxEntries) throw new RangeError("Sample search entry limit exceeded");
1213
+ const p = path.join(directory, entry.name);
1214
+ if (entry.isDirectory()) queue.push(p);
1215
+ else if (entry.isFile()) {
1216
+ const name = entry.name.normalize("NFC").toLowerCase();
1217
+ if (needed.has(name)) names.set(name, [...names.get(name) ?? [], p]);
1218
+ }
1219
+ }
1220
+ } catch (e) {
1221
+ if (e instanceof RangeError || e instanceof Error && e.name === "AbortError") throw e;
1222
+ incomplete = true;
1223
+ }
1224
+ }
1225
+ for (const result of unresolved) {
1226
+ const name = path.posix.basename(result.recordedPath.replace(/\\/g, "/")).normalize("NFC").toLowerCase();
1227
+ const matches = names.get(name) ?? [];
1228
+ for (const p of matches) if (!result.candidates.some((c) => c.path === p)) result.candidates.push({ path: p, source: "search", confidence: "name-only" });
1229
+ if (result.status === "ambiguous") continue;
1230
+ if (matches.length) {
1231
+ result.status = matches.length > 1 ? "ambiguous" : "unresolved";
1232
+ result.reason = "Name-only candidates require caller confirmation";
1233
+ } else if (incomplete && result.status !== "inaccessible") {
1234
+ result.status = "inaccessible";
1235
+ result.reason = "Search could not inspect every requested directory";
1236
+ }
1237
+ }
1238
+ }
1239
+ abort(options.signal);
1240
+ return results;
1241
+ }
1242
+
1243
+ // src/api/inspectSamples.ts
1244
+ function inspectSamples(parsed, options = {}) {
1245
+ return inspectReferences(listSamples(parsed).map((s) => ({ referenceIndex: s.eventIndex, path: s.path, relativeBase: "project-file" })), options);
1246
+ }
1099
1247
  export {
1100
1248
  BinaryReader,
1101
1249
  BinaryWriter,
@@ -1113,6 +1261,7 @@ export {
1113
1261
  getEventString,
1114
1262
  getFlVersion,
1115
1263
  getPPQ,
1264
+ inspectSamples,
1116
1265
  listPlugins,
1117
1266
  listSamples,
1118
1267
  parseFlp,