@mostlyrightmd/core 0.1.0-rc.7

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/discovery/index.cjs +1646 -0
  4. package/dist/discovery/index.cjs.map +1 -0
  5. package/dist/discovery/index.d.cts +313 -0
  6. package/dist/discovery/index.d.ts +313 -0
  7. package/dist/discovery/index.mjs +1609 -0
  8. package/dist/discovery/index.mjs.map +1 -0
  9. package/dist/formats/index.cjs +498 -0
  10. package/dist/formats/index.cjs.map +1 -0
  11. package/dist/formats/index.d.cts +97 -0
  12. package/dist/formats/index.d.ts +97 -0
  13. package/dist/formats/index.mjs +465 -0
  14. package/dist/formats/index.mjs.map +1 -0
  15. package/dist/index.cjs +1624 -0
  16. package/dist/index.cjs.map +1 -0
  17. package/dist/index.d.cts +559 -0
  18. package/dist/index.d.ts +559 -0
  19. package/dist/index.global.js +1582 -0
  20. package/dist/index.global.js.map +1 -0
  21. package/dist/index.mjs +1557 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/internal/bounds.cjs +125 -0
  24. package/dist/internal/bounds.cjs.map +1 -0
  25. package/dist/internal/bounds.d.cts +36 -0
  26. package/dist/internal/bounds.d.ts +36 -0
  27. package/dist/internal/bounds.mjs +81 -0
  28. package/dist/internal/bounds.mjs.map +1 -0
  29. package/dist/internal/cache/fs.cjs +217 -0
  30. package/dist/internal/cache/fs.cjs.map +1 -0
  31. package/dist/internal/cache/fs.d.cts +57 -0
  32. package/dist/internal/cache/fs.d.ts +57 -0
  33. package/dist/internal/cache/fs.mjs +179 -0
  34. package/dist/internal/cache/fs.mjs.map +1 -0
  35. package/dist/internal/cache/index.browser.cjs +1184 -0
  36. package/dist/internal/cache/index.browser.cjs.map +1 -0
  37. package/dist/internal/cache/index.browser.d.cts +20 -0
  38. package/dist/internal/cache/index.browser.d.ts +20 -0
  39. package/dist/internal/cache/index.browser.mjs +36 -0
  40. package/dist/internal/cache/index.browser.mjs.map +1 -0
  41. package/dist/internal/cache/index.cjs +1389 -0
  42. package/dist/internal/cache/index.cjs.map +1 -0
  43. package/dist/internal/cache/index.d.cts +16 -0
  44. package/dist/internal/cache/index.d.ts +16 -0
  45. package/dist/internal/cache/index.mjs +40 -0
  46. package/dist/internal/cache/index.mjs.map +1 -0
  47. package/dist/internal/chunk-PKJXHY27.mjs +1137 -0
  48. package/dist/internal/chunk-PKJXHY27.mjs.map +1 -0
  49. package/dist/internal/convert.cjs +161 -0
  50. package/dist/internal/convert.cjs.map +1 -0
  51. package/dist/internal/convert.d.cts +44 -0
  52. package/dist/internal/convert.d.ts +44 -0
  53. package/dist/internal/convert.mjs +117 -0
  54. package/dist/internal/convert.mjs.map +1 -0
  55. package/dist/internal/fs-O6XR4WWW.mjs +183 -0
  56. package/dist/internal/fs-O6XR4WWW.mjs.map +1 -0
  57. package/dist/internal/keys-B7C8C88N.d.cts +191 -0
  58. package/dist/internal/keys-B7C8C88N.d.ts +191 -0
  59. package/dist/internal/merge/index.cjs +75 -0
  60. package/dist/internal/merge/index.cjs.map +1 -0
  61. package/dist/internal/merge/index.d.cts +74 -0
  62. package/dist/internal/merge/index.d.ts +74 -0
  63. package/dist/internal/merge/index.mjs +46 -0
  64. package/dist/internal/merge/index.mjs.map +1 -0
  65. package/dist/internal/pairs.cjs +328 -0
  66. package/dist/internal/pairs.cjs.map +1 -0
  67. package/dist/internal/pairs.d.cts +105 -0
  68. package/dist/internal/pairs.d.ts +105 -0
  69. package/dist/internal/pairs.mjs +298 -0
  70. package/dist/internal/pairs.mjs.map +1 -0
  71. package/dist/qc/index.cjs +247 -0
  72. package/dist/qc/index.cjs.map +1 -0
  73. package/dist/qc/index.d.cts +140 -0
  74. package/dist/qc/index.d.ts +140 -0
  75. package/dist/qc/index.mjs +212 -0
  76. package/dist/qc/index.mjs.map +1 -0
  77. package/dist/temporal/index.cjs +504 -0
  78. package/dist/temporal/index.cjs.map +1 -0
  79. package/dist/temporal/index.d.cts +121 -0
  80. package/dist/temporal/index.d.ts +121 -0
  81. package/dist/temporal/index.mjs +474 -0
  82. package/dist/temporal/index.mjs.map +1 -0
  83. package/dist/transforms/index.cjs +399 -0
  84. package/dist/transforms/index.cjs.map +1 -0
  85. package/dist/transforms/index.d.cts +193 -0
  86. package/dist/transforms/index.d.ts +193 -0
  87. package/dist/transforms/index.mjs +362 -0
  88. package/dist/transforms/index.mjs.map +1 -0
  89. package/dist/validator.cjs +1870 -0
  90. package/dist/validator.cjs.map +1 -0
  91. package/dist/validator.d.cts +30 -0
  92. package/dist/validator.d.ts +30 -0
  93. package/dist/validator.mjs +1843 -0
  94. package/dist/validator.mjs.map +1 -0
  95. package/package.json +115 -0
@@ -0,0 +1,212 @@
1
+ // src/data/generated/qc-alpha-rules.ts
2
+ var QC_ALPHA_RULES = [
3
+ {
4
+ bit_position: 0,
5
+ description: "Temperature outside [-89C, 57C] (world-record bounds).",
6
+ field: "temp_c",
7
+ rule_id: "temp_c.out_of_range"
8
+ },
9
+ {
10
+ bit_position: 1,
11
+ description: "Dewpoint greater than temperature (physically impossible).",
12
+ field: "dew_point_c",
13
+ rule_id: "dew_point_c.exceeds_temp"
14
+ },
15
+ {
16
+ bit_position: 2,
17
+ description: "Wind speed negative.",
18
+ field: "wind_speed_ms",
19
+ rule_id: "wind_speed_ms.negative"
20
+ },
21
+ {
22
+ bit_position: 3,
23
+ description: "Wind direction outside [0, 360].",
24
+ field: "wind_dir_deg",
25
+ rule_id: "wind_dir_deg.out_of_range"
26
+ },
27
+ {
28
+ bit_position: 4,
29
+ description: "Sea-level pressure outside [870, 1085] mb.",
30
+ field: "slp_hpa",
31
+ rule_id: "slp_hpa.out_of_range"
32
+ }
33
+ ];
34
+ var QC_ALPHA_RULES_BY_ID = /* @__PURE__ */ new Map([
35
+ ["temp_c.out_of_range", QC_ALPHA_RULES[0]],
36
+ ["dew_point_c.exceeds_temp", QC_ALPHA_RULES[1]],
37
+ ["wind_speed_ms.negative", QC_ALPHA_RULES[2]],
38
+ ["wind_dir_deg.out_of_range", QC_ALPHA_RULES[3]],
39
+ ["slp_hpa.out_of_range", QC_ALPHA_RULES[4]]
40
+ ]);
41
+
42
+ // src/qc/rules.ts
43
+ function getNum(row, col) {
44
+ const v = row[col];
45
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
46
+ }
47
+ function evalTempOutOfRange(rows) {
48
+ return rows.map((r) => {
49
+ const t = getNum(r, "temp_c");
50
+ if (t === null) return false;
51
+ return t < -89 || t > 57;
52
+ });
53
+ }
54
+ function evalDewpointExceedsTemp(rows) {
55
+ return rows.map((r) => {
56
+ const t = getNum(r, "temp_c");
57
+ const dp = getNum(r, "dew_point_c");
58
+ if (t === null || dp === null) return false;
59
+ return dp > t;
60
+ });
61
+ }
62
+ function evalWindSpeedNegative(rows) {
63
+ return rows.map((r) => {
64
+ const v = getNum(r, "wind_speed_ms");
65
+ if (v === null) return false;
66
+ return v < 0;
67
+ });
68
+ }
69
+ function evalWindDirOutOfRange(rows) {
70
+ return rows.map((r) => {
71
+ const v = getNum(r, "wind_dir_deg");
72
+ if (v === null) return false;
73
+ return v < 0 || v > 360;
74
+ });
75
+ }
76
+ function evalSlpOutOfRange(rows) {
77
+ return rows.map((r) => {
78
+ const v = getNum(r, "slp_hpa");
79
+ if (v === null) return false;
80
+ return v < 870 || v > 1085;
81
+ });
82
+ }
83
+ function makeRule(ruleId, evaluate) {
84
+ const spec = QC_ALPHA_RULES_BY_ID.get(ruleId);
85
+ if (spec === void 0) {
86
+ throw new Error(
87
+ `QC rule '${ruleId}' missing from codegen QC_ALPHA_RULES_BY_ID; regenerate via 'pnpm codegen' or align rule IDs.`
88
+ );
89
+ }
90
+ return {
91
+ ruleId: spec.rule_id,
92
+ bitPosition: spec.bit_position,
93
+ description: spec.description,
94
+ field: spec.field,
95
+ evaluate
96
+ };
97
+ }
98
+ var ALPHA_RULES = [
99
+ makeRule("temp_c.out_of_range", evalTempOutOfRange),
100
+ makeRule("dew_point_c.exceeds_temp", evalDewpointExceedsTemp),
101
+ makeRule("wind_speed_ms.negative", evalWindSpeedNegative),
102
+ makeRule("wind_dir_deg.out_of_range", evalWindDirOutOfRange),
103
+ makeRule("slp_hpa.out_of_range", evalSlpOutOfRange)
104
+ ];
105
+ if (QC_ALPHA_RULES.length !== ALPHA_RULES.length) {
106
+ throw new Error(
107
+ `QC codegen drift: QC_ALPHA_RULES has ${QC_ALPHA_RULES.length} entries but ALPHA_RULES has ${ALPHA_RULES.length}. Python Phase 3.5+ may have added rules; add the matching evaluator in qc/rules.ts.`
108
+ );
109
+ }
110
+
111
+ // src/qc/engine.ts
112
+ var QCEngine = class {
113
+ rules;
114
+ constructor(rules = ALPHA_RULES) {
115
+ for (const rule of rules) {
116
+ if (rule.bitPosition < 0 || rule.bitPosition >= 32) {
117
+ throw new RangeError(
118
+ `QCEngine: rule '${rule.ruleId}' bitPosition=${rule.bitPosition} out of 32-bit range; JS bitwise OR supports bits 0-31.`
119
+ );
120
+ }
121
+ }
122
+ this.rules = rules;
123
+ }
124
+ /**
125
+ * Apply all registered rules to `rows`; return new rows with an
126
+ * `obsQcStatus` bitfield column appended.
127
+ *
128
+ * `obsQcStatus[i]` has bit N set iff `this.rules[N].evaluate(rows)[i] === true`.
129
+ * Source rows are NOT mutated; output rows are fresh objects.
130
+ * Empty input → empty output (no throw).
131
+ *
132
+ * Each rule's `evaluate(rows)` is called ONCE (vectorized contract) — the
133
+ * rule sees the full row array and returns a parallel `boolean[]`.
134
+ */
135
+ apply(rows) {
136
+ if (rows.length === 0) return [];
137
+ const masks = this.rules.map((rule) => rule.evaluate(rows));
138
+ const out = [];
139
+ for (let i = 0; i < rows.length; i++) {
140
+ const row = rows[i];
141
+ if (row === void 0) continue;
142
+ let status = 0;
143
+ for (let r = 0; r < this.rules.length; r++) {
144
+ const rule = this.rules[r];
145
+ if (rule === void 0) continue;
146
+ const fired = masks[r]?.[i] === true;
147
+ if (fired) {
148
+ status |= 1 << rule.bitPosition;
149
+ }
150
+ }
151
+ out.push({ ...row, obsQcStatus: status });
152
+ }
153
+ return out;
154
+ }
155
+ };
156
+
157
+ // src/qc/crosscheck.ts
158
+ function crosscheckIemGhcnh(iemRows, ghcnhRows, opts = {}) {
159
+ const tolC = opts.tolC ?? 2;
160
+ if (iemRows.length === 0 || ghcnhRows.length === 0) return [];
161
+ for (const r of iemRows) {
162
+ if (typeof r?.station !== "string" || typeof r?.eventTime !== "string") {
163
+ throw new Error(
164
+ "crosscheckIemGhcnh: iem rows must carry 'station' (string) and 'eventTime' (string) keys"
165
+ );
166
+ }
167
+ }
168
+ for (const r of ghcnhRows) {
169
+ if (typeof r?.station !== "string" || typeof r?.eventTime !== "string") {
170
+ throw new Error(
171
+ "crosscheckIemGhcnh: ghcnh rows must carry 'station' (string) and 'eventTime' (string) keys"
172
+ );
173
+ }
174
+ }
175
+ const iemMap = /* @__PURE__ */ new Map();
176
+ for (const r of iemRows) {
177
+ const key = `${r.station}|${r.eventTime}`;
178
+ iemMap.set(key, r);
179
+ }
180
+ const out = [];
181
+ for (const g of ghcnhRows) {
182
+ const key = `${g.station}|${g.eventTime}`;
183
+ const i = iemMap.get(key);
184
+ if (i === void 0) continue;
185
+ const iT = typeof i.temp_c === "number" && Number.isFinite(i.temp_c) ? i.temp_c : null;
186
+ const gT = typeof g.temp_c === "number" && Number.isFinite(g.temp_c) ? g.temp_c : null;
187
+ if (iT === null || gT === null) continue;
188
+ const delta = Math.abs(iT - gT);
189
+ if (delta > tolC) {
190
+ out.push({
191
+ station: g.station,
192
+ eventTime: g.eventTime,
193
+ tempCIem: iT,
194
+ tempCGhcnh: gT,
195
+ deltaC: delta
196
+ });
197
+ }
198
+ }
199
+ return out;
200
+ }
201
+ export {
202
+ ALPHA_RULES,
203
+ QCEngine,
204
+ QC_ALPHA_RULES,
205
+ crosscheckIemGhcnh,
206
+ evalDewpointExceedsTemp,
207
+ evalSlpOutOfRange,
208
+ evalTempOutOfRange,
209
+ evalWindDirOutOfRange,
210
+ evalWindSpeedNegative
211
+ };
212
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/data/generated/qc-alpha-rules.ts","../../src/qc/rules.ts","../../src/qc/engine.ts","../../src/qc/crosscheck.ts"],"sourcesContent":["// AUTO-GENERATED by @mostlyrightmd/codegen from schemas/qc-alpha-rules.json.\n// DO NOT EDIT — regenerate with: pnpm codegen\n// Last manifest SHA recorded in schemas/EXPORT_MANIFEST.json\n\nexport interface QcAlphaRule {\n rule_id: string;\n bit_position: number;\n description: string;\n field: string;\n}\n\nexport const QC_ALPHA_RULES: ReadonlyArray<QcAlphaRule> = [\n {\n bit_position: 0,\n description: \"Temperature outside [-89C, 57C] (world-record bounds).\",\n field: \"temp_c\",\n rule_id: \"temp_c.out_of_range\",\n },\n {\n bit_position: 1,\n description: \"Dewpoint greater than temperature (physically impossible).\",\n field: \"dew_point_c\",\n rule_id: \"dew_point_c.exceeds_temp\",\n },\n {\n bit_position: 2,\n description: \"Wind speed negative.\",\n field: \"wind_speed_ms\",\n rule_id: \"wind_speed_ms.negative\",\n },\n {\n bit_position: 3,\n description: \"Wind direction outside [0, 360].\",\n field: \"wind_dir_deg\",\n rule_id: \"wind_dir_deg.out_of_range\",\n },\n {\n bit_position: 4,\n description: \"Sea-level pressure outside [870, 1085] mb.\",\n field: \"slp_hpa\",\n rule_id: \"slp_hpa.out_of_range\",\n },\n] as const;\n\nexport const QC_ALPHA_RULES_BY_ID: ReadonlyMap<string, QcAlphaRule> = new Map<string, QcAlphaRule>([\n [\"temp_c.out_of_range\", QC_ALPHA_RULES[0]!],\n [\"dew_point_c.exceeds_temp\", QC_ALPHA_RULES[1]!],\n [\"wind_speed_ms.negative\", QC_ALPHA_RULES[2]!],\n [\"wind_dir_deg.out_of_range\", QC_ALPHA_RULES[3]!],\n [\"slp_hpa.out_of_range\", QC_ALPHA_RULES[4]!],\n]);\n","// TS-W4 Plan 05 Task 1 — 5 alpha QC rule evaluators.\n//\n// Bit positions + rule IDs are CONSUMED from the codegen table at\n// src/data/generated/qc-alpha-rules.ts (NEVER hand-coded). Mirrors Python\n// `packages/core/src/mostlyright/qc.py:53-134`.\n//\n// If a future codegen run adds a new rule, this file MUST be updated to\n// register a matching evaluator — the module-load drift guard fires loud\n// otherwise (see end of file).\n\nimport {\n QC_ALPHA_RULES,\n QC_ALPHA_RULES_BY_ID,\n type QcAlphaRule,\n} from \"../data/generated/qc-alpha-rules.js\";\n\n/**\n * A QC rule: ruleId + bit position (both consumed from the codegen table)\n * plus a per-row evaluator. `evaluate(rows)` returns a `boolean[]` of length\n * === rows.length where `true` means the rule fired for that row.\n */\nexport interface QCRule {\n readonly ruleId: string;\n readonly bitPosition: number;\n readonly description: string;\n readonly field: string;\n evaluate(rows: ReadonlyArray<Record<string, unknown>>): boolean[];\n}\n\n/**\n * Safely read a numeric field from a row. Returns the number only if it's\n * finite (rejects null/undefined/NaN/Infinity/non-numeric). Null/missing/\n * non-finite → null, so the calling rule does NOT fire (Python\n * qc.py:57-58 + notna() parity).\n */\nfunction getNum(row: Record<string, unknown>, col: string): number | null {\n const v = row[col];\n return typeof v === \"number\" && Number.isFinite(v) ? v : null;\n}\n\n/** Bit 0 — Temperature outside [-89C, 57C] (world-record bounds). */\nexport function evalTempOutOfRange(rows: ReadonlyArray<Record<string, unknown>>): boolean[] {\n return rows.map((r) => {\n const t = getNum(r, \"temp_c\");\n if (t === null) return false;\n return t < -89.0 || t > 57.0;\n });\n}\n\n/** Bit 1 — Dewpoint > temperature (physically impossible; strict `>`). */\nexport function evalDewpointExceedsTemp(rows: ReadonlyArray<Record<string, unknown>>): boolean[] {\n return rows.map((r) => {\n const t = getNum(r, \"temp_c\");\n const dp = getNum(r, \"dew_point_c\");\n if (t === null || dp === null) return false;\n return dp > t;\n });\n}\n\n/** Bit 2 — Wind speed negative. */\nexport function evalWindSpeedNegative(rows: ReadonlyArray<Record<string, unknown>>): boolean[] {\n return rows.map((r) => {\n const v = getNum(r, \"wind_speed_ms\");\n if (v === null) return false;\n return v < 0;\n });\n}\n\n/** Bit 3 — Wind direction outside [0, 360] (inclusive). */\nexport function evalWindDirOutOfRange(rows: ReadonlyArray<Record<string, unknown>>): boolean[] {\n return rows.map((r) => {\n const v = getNum(r, \"wind_dir_deg\");\n if (v === null) return false;\n return v < 0 || v > 360;\n });\n}\n\n/** Bit 4 — Sea-level pressure outside [870, 1085] mb. */\nexport function evalSlpOutOfRange(rows: ReadonlyArray<Record<string, unknown>>): boolean[] {\n return rows.map((r) => {\n const v = getNum(r, \"slp_hpa\");\n if (v === null) return false;\n return v < 870 || v > 1085;\n });\n}\n\n/**\n * Build a QCRule by looking up the codegen spec (ruleId, bitPosition,\n * description, field) and binding the per-row evaluator. Throws at module\n * load if the codegen table is missing the rule — protects against the\n * codegen contract drifting out from under the TS implementation.\n */\nfunction makeRule(\n ruleId: string,\n evaluate: (rows: ReadonlyArray<Record<string, unknown>>) => boolean[],\n): QCRule {\n const spec: QcAlphaRule | undefined = QC_ALPHA_RULES_BY_ID.get(ruleId);\n if (spec === undefined) {\n throw new Error(\n `QC rule '${ruleId}' missing from codegen QC_ALPHA_RULES_BY_ID; regenerate via 'pnpm codegen' or align rule IDs.`,\n );\n }\n return {\n ruleId: spec.rule_id,\n bitPosition: spec.bit_position,\n description: spec.description,\n field: spec.field,\n evaluate,\n };\n}\n\n/**\n * The 5 alpha rules, indexed by bit position (0..4). Order matches the\n * codegen QC_ALPHA_RULES (which is sorted by bit_position). Phase 3.5+\n * additions in the codegen table MUST land a matching evaluator here or\n * the drift guard below throws at module load.\n */\nexport const ALPHA_RULES: ReadonlyArray<QCRule> = [\n makeRule(\"temp_c.out_of_range\", evalTempOutOfRange),\n makeRule(\"dew_point_c.exceeds_temp\", evalDewpointExceedsTemp),\n makeRule(\"wind_speed_ms.negative\", evalWindSpeedNegative),\n makeRule(\"wind_dir_deg.out_of_range\", evalWindDirOutOfRange),\n makeRule(\"slp_hpa.out_of_range\", evalSlpOutOfRange),\n];\n\n// Module-load safety net: codegen table must match our evaluator count.\n// If Python Phase 3.5+ adds a new rule (e.g. a 6th entry to\n// schemas/qc-alpha-rules.json → regenerated QC_ALPHA_RULES), the\n// developer must add the matching evaluator here. The drift guard fires\n// loud rather than silently dropping rules.\nif (QC_ALPHA_RULES.length !== ALPHA_RULES.length) {\n throw new Error(\n `QC codegen drift: QC_ALPHA_RULES has ${QC_ALPHA_RULES.length} entries but ALPHA_RULES has ${ALPHA_RULES.length}. Python Phase 3.5+ may have added rules; add the matching evaluator in qc/rules.ts.`,\n );\n}\n\nexport { QC_ALPHA_RULES };\n","// TS-W4 Plan 05 Task 2 — QCEngine: apply alpha rules; emit obsQcStatus bitfield.\n//\n// Mirrors Python `packages/core/src/mostlyright/qc.py:137-160`. The bitfield is a\n// 32-bit signed integer (JS `|` semantics); the alpha rule set uses bits 0-4\n// of 32, leaving ample headroom. Phase 3.5+ additions to QC_ALPHA_RULES are\n// picked up automatically by qc/rules.ts (which registers the matching\n// evaluator) and flow through this engine unchanged.\n\nimport { ALPHA_RULES, type QCRule } from \"./rules.js\";\n\n/**\n * QCEngine — orchestrates per-rule evaluation and OR-aggregates each rule's\n * bit into the per-row `obsQcStatus` bitfield column.\n *\n * Defaults to ALPHA_RULES; custom rule sets can be injected via the\n * constructor (used for testing, future Phase 3.5+ rule additions, or\n * downstream-defined custom rules).\n *\n * The `obsQcStatus` column name is camelCase (TS-side convention) — Python\n * uses snake_case `obs_qc_status`. The wire-format conversion happens at the\n * JSON serializer boundary (TS-W3 jsonDumps handles snake_case for export).\n *\n * JS bitwise OR (`|`) operates on 32-bit signed integers, so this engine\n * accepts rules with bitPosition in [0, 31]. A defensive RangeError fires at\n * construction if any rule violates that ceiling.\n */\nexport class QCEngine {\n readonly rules: ReadonlyArray<QCRule>;\n\n constructor(rules: ReadonlyArray<QCRule> = ALPHA_RULES) {\n // Defensive: bit-31 ceiling for JS 32-bit signed-integer OR.\n for (const rule of rules) {\n if (rule.bitPosition < 0 || rule.bitPosition >= 32) {\n throw new RangeError(\n `QCEngine: rule '${rule.ruleId}' bitPosition=${rule.bitPosition} out of 32-bit range; JS bitwise OR supports bits 0-31.`,\n );\n }\n }\n this.rules = rules;\n }\n\n /**\n * Apply all registered rules to `rows`; return new rows with an\n * `obsQcStatus` bitfield column appended.\n *\n * `obsQcStatus[i]` has bit N set iff `this.rules[N].evaluate(rows)[i] === true`.\n * Source rows are NOT mutated; output rows are fresh objects.\n * Empty input → empty output (no throw).\n *\n * Each rule's `evaluate(rows)` is called ONCE (vectorized contract) — the\n * rule sees the full row array and returns a parallel `boolean[]`.\n */\n apply<Row extends Record<string, unknown>>(\n rows: ReadonlyArray<Row>,\n ): ReadonlyArray<Row & { obsQcStatus: number }> {\n if (rows.length === 0) return [];\n\n // Step 1: evaluate each rule ONCE; collect parallel boolean masks.\n const masks: boolean[][] = this.rules.map((rule) => rule.evaluate(rows));\n\n // Step 2: per-row OR-aggregation.\n const out: Array<Row & { obsQcStatus: number }> = [];\n for (let i = 0; i < rows.length; i++) {\n const row = rows[i];\n if (row === undefined) continue;\n let status = 0;\n for (let r = 0; r < this.rules.length; r++) {\n const rule = this.rules[r];\n if (rule === undefined) continue;\n const fired = masks[r]?.[i] === true;\n if (fired) {\n status |= 1 << rule.bitPosition;\n }\n }\n out.push({ ...row, obsQcStatus: status });\n }\n return out;\n }\n}\n","// TS-W4 Plan 06 — crosscheckIemGhcnh: disagreement detection between IEM +\n// GHCNh temperature readings. Mirrors Python\n// `mostlyright.qc.crosscheck_iem_ghcnh` at\n// `packages/core/src/mostlyright/qc.py:191-228`.\n//\n// Inner-joins by composite key `(station, eventTime)`. For matched pairs\n// where both temp_c values are finite numbers and the absolute delta\n// exceeds `opts.tolC` (default 2.0 °C), emits a disagreement row.\n//\n// Threshold is STRICT `>` (NOT `>=`) per Python qc.py:228 —\n// `merged.loc[merged[\"delta_c\"] > tol_c]`. A delta exactly equal to the\n// tolerance produces NO disagreement.\n//\n// Parity-Ticket: Python returns snake_case keys\n// (event_time, temp_c_iem, temp_c_ghcnh, delta_c); TS returns camelCase\n// (eventTime, tempCIem, tempCGhcnh, deltaC) to match the TS-idiom used\n// elsewhere in the codebase (see `obsQcStatus` from Wave 5). Wire-format\n// conversion to snake_case happens at the JSON serializer boundary\n// (TS-W3 Plan 07 `jsonDumps`).\n//\n// Lives at the `@mostlyrightmd/core/qc` subpath (NOT root barrel) to keep\n// the main `@mostlyrightmd/core` bundle under its 25 KB size-limit gate.\n\n/** Options for {@link crosscheckIemGhcnh}. */\nexport interface CrosscheckOptions {\n /**\n * Maximum acceptable absolute delta in °C between paired IEM/GHCNh\n * `temp_c` values. Defaults to `2.0` °C (matches Python\n * `crosscheck_iem_ghcnh(tol_c=2.0)`). A delta strictly greater than\n * `tolC` produces a disagreement row; equality does NOT.\n */\n tolC?: number;\n}\n\n/**\n * Disagreement row emitted by {@link crosscheckIemGhcnh}. Keys are\n * camelCase per the TS-idiom Parity-Ticket; Python's snake_case\n * equivalents are `event_time`, `temp_c_iem`, `temp_c_ghcnh`, `delta_c`.\n */\nexport interface CrosscheckDisagreement {\n readonly station: string;\n readonly eventTime: string;\n readonly tempCIem: number;\n readonly tempCGhcnh: number;\n readonly deltaC: number;\n}\n\n/**\n * Minimal row shape consumed by {@link crosscheckIemGhcnh}. Rows MUST\n * carry `station: string`, `eventTime: string`, and `temp_c: number |\n * null` (or `undefined`/non-finite, which are skipped). Additional keys\n * are allowed and ignored.\n */\ninterface CrosscheckRowIn {\n station?: unknown;\n eventTime?: unknown;\n temp_c?: unknown;\n}\n\n/**\n * Cross-check IEM and GHCNh temperatures; return rows where the two\n * sources disagree above `opts.tolC` (default 2.0 °C).\n *\n * Algorithm:\n * 1. If `iemRows.length === 0 || ghcnhRows.length === 0` → return `[]`\n * (matches Python qc.py:212-215).\n * 2. Validate `station` + `eventTime` present (string) on every input\n * row; throw `Error` on first violation (parity with Python\n * `ValueError` at qc.py:217-220).\n * 3. Build `iemMap: Map<string, IemRow>` keyed by\n * `${row.station}|${row.eventTime}`. On duplicate keys, LAST iem row\n * wins — deterministic but a documented deviation from Python's\n * `pd.merge` (which would cartesian-product duplicates).\n * 4. For each GHCNh row, look up the matching IEM row by composite key.\n * If missing → skip. If either `temp_c` is null / non-finite →\n * skip.\n * 5. If `Math.abs(iem.temp_c - ghcnh.temp_c) > tolC` → emit a\n * disagreement row. STRICT `>` (NOT `>=`).\n *\n * Output array order matches the iteration order of `ghcnhRows`\n * (deterministic, independent of `iemRows` order).\n *\n * Pure: input arrays are NOT mutated.\n *\n * @param iemRows IEM observation rows.\n * @param ghcnhRows GHCNh observation rows.\n * @param opts Tolerance options. `tolC` default = 2.0.\n * @throws Error if any iem or ghcnh row is missing `station` or\n * `eventTime` (or they are not strings).\n */\nexport function crosscheckIemGhcnh(\n iemRows: ReadonlyArray<CrosscheckRowIn>,\n ghcnhRows: ReadonlyArray<CrosscheckRowIn>,\n opts: CrosscheckOptions = {},\n): ReadonlyArray<CrosscheckDisagreement> {\n const tolC = opts.tolC ?? 2.0;\n\n if (iemRows.length === 0 || ghcnhRows.length === 0) return [];\n\n // Validate column presence upfront (parity with Python ValueError).\n for (const r of iemRows) {\n if (typeof r?.station !== \"string\" || typeof r?.eventTime !== \"string\") {\n throw new Error(\n \"crosscheckIemGhcnh: iem rows must carry 'station' (string) and 'eventTime' (string) keys\",\n );\n }\n }\n for (const r of ghcnhRows) {\n if (typeof r?.station !== \"string\" || typeof r?.eventTime !== \"string\") {\n throw new Error(\n \"crosscheckIemGhcnh: ghcnh rows must carry 'station' (string) and 'eventTime' (string) keys\",\n );\n }\n }\n\n // Build iem lookup map. Last-wins on duplicate (station, eventTime).\n const iemMap = new Map<string, CrosscheckRowIn>();\n for (const r of iemRows) {\n const key = `${r.station as string}|${r.eventTime as string}`;\n iemMap.set(key, r);\n }\n\n const out: CrosscheckDisagreement[] = [];\n for (const g of ghcnhRows) {\n const key = `${g.station as string}|${g.eventTime as string}`;\n const i = iemMap.get(key);\n if (i === undefined) continue;\n const iT = typeof i.temp_c === \"number\" && Number.isFinite(i.temp_c) ? i.temp_c : null;\n const gT = typeof g.temp_c === \"number\" && Number.isFinite(g.temp_c) ? g.temp_c : null;\n if (iT === null || gT === null) continue;\n const delta = Math.abs(iT - gT);\n if (delta > tolC) {\n out.push({\n station: g.station as string,\n eventTime: g.eventTime as string,\n tempCIem: iT,\n tempCGhcnh: gT,\n deltaC: delta,\n });\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}