@tiangong-ai/cli 0.0.50 → 0.0.55

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.
Files changed (218) hide show
  1. package/AGENTS.md +22 -2
  2. package/README.md +200 -7
  3. package/dist/cli.js +10 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/data/builtins.d.ts +1 -0
  6. package/dist/data/builtins.js +40 -0
  7. package/dist/data/builtins.js.map +1 -0
  8. package/dist/data/catalog.d.ts +25 -0
  9. package/dist/data/catalog.js +305 -0
  10. package/dist/data/catalog.js.map +1 -0
  11. package/dist/data/commands.d.ts +9 -0
  12. package/dist/data/commands.js +307 -0
  13. package/dist/data/commands.js.map +1 -0
  14. package/dist/data/connectors/airnow-hourly-observations.d.ts +2 -0
  15. package/dist/data/connectors/airnow-hourly-observations.js +522 -0
  16. package/dist/data/connectors/airnow-hourly-observations.js.map +1 -0
  17. package/dist/data/connectors/airnow-hourly-observations.schemas.d.ts +299 -0
  18. package/dist/data/connectors/airnow-hourly-observations.schemas.js +188 -0
  19. package/dist/data/connectors/airnow-hourly-observations.schemas.js.map +1 -0
  20. package/dist/data/connectors/bluesky-public-posts.d.ts +2 -0
  21. package/dist/data/connectors/bluesky-public-posts.js +705 -0
  22. package/dist/data/connectors/bluesky-public-posts.js.map +1 -0
  23. package/dist/data/connectors/bluesky-public-posts.schemas.d.ts +733 -0
  24. package/dist/data/connectors/bluesky-public-posts.schemas.js +422 -0
  25. package/dist/data/connectors/bluesky-public-posts.schemas.js.map +1 -0
  26. package/dist/data/connectors/epa-eis-records.d.ts +2 -0
  27. package/dist/data/connectors/epa-eis-records.js +566 -0
  28. package/dist/data/connectors/epa-eis-records.js.map +1 -0
  29. package/dist/data/connectors/epa-eis-records.schemas.d.ts +199 -0
  30. package/dist/data/connectors/epa-eis-records.schemas.js +146 -0
  31. package/dist/data/connectors/epa-eis-records.schemas.js.map +1 -0
  32. package/dist/data/connectors/federal-register-documents.d.ts +2 -0
  33. package/dist/data/connectors/federal-register-documents.js +517 -0
  34. package/dist/data/connectors/federal-register-documents.js.map +1 -0
  35. package/dist/data/connectors/federal-register-documents.schemas.d.ts +357 -0
  36. package/dist/data/connectors/federal-register-documents.schemas.js +243 -0
  37. package/dist/data/connectors/federal-register-documents.schemas.js.map +1 -0
  38. package/dist/data/connectors/gdelt-doc-search.d.ts +2 -0
  39. package/dist/data/connectors/gdelt-doc-search.js +658 -0
  40. package/dist/data/connectors/gdelt-doc-search.js.map +1 -0
  41. package/dist/data/connectors/gdelt-doc-search.schemas.d.ts +446 -0
  42. package/dist/data/connectors/gdelt-doc-search.schemas.js +330 -0
  43. package/dist/data/connectors/gdelt-doc-search.schemas.js.map +1 -0
  44. package/dist/data/connectors/gdelt-file-feeds.d.ts +4 -0
  45. package/dist/data/connectors/gdelt-file-feeds.js +591 -0
  46. package/dist/data/connectors/gdelt-file-feeds.js.map +1 -0
  47. package/dist/data/connectors/gdelt-file-feeds.schemas.d.ts +40 -0
  48. package/dist/data/connectors/gdelt-file-feeds.schemas.js +256 -0
  49. package/dist/data/connectors/gdelt-file-feeds.schemas.js.map +1 -0
  50. package/dist/data/connectors/nasa-firms-fire.d.ts +2 -0
  51. package/dist/data/connectors/nasa-firms-fire.js +578 -0
  52. package/dist/data/connectors/nasa-firms-fire.js.map +1 -0
  53. package/dist/data/connectors/nasa-firms-fire.schemas.d.ts +369 -0
  54. package/dist/data/connectors/nasa-firms-fire.schemas.js +238 -0
  55. package/dist/data/connectors/nasa-firms-fire.schemas.js.map +1 -0
  56. package/dist/data/connectors/open-meteo-air-quality.d.ts +2 -0
  57. package/dist/data/connectors/open-meteo-air-quality.js +401 -0
  58. package/dist/data/connectors/open-meteo-air-quality.js.map +1 -0
  59. package/dist/data/connectors/open-meteo-air-quality.schemas.d.ts +335 -0
  60. package/dist/data/connectors/open-meteo-air-quality.schemas.js +225 -0
  61. package/dist/data/connectors/open-meteo-air-quality.schemas.js.map +1 -0
  62. package/dist/data/connectors/open-meteo-flood.d.ts +2 -0
  63. package/dist/data/connectors/open-meteo-flood.js +446 -0
  64. package/dist/data/connectors/open-meteo-flood.js.map +1 -0
  65. package/dist/data/connectors/open-meteo-flood.schemas.d.ts +388 -0
  66. package/dist/data/connectors/open-meteo-flood.schemas.js +227 -0
  67. package/dist/data/connectors/open-meteo-flood.schemas.js.map +1 -0
  68. package/dist/data/connectors/open-meteo-historical-weather.d.ts +2 -0
  69. package/dist/data/connectors/open-meteo-historical-weather.js +468 -0
  70. package/dist/data/connectors/open-meteo-historical-weather.js.map +1 -0
  71. package/dist/data/connectors/open-meteo-historical-weather.schemas.d.ts +420 -0
  72. package/dist/data/connectors/open-meteo-historical-weather.schemas.js +282 -0
  73. package/dist/data/connectors/open-meteo-historical-weather.schemas.js.map +1 -0
  74. package/dist/data/connectors/openaq-air-quality.d.ts +2 -0
  75. package/dist/data/connectors/openaq-air-quality.js +734 -0
  76. package/dist/data/connectors/openaq-air-quality.js.map +1 -0
  77. package/dist/data/connectors/openaq-air-quality.schemas.d.ts +1067 -0
  78. package/dist/data/connectors/openaq-air-quality.schemas.js +512 -0
  79. package/dist/data/connectors/openaq-air-quality.schemas.js.map +1 -0
  80. package/dist/data/connectors/regulations-gov-attachments.d.ts +2 -0
  81. package/dist/data/connectors/regulations-gov-attachments.js +486 -0
  82. package/dist/data/connectors/regulations-gov-attachments.js.map +1 -0
  83. package/dist/data/connectors/regulations-gov-attachments.schemas.d.ts +270 -0
  84. package/dist/data/connectors/regulations-gov-attachments.schemas.js +181 -0
  85. package/dist/data/connectors/regulations-gov-attachments.schemas.js.map +1 -0
  86. package/dist/data/connectors/regulations-gov-comments.d.ts +2 -0
  87. package/dist/data/connectors/regulations-gov-comments.js +718 -0
  88. package/dist/data/connectors/regulations-gov-comments.js.map +1 -0
  89. package/dist/data/connectors/regulations-gov-comments.schemas.d.ts +717 -0
  90. package/dist/data/connectors/regulations-gov-comments.schemas.js +389 -0
  91. package/dist/data/connectors/regulations-gov-comments.schemas.js.map +1 -0
  92. package/dist/data/connectors/usbr-project-records.d.ts +2 -0
  93. package/dist/data/connectors/usbr-project-records.js +467 -0
  94. package/dist/data/connectors/usbr-project-records.js.map +1 -0
  95. package/dist/data/connectors/usbr-project-records.schemas.d.ts +194 -0
  96. package/dist/data/connectors/usbr-project-records.schemas.js +137 -0
  97. package/dist/data/connectors/usbr-project-records.schemas.js.map +1 -0
  98. package/dist/data/connectors/usbr-rise.d.ts +2 -0
  99. package/dist/data/connectors/usbr-rise.js +625 -0
  100. package/dist/data/connectors/usbr-rise.js.map +1 -0
  101. package/dist/data/connectors/usbr-rise.schemas.d.ts +623 -0
  102. package/dist/data/connectors/usbr-rise.schemas.js +407 -0
  103. package/dist/data/connectors/usbr-rise.schemas.js.map +1 -0
  104. package/dist/data/connectors/usgs-water-instantaneous-values.d.ts +2 -0
  105. package/dist/data/connectors/usgs-water-instantaneous-values.js +521 -0
  106. package/dist/data/connectors/usgs-water-instantaneous-values.js.map +1 -0
  107. package/dist/data/connectors/usgs-water-instantaneous-values.schemas.d.ts +496 -0
  108. package/dist/data/connectors/usgs-water-instantaneous-values.schemas.js +309 -0
  109. package/dist/data/connectors/usgs-water-instantaneous-values.schemas.js.map +1 -0
  110. package/dist/data/connectors/youtube-public-content.d.ts +2 -0
  111. package/dist/data/connectors/youtube-public-content.js +974 -0
  112. package/dist/data/connectors/youtube-public-content.js.map +1 -0
  113. package/dist/data/connectors/youtube-public-content.schemas.d.ts +773 -0
  114. package/dist/data/connectors/youtube-public-content.schemas.js +567 -0
  115. package/dist/data/connectors/youtube-public-content.schemas.js.map +1 -0
  116. package/dist/data/contracts.d.ts +374 -0
  117. package/dist/data/contracts.js +10 -0
  118. package/dist/data/contracts.js.map +1 -0
  119. package/dist/data/runtime/artifacts.d.ts +6 -0
  120. package/dist/data/runtime/artifacts.js +103 -0
  121. package/dist/data/runtime/artifacts.js.map +1 -0
  122. package/dist/data/runtime/bounded-http.d.ts +10 -0
  123. package/dist/data/runtime/bounded-http.js +403 -0
  124. package/dist/data/runtime/bounded-http.js.map +1 -0
  125. package/dist/data/runtime/canonical-json.d.ts +3 -0
  126. package/dist/data/runtime/canonical-json.js +59 -0
  127. package/dist/data/runtime/canonical-json.js.map +1 -0
  128. package/dist/data/runtime/credentials.d.ts +14 -0
  129. package/dist/data/runtime/credentials.js +67 -0
  130. package/dist/data/runtime/credentials.js.map +1 -0
  131. package/dist/data/runtime/csv.d.ts +4 -0
  132. package/dist/data/runtime/csv.js +51 -0
  133. package/dist/data/runtime/csv.js.map +1 -0
  134. package/dist/data/runtime/errors.d.ts +22 -0
  135. package/dist/data/runtime/errors.js +101 -0
  136. package/dist/data/runtime/errors.js.map +1 -0
  137. package/dist/data/runtime/execute.d.ts +11 -0
  138. package/dist/data/runtime/execute.js +358 -0
  139. package/dist/data/runtime/execute.js.map +1 -0
  140. package/dist/data/runtime/receipts.d.ts +13 -0
  141. package/dist/data/runtime/receipts.js +38 -0
  142. package/dist/data/runtime/receipts.js.map +1 -0
  143. package/dist/data/schemas/catalog.v1.json +73 -0
  144. package/dist/data/schemas/core-receipt.v1.json +117 -0
  145. package/dist/data/schemas/describe.v1.json +24 -0
  146. package/dist/data/schemas/discovery.v1.json +130 -0
  147. package/dist/data/schemas/doctor.v1.json +40 -0
  148. package/dist/data/schemas/error.v1.json +48 -0
  149. package/dist/data/schemas/manifest.v1.json +174 -0
  150. package/dist/data/schemas/run-request.v1.json +37 -0
  151. package/dist/data/schemas/run-result.v1.json +77 -0
  152. package/dist/data/schemas.d.ts +27 -0
  153. package/dist/data/schemas.js +69 -0
  154. package/dist/data/schemas.js.map +1 -0
  155. package/dist/env.js.map +1 -1
  156. package/dist/errors.js.map +1 -1
  157. package/dist/http.js.map +1 -1
  158. package/dist/kb/config.js.map +1 -1
  159. package/dist/kb/pipeline-health.js.map +1 -1
  160. package/dist/research/orchestration.js +71 -11
  161. package/dist/research/orchestration.js.map +1 -1
  162. package/dist/research/workspace/acquisition.js +70 -5
  163. package/dist/research/workspace/acquisition.js.map +1 -1
  164. package/dist/research/workspace/audit-bundle.js +7 -2
  165. package/dist/research/workspace/audit-bundle.js.map +1 -1
  166. package/dist/research/workspace/broker.js.map +1 -1
  167. package/dist/research/workspace/constants.js.map +1 -1
  168. package/dist/research/workspace/credentials.d.ts +3 -0
  169. package/dist/research/workspace/credentials.js +20 -3
  170. package/dist/research/workspace/credentials.js.map +1 -1
  171. package/dist/research/workspace/data-evidence-adapter.d.ts +60 -0
  172. package/dist/research/workspace/data-evidence-adapter.js +314 -0
  173. package/dist/research/workspace/data-evidence-adapter.js.map +1 -0
  174. package/dist/research/workspace/discovery-status.js +10 -7
  175. package/dist/research/workspace/discovery-status.js.map +1 -1
  176. package/dist/research/workspace/discovery.js +12 -11
  177. package/dist/research/workspace/discovery.js.map +1 -1
  178. package/dist/research/workspace/evidence-ledger.d.ts +8 -1
  179. package/dist/research/workspace/evidence-ledger.js +46 -4
  180. package/dist/research/workspace/evidence-ledger.js.map +1 -1
  181. package/dist/research/workspace/evidence.d.ts +31 -0
  182. package/dist/research/workspace/evidence.js +77 -0
  183. package/dist/research/workspace/evidence.js.map +1 -1
  184. package/dist/research/workspace/executor.js.map +1 -1
  185. package/dist/research/workspace/external-skills.d.ts +49 -49
  186. package/dist/research/workspace/external-skills.js.map +1 -1
  187. package/dist/research/workspace/platform-capabilities.js.map +1 -1
  188. package/dist/research/workspace/preflight.d.ts +4 -4
  189. package/dist/research/workspace/preflight.js +33 -8
  190. package/dist/research/workspace/preflight.js.map +1 -1
  191. package/dist/research/workspace/projects.js +203 -76
  192. package/dist/research/workspace/projects.js.map +1 -1
  193. package/dist/research/workspace/publication.js.map +1 -1
  194. package/dist/research/workspace/research-policy-wizard.js.map +1 -1
  195. package/dist/research/workspace/research-policy.js.map +1 -1
  196. package/dist/research/workspace/runtime.d.ts +7 -0
  197. package/dist/research/workspace/runtime.js +82 -23
  198. package/dist/research/workspace/runtime.js.map +1 -1
  199. package/dist/research/workspace/sanitization.js.map +1 -1
  200. package/dist/research/workspace/schemas.js +1 -1
  201. package/dist/research/workspace/schemas.js.map +1 -1
  202. package/dist/research/workspace/scientific-design.js.map +1 -1
  203. package/dist/research/workspace/scientific-review.js.map +1 -1
  204. package/dist/research/workspace/setup-catalog.d.ts +9 -9
  205. package/dist/research/workspace/setup-catalog.js +2 -2
  206. package/dist/research/workspace/setup-catalog.js.map +1 -1
  207. package/dist/research/workspace/setup-declarative.js.map +1 -1
  208. package/dist/research/workspace/setup-instructions.d.ts +42 -0
  209. package/dist/research/workspace/setup-instructions.js +413 -0
  210. package/dist/research/workspace/setup-instructions.js.map +1 -0
  211. package/dist/research/workspace/setup-invocation.js.map +1 -1
  212. package/dist/research/workspace/setup-wizard.js +7 -0
  213. package/dist/research/workspace/setup-wizard.js.map +1 -1
  214. package/dist/research/workspace/setup.d.ts +18 -15
  215. package/dist/research/workspace/setup.js +68 -3
  216. package/dist/research/workspace/setup.js.map +1 -1
  217. package/dist/research/workspace/workspace.js.map +1 -1
  218. package/package.json +3 -3
@@ -0,0 +1,734 @@
1
+ import { DATA_MANIFEST_SCHEMA_VERSION } from "../contracts.js";
2
+ import { DataRuntimeError } from "../runtime/errors.js";
3
+ import { OPENAQ_LOCATION_SEARCH_INPUT_SCHEMA, OPENAQ_LOCATION_SEARCH_OUTPUT_SCHEMA, OPENAQ_MEASUREMENT_INPUT_SCHEMA, OPENAQ_MEASUREMENT_OUTPUT_SCHEMA, } from "./openaq-air-quality.schemas.js";
4
+ const API_PATH_PREFIX = "/v3/";
5
+ const MAX_MEASUREMENT_WINDOW_DAYS = 366;
6
+ const MAX_BBOX_SQUARE_DEGREES = 3_600;
7
+ export const openAqAirQualityConnector = {
8
+ schemaVersion: DATA_MANIFEST_SCHEMA_VERSION,
9
+ capabilityId: "openaq.air-quality",
10
+ capabilityVersion: "1.0.0",
11
+ minimumCliVersion: "0.0.55",
12
+ provider: { providerId: "openaq", name: "OpenAQ" },
13
+ sourceCategory: "air-quality-observations",
14
+ endpoints: [
15
+ {
16
+ endpointId: "openaq-api",
17
+ baseUrl: "https://api.openaq.org",
18
+ pathPrefixes: [API_PATH_PREFIX],
19
+ allowedMethods: ["GET"],
20
+ allowedContentTypes: ["application/json"],
21
+ },
22
+ ],
23
+ license: {
24
+ name: "OpenAQ Terms and source-specific data licenses",
25
+ url: "https://docs.openaq.org/about/terms",
26
+ restrictions: [
27
+ "Attribute OpenAQ and preserve the attribution and license requirements of each original data provider.",
28
+ "OpenAQ data are provided as-is; users must assess source-specific accuracy, quality, and fitness for purpose.",
29
+ "Do not use the hosted API for abusive, perpetual, or bulk-replication requests that duplicate OpenAQ's core service.",
30
+ ],
31
+ },
32
+ credentials: [
33
+ {
34
+ credentialId: "api-key",
35
+ environmentVariable: "OPENAQ_API_KEY",
36
+ required: true,
37
+ endpointIds: ["openaq-api"],
38
+ injection: { kind: "header", name: "X-API-Key", prefix: "" },
39
+ },
40
+ ],
41
+ limits: {
42
+ timeoutMs: 45_000,
43
+ maxRequestBytes: 2_048,
44
+ maxResponseBytes: 20_000_000,
45
+ maxPages: 10,
46
+ maxRecords: 10_000,
47
+ maxRetries: 2,
48
+ maxRetryDelayMs: 60_000,
49
+ maxRedirects: 1,
50
+ },
51
+ diagnostics: { static: true, live: false },
52
+ freshness: {
53
+ kind: "provider-and-source-dependent",
54
+ description: "OpenAQ update latency and historical completeness vary by original provider; public S3 archive objects are typically delayed relative to the API.",
55
+ },
56
+ limitations: [
57
+ "OpenAQ aggregates heterogeneous provider measurements whose calibration, validation, coverage, and reporting latency differ by source.",
58
+ "The hosted API is rate limited to the provider's published per-minute and per-hour quotas and may change or be discontinued.",
59
+ "Location metadata and sensor availability can change; a discovered sensor ID is not proof of continuous coverage for the requested window.",
60
+ "OpenAQ daily records are precomputed using the sensor location's local-day boundaries; use returned period and coverage metadata rather than assuming UTC calendar days.",
61
+ "This connector does not calculate an AQI, make health or regulatory determinations, or establish compliance with an air-quality standard.",
62
+ ],
63
+ discovery: {
64
+ source: {
65
+ maintainedBy: "OpenAQ",
66
+ summary: "Open, harmonized air-quality location, sensor, and measurement data aggregated from many original providers.",
67
+ description: "OpenAQ's v3 API exposes metadata and measurements from government, research, community, and other provider networks. OpenAQ standardizes access, while provenance, license, instrument method, quality, and coverage remain source specific.",
68
+ coverage: {
69
+ geographic: "Global but uneven, limited to locations and providers represented in OpenAQ at request time.",
70
+ temporal: "Provider-dependent historical and near-current coverage; bounded measurement requests are limited here to 366 days.",
71
+ granularity: "Location and sensor metadata plus raw, hourly, or daily records for one explicit sensor.",
72
+ },
73
+ },
74
+ summary: "Discover bounded OpenAQ monitoring locations and retrieve bounded measurements for one known sensor.",
75
+ description: "This capability provides closed, credentialed OpenAQ v3 operations for filtered location discovery and one-sensor measurement retrieval. It validates pagination and provider metadata, preserves attribution, and rejects unbounded scans.",
76
+ provides: [
77
+ "Filtered location metadata including provider, owner, coordinates, sensor IDs, parameter metadata, time coverage, and per-location licenses.",
78
+ "Raw, hourly, or daily measurements for one sensor and one explicit RFC3339 window, including provider coverage and aggregate summaries when supplied.",
79
+ "OpenAQ measurement flags and explicit nulls for unavailable values, periods, coordinates, summaries, or coverage.",
80
+ "Bounded pagination with stable normalized queries, explicit truncation, and later-page partial-result reporting.",
81
+ ],
82
+ doesNotProvide: [
83
+ "OpenAQ S3 archive listing, bulk archive download, CSV file transfer, or arbitrary API-path execution.",
84
+ "Standalone global catalogs of every OpenAQ country, provider, owner, parameter, license, manufacturer, instrument, or sensor; this operation discovers their metadata only through bounded location results.",
85
+ "Regulatory compliance, official monitoring-network status, health advice, AQI calculation, forecasting, or pollutant-source attribution.",
86
+ "Cross-sensor aggregation, spatial interpolation, unit conversion, deduplication, calibration correction, or guaranteed source completeness.",
87
+ "An API key, quota increase, recurring monitoring, or permission to ignore OpenAQ and original-provider attribution terms.",
88
+ ],
89
+ selectionHints: [
90
+ "Use search-locations first when the relevant OpenAQ sensor ID is unknown, and inspect provider, monitor classification, parameter, license, and coverage metadata before selection.",
91
+ "Choose raw for upstream-reported observations and hourly or daily for OpenAQ's preferred precomputed aggregates; do not mix granularities without an explicit analytical method.",
92
+ "Interpret daily records against the selected location's timezone and the returned period; do not relabel them as UTC calendar-day averages.",
93
+ "Use a separately governed content-download workflow for the delayed public S3 archive or other bulk files.",
94
+ "Verify sensitive environmental, health, or compliance conclusions against the original provider and applicable official authority.",
95
+ "Preserve null values and flagInfo.hasFlags: OpenAQ can return structurally valid records whose measurement, period, summary, coordinates, or coverage is unavailable.",
96
+ ],
97
+ typicalUseCases: [
98
+ "Find PM2.5 sensors for a known country, provider, or bounded study area.",
99
+ "Retrieve a short, explicit raw or aggregate time series for a previously selected sensor.",
100
+ ],
101
+ sourceDocumentation: [
102
+ { title: "OpenAQ API documentation", url: "https://docs.openaq.org/" },
103
+ { title: "OpenAQ locations resource", url: "https://docs.openaq.org/resources/locations" },
104
+ {
105
+ title: "OpenAQ measurements resource",
106
+ url: "https://docs.openaq.org/resources/measurements",
107
+ },
108
+ { title: "OpenAQ API rate limits", url: "https://docs.openaq.org/using-the-api/rate-limits" },
109
+ { title: "OpenAQ terms", url: "https://docs.openaq.org/about/terms" },
110
+ { title: "OpenAQ public archive", url: "https://docs.openaq.org/aws/about" },
111
+ ],
112
+ },
113
+ operations: [
114
+ {
115
+ operationId: "search-locations",
116
+ operationVersion: "1.0.0",
117
+ summary: "Search bounded OpenAQ v3 location and sensor metadata.",
118
+ description: "Requires at least one country, provider, parameter, license, monitor, mobile, center-radius, or bounding-box filter and returns normalized location, sensor, coverage, and attribution metadata.",
119
+ inputSchema: OPENAQ_LOCATION_SEARCH_INPUT_SCHEMA,
120
+ outputSchema: OPENAQ_LOCATION_SEARCH_OUTPUT_SCHEMA,
121
+ execute: executeLocationSearch,
122
+ },
123
+ {
124
+ operationId: "fetch-sensor-measurements",
125
+ operationVersion: "1.0.0",
126
+ summary: "Fetch bounded raw, hourly, or daily measurements for one OpenAQ sensor.",
127
+ description: "Retrieves at most 366 days for one explicit sensor and granularity, validates provider pagination, and normalizes parameter, period, coordinate, summary, and coverage fields.",
128
+ inputSchema: OPENAQ_MEASUREMENT_INPUT_SCHEMA,
129
+ outputSchema: OPENAQ_MEASUREMENT_OUTPUT_SCHEMA,
130
+ execute: executeSensorMeasurements,
131
+ },
132
+ ],
133
+ };
134
+ async function executeLocationSearch(context) {
135
+ const query = normalizeLocationQuery(context.input);
136
+ return executePaged({
137
+ context,
138
+ query,
139
+ path: "/v3/locations",
140
+ queryParameters: (pageNumber) => buildLocationParameters(query, pageNumber),
141
+ normalizeRecord: (value, pageNumber, recordIndex) => normalizeLocation(value, pageNumber, recordIndex),
142
+ sourceId: (pageNumber) => `locations:page:${pageNumber}`,
143
+ source: {
144
+ providerId: "openaq",
145
+ service: "OpenAQ API",
146
+ apiVersion: "v3",
147
+ sourceSpecificTerms: true,
148
+ },
149
+ warnings: [
150
+ "Attribute OpenAQ and preserve each original provider's attribution and license requirements returned with location metadata.",
151
+ "Location, sensor, and monitor classifications are provider-dependent metadata and do not establish regulatory status or measurement quality.",
152
+ ],
153
+ });
154
+ }
155
+ async function executeSensorMeasurements(context) {
156
+ const query = normalizeMeasurementQuery(context.input);
157
+ return executePaged({
158
+ context,
159
+ query,
160
+ path: measurementPath(query),
161
+ queryParameters: (pageNumber) => buildMeasurementParameters(query, pageNumber),
162
+ normalizeRecord: (value, pageNumber, recordIndex) => normalizeMeasurement(value, query, pageNumber, recordIndex),
163
+ sourceId: (pageNumber) => `sensor:${query.sensorId}:${query.granularity}:page:${pageNumber}`,
164
+ source: {
165
+ providerId: "openaq",
166
+ service: "OpenAQ API",
167
+ apiVersion: "v3",
168
+ sourceSpecificTerms: true,
169
+ },
170
+ warnings: [
171
+ "Attribute OpenAQ and the original data provider identified through the selected sensor's location metadata.",
172
+ query.granularity === "raw"
173
+ ? "Raw records reflect upstream reporting and must not be treated as quality-controlled OpenAQ aggregates."
174
+ : `${query.granularity === "hourly" ? "Hourly" : "Daily"} records are OpenAQ precomputed aggregates; preserve the returned coverage fields when interpreting them.`,
175
+ ...(query.granularity === "daily"
176
+ ? [
177
+ "OpenAQ daily aggregates use the sensor location's local-day boundaries; preserve returned periods and do not relabel them as UTC calendar days.",
178
+ ]
179
+ : []),
180
+ "Measurements are provided as-is and do not constitute an AQI, health assessment, or regulatory determination.",
181
+ ],
182
+ });
183
+ }
184
+ async function executePaged(options) {
185
+ const { context } = options;
186
+ const pageSize = requireQueryPageSize(options.query);
187
+ const records = [];
188
+ const pages = [];
189
+ const observations = [];
190
+ let provider = null;
191
+ let stopReason = "completed";
192
+ let failedPage = null;
193
+ let failure;
194
+ for (let pageNumber = 1; pageNumber <= context.limits.maxPages; pageNumber += 1) {
195
+ try {
196
+ const response = await context.http.request({
197
+ endpointId: "openaq-api",
198
+ method: "GET",
199
+ path: options.path,
200
+ query: options.queryParameters(pageNumber),
201
+ credentialId: "api-key",
202
+ });
203
+ const remaining = Math.max(0, context.limits.maxRecords - records.length);
204
+ const parsed = parseProviderPage(response.json(), pageNumber, pageSize, remaining, records.length, options.normalizeRecord);
205
+ validateProviderConsistency(provider, parsed.meta);
206
+ provider ??= parsed.meta;
207
+ observations.push({ ...response.observation, sourceId: options.sourceId(pageNumber) });
208
+ records.push(...parsed.records);
209
+ pages.push({
210
+ pageNumber,
211
+ inputRecords: parsed.rawRecordCount,
212
+ emittedRecords: parsed.records.length,
213
+ });
214
+ const hasMore = providerHasMore(parsed.meta.found, pageNumber, pageSize, parsed.rawRecordCount);
215
+ if (records.length >= context.limits.maxRecords &&
216
+ (hasMore || parsed.rawRecordCount > parsed.records.length)) {
217
+ stopReason = "max-records";
218
+ break;
219
+ }
220
+ if (!hasMore) {
221
+ stopReason = pageNumber === 1 && parsed.rawRecordCount === 0 ? "no-results" : "completed";
222
+ break;
223
+ }
224
+ if (pageNumber >= context.limits.maxPages) {
225
+ stopReason = "max-pages";
226
+ break;
227
+ }
228
+ }
229
+ catch (error) {
230
+ if (isCredentialFailure(error))
231
+ throw error;
232
+ if (pages.length === 0)
233
+ throw normalizeProviderFailure(error);
234
+ failedPage = pageNumber;
235
+ failure = error;
236
+ stopReason = "partial";
237
+ break;
238
+ }
239
+ }
240
+ if (!provider) {
241
+ throw new DataRuntimeError("provider-response-invalid", "OpenAQ pagination completed without provider metadata.");
242
+ }
243
+ const partial = failedPage !== null;
244
+ const truncated = stopReason === "max-pages" || stopReason === "max-records";
245
+ const errors = partial
246
+ ? [
247
+ {
248
+ code: "partial-result",
249
+ message: "A later OpenAQ result page could not be retrieved or validated.",
250
+ retryable: failure instanceof DataRuntimeError ? (failure.options.retryable ?? false) : false,
251
+ userActionRequired: false,
252
+ details: {
253
+ missingPages: [failedPage],
254
+ causeCode: failure instanceof DataRuntimeError ? failure.code : "provider-response-invalid",
255
+ },
256
+ },
257
+ ]
258
+ : [];
259
+ return {
260
+ status: partial ? "partial" : "success",
261
+ data: {
262
+ source: options.source,
263
+ query: options.query,
264
+ provider,
265
+ pages,
266
+ records,
267
+ stopReason,
268
+ },
269
+ summary: {
270
+ recordCount: records.length,
271
+ pageCount: pages.length,
272
+ chunkCount: 0,
273
+ truncated,
274
+ completeness: partial ? "partial" : "complete",
275
+ ...(partial
276
+ ? { missing: [{ kind: "page", identifiers: [String(failedPage)] }] }
277
+ : {}),
278
+ },
279
+ warnings: [
280
+ ...options.warnings,
281
+ ...(truncated ? ["The OpenAQ result stopped at an explicit page or record limit."] : []),
282
+ ],
283
+ errors,
284
+ observations,
285
+ };
286
+ }
287
+ function normalizeLocationQuery(input) {
288
+ const countryIds = normalizeIds(input.countryIds);
289
+ const providerIds = normalizeIds(input.providerIds);
290
+ const parameterIds = normalizeIds(input.parameterIds);
291
+ const licenseIds = normalizeIds(input.licenseIds);
292
+ const center = input.center ? { ...input.center } : null;
293
+ const boundingBox = input.boundingBox ? { ...input.boundingBox } : null;
294
+ if (center && boundingBox) {
295
+ throw new DataRuntimeError("invalid-request", "OpenAQ location search cannot combine center and boundingBox.");
296
+ }
297
+ if (boundingBox) {
298
+ if (boundingBox.east <= boundingBox.west || boundingBox.north <= boundingBox.south) {
299
+ throw new DataRuntimeError("invalid-request", "OpenAQ boundingBox west/south values must be lower than east/north values.");
300
+ }
301
+ const squareDegrees = (boundingBox.east - boundingBox.west) * (boundingBox.north - boundingBox.south);
302
+ if (squareDegrees > MAX_BBOX_SQUARE_DEGREES) {
303
+ throw new DataRuntimeError("invalid-request", "OpenAQ world-scale or excessively broad bounding-box scans are not allowed.", { details: { squareDegrees, maximumSquareDegrees: MAX_BBOX_SQUARE_DEGREES } });
304
+ }
305
+ }
306
+ const countryCode = input.countryCode ?? null;
307
+ const monitor = input.monitor ?? null;
308
+ const mobile = input.mobile ?? null;
309
+ if (!countryCode &&
310
+ countryIds.length === 0 &&
311
+ providerIds.length === 0 &&
312
+ parameterIds.length === 0 &&
313
+ licenseIds.length === 0 &&
314
+ monitor === null &&
315
+ mobile === null &&
316
+ !center &&
317
+ !boundingBox) {
318
+ throw new DataRuntimeError("invalid-request", "OpenAQ location search requires at least one narrowing filter.");
319
+ }
320
+ return {
321
+ countryCode,
322
+ countryIds,
323
+ providerIds,
324
+ parameterIds,
325
+ licenseIds,
326
+ monitor,
327
+ mobile,
328
+ center,
329
+ boundingBox,
330
+ pageSize: input.pageSize ?? 1000,
331
+ sortOrder: input.sortOrder ?? "asc",
332
+ };
333
+ }
334
+ function normalizeMeasurementQuery(input) {
335
+ const start = parseRfc3339(input.startDateTime, "startDateTime");
336
+ const end = parseRfc3339(input.endDateTime, "endDateTime");
337
+ if (start > end) {
338
+ throw new DataRuntimeError("invalid-request", "startDateTime must not follow endDateTime.");
339
+ }
340
+ const windowDays = (end - start) / 86_400_000;
341
+ if (windowDays > MAX_MEASUREMENT_WINDOW_DAYS) {
342
+ throw new DataRuntimeError("invalid-request", `The OpenAQ measurement window must not exceed ${MAX_MEASUREMENT_WINDOW_DAYS} days.`, { details: { windowDays, maximumWindowDays: MAX_MEASUREMENT_WINDOW_DAYS } });
343
+ }
344
+ return {
345
+ sensorId: input.sensorId,
346
+ granularity: input.granularity,
347
+ startDateTime: input.startDateTime,
348
+ endDateTime: input.endDateTime,
349
+ pageSize: input.pageSize ?? 1000,
350
+ };
351
+ }
352
+ function buildLocationParameters(query, pageNumber) {
353
+ return {
354
+ ...(query.boundingBox
355
+ ? {
356
+ bbox: [
357
+ query.boundingBox.west,
358
+ query.boundingBox.south,
359
+ query.boundingBox.east,
360
+ query.boundingBox.north,
361
+ ].join(","),
362
+ }
363
+ : {}),
364
+ ...(query.center
365
+ ? {
366
+ coordinates: `${query.center.latitude},${query.center.longitude}`,
367
+ radius: query.center.radiusMeters,
368
+ }
369
+ : {}),
370
+ ...(query.countryIds.length > 0 ? { countries_id: query.countryIds.join(",") } : {}),
371
+ ...(query.countryCode ? { iso: query.countryCode } : {}),
372
+ ...(query.licenseIds.length > 0 ? { licenses_id: query.licenseIds.join(",") } : {}),
373
+ ...(query.mobile !== null ? { mobile: query.mobile } : {}),
374
+ ...(query.monitor !== null ? { monitor: query.monitor } : {}),
375
+ limit: query.pageSize,
376
+ order_by: "id",
377
+ page: pageNumber,
378
+ ...(query.parameterIds.length > 0 ? { parameters_id: query.parameterIds.join(",") } : {}),
379
+ ...(query.providerIds.length > 0 ? { providers_id: query.providerIds.join(",") } : {}),
380
+ sort_order: query.sortOrder,
381
+ };
382
+ }
383
+ function measurementPath(query) {
384
+ const suffix = query.granularity === "raw"
385
+ ? "measurements"
386
+ : query.granularity === "hourly"
387
+ ? "hours"
388
+ : "days";
389
+ return `/v3/sensors/${query.sensorId}/${suffix}`;
390
+ }
391
+ function buildMeasurementParameters(query, pageNumber) {
392
+ const bounds = query.granularity === "daily"
393
+ ? { date_from: query.startDateTime, date_to: query.endDateTime }
394
+ : { datetime_from: query.startDateTime, datetime_to: query.endDateTime };
395
+ return { ...bounds, limit: query.pageSize, page: pageNumber };
396
+ }
397
+ function parseProviderPage(value, pageNumber, pageSize, remainingRecords, recordOffset, normalizeRecord) {
398
+ const payload = requireObject(value, "OpenAQ response");
399
+ const metaValue = requireObject(payload.meta, "meta");
400
+ const name = requireString(metaValue.name, "meta.name");
401
+ if (name !== "openaq-api")
402
+ throw providerInvalid("meta.name must identify openaq-api.");
403
+ const website = requireString(metaValue.website, "meta.website", true);
404
+ const providerPage = requirePositiveInteger(metaValue.page, "meta.page");
405
+ const providerLimit = requirePositiveInteger(metaValue.limit, "meta.limit");
406
+ if (providerPage !== pageNumber || providerLimit !== pageSize) {
407
+ throw providerInvalid("OpenAQ pagination metadata does not match the requested page and limit.");
408
+ }
409
+ const found = parseFound(metaValue.found);
410
+ const results = requireArray(payload.results, "results");
411
+ if (results.length > pageSize) {
412
+ throw providerInvalid("OpenAQ page records exceed the requested page limit.");
413
+ }
414
+ const records = results
415
+ .slice(0, remainingRecords)
416
+ .map((record, index) => normalizeRecord(record, pageNumber, recordOffset + index));
417
+ return {
418
+ meta: { name: "openaq-api", website, found },
419
+ rawRecordCount: results.length,
420
+ records,
421
+ };
422
+ }
423
+ function normalizeLocation(value, pageNumber, recordIndex) {
424
+ const record = requireObject(value, `results[${recordIndex}]`);
425
+ const country = requireObject(record.country, "country");
426
+ const instruments = requireArray(record.instruments, "instruments").map((item, index) => normalizeEntity(item, `instruments[${index}]`));
427
+ const sensors = requireArray(record.sensors, "sensors").map((item, index) => {
428
+ const sensor = requireObject(item, `sensors[${index}]`);
429
+ return {
430
+ id: requirePositiveInteger(sensor.id, `sensors[${index}].id`),
431
+ name: requireString(sensor.name, `sensors[${index}].name`),
432
+ parameter: normalizeParameter(sensor.parameter, `sensors[${index}].parameter`),
433
+ };
434
+ });
435
+ const licenses = record.licenses === null || record.licenses === undefined
436
+ ? []
437
+ : requireArray(record.licenses, "licenses").map((item, index) => {
438
+ const license = requireObject(item, `licenses[${index}]`);
439
+ const attribution = requireObject(license.attribution, `licenses[${index}].attribution`);
440
+ return {
441
+ id: requirePositiveInteger(license.id, `licenses[${index}].id`),
442
+ name: requireString(license.name, `licenses[${index}].name`),
443
+ attributionName: requireString(attribution.name, `licenses[${index}].attribution.name`),
444
+ attributionUrl: nullableString(attribution.url, `licenses[${index}].attribution.url`),
445
+ dateFrom: nullableDate(license.dateFrom, `licenses[${index}].dateFrom`),
446
+ dateTo: nullableDate(license.dateTo, `licenses[${index}].dateTo`),
447
+ };
448
+ });
449
+ return {
450
+ recordIndex,
451
+ sourcePageNumber: pageNumber,
452
+ locationId: requirePositiveInteger(record.id, "id"),
453
+ name: nullableString(record.name, "name"),
454
+ locality: nullableString(record.locality, "locality"),
455
+ timezone: requireString(record.timezone, "timezone"),
456
+ country: {
457
+ id: country.id === null || country.id === undefined
458
+ ? null
459
+ : requirePositiveInteger(country.id, "country.id"),
460
+ code: requireString(country.code, "country.code"),
461
+ name: requireString(country.name, "country.name"),
462
+ },
463
+ owner: normalizeEntity(record.owner, "owner"),
464
+ provider: normalizeEntity(record.provider, "provider"),
465
+ isMobile: requireBoolean(record.isMobile, "isMobile"),
466
+ isMonitor: requireBoolean(record.isMonitor, "isMonitor"),
467
+ coordinates: normalizeNullableCoordinates(record.coordinates, "coordinates"),
468
+ bounds: normalizeBounds(record.bounds),
469
+ distanceMeters: nullableNonNegativeNumber(record.distance, "distance"),
470
+ datetimeFirstUtc: nullableDateTimeObject(record.datetimeFirst, "datetimeFirst"),
471
+ datetimeLastUtc: nullableDateTimeObject(record.datetimeLast, "datetimeLast"),
472
+ instruments,
473
+ sensors,
474
+ licenses,
475
+ };
476
+ }
477
+ function normalizeMeasurement(value, query, pageNumber, recordIndex) {
478
+ const record = requireObject(value, `results[${recordIndex}]`);
479
+ const flagInfo = requireObject(record.flagInfo, "flagInfo");
480
+ const period = normalizeNullablePeriod(record.period);
481
+ const summaryValue = record.summary === null || record.summary === undefined
482
+ ? null
483
+ : requireObject(record.summary, "summary");
484
+ const coverage = normalizeNullableCoverage(record.coverage);
485
+ return {
486
+ recordIndex,
487
+ sourcePageNumber: pageNumber,
488
+ sensorId: query.sensorId,
489
+ granularity: query.granularity,
490
+ value: nullableFiniteNumber(record.value, "value"),
491
+ flagInfo: { hasFlags: requireBoolean(flagInfo.hasFlags, "flagInfo.hasFlags") },
492
+ parameter: normalizeParameter(record.parameter, "parameter"),
493
+ period,
494
+ coordinates: normalizeNullableCoordinates(record.coordinates, "coordinates"),
495
+ summary: summaryValue
496
+ ? {
497
+ min: nullableFiniteNumber(summaryValue.min, "summary.min"),
498
+ q02: nullableFiniteNumber(summaryValue.q02, "summary.q02"),
499
+ q25: nullableFiniteNumber(summaryValue.q25, "summary.q25"),
500
+ median: nullableFiniteNumber(summaryValue.median, "summary.median"),
501
+ q75: nullableFiniteNumber(summaryValue.q75, "summary.q75"),
502
+ q98: nullableFiniteNumber(summaryValue.q98, "summary.q98"),
503
+ max: nullableFiniteNumber(summaryValue.max, "summary.max"),
504
+ average: nullableFiniteNumber(summaryValue.avg, "summary.avg"),
505
+ standardDeviation: nullableFiniteNumber(summaryValue.sd, "summary.sd"),
506
+ }
507
+ : null,
508
+ coverage,
509
+ };
510
+ }
511
+ function normalizeNullablePeriod(value) {
512
+ if (value === null || value === undefined)
513
+ return null;
514
+ const period = requireObject(value, "period");
515
+ return {
516
+ label: requireString(period.label, "period.label"),
517
+ interval: requireString(period.interval, "period.interval"),
518
+ datetimeFromUtc: nullableDateTimeObject(period.datetimeFrom, "period.datetimeFrom"),
519
+ datetimeToUtc: nullableDateTimeObject(period.datetimeTo, "period.datetimeTo"),
520
+ };
521
+ }
522
+ function normalizeNullableCoverage(value) {
523
+ if (value === null || value === undefined)
524
+ return null;
525
+ const coverage = requireObject(value, "coverage");
526
+ return {
527
+ expectedCount: requireNonNegativeInteger(coverage.expectedCount, "coverage.expectedCount"),
528
+ expectedInterval: requireString(coverage.expectedInterval, "coverage.expectedInterval"),
529
+ observedCount: requireNonNegativeInteger(coverage.observedCount, "coverage.observedCount"),
530
+ observedInterval: requireString(coverage.observedInterval, "coverage.observedInterval"),
531
+ percentComplete: requirePercentage(coverage.percentComplete, "coverage.percentComplete"),
532
+ percentCoverage: requirePercentage(coverage.percentCoverage, "coverage.percentCoverage"),
533
+ datetimeFromUtc: nullableDateTimeObject(coverage.datetimeFrom, "coverage.datetimeFrom"),
534
+ datetimeToUtc: nullableDateTimeObject(coverage.datetimeTo, "coverage.datetimeTo"),
535
+ };
536
+ }
537
+ function normalizeEntity(value, field) {
538
+ const entity = requireObject(value, field);
539
+ return {
540
+ id: requirePositiveInteger(entity.id, `${field}.id`),
541
+ name: requireString(entity.name, `${field}.name`),
542
+ };
543
+ }
544
+ function normalizeParameter(value, field) {
545
+ const parameter = requireObject(value, field);
546
+ return {
547
+ id: requirePositiveInteger(parameter.id, `${field}.id`),
548
+ name: requireString(parameter.name, `${field}.name`),
549
+ units: requireString(parameter.units, `${field}.units`),
550
+ displayName: nullableString(parameter.displayName, `${field}.displayName`),
551
+ };
552
+ }
553
+ function normalizeNullableCoordinates(value, field) {
554
+ if (value === null || value === undefined)
555
+ return null;
556
+ const coordinates = requireObject(value, field);
557
+ const latitude = nullableFiniteNumber(coordinates.latitude, `${field}.latitude`);
558
+ const longitude = nullableFiniteNumber(coordinates.longitude, `${field}.longitude`);
559
+ if ((latitude !== null && (latitude < -90 || latitude > 90)) ||
560
+ (longitude !== null && (longitude < -180 || longitude > 180))) {
561
+ throw providerInvalid(`${field} falls outside WGS84 coordinate bounds.`);
562
+ }
563
+ return { latitude, longitude };
564
+ }
565
+ function normalizeBounds(value) {
566
+ const values = requireArray(value, "bounds");
567
+ if (values.length !== 4)
568
+ throw providerInvalid("bounds must contain exactly four numbers.");
569
+ return [
570
+ requireFiniteNumber(values[0], "bounds[0]"),
571
+ requireFiniteNumber(values[1], "bounds[1]"),
572
+ requireFiniteNumber(values[2], "bounds[2]"),
573
+ requireFiniteNumber(values[3], "bounds[3]"),
574
+ ];
575
+ }
576
+ function requireDateTimeObject(value, field) {
577
+ const datetime = requireObject(value, field);
578
+ const utc = requireString(datetime.utc, `${field}.utc`);
579
+ parseProviderRfc3339(utc, `${field}.utc`);
580
+ return utc;
581
+ }
582
+ function nullableDateTimeObject(value, field) {
583
+ if (value === null || value === undefined)
584
+ return null;
585
+ return requireDateTimeObject(value, field);
586
+ }
587
+ function normalizeIds(values) {
588
+ if (!values)
589
+ return [];
590
+ return [...new Set(values)].sort((left, right) => left - right);
591
+ }
592
+ function requireQueryPageSize(value) {
593
+ if (!value || typeof value !== "object" || !("pageSize" in value)) {
594
+ throw new DataRuntimeError("internal-error", "OpenAQ normalized query lacks pageSize.");
595
+ }
596
+ const pageSize = value.pageSize;
597
+ if (!Number.isInteger(pageSize) || pageSize < 1) {
598
+ throw new DataRuntimeError("internal-error", "OpenAQ normalized pageSize is invalid.");
599
+ }
600
+ return pageSize;
601
+ }
602
+ function validateProviderConsistency(current, next) {
603
+ if (!current)
604
+ return;
605
+ if (current.name !== next.name ||
606
+ current.website !== next.website ||
607
+ JSON.stringify(current.found) !== JSON.stringify(next.found)) {
608
+ throw providerInvalid("OpenAQ pagination metadata changed between pages.");
609
+ }
610
+ }
611
+ function providerHasMore(found, pageNumber, pageSize, rawRecordCount) {
612
+ const numericFound = typeof found === "number"
613
+ ? found
614
+ : typeof found === "string" && /^\d+$/.test(found)
615
+ ? Number(found)
616
+ : null;
617
+ return numericFound === null ? rawRecordCount >= pageSize : pageNumber * pageSize < numericFound;
618
+ }
619
+ function parseFound(value) {
620
+ if (value === null || value === undefined)
621
+ return null;
622
+ if (typeof value === "number") {
623
+ if (!Number.isSafeInteger(value) || value < 0) {
624
+ throw providerInvalid("meta.found must be a non-negative safe integer.");
625
+ }
626
+ return value;
627
+ }
628
+ if (typeof value === "string" && value.trim())
629
+ return value.trim();
630
+ throw providerInvalid("meta.found must be a number, non-empty string, or null.");
631
+ }
632
+ function requireObject(value, field) {
633
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
634
+ throw providerInvalid(`${field} must be an object.`);
635
+ }
636
+ return value;
637
+ }
638
+ function requireArray(value, field) {
639
+ if (!Array.isArray(value))
640
+ throw providerInvalid(`${field} must be an array.`);
641
+ return value;
642
+ }
643
+ function requireString(value, field, allowEmpty = false) {
644
+ if (typeof value !== "string" || (!allowEmpty && !value.trim())) {
645
+ throw providerInvalid(`${field} must be ${allowEmpty ? "a string" : "a non-empty string"}.`);
646
+ }
647
+ return value.trim();
648
+ }
649
+ function nullableString(value, field) {
650
+ if (value === null || value === undefined)
651
+ return null;
652
+ return requireString(value, field, true) || null;
653
+ }
654
+ function requireFiniteNumber(value, field) {
655
+ if (typeof value !== "number" || !Number.isFinite(value)) {
656
+ throw providerInvalid(`${field} must be a finite number.`);
657
+ }
658
+ return value;
659
+ }
660
+ function nullableFiniteNumber(value, field) {
661
+ if (value === null || value === undefined)
662
+ return null;
663
+ return requireFiniteNumber(value, field);
664
+ }
665
+ function nullableNonNegativeNumber(value, field) {
666
+ if (value === null || value === undefined)
667
+ return null;
668
+ const number = requireFiniteNumber(value, field);
669
+ if (number < 0)
670
+ throw providerInvalid(`${field} must not be negative.`);
671
+ return number;
672
+ }
673
+ function requirePositiveInteger(value, field) {
674
+ if (!Number.isSafeInteger(value) || value < 1) {
675
+ throw providerInvalid(`${field} must be a positive safe integer.`);
676
+ }
677
+ return value;
678
+ }
679
+ function requireNonNegativeInteger(value, field) {
680
+ if (!Number.isSafeInteger(value) || value < 0) {
681
+ throw providerInvalid(`${field} must be a non-negative safe integer.`);
682
+ }
683
+ return value;
684
+ }
685
+ function requireBoolean(value, field) {
686
+ if (typeof value !== "boolean")
687
+ throw providerInvalid(`${field} must be a boolean.`);
688
+ return value;
689
+ }
690
+ function requirePercentage(value, field) {
691
+ const percentage = requireFiniteNumber(value, field);
692
+ if (percentage < 0 || percentage > 100) {
693
+ throw providerInvalid(`${field} must be between 0 and 100.`);
694
+ }
695
+ return percentage;
696
+ }
697
+ function nullableDate(value, field) {
698
+ if (value === null || value === undefined)
699
+ return null;
700
+ const date = requireString(value, field);
701
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) {
702
+ throw providerInvalid(`${field} must use YYYY-MM-DD.`);
703
+ }
704
+ const parsed = new Date(`${date}T00:00:00Z`);
705
+ if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== date) {
706
+ throw providerInvalid(`${field} must be a valid calendar date.`);
707
+ }
708
+ return date;
709
+ }
710
+ function parseRfc3339(value, field) {
711
+ const parsed = Date.parse(value);
712
+ if (!Number.isFinite(parsed)) {
713
+ throw new DataRuntimeError("invalid-request", `${field} must be a valid RFC3339 timestamp.`);
714
+ }
715
+ return parsed;
716
+ }
717
+ function parseProviderRfc3339(value, field) {
718
+ if (!Number.isFinite(Date.parse(value))) {
719
+ throw providerInvalid(`${field} must be a valid RFC3339 timestamp.`);
720
+ }
721
+ }
722
+ function isCredentialFailure(error) {
723
+ return (error instanceof DataRuntimeError &&
724
+ ["credential-invalid", "credential-missing", "provider-auth-blocked"].includes(error.code));
725
+ }
726
+ function normalizeProviderFailure(error) {
727
+ if (error instanceof DataRuntimeError)
728
+ return error;
729
+ return new DataRuntimeError("provider-response-invalid", "The OpenAQ response could not be retrieved, parsed, or validated.");
730
+ }
731
+ function providerInvalid(message) {
732
+ return new DataRuntimeError("provider-response-invalid", message);
733
+ }
734
+ //# sourceMappingURL=openaq-air-quality.js.map