PageSourceSearch

https://droneskycheck-d0136.web.app/js/core/account/mission-best-windows-core.js?v=__VERSION__

js droneskycheck-d0136.web.app collected 2026-10-03 12:22:36 UTC 56,303 bytes, 705 lines download raw bytes

1(function initMissionBestWindows(global, factory) {
2  const api = factory(global);
3  if (typeof module !== "undefined" && module.exports) module.exports = api;
4  if (global) global.DSCMissionBestWindows = api;
5})(typeof window !== "undefined" ? window : globalThis, function buildMissionBestWindows(global) {
6  "use strict";
7
8  const ENGINE_VERSION = "MISSION_BEST_WINDOWS_V1";
9  const LIGHT_VERSION = "MISSION_LIGHT_SUNRISE_SUNSET_V1";
10  const DRONE_VERSION = "MISSION_DRONE_WIND_TECHNICAL_V1";
11  const PREF_VERSION = "MISSION_WEATHER_PREFERENCES_EVALUATOR_V1";
12  const POINT_VERSION = "MISSION_REPRESENTATIVE_POINT_V1";
13  const WEATHER_ENGINE_VERSION = "MISSION_WEATHER_SNAPSHOT_ENGINE_V1";
14  const LEGAL_ENGINE_VERSION = "mission-legal-snapshot-v1";
15  const MAX_INPUT_BYTES = 512 * 1024;
16  const MAX_SNAPSHOT_BYTES = 200 * 1024;
17  const MAX_GEOMETRY_BYTES = 64 * 1024;
18  const MAX_OUTPUT_BYTES = 200 * 1024;
19  const MAX_DURATION_MS = 168 * 60 * 60 * 1000;
20  const MAX_WEATHER_SLOTS = 169;
21  const MAX_WINDOWS = 256;
22  const WIND_LEVELS = Object.freeze([10, 80, 120]);
23  const UNITS = Object.freeze({
24    temperatureC: "celsius", precipitationMm: "mm", precipitationProbabilityPct: "%",
25    weatherCode: "WMO code", visibilityMeters: "m", cloudCoverPct: "%",
26    windSpeedKmh: "km/h", windDirectionDegrees: "degrees", windGustKmh: "km/h"
27  });
28  const REASON_ORDER = Object.freeze([
29    "INPUT_SCOPE_MISMATCH", "LEGAL_SNAPSHOT_MISSING", "WEATHER_SNAPSHOT_MISSING",
30    "LEGAL_SNAPSHOT_NOT_CURRENT", "WEATHER_SNAPSHOT_NOT_CURRENT", "POINT_ONLY",
31    "LEGAL_UNAVAILABLE", "LEGAL_UNKNOWN", "LEGAL_LIMIT_BELOW_PLANNED_ALTITUDE",
32    "LEGAL_LIMIT_UNKNOWN", "AUTHORIZATION_REQUIRED", "PREFERENCE_WIND_EXCEEDED",
33    "PREFERENCE_GUST_EXCEEDED", "PREFERENCE_PRECIPITATION_EXCEEDED",
34    "PREFERENCE_MIN_TEMPERATURE_EXCEEDED", "PREFERENCE_MAX_TEMPERATURE_EXCEEDED",
35    "PREFERENCE_DATA_UNAVAILABLE", "DRONE_NOT_SELECTED", "DRONE_MATCH_UNAVAILABLE",
36    "DRONE_WIND_LIMIT_UNAVAILABLE", "DRONE_WIND_LIMIT_EXCEEDED", "DRONE_CATALOG_STALE",
37    "WEATHER_PARTIAL", "WEATHER_FIELDS_MISSING", "WEATHER_STALE", "LIGHT_UNKNOWN",
38    "DAYLIGHT", "NIGHT"
39  ]);
40  const REASON_RANK = new Map(REASON_ORDER.map((value, index) => [value, index]));
41  const RANKS = Object.freeze({
42    placement: ["CANDIDATE", "LIMITED", "REQUIRES_ATTENTION", "NOT_CANDIDATE", "
42UNKNOWN"],
43    preferences: ["NO_EXCEEDANCE", "NOT_CONFIGURED", "EXCEEDANCE_PRESENT", "UNAVAILABLE"],
44    droneTechnical: ["WITHIN_AVAILABLE_WIND_DATA", "NOT_CONFIGURED", "DECLARED_WIND_LIMIT_EXCEEDED", "UNKNOWN"],
45    legalCompleteness: ["COMPLETE", "PARTIAL", "UNKNOWN"],
46    weatherTemporalCoverage: ["FULL", "PARTIAL", "NONE"],
47    weatherFieldQuality: ["COMPLETE", "PARTIAL", "UNKNOWN"],
48    weatherFreshness: ["RECENT", "AGED", "STALE"],
49    droneCatalog: ["CURRENT", "NOT_CONFIGURED", "STALE", "UNKNOWN"]
50  });
51
52  class MissionBestWindowsInputError extends TypeError {
53    constructor(code, path, message = code) {
54      super(message);
55      this.name = "MissionBestWindowsInputError";
56      this.code = code;
57      this.path = path;
58    }
59  }
60
61  class MissionBestWindowsLimitError extends RangeError {
62    constructor(code, path, message = code) {
63      super(message);
64      this.name = "MissionBestWindowsLimitError";
65      this.code = code;
66      this.path = path;
67    }
68  }
69
70  function fail(code, path, message) { throw new MissionBestWindowsInputError(code, path, message); }
71  function limit(code, path) { throw new MissionBestWindowsLimitError(code, path); }
72  function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
73  function exactKeys(value, keys, path) {
74    if (!isObject(value)) fail("MALFORMED_INPUT", path);
75    const actual = Object.keys(value).sort();
76    const expected = [...keys].sort();
77    if (actual.length !== expected.length || actual.some((key, i) => key !== expected[i])) fail("MALFORMED_INPUT", path);
78  }
79  function assertPlain(value, path = "input", seen = new Set()) {
80    if (value === null || typeof value === "string" || typeof value === "boolean") return;
81    if (typeof value === "number") { if (!Number.isFinite(value)) fail("MALFORMED_INPUT", path); return; }
82    if (typeof value !== "object" || value instanceof Date) fail("MALFORMED_INPUT", path);
83    if (seen.has(value)) fail("MALFORMED_INPUT", path);
84    seen.add(value);
85    if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== null) fail("MALFORMED_INPUT", path);
86    for (const key of Object.keys(value)) {
87      if (value[key] === undefined) fail("MALFORMED_INPUT", `${path}.${key}`);
88      assertPlain(value[key], `${path}.${key}`, seen);
89    }
90    seen.delete(value);
91  }
92  function contract() {
93    const value = global?.DSCMissionGeometryContract;
94    if (!value?.fingerprintValue || !value?.fingerprint || !value?.canonicalizeDomain || !value?.representativePoint) {
95      fail("UNSUPPORTED_CONTRACT", "mission.operationalGeometry", "MISSION_GEOMETRY_CONTRACT_UNAVAILABLE");
96    }
97    return value;
98  }
99  function temporalCore() {
100    const value = global?.DSCMissionAtomicTimeline;
101    if (!value?.buildMissionAtomicTimeline || !value?.buildMissionLightSegments) {
102      fail("UNSUPPORTED_CONTRACT", "input.mission", "MISSION_ATOMIC_TIMELINE_CORE_UNAVAILABLE");
103    }
104    return value;
105  }
106  function temporalCall(callback, fallbackPath) {
107    try { return callback(); }
108    catch (error) {
109      if (error?.code === "ATOMIC_INTERVAL_LIMIT_EXCEEDED") limit(error.code, "output.summary.atomicIntervalCount");
110      if (error?.code) fail(error.code, error.path || fallbackPath, error.message);
111      throw error;
112    }
113  }
114  function stable(value) { return contract().stableStringify(value); }
115  function bytes(value) { return new TextEncoder().encode(stable(value)).byteLength; }
116  function fingerprint(prefix, value) { return contract().fingerprintValue(prefix, value); }
117  function iso(value, path) {
118    if (typeof value !== "string") fail("MALFORMED_INPUT", path);
119    const ms = Date.parse(value);
120    if (!Number.isFinite(ms) || new Date(ms).toISOString() !== value) fail("MALFORMED_INPUT", path);
121    return { value, ms };
122  }
123  function text(value, path) {
124    if (typeof value !== "string" || !value.trim()) fail("MALFORMED_INPUT", path);
125    return value.trim();
126  }
127  function finiteOrNull(value, path) {
128    if (value === null) return null;
129    if (typeof value !== "number" || !Number.isFinite(value)) fail("MALFORMED_INPUT", path);
130    return value;
131  }
132  function stringList(value, path) {
133    if (!Array.isArray(value) || value.some(item => typeof item !== "string")) fail("MALFORMED_INPUT", path);
134    return value;
135  }
136  function orderedReasons(values) {
137    const unique = [...new Set(values.filter(value => REASON_RANK.has(value)))];
138    return unique.sort((a, b) => REASON_RANK.get(a) - REASON_RANK.get(b));
139  }
140  function sortedUnique(values, canonicalize = value => value) {
141    return [...new Set(values.map(canonicalize))].sort((a, b) => typeof a === "number" ? a - b : String(a).localeCompare(String(b)));
142  }
143
144  function validateMission(value) {
145    exactKeys(value, ["schemaVersion", "id", "operatorId", "plannedAt", "plannedUntil", "plannedAltitude", "operationalGeometry"], "input.mission");
146    if (value.schemaVersion !== 2 && value.schemaVersion !== 3) fail("UNSUPPORTED_SCHEMA", "input.mission.schemaVersion");
147    const id = text(value.id, "input.mission.id");
148    const operatorId = text(value.operatorId, "input.mission.operatorId");
149    const from = iso(value.plannedAt, "input.mission.plannedAt");
150    const to = iso(value.plannedUntil, "input.mission.plannedUntil");
151    if (to.ms <= from.ms || to.ms - from.ms > MAX_DURATION_MS) fail("MALFORMED_INPUT", "input.mission.plannedUntil");
152    exactKeys(value.plannedAltitude, ["valueMeters", "reference"], "input.mission.plannedAltitude");
153    const altitude = value.plannedAltitude.valueMeters;
154    if (typeof altitude !== "number" || !Number.isFinite(altitude) || altitude <= 0 || altitude > 10000 || Number(altitude.toFixed(1)) !== altitude || value.plannedAltitude.reference !== "AGL") fail("MALFORMED_INPUT", "input.mission.plannedAltitude");
155    if (bytes(value.operationalGeometry) > MAX_GEOMETRY_BYTES) fail("INPUT_TOO_LARGE", "input.mission.operationalGeometry");
156    let geometry;
157    try { geometry = contract().canonicalizeDomain(value.operationalGeometry); }
158    catch { fail("MALFORMED_INPUT", "input.mission.operationalGeometry"); }
159    return { value, id, operatorId, from, to, altitude, geometry, geometryFingerprint: contract().fingerprint(geometry) };
160  }
161
162  function validatePreferences(value) {
163    exactKeys(value, ["schemaVersion", "weather"], "input.preferences");
164    if (value.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.preferences.schemaVersion");
165    const keys = ["maxWindKmh", "maxGustKmh", "avoidPrecipitation", "minTemperatureC", "maxTemperatureC"];
166    exactKeys(value.weather, keys, "input.preferences.weather");
167    for (const key of ["maxWindKmh", "maxGustKmh", "minTemperatureC", "maxTemperatureC"]) {
168      const item = value.weather[key];
169      if (item !== null && (typeof item !== "number" || !Number.isFinite(item))) fail("MALFORMED_INPUT", `input.preferences.weather.${key}`);
170    }
171    if ((value.weather.maxWindKmh !== null && value.weather.maxWindKmh < 0) || (value.weather.maxGustKmh !== null && value.weather.maxGustKmh < 0)) fail("MALFORMED_INPUT", "input.preferences.weather");
172    if (value.weather.avoidPrecipitation !== null && typeof value.weather.avoidPrecipitation !== "boolean") fail("MALFORMED_INPUT", "input.preferences.weather.avoidPrecipitation");
173    if (value.weather.minTemperatureC !== null && value.weather.maxTemperatureC !== null && value.weather.minTemperatureC >= value.weather.maxTemperatureC) fail("MALFORMED_INPUT", "input.preferences.weather");
174    return value;
175  }
176
177  function validateDrone(value) {
178    if (value === null) return null;
179    exactKeys(value, ["schemaVersion", "contractVersion", "droneId", "catalog", "matchStatus", "windResistance"], "input.droneTechnical");
180    if (value.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.droneTechnical.schemaVersion");
181    if (value.contractVersion !== DRONE_VERSION) fail("UNSUPPORTED_CONTRACT", "input.droneTechnical.contractVersion");
182    text(value.droneId, "input.droneTechnical.droneId");
183    exactKeys(value.catalog, ["schemaVersion", "catalogVersion", "sha256", "loadSource", "stale"], "input.droneTechnical.catalog");
184    if (value.catalog.schemaVersion !== 2 || !Number.isInteger(value.catalog.catalogVersion) || value.catalog.catalogVersion < 1 || !/^[0-9a-f]{64}$/.test(value.catalog.sha256)) fail("MALFORMED_INPUT", "input.droneTechnical.catalog");
185    if (!["NETWORK", "CACHE", "LAST_KNOWN_GOOD"].includes(value.catalog.loadSource) || typeof value.catalog.stale !== "boolean" || (value.catalog.loadSource === "LAST_KNOWN_GOOD") !== value.catalog.stale) fail("MALFORMED_INPUT", "input.droneTechnical.catalog");
186    if (!["EXACT", "ALIAS", "NOT_FOUND", "AMBIGUOUS"].includes(value.matchStatus)) fail("MALFORMED_INPUT", "input.droneTechnical.matchStatus");
187    if (value.windResistance !== null) {
188      exactKeys(value.windResistance, ["valueMs", "basis"], "input.droneTechnical.windResistance");
189      if (typeof value.windResistance.valueMs !== "number" || !Number.isFinite(value.windResistance.valueMs) || value.windResistance.valueMs <= 0 || !["TAKEOFF_LANDING", "GENERAL", "GENERAL_RANGE_MINIMUM", "CRUISE_ONLY"].includes(value.windResistance.basis)) fail("MALFORMED_INPUT", "input.droneTechnical.windResistance");
190    }
191    if (["NOT_FOUND", "AMBIGUOUS"].includes(value.matchStatus) && value.windResistance !== null) fail("MALFORMED_INPUT", "input.droneTechnical.windResistance");
192    return value;
193  }
194
195  function legalFingerprint(mission) {
196    return fingerprint("ml1", { schemaVersion: 1, domain: "MISSION_LEGAL_INPUT", geometryFingerprint: mission.geometryFingerprint,
197      plannedAt: mission.from.value, plannedUntil: mission.to.value, coverage: "POINT_ONLY", representativePointContractVersion: POINT_VERSION });
198  }
199  function weatherFingerprint(mission) {
200    return fingerprint("mw1", { schemaVersion: 1, domain: "MISSION_WEATHER_TARGET", geometryFingerprint: mission.geometryFingerprint,
201      plannedAt: mission.from.value, plannedUntil: mission.to.value,
202      plannedAltitude: { valueMeters: mission.altitude, reference: "AGL" },
203      representativePointContractVersion: POINT_VERSION, samplingMethodVersion: "REPRESENTATIVE_POINT_ONLY_V1" });
204  }
205
206  function validateAuthorization(value, path) {
207    if (!isObject(value) || typeof value.required !== "boolean" || !["RESOLVED", "MANUAL_CHECK", "BLOCKED"].includes(value.resolutionStatus) || !Array.isArray(value.procedures) || !Array.isArray(value.additionalRequirements) || !Array.isArray(value.blockingReasons) || !Array.isArray(value.reasonCodes)) fail("MALFORMED_INPUT", path);
208    stringList(value.reasonCodes, `${path}.reasonCodes`);
209    for (const [key, items] of [["procedures", value.procedures], ["additionalRequirements", value.additionalRequirements], ["blockingReasons", value.blockingReasons]]) {
210      items.forEach((item, index) => {
211        if (!isObject(item) || (item.type != null && typeof item.type !== "string") ||
212            (item.reasonCode != null && typeof item.reasonCode !== "string") ||
213            (item.version != null && typeof item.version !== "string" && typeof item.version !== "number")) fail("MALFORMED_INPUT", `${path}.${key}[${index}]`);
214      });
215    }
216    return value;
217  }
218  function validateLegal(value) {
219    if (value === null) return null;
220    if (bytes(value) > MAX_SNAPSHOT_BYTES) fail("INPUT_TOO_LARGE", "input.legalSnapshot");
221    if (!isObject(value) || value.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.legalSnapshot.schemaVersion");
222    if (value.engine?.version !== LEGAL_ENGINE_VERSION) fail("UNSUPPORTED_CONTRACT", "input.legalSnapshot.engine.version");
223    text(value.id, "input.legalSnapshot.id"); text(value.operatorId, "input.legalSnapshot.operatorId"); text(value.missionId, "input.legalSnapshot.missionId");
224    if (!isObject(value.mission) || typeof value.mission.inputFingerprint !== "string" || value.mission.inputFingerprintVersion !== 1 ||
225        !isObject(value.target) || !isObject(value.geometry) || !Array.isArray(value.segments) || !Array.isArray(value.contributors) ||
226        !Array.isArray(value.sources) || !isObject(value.quality)) fail("MALFORMED_INPUT", "input.legalSnapshot");
227    const targetFrom = iso(value.target.from, "input.legalSnapshot.target.from");
228    const targetTo = iso(value.target.to, "input.legalSnapshot.target.to");
229    if (targetTo.ms <= targetFrom.ms || value.target.timezone !== "Europe/Rome" || typeof value.geometry.fingerprint !== "string" ||
230        value.geometry.coverage?.level !== "POINT_ONLY" || value.geometry.coverage?.contractVersion !== POINT_VERSION ||
231        !["COMPLETE", "PARTIAL"].includes(value.quality.sourceCompleteness)) fail("MALFORMED_INPUT", "input.legalSnapshot");
232    const expectedFingerprint = fingerprint("ml1", { schemaVersion: 1, domain: "MISSION_LEGAL_INPUT",
233      geometryFingerprint: value.geometry.fingerprint, plannedAt: targetFrom.value, plannedUntil: targetTo.value,
234      coverage: "POINT_ONLY", representativePointContractVersion: value.geometry.coverage.contractVersion });
235    if (expectedFingerprint !== value.mission.inputFingerprint) fail("INTEGRITY_MISMATCH", "input.legalSnapshot.mission.inputFingerprint");
236    let cursor = targetFrom.ms;
237    const segments = value.segments.map((segment, index) => {
238      const path = `input.legalSnapshot.segments[${index}]`;
239      if (!isObject(segment) || !["AVAILABLE", "AVAILABLE_WITH_LIMIT", "AUTH_REQUIRED", "UNAVAILABLE", "UNKNOWN"].includes(segment.status) || !Array.isArray(segment.contributorIds) || !Array.isArray(segment.reasonCodes)) fail("MALFORMED_INPUT", path);
240      const from = iso(segment.from, `${path}.from`); const to = iso(segment.to, `${path}.to`);
241      if (from.ms !== cursor || to.ms <= from.ms || to.ms > targetTo.ms) fail("MALFORMED_INPUT", path);
242      cursor = to.ms;
243      validateAuthorization(segment.authorization, `${path}.authorization`);
244      stringList(segment.contributorIds, `${path}.contributorIds`);
245      stringList(segment.reasonCodes, `${path}.reasonCodes`);
246      finiteOrNull(segment.maxAltitudeMetersAgl, `${path}.maxAltitudeMetersAgl`);
247      return { ...segment, fromMs: from.ms, toMs: to.ms };
248    });
249    if (!segments.length || cursor !== targetTo.ms) fail("MALFORMED_INPUT", "input.legalSnapshot.segments");
250    value.contributors.forEach((item, index) => {
251      if (!isObject(item) || typeof item.id !== "string" || !item.id || !Array.isArray(item.effects) ||
252          !Array.isArray(item.reasonCodes) || !Array.isArray(item.warnings)) fail("MALFORMED_INPUT", `input.legalSnapshot.contributors[${index}]`);
253      stringList(item.effects, `input.legalSnapshot.contributors[${index}].effects`);
254      stringList(item.reasonCodes, `input.legalSnapshot.contributors[${index}].reasonCodes`);
255      stringList(item.warnings, `input.legalSnapshot.contributors[${index}].warnings`);
256    });
257    iso(value.evaluatedAt, "input.legalSnapshot.evaluatedAt");
258    return { value, segments, targetFrom, targetTo };
259  }
260
261  function dataFingerprint(snapshot) {
262    return fingerprint("mwd1", {
263      schemaVersion: 1, domain: "MISSION_WEATHER_DATA", normalizerVersion: snapshot.provenance.normalizerVersion,
264      variableSetVersion: snapshot.provenance.variableSetVersion, temporalSamplingVersion: snapshot.coverage.temporal.samplingVersion,
265      sampling: { queryCoordinate: { latitude: snapshot.provenance.queryCoordinate.latitude, longitude: snapshot.provenance.queryCoordinate.longitude },
266        providerCoordinate: snapshot.provenance.providerCoordinate, windLevelsMetersAgl: [...WIND_LEVELS], interpolation: "NONE" },
267      units: UNITS, hours: snapshot.hours
268    });
269  }
270  function preferenceEvaluationFingerprint(dataFp, evaluation) {
271    if (!evaluation?.configured) return null;
272    return fingerprint("mwe1", { schemaVersion: 1, domain: "MISSION_WEATHER_PREFERENCE_EVALUATION", dataFingerprint: dataFp,
273      preferencesSchemaVersion: 1, evaluatorVersion: PREF_VERSION, thresholds: evaluation.thresholds,
274      windPolicy: "EACH_AVAILABLE_LEVEL_SEPARATELY", gustPolicy: "LEVEL_10M_ONLY" });
275  }
276  function validateWeather(value) {
277    if (value === null) return null;
278    if (bytes(value) > MAX_SNAPSHOT_BYTES) fail("INPUT_TOO_LARGE", "input.weatherSnapshot");
279    if (!isObject(value) || value.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.weatherSnapshot.schemaVersion");
280    if (value.engine?.version !== WEATHER_ENGINE_VERSION) fail("UNSUPPORTED_CONTRACT", "input.weatherSnapshot.engine.version");
281    text(value.id, "input.weatherSnapshot.id"); text(value.operatorId, "input.weatherSnapshot.operatorId"); text(value.missionId, "input.weatherSnapshot.missionId");
282    if (!isObject(value.target) || !isObject(value.provenance) || !isObject(value.coverage) || !Array.isArray(value.hours) || !isObject(value.quality) || stable(value.units) !== stable(UNITS)) fail("MALFORMED_INPUT", "input.weatherSnapshot");
283    if (value.hours.length > MAX_WEATHER_SLOTS || value.coverage.spatial?.level !== "POINT_ONLY" || value.coverage.spatial?.samplingMethodVersion !== "REPRESENTATIVE_POINT_ONLY_V1" || value.coverage.temporal?.samplingVersion !== "INTERSECTING_UTC_HOUR_SLOTS_V1" || value.coverage.temporal?.resolutionMinutes !== 60 ||
284        !["FULL_TEMPORAL_COVERAGE", "PARTIAL_TEMPORAL_COVERAGE"].includes(value.coverage.temporal?.status) ||
285        !["EXACT_LEVEL_AVAILABLE", "EXACT_LEVEL_PARTIAL", "BETWEEN_SAMPLED_LEVELS", "OUTSIDE_SAMPLED_RANGE", "PROFILE_UNAVAILABLE"].includes(value.coverage.altitude?.status)) fail("UNSUPPORTED_CONTRACT", "input.weatherSnapshot.coverage");
286    if (value.provenance.adapterVersion !== "OPEN_METEO_POINT_ADAPTER_V1" || value.provenance.normalizerVersion !== "MISSION_WEATHER_NORMALIZER_V1" || value.provenance.variableSetVersion !== "MISSION_WEATHER_POINT_V1") fail("UNSUPPORTED_CONTRACT", "input.weatherSnapshot.provenance");
287    const targetFrom = iso(value.target.plannedAt, "input.weatherSnapshot.target.plannedAt");
288    const targetTo = iso(value.target.plannedUntil, "input.weatherSnapshot.target.plannedUntil");
289    if (targetTo.ms <= targetFrom.ms || !isObject(value.target.plannedAltitude) || value.target.plannedAltitude.reference !== "AGL" ||
290        typeof value.target.plannedAltitude.valueMeters !== "number" || !Number.isFinite(value.target.plannedAltitude.valueMeters) ||
291        value.target.representativePoint?.contractVersion !== POINT_VERSION || value.target.samplingMethodVersion !== "REPRESENTATIVE_POINT_ONLY_V1") fail("MALFORMED_INPUT", "input.weatherSnapshot.target");
292    const expectedTargetFingerprint = fingerprint("mw1", { schemaVersion: 1, domain: "MISSION_WEATHER_TARGET",
293      geometryFingerprint: value.target.geometryFingerprint, plannedAt: targetFrom.value, plannedUntil: targetTo.value,
294      plannedAltitude: value.target.plannedAltitude, representativePointContractVersion: POINT_VERSION,
295      samplingMethodVersion: value.target.samplingMethodVersion });
296    if (expectedTargetFingerprint !== value.target.inputFingerprint) fail("INTEGRITY_MISMATCH", "input.weatherSnapshot.target.inputFingerprint");
297    const hours = value.hours.map((hour, index) => {
298      const path = `input.weatherSnapshot.hours[${index}]`;
299      if (!isObject(hour) || !["FULL", "PARTIAL", "NONE"].includes(hour.sourceAvailability) || !Array.isArray(hour.windLevels) || hour.windLevels.length !== 3 || !Array.isArray(hour.missingFields)) fail("MALFORMED_INPUT", path);
300      const slotFrom = iso(hour.slotFromUtc, `${path}.slotFromUtc`); const slotTo = iso(hour.slotToUtc, `${path}.slotToUtc`);
301      const overlapFrom = iso(hour.overlapFromUtc, `${path}.overlapFromUtc`); const overlapTo = iso(hour.overlapToUtc, `${path}.overlapToUtc`);
302      if (slotTo.ms - slotFrom.ms !== 3600000 || overlapFrom.ms !== Math.max(slotFrom.ms, targetFrom.ms) ||
303          overlapTo.ms !== Math.min(slotTo.ms, targetTo.ms) || overlapTo.ms <= overlapFrom.ms ||
304          (index && slotFrom.ms !== Date.parse(value.hours[index - 1].slotToUtc))) fail("MALFORMED_INPUT", path);
305      if (!isObject(hour.sourceTimes) || hour.sourceTimes.instantAtUtc !== hour.slotFromUtc ||
306          hour.sourceTimes.precedingHourEndsAtUtc !== hour.slotToUtc) fail("MALFORMED_INPUT", `${path}.sourceTimes`);
307      for (const field of ["temperatureC", "precipitationMm", "precipitationProbabilityPct", "weatherCode", "visibilityMeters", "cloudCoverPct"]) finiteOrNull(hour[field], `${path}.${field}`);
308      hour.windLevels.forEach((level, levelIndex) => {
309        if (!isObject(level) || level.heightMetersAgl !== WIND_LEVELS[levelIndex]) fail("MALFORMED_INPUT", `${path}.windLevels[${levelIndex}]`);
310        finiteOrNull(level.speedKmh, `${path}.windLevels[${levelIndex}].speedKmh`);
311        const direction = finiteOrNull(level.directionDegrees, `${path}.windLevels[${levelIndex}].directionDegrees`);
312        if (direction !== null && (direction < 0 || direction > 360)) fail("MALFORMED_INPUT", `${path}.windLevels[${levelIndex}].directionDegrees`);
313        finiteOrNull(level.gustKmh, `${path}.windLevels[${levelIndex}].gustKmh`);
314        if (!isObject(level.fieldStatus) || level.fieldStatus.speedKmh !== (level.speedKmh === null ? "MISSING" : "AVAILABLE") ||
315            level.fieldStatus.directionDegrees !== (level.directionDegrees === null ? "MISSING" : "AVAILABLE") ||
316            (levelIndex === 0 && level.fieldStatus.gustKmh !== (level.gustKmh === null ? "MISSING" : "AVAILABLE")) ||
317            (levelIndex > 0 && (level.gustKmh !== null || level.fieldStatus.gustKmh !== "UNSUPPORTED"))) fail("MALFORMED_INPUT", `${path}.windLevels[${levelIndex}].fieldStatus`);
318      });
319      const expectedMissing = [];
320      for (const field of ["temperatureC", "precipitationMm", "precipitationProbabilityPct", "weatherCode", "visibilityMeters", "cloudCoverPct"]) if (hour[field] === null) expectedMissing.push(field);
321      hour.windLevels.forEach(level => {
322        if (level.speedKmh === null) expectedMissing.push(`wind.${level.heightMetersAgl}.speedKmh`);
323        if (level.directionDegrees === null) expectedMissing.push(`wind.${level.heightMetersAgl}.directionDegrees`);
324        if (level.heightMetersAgl === 10 && level.gustKmh === null) expectedMissing.push("wind.10.gustKmh");
325      });
326      if (stable([...hour.missingFields].sort()) !== stable(expectedMissing.sort())) fail("MALFORMED_INPUT", `${path}.missingFields`);
327      if (hour.sourceAvailability === "NONE" &&
328          [hour.temperatureC, hour.precipitationMm, hour.precipitationProbabilityPct, hour.weatherCode,
329            hour.visibilityMeters, hour.cloudCoverPct,
330            ...hour.windLevels.flatMap(level => [level.speedKmh, level.directionDegrees, level.gustKmh])]
331            .some(item => item !== null)) fail("MALFORMED_INPUT", `${path}.sourceAvailability`);
332      return { ...hour, slotFromMs: slotFrom.ms, slotToMs: slotTo.ms, overlapFromMs: overlapFrom.ms, overlapToMs: overlapTo.ms };
333    });
334    const availableCount = hours.filter(hour => hour.sourceAvailability === "FULL").length;
335    const partialCount = hours.filter(hour => hour.sourceAvailability === "PARTIAL").length;
336    const missingCount = hours.filter(hour => hour.sourceAvailability === "NONE").length;
337    const expectedTemporalStatus = availableCount === hours.length ? "FULL_TEMPORAL_COVERAGE" : "PARTIAL_TEMPORAL_COVERAGE";
338    const expectedFields = fieldCoverage(hours);
339    const hasProfile = hours.some(hour => hour.windLevels.some(level => level.speedKmh !== null || level.directionDegrees !== null));
340    const exactLevels = hours.flatMap(hour => hour.windLevels).filter(level => level.heightMetersAgl === value.target.plannedAltitude.valueMeters);
341    const expectedAltitudeStatus = !hasProfile ? "PROFILE_UNAVAILABLE" : exactLevels.length
342      ? (exactLevels.every(level => level.speedKmh !== null && level.directionDegrees !== null) ? "EXACT_LEVEL_AVAILABLE" : "EXACT_LEVEL_PARTIAL")
343      : value.target.plannedAltitude.valueMeters < 10 || value.target.plannedAltitude.valueMeters > 120 ? "OUTSIDE_SAMPLED_RANGE" : "BETWEEN_SAMPLED_LEVELS";
344    const expectedAltitude = { status: expectedAltitudeStatus, plannedAltitude: value.target.plannedAltitude,
345      sampledLevelsMetersAgl: [...WIND_LEVELS], interpolation: "NONE" };
346    const expectedQuality = expectedTemporalStatus === "FULL_TEMPORAL_COVERAGE" && !Object.values(expectedFields).some(item => item.missing > 0) ? "COMPLETE" : "PARTIAL";
347    if (!hours.length || hours[0].overlapFromMs !== targetFrom.ms || hours[hours.length - 1].overlapToMs !== targetTo.ms ||
348        value.coverage.temporal.requestedHourCount !== hours.length || value.coverage.temporal.availableHourCount !== availableCount ||
349        value.coverage.temporal.partialHourCount !== partialCount || value.coverage.temporal.missingHourCount !== missingCount ||
350        value.coverage.temporal.status !== expectedTemporalStatus || stable(value.coverage.altitude) !== stable(expectedAltitude) ||
351        stable(value.quality.fieldCoverage) !== stable(expectedFields) || value.quality.status !== expectedQuality) fail("MALFORMED_INPUT", "input.weatherSnapshot.hours");
352    const recomputed = dataFingerprint(value);
353    if (recomputed !== value.dataFingerprint) fail("INTEGRITY_MISMATCH", "input.weatherSnapshot.dataFingerprint");
354    const frozen = value.evaluation?.personalPreferences;
355    if (frozen && frozen.evaluatorVersion !== PREF_VERSION) fail("UNSUPPORTED_CONTRACT", "input.weatherSnapshot.evaluation.personalPreferences.evaluatorVersion");
356    if (frozen && preferenceEvaluationFingerprint(value.dataFingerprint, frozen) !== frozen.inputFingerprint) fail("INTEGRITY_MISMATCH", "input.weatherSnapshot.evaluation.personalPreferences.inputFingerprint");
357    if (frozen) {
358      const allowed = ["maxWindKmh", "maxGustKmh", "avoidPrecipitation", "minTemperatureC", "maxTemperatureC"];
359      if (frozen.schemaVersion !== 1 || !isObject(frozen.thresholds) || Object.keys(frozen.thresholds).some(key => !allowed.includes(key)) || frozen.configured !== (Object.keys(frozen.thresholds).length > 0)) fail("MALFORMED_INPUT", "input.weatherSnapshot.evaluation.personalPreferences");
360      const frozenPreferences = { schemaVersion: 1, weather: Object.fromEntries(allowed.map(key => [key, Object.hasOwn(frozen.thresholds, key) ? frozen.thresholds[key] : null])) };
361      const expectedChecks = evaluatePreferences(hours, frozenPreferences).checks;
362      if (stable(frozen.checks) !== stable(expectedChecks)) fail("INTEGRITY_MISMATCH", "input.weatherSnapshot.evaluation.personalPreferences.checks");
363    }
364    return { value, hours };
365  }
366
367  function currentPreferenceFingerprint(preferences) {
368    return fingerprint("bwp1", { schemaVersion: 1, evaluatorVersion: PREF_VERSION, thresholds: preferences.weather });
369  }
370  function freshness(snapshot, evaluatedMs) {
371    const retrieved = iso(snapshot.provenance.dataRetrievedAt, "input.weatherSnapshot.provenance.dataRetrievedAt");
372    const age = evaluatedMs - retrieved.ms;
373    if (age < 0) fail("MALFORMED_INPUT", "input.weatherSnapshot.provenance.dataRetrievedAt");
374    return { value: age <= 90 * 60000 ? "RECENT" : age <= 6 * 3600000 ? "AGED" : "STALE", retrieved };
375  }
376
377  function lightSegments(mission) {
378    return temporalCall(() => temporalCore().buildMissionLightSegments({
379      schemaVersion: 1,
380      interval: { fromUtc: mission.from.value, toUtc: mission.to.value },
381      operationalGeometry: mission.geometry
382    }), "input.mission.operationalGeometry");
383  }
384  function lightAt(segments, ms) {
385    const segment = segments.find(item => ms >= Date.parse(item.fromUtc) && ms < Date.parse(item.toUtc));
386    return segment?.status || "UNKNOWN";
387  }
388
389  function numeric(values) {
390    const comparable = values.filter(value => value !== null && Number.isFinite(value));
391    return { min: comparable.length ? Math.min(...comparable) : null, max: comparable.length ? Math.max(...comparable) : null,
392      comparableSampleCount: comparable.length, missingSampleCount: values.length - comparable.length };
393  }
394  function weatherFacts(hours) {
395    const field = key => numeric(hours.map(hour => hour[key]));
396    return {
397      temperatureC: field("temperatureC"), precipitationMm: field("precipitationMm"),
398      precipitationProbabilityPct: field("precipitationProbabilityPct"),
399      weatherCodes: sortedUnique(hours.map(hour => hour.weatherCode).filter(value => value !== null)),
400      visibilityMeters: field("visibilityMeters"), cloudCoverPct: field("cloudCoverPct"),
401      windLevels: WIND_LEVELS.map((height, index) => {
402        const levels = hours.map(hour => hour.windLevels[index]);
403        const output = { heightMeters: height, speedKmh: numeric(levels.map(level => level.speedKmh)),
404          directionDegreesObserved: sortedUnique(levels.map(level => level.directionDegrees).filter(value => value !== null), value => value === 360 ? 0 : value) };
405        if (height === 10) output.gustKmh = numeric(levels.map(level => level.gustKmh));
406        return output;
407      })
408    };
409  }
410  function coverageEntry(hours, getter) { const available = hours.filter(hour => getter(hour) !== null).length; return { expected: hours.length, available, missing: hours.length - available }; }
411  function fieldCoverage(hours) {
412    const output = {};
413    for (const key of ["temperatureC", "precipitationMm", "precipitationProbabilityPct", "weatherCode", "visibilityMeters", "cloudCoverPct"]) output[key] = coverageEntry(hours, hour => hour[key]);
414    WIND_LEVELS.forEach((height, index) => {
415      output[`wind${height}mSpeedKmh`] = coverageEntry(hours, hour => hour.windLevels[index].speedKmh);
416      output[`wind${height}mDirectionDegrees`] = coverageEntry(hours, hour => hour.windLevels[index].directionDegrees);
417      if (height === 10) output.wind10mGustKmh = coverageEntry(hours, hour => hour.windLevels[index].gustKmh);
418    });
419    return output;
420  }
421  function sourceAvailability(hours) {
422    if (hours.every(hour => hour.sourceAvailability === "FULL")) return "FULL";
423    if (hours.some(hour => hour.sourceAvailability !== "NONE")) return "PARTIAL";
424    return "NONE";
425  }
426  function check(criterion, limitValue, values, exceeds, heightMetersAgl) {
427    const comparable = values.filter(value => value !== null);
428    const exceeded = comparable.filter(exceeds);
429    const result = { criterion, limit: limitValue, status: !comparable.length ? "UNAVAILABLE" : exceeded.length ? "EXCEEDANCE_PRESENT" : "NO_EXCEEDANCE",
430      comparableSampleCount: comparable.length, exceedanceCount: exceeded.length, missingSampleCount: values.length - comparable.length,
431      observedExtreme: comparable.length ? (criterion === "MIN_TEMPERATURE" ? Math.min(...comparable) : Math.max(...comparable)) : null };
432    if (heightMetersAgl !== undefined) result.heightMetersAgl = heightMetersAgl;
433    return result;
434  }
435  function evaluatePreferences(hours, preferences) {
436    const p = preferences.weather; const checks = [];
437    if (p.maxWindKmh !== null) WIND_LEVELS.forEach((height, index) => checks.push(check("MAX_WIND", p.maxWindKmh, hours.map(hour => hour.windLevels[index].speedKmh), value => value > p.maxWindKmh, height)));
438    if (p.maxGustKmh !== null) checks.push(check("MAX_GUST", p.maxGustKmh, hours.map(hour => hour.windLevels[0].gustKmh), value => value > p.maxGustKmh, 10));
439    if (p.avoidPrecipitation !== null) checks.push(check("AVOID_PRECIPITATION", p.avoidPrecipitation, hours.map(hour => hour.precipitationMm), value => p.avoidPrecipitation && value > 0));
440    if (p.minTemperatureC !== null) checks.push(check("MIN_TEMPERATURE", p.minTemperatureC, hours.map(hour => hour.temperatureC), value => value < p.minTemperatureC));
441    if (p.maxTemperatureC !== null) checks.push(check("MAX_TEMPERATURE", p.maxTemperatureC, hours.map(hour => hour.temperatureC), value => value > p.maxTemperatureC));
442    const status = !checks.length ? "NOT_CONFIGURED" : checks.some(item => item.status === "EXCEEDANCE_PRESENT") ? "EXCEEDANCE_PRESENT" : checks.some(item => item.status === "UNAVAILABLE") ? "UNAVAILABLE" : "NO_EXCEEDANCE";
443    return { status, checks };
444  }
445  function evaluateDrone(hours, drone) {
446    if (!drone) return { status: "NOT_CONFIGURED", comparisonCompleteness: "NONE", observedMaxWindKmh: null, declaredLimitKmh: null, basis: null, catalogState: "NOT_CONFIGURED" };
447    const values = hours.flatMap(hour => hour.windLevels.map(level => level.speedKmh)).filter(value => value !== null);
448    const expected = hours.length * 3; const completeness = !values.length ? "NONE" : values.length === expected ? "FULL" : "PARTIAL";
449    const observed = values.length ? Math.max(...values) : null;
450    const resistance = drone.windResistance; const declared = resistance ? resistance.valueMs * 3.6 : null;
451    const matchUnavailable = ["NOT_FOUND", "AMBIGUOUS"].includes(drone.matchStatus);
452    const status = matchUnavailable || declared === null || observed === null ? "UNKNOWN" : observed > declared ? "DECLARED_WIND_LIMIT_EXCEEDED" : "WITHIN_AVAILABLE_WIND_DATA";
453    return { status, comparisonCompleteness: completeness, observedMaxWindKmh: observed, declaredLimitKmh: declared,
454      basis: resistance?.basis || null, catalogState: drone.catalog.stale ? "STALE" : "CURRENT" };
455  }
456
457  function legalEvaluation(segment, altitude) {
458    const status = segment.status; const required = segment.authorization.required === true || status === "AUTH_REQUIRED";
459    const max = segment.maxAltitudeMetersAgl; const relation = max === null ? "UNKNOWN" : altitude < max ? "BELOW_LIMIT" : altitude === max ? "AT_LIMIT" : "ABOVE_LIMIT";
460    let placement;
461    if (status === "UNAVAILABLE") placement = "NOT_CANDIDATE";
462    else if (status === "UNKNOWN") placement = "UNKNOWN";
463    else if (required) placement = "REQUIRES_ATTENTION";
464    else if (status === "AVAILABLE_WITH_LIMIT") placement = max === null ? "UNKNOWN" : altitude <= max ? "LIMITED" : "NOT_CANDIDATE";
465    else placement = "CANDIDATE";
466    const refs = items => items.map(item => ({ type: item.type || null, version: item.version == null ? null : String(item.version), reasonCode: item.reasonCode || null })).filter(item => item.type || item.reasonCode).sort((a, b) => stable(a).localeCompare(stable(b)));
467    return { placement, output: { sourceStatus: status, maxAltitudeMetersAgl: max, plannedAltitudeRelation: relation,
468      authorization: { required, resolutionStatus: segment.authorization.resolutionStatus,
469        procedureRefs: refs(segment.authorization.procedures), additionalRequirementRefs: refs(segment.authorization.additionalRequirements),
470        blockingReasonCodes: sortedUnique(segment.authorization.blockingReasons.map(item =>
470 item.reasonCode || item.type).filter(Boolean)) },
471      contributorIds: sortedUnique(segment.contributorIds), reasonCodes: sortedUnique(segment.reasonCodes) } };
472  }
473  function temporalCoverage(hours) { const state = sourceAvailability(hours); return state === "FULL" ? "FULL" : state === "NONE" ? "NONE" : "PARTIAL"; }
474  function fieldQuality(fields) { return Object.values(fields).some(item => item.missing > 0) ? "PARTIAL" : "COMPLETE"; }
475  function reasonsFor({ legal, preferences, drone, droneProjection, temporal, quality, fresh, light }) {
476    const reasons = ["POINT_ONLY", light];
477    if (legal.sourceStatus === "UNAVAILABLE") reasons.push("LEGAL_UNAVAILABLE");
478    if (legal.sourceStatus === "UNKNOWN") reasons.push("LEGAL_UNKNOWN");
479    if (legal.plannedAltitudeRelation === "ABOVE_LIMIT") reasons.push("LEGAL_LIMIT_BELOW_PLANNED_ALTITUDE");
480    if (legal.sourceStatus === "AVAILABLE_WITH_LIMIT" && legal.maxAltitudeMetersAgl === null) reasons.push("LEGAL_LIMIT_UNKNOWN");
481    if (legal.authorization.required) reasons.push("AUTHORIZATION_REQUIRED");
482    preferences.checks.filter(item => item.status === "EXCEEDANCE_PRESENT").forEach(item => reasons.push({ MAX_WIND: "PREFERENCE_WIND_EXCEEDED", MAX_GUST: "PREFERENCE_GUST_EXCEEDED", AVOID_PRECIPITATION: "PREFERENCE_PRECIPITATION_EXCEEDED", MIN_TEMPERATURE: "PREFERENCE_MIN_TEMPERATURE_EXCEEDED", MAX_TEMPERATURE: "PREFERENCE_MAX_TEMPERATURE_EXCEEDED" }[item.criterion]));
483    if (preferences.status === "UNAVAILABLE") reasons.push("PREFERENCE_DATA_UNAVAILABLE");
484    if (drone.status === "NOT_CONFIGURED") reasons.push("DRONE_NOT_SELECTED");
485    if (drone.status === "UNKNOWN") reasons.push(["NOT_FOUND", "AMBIGUOUS"].includes(droneProjection?.matchStatus) ? "DRONE_MATCH_UNAVAILABLE" : "DRONE_WIND_LIMIT_UNAVAILABLE");
486    if (drone.status === "DECLARED_WIND_LIMIT_EXCEEDED") reasons.push("DRONE_WIND_LIMIT_EXCEEDED");
487    if (drone.catalogState === "STALE") reasons.push("DRONE_CATALOG_STALE");
488    if (temporal !== "FULL") reasons.push("WEATHER_PARTIAL");
489    if (quality !== "COMPLETE") reasons.push("WEATHER_FIELDS_MISSING");
490    if (fresh === "STALE") reasons.push("WEATHER_STALE");
491    if (light === "UNKNOWN") reasons.push("LIGHT_UNKNOWN");
492    return orderedReasons(reasons);
493  }
494  function limitationCount(reasons) { return reasons.filter(reason => /LIMIT|EXCEEDED|PARTIAL|STALE|AUTHORIZATION/.test(reason)).length; }
495  function unknownCount({ placement, preferences, drone, temporal, quality, light }) { return [placement, preferences, drone, temporal, quality, light].filter(value => ["UNKNOWN", "UNAVAILABLE", "NONE"].includes(value)).length; }
496  function disposition(placement, preferences, drone, temporal, quality, freshnessValue, light, altitudeSamplingValue) {
497    if (placement === "NOT_CANDIDATE") return "EXCLUDED";
498    if (["REQUIRES_ATTENTION", "UNKNOWN"].includes(placement) || ["EXCEEDANCE_PRESENT", "UNAVAILABLE"].includes(preferences) || ["DECLARED_WIND_LIMIT_EXCEEDED", "UNKNOWN"].includes(drone) || temporal !== "FULL" || quality !== "COMPLETE" || freshnessValue === "STALE" || light === "UNKNOWN" || altitudeSamplingValue !== "EXACT_LEVEL_AVAILABLE") return "DEPRIORITIZED";
499    return "INCLUDED";
500  }
501
502  function rankIndex(axis, value) { const index = RANKS[axis].indexOf(value); return index < 0 ? Number.MAX_SAFE_INTEGER : index; }
503  function compareWindows(a, b) {
504    for (const axis of Object.keys(RANKS)) { const diff = rankIndex(axis, a.rank[axis]) - rankIndex(axis, b.rank[axis]); if (diff) return diff; }
505    if (a.rank.limitationCount !== b.rank.limitationCount) return a.rank.limitationCount - b.rank.limitationCount;
506    if (a.rank.unknownCount !== b.rank.unknownCount) return a.rank.unknownCount - b.rank.unknownCount;
507    if (a.durationMinutes !== b.durationMinutes) return b.durationMinutes - a.durationMinutes;
508    if (a.fromUtc !== b.fromUtc) return a.fromUtc.localeCompare(b.fromUtc);
509    if (a.toUtc !== b.toUtc) return a.toUtc.localeCompare(b.toUtc);
510    return a._semanticFingerprint.localeCompare(b._semanticFingerprint);
511  }
512  function mergeSignature(window) {
513    return stable({ candidateDisposition: window.candidateDisposition, placementBand: window.placementBand,
514      legal: window.legal, preferences: { status: window.preferences.status, checks: window.preferences.checks.map(({ comparableSampleCount, exceedanceCount, missingSampleCount, observedExtreme, ...semantic }) => semantic) },
515      droneTechnical: { status: window.droneTechnical.status, comparisonCompleteness: window.droneTechnical.comparisonCompleteness,
516        declaredLimitKmh: window.droneTechnical.declaredLimitKmh, basis: window.droneTechnical.basis, catalogState: window.droneTechnical.catalogState },
517      light: window.light, coverage: window.coverage, reasonCodes: window.reasonCodes });
518  }
519  function buildWindow(fromMs, toMs, segment, hours, context) {
520    const legal = legalEvaluation(segment, context.mission.altitude); const prefs = evaluatePreferences(hours, context.preferences);
521    const drone = evaluateDrone(hours, context.drone); const facts = weatherFacts(hours); const fields = fieldCoverage(hours);
522    const temporal = temporalCoverage(hours); const quality = fieldQuality(fields); const light = lightAt(context.light, fromMs);
523    const reasons = reasonsFor({ legal: legal.output, preferences: prefs, drone, droneProjection: context.drone, temporal, quality, fresh: context.freshness, light });
524    const candidateDisposition = disposition(legal.placement, prefs.status, drone.status, temporal, quality, context.freshness, light, context.altitudeSampling);
525    const output = { fromUtc: new Date(fromMs).toISOString(), toUtc: new Date(toMs).toISOString(), durationMinutes: (toMs - fromMs) / 60000,
526      candidateDisposition, placementBand: legal.placement,
527      rank: { placement: legal.placement, preferences: prefs.status, droneTechnical: drone.status,
528        legalCompleteness: context.legalCompleteness, weatherTemporalCoverage: temporal, weatherFieldQuality: quality,
529        weatherFreshness: context.freshness, droneCatalog: drone.catalogState, limitationCount: limitationCount(reasons),
530        unknownCount: unknownCount({ placement: legal.placement, preferences: prefs.status, drone: drone.status, temporal, quality, light }) },
531      legal: legal.output, weather: { facts, sourceAvailability: sourceAvailability(hours), fieldCoverage: fields },
532      preferences: prefs, droneTechnical: drone, light: { status: light },
533      coverage: { spatial: "POINT_ONLY", temporal, legalSourceCompleteness: context.legalCompleteness,
534        weatherQuality: quality, weatherFreshness: context.freshness, altitudeSampling: context.altitudeSampling }, reasonCodes: reasons };
535    output._hours = hours; output._segment = segment; output._semanticFingerprint = fingerprint("bww1", { fromUtc: output.fromUtc, toUtc: output.toUtc, signature: mergeSignature(output) });
536    return output;
537  }
538  function stripInternal(window) { const { _hours, _segment, _semanticFingerprint, ...output } = window; return output; }
539
540  function emptyOutput(context, legalInfo, weatherInfo, reasons) {
541    const output = baseOutput(context, legalInfo, weatherInfo, { level: "NONE", reasonCodes: orderedReasons(reasons) }, { atomicIntervalCount: 0, windowCount: 0, includedCount: 0, deprioritizedCount: 0, excludedCount: 0 }, []);
542    output.calculationFingerprint = calculationFingerprint(context, legalInfo, weatherInfo);
543    enforceOutput(output);
544    return output;
545  }
546  function inputLegal(value, currentness) { return value ? { snapshotId: value.id, inputFingerprint: value.mission.inputFingerprint, evaluatedAt: value.evaluatedAt,
547    currentness, sourceCompleteness: value.quality.sourceCompleteness, engineVersion: value.engine.version } : null; }
548  function inputWeather(value, currentness, fresh) { return value ? { snapshotId: value.id, targetFingerprint: value.target.inputFingerprint,
549    dataFingerprint: value.dataFingerprint, dataRetrievedAt: value.provenance.dataRetrievedAt, currentness, freshness: fresh,
550    temporalCoverage: value.coverage.temporal.status === "FULL_TEMPORAL_COVERAGE" ? "FULL" : "PARTIAL", quality: value.quality.status,
551    engineVersion: value.engine.version } : null; }
552  function altitudeSampling(snapshot) { return snapshot?.coverage?.altitude?.status || "PROFILE_UNAVAILABLE"; }
553  function baseOutput(context, legalInfo, weatherInfo, availability, summary, windows) {
554    const drone = context.drone;
555    return { schemaVersion: 1, engine: { name: "DSC", version: ENGINE_VERSION }, evaluatedAtUtc: context.evaluated.value,
556      mission: { operatorId: context.mission.operatorId, missionId: context.mission.id, plannedAt: context.mission.from.value,
557        plannedUntil: context.mission.to.value, plannedAltitude: { valueMeters: context.mission.altitude, reference: "AGL" },
558        geometryFingerprint: context.mission.geometryFingerprint, representativePointContractVersion: POINT_VERSION },
559      inputs: { legal: legalInfo, weather: weatherInfo,
560        preferences: { schemaVersion: 1, evaluatorVersion: PREF_VERSION, inputFingerprint: context.preferenceFingerprint,
561          configured: Object.values(context.preferences.weather).some(value => value !== null) },
562        droneTechnical: drone ? { droneId: drone.droneId, contractVersion: drone.contractVersion, matchStatus: drone.matchStatus,
563          resistanceBasis: drone.windResistance?.basis || null, catalogVersion: drone.catalog.catalogVersion,
564          catalogSha256: drone.catalog.sha256, catalogState: drone.catalog.stale ? "STALE" : "CURRENT" } : null,
565        light: { contractVersion: LIGHT_VERSION, source: "SUN_CALC", sourceVersion: "1.9.0", timezone: "Europe/Rome" } },
566      coverage: { spatial: "POINT_ONLY", representativeness: "LIMITED", temporal: weatherInfo?.temporalCoverage || "NONE",
567        altitudeSampling: altitudeSampling(context.weather?.value), weatherResolutionMinutes: 60, interpolation: "NONE" },
568      availability, summary, calculationFingerprint: null, windows };
569  }
570  function calculationFingerprint(context, legalInfo, weatherInfo) {
571    const contributors = legalInfo && context.legal?.value?.contributors?.map(item => ({ id: item.id, sourceType: item.sourceType || null,
572      temporalPolicy: item.temporalPolicy || null, operationalRelevance: item.operationalRelevance || null,
573      maxAltitudeMetersAgl: Number.isFinite(item.maxAltitudeMetersAgl) ? item.maxAltitudeMetersAgl : null,
574      effects: sortedUnique(item.effects), reasonCodes: sortedUnique(item.reasonCodes), warnings: sortedUnique(item.warnings) }))
575      .sort((a, b) => a.id.localeCompare(b.id)) || [];
576    return fingerprint("bw1", { schemaVersion: 1, engineVersion: ENGINE_VERSION, evaluatedAtUtc: context.evaluated.value,
577      mission: { operatorId: context.mission.operatorId, missionId: context.mission.id, legalTargetFingerprint: context.legalTarget,
578        weatherTargetFingerprint: context.weatherTarget }, legal: legalInfo && context.legal?.value ? { inputFingerprint: legalInfo.inputFingerprint,
579          engineVersion: legalInfo.engineVersion,
580          segments: context.legal.value.segments.map(segment => ({ from: segment.from, to: segment.to,
581            legal: legalEvaluation(segment, context.mission.altitude).output })),
582          contributors, sourceCompleteness: context.legal.value.quality.sourceCompleteness } : null,
583      weatherDataFingerprint: weatherInfo?.dataFingerprint || null, preferenceFingerprint: context.preferenceFingerprint,
584      droneTechnical: context.drone, light: { contractVersion: LIGHT_VERSION, sourceVersion: "1.9.0", timezone: "Europe/Rome" } });
585  }
586  function enforceOutput(output) { if (bytes(output) > MAX_OUTPUT_BYTES) limit("BEST_WINDOWS_OUTPUT_TOO_LARGE", "output"); }
587
588  function buildMissionBestWindowsAtomicTimeline(context, legal, weather) {
589    const { mission } = context;
590    const timeline = temporalCall(() => temporalCore().buildMissionAtomicTimeline({
591      schemaVersion: 1,
592      interval: { fromUtc: mission.from.value, toUtc: mission.to.value },
593      legalSegments: legal.segments.map(segment => ({ fromUtc: segment.from, toUtc: segment.to })),
594      weatherSlots: weather.hours.map(hour => ({ fromUtc: hour.overlapFromUtc, toUtc: hour.overlapToUtc })),
595      lightSegments: context.light
596    }), "input.snapshots.timeline");
597    return timeline.atomicIntervals.map(atomic => buildWindow(
598      Date.parse(atomic.fromUtc),
599      Date.parse(atomic.toUtc),
600      legal.segments[atomic.legalSegmentIndex],
601      [weather.hours[atomic.weatherSlotIndex]],
602      context
603    ));
604  }
605
606  function computeMissionBestWindows(input) {
607    assertPlain(input);
608    if (bytes(input) > MAX_INPUT_BYTES) fail("INPUT_TOO_LARGE", "input");
609    exactKeys(input, ["schemaVersion", "evaluatedAtUtc", "mission", "legalSnapshot", "weatherSnapshot", "preferences", "droneTechnical"], "input");
610    if (input.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.schemaVersion");
611    const evaluated = iso(input.evaluatedAtUtc, "input.evaluatedAtUtc");
612    const mission = validateMission(input.mission); const preferences = validatePreferences(input.preferences);
612 const drone = validateDrone(input.droneTechnical);
613    const legal = validateLegal(input.legalSnapshot); const weather = validateWeather(input.weatherSnapshot);
614    const context = { evaluated, mission, preferences, drone, legal, weather, preferenceFingerprint: currentPreferenceFingerprint(preferences),
615      legalTarget: legalFingerprint(mission), weatherTarget: weatherFingerprint(mission) };
616    if ((legal && (legal.value.operatorId !== mission.operatorId || legal.value.missionId !== mission.id)) || (weather && (weather.value.operatorId !== mission.operatorId || weather.value.missionId !== mission.id))) {
617      context.light = lightSegments(mission);
618      return emptyOutput(context, null, null, ["INPUT_SCOPE_MISMATCH"]);
619    }
620    const legalCurrent = !!legal && legal.value.mission.inputFingerprint === context.legalTarget;
621    const weatherCurrent = !!weather && weather.value.target.inputFingerprint === context.weatherTarget;
622    const fresh = weather ? freshness(weather.value, evaluated.ms).value : null;
623    const legalInfo = inputLegal(legal?.value, legalCurrent ? "CURRENT" : "NOT_CURRENT");
624    const weatherInfo = inputWeather(weather?.value, weatherCurrent ? "CURRENT" : "NOT_CURRENT", fresh);
625    context.light = lightSegments(mission);
626    if (!legalCurrent || !weatherCurrent) {
627      const reasons = [];
628      if (!legal) reasons.push("LEGAL_SNAPSHOT_MISSING"); else if (!legalCurrent) reasons.push("LEGAL_SNAPSHOT_NOT_CURRENT");
629      if (!weather) reasons.push("WEATHER_SNAPSHOT_MISSING"); else if (!weatherCurrent) reasons.push("WEATHER_SNAPSHOT_NOT_CURRENT");
630      return emptyOutput(context, legalInfo, weatherInfo, reasons);
631    }
632    context.freshness = fresh; context.legalCompleteness = legal.value.quality.sourceCompleteness;
633    context.altitudeSampling = altitudeSampling(weather.value);
634    const atomics = buildMissionBestWindowsAtomicTimeline(context, legal, weather);
635    const groups = [];
636    for (const window of atomics) {
637      const signature = mergeSignature(window); const previous = groups[groups.length - 1];
638      if (previous && previous.toMs === Date.parse(window.fromUtc) && previous.signature === signature) {
639        previous.toMs = Date.parse(window.toUtc);
640        window._hours.forEach(hour => previous.hours.set(hour.slotFromUtc, hour));
641      } else {
642        groups.push({ fromMs: Date.parse(window.fromUtc), toMs: Date.parse(window.toUtc), signature,
643          segment: window._segment, hours: new Map(window._hours.map(hour => [hour.slotFromUtc, hour])) });
644      }
645    }
646    const merged = groups.map(group => buildWindow(group.fromMs, group.toMs, group.segment,
647      [...group.hours.values()].sort((a, b) => a.slotFromMs - b.slotFromMs), context));
648    merged.sort(compareWindows);
649    if (merged.length > MAX_WINDOWS) limit("BEST_WINDOWS_TOO_MANY", "output.windows");
650    const windows = merged.map(stripInternal);
651    const summary = { atomicIntervalCount: atomics.length, windowCount: windows.length,
652      includedCount: windows.filter(item => item.candidateDisposition === "INCLUDED").length,
653      deprioritizedCount: windows.filter(item => item.candidateDisposition === "DEPRIORITIZED").length,
654      excludedCount: windows.filter(item => item.candidateDisposition === "EXCLUDED").length };
655    const partial = context.altitudeSampling !== "EXACT_LEVEL_AVAILABLE" || windows.some(item => item.placementBand === "UNKNOWN" ||
656      item.preferences.status === "UNAVAILABLE" || item.coverage.temporal !== "FULL" ||
657      item.coverage.legalSourceCompleteness !== "COMPLETE" || item.coverage.weatherQuality !== "COMPLETE" ||
658      item.light.status === "UNKNOWN" || (drone && ["UNKNOWN", "PARTIAL", "NONE"].includes(
659        item.droneTechnical.status === "UNKNOWN" ? "UNKNOWN" : item.droneTechnical.comparisonCompleteness)));
660    const availability = { level: partial ? "PARTIAL" : "FULL", reasonCodes: orderedReasons(windows.flatMap(item => item.reasonCodes)) };
661    const output = baseOutput(context, legalInfo, weatherInfo, availability, summary, windows);
662    output.calculationFingerprint = calculationFingerprint(context, legalInfo, weatherInfo);
663    enforceOutput(output);
664    return output;
665  }
666
667  function prepareMissionTimelineInput(input) {
668    assertPlain(input);
669    if (bytes(input) > MAX_INPUT_BYTES) fail("INPUT_TOO_LARGE", "input");
670    exactKeys(input, ["schemaVersion", "evaluatedAtUtc", "mission", "legalSnapshot", "weatherSnapshot", "preferences", "droneTechnical"], "input");
671    if (input.schemaVersion !== 1) fail("UNSUPPORTED_SCHEMA", "input.schemaVersion");
672    const evaluated = iso(input.evaluatedAtUtc, "input.evaluatedAtUtc");
673    const mission = validateMission(input.mission); const preferences = validatePreferences(input.preferences);
673 const drone = validateDrone(input.droneTechnical);
674    let legal = validateLegal(input.legalSnapshot); let weather = validateWeather(input.weatherSnapshot);
675    const legalTarget = legalFingerprint(mission); const weatherTarget = weatherFingerprint(mission);
676    if (legal && (legal.value.operatorId !== mission.operatorId || legal.value.missionId !== mission.id)) legal = null;
677    if (weather && (weather.value.operatorId !== mission.operatorId || weather.value.missionId !== mission.id)) weather = null;
678    const legalCurrent = !!legal && legal.value.mission.inputFingerprint === legalTarget;
679    const weatherCurrent = !!weather && weather.value.target.inputFingerprint === weatherTarget;
680    const fresh = weather ? freshness(weather.value, evaluated.ms) : null;
681    return {
682      evaluated, mission, preferences, drone, legal, weather, legalTarget, weatherTarget,
683      legalCurrent, weatherCurrent, fresh,
684      light: lightSegments(mission),
685      legalCompleteness: legal?.value?.quality?.sourceCompleteness || null,
686      altitudeSampling: altitudeSampling(weather?.value)
687    };
688  }
689
690  const timelineSupport = Object.freeze({
691    prepareMissionTimelineInput,
692    stable,
693    orderedReasons,
694    legalEvaluation,
695    evaluatePreferences,
696    evaluateDrone,
697    weatherFacts,
698    fieldCoverage,
699    fieldQuality,
700    sourceAvailability,
701    temporalCoverage
702  });
703
704  return Object.freeze({ computeMissionBestWindows, MissionBestWindowsInputError, MissionBestWindowsLimitError, timelineSupport });
705});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.