logiparity 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/README.md +16 -7
- package/docs/configuration.md +28 -5
- package/docs/recipes/socrata.md +38 -0
- package/docs/release.md +1 -1
- package/docs/report-formats.md +6 -2
- package/docs/report-schema.json +32 -6
- package/docs/report-schema.md +6 -4
- package/docs/scenario-report-schema.json +2 -2
- package/lib/adapters/dist/jsonpath.d.ts +2 -2
- package/lib/adapters/dist/jsonpath.js +14 -6
- package/lib/adapters/dist/rest.js +8 -2
- package/lib/cli/dist/version.d.ts +1 -1
- package/lib/cli/dist/version.js +1 -1
- package/lib/core/dist/aggregates.d.ts +8 -3
- package/lib/core/dist/aggregates.js +30 -3
- package/lib/core/dist/calendar.d.ts +3 -0
- package/lib/core/dist/calendar.js +30 -0
- package/lib/core/dist/config.d.ts +33 -6
- package/lib/core/dist/config.js +41 -2
- package/lib/core/dist/engine.js +13 -6
- package/lib/core/dist/groups.d.ts +1 -1
- package/lib/core/dist/groups.js +33 -11
- package/lib/core/dist/html-patterns.d.ts +26 -0
- package/lib/core/dist/html-patterns.js +11 -3
- package/lib/core/dist/html.js +23 -16
- package/lib/core/dist/human.js +30 -3
- package/lib/core/dist/labels.d.ts +12 -3
- package/lib/core/dist/money-display.js +12 -7
- package/lib/core/dist/normalize.js +10 -2
- package/lib/core/dist/reconciliation-display.js +8 -5
- package/lib/core/dist/reconciliation.js +29 -23
- package/lib/core/dist/source-state.d.ts +1 -0
- package/lib/core/dist/source-state.js +2 -1
- package/lib/core/dist/types.d.ts +3 -2
- package/lib/scenarios/dist/config.d.ts +14 -0
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.2.0] - 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Calendar-day comparisons with `type: date`, per-source `dateFormat`/field `format`, and a fixed 1969–2068 two-digit-year window.
|
|
8
|
+
- Opt-in REST `absentAsNull: true` and per-field `missing: null`, retaining strict extraction by default.
|
|
9
|
+
- Exact decimal reconciliation for number sums in every report format; additive `exactValues` retains totals beyond combined JavaScript-number precision.
|
|
10
|
+
- Separate formatting and repeated numeric-change patterns; opt-in `whitespaceFold` for string/enum fields.
|
|
11
|
+
- A warning when page/offset pagination has no completeness metadata, including later short pages, and a Socrata recipe.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- Case/whitespace-only differences retain actual candidate spelling and no longer become stale or vocabulary diagnoses. Numeric fields never receive a vocabulary cause.
|
|
16
|
+
- Consistent enum changes name the field and compare timing/definitions in both sources without assigning blame to the candidate.
|
|
17
|
+
- Validation rejects datetime formats without a time and points to `type: date`.
|
|
18
|
+
- Package verification pins optional peers to the recorded tested versions, matching the browser installed by CI and the offline kit.
|
|
19
|
+
- Local checks permit only this version's exact development changelog heading; release preparation and publication still reject that heading.
|
|
20
|
+
|
|
3
21
|
## [1.1.0] - 2026-11-03
|
|
4
22
|
|
|
5
23
|
### Added
|
package/README.md
CHANGED
|
@@ -28,6 +28,14 @@ An endpoint test can pass while another surface shows different records. A contr
|
|
|
28
28
|
|
|
29
29
|
One configuration compares records across browser tables, APIs, files and SQL. Scenario runs add invariant checks after disruptions, such as whether every order remains present and has one assignment.
|
|
30
30
|
|
|
31
|
+
### When you may not need LogiParity
|
|
32
|
+
|
|
33
|
+
- **A one-off check by someone fluent in SQL.** [DuckDB](https://duckdb.org/docs/) reads CSV, XLSX, JSON and PostgreSQL, and a `FULL OUTER JOIN` on the key finds missing rows and changed values. If you already know which defect you are looking for, a short query is often enough.
|
|
34
|
+
- **Large tables between two databases.** [reladiff](https://github.com/erezsh/reladiff) compares tables by hashing segments, so it does not pull every row to your machine.
|
|
35
|
+
- **Two DataFrames in Python.** [datacompy](https://github.com/capitalone/datacompy) reports row and column differences between pandas or Spark DataFrames.
|
|
36
|
+
|
|
37
|
+
LogiParity is meant for the cases those tools leave to you: reading paginated REST APIs and browser tables, refusing money and time values it cannot parse unambiguously, grouping thousands of differences into a few likely causes with a next step, explaining a total's difference line by line, checking orders after disruptions, and returning CI-friendly exit codes and reports.
|
|
38
|
+
|
|
31
39
|
## Install
|
|
32
40
|
|
|
33
41
|
Use Node.js 22 or later. CI tests Node 22, 24 and 26.
|
|
@@ -49,7 +57,7 @@ npm install --save-dev --save-exact pg@8.23.1 pg-query-stream@4.17.1 pgsql-ast-p
|
|
|
49
57
|
|
|
50
58
|
These are optional peers loaded only for UI or SQL configurations. Missing peers fail with exit 2 and the exact install command. External runtime dependencies are exact-pinned ordinary dependencies, so consumer `overrides` can patch them. See [dependencies and sizes](docs/dependencies.md) and the [offline kit](docs/release.md#offline-installation).
|
|
51
59
|
|
|
52
|
-
Version **1.
|
|
60
|
+
Version **1.2.0** adds calendar dates, opt-in absent REST properties, exact number-total breakdowns and more precise cause labels.
|
|
53
61
|
|
|
54
62
|
## Quickstart
|
|
55
63
|
|
|
@@ -81,7 +89,7 @@ cd logiparity-demo
|
|
|
81
89
|
npx logiparity check --config parity-ui.yaml
|
|
82
90
|
```
|
|
83
91
|
|
|
84
|
-
The login command records a private synthetic session in `logiparity-demo/auth/state.json`; restart invalidates it. The generated directory ignores auth and reports. Download CSV/XLSX as above before the UI check. With `--bugs`, the UI adds the stale-status cause to the API/export checks. `demo login` uses the generated service marker, including its actual port.
|
|
92
|
+
The login command records a private synthetic session in `logiparity-demo/auth/state.json`; restart invalidates it. The generated directory ignores auth and reports. Download CSV/XLSX as above before the UI check. With `--bugs`, the UI adds the stale-status cause to the API/export checks. Its explanation asks to compare refresh timing in both sources and field definitions; the planted demo fixture establishes the UI lag, while this heuristic alone cannot assign blame in real datasets. `demo login` uses the generated service marker, including its actual port.
|
|
85
93
|
|
|
86
94
|
For a source checkout:
|
|
87
95
|
|
|
@@ -178,7 +186,7 @@ npx logiparity validate --config parity.yaml
|
|
|
178
186
|
npx logiparity check --config parity.yaml
|
|
179
187
|
```
|
|
180
188
|
|
|
181
|
-
Read the export and API at the same cutoff time: freeze a snapshot or pass the same `as_of` value to both requests if the target supports it. Downloading them one after another does not freeze changing data. Otherwise compare a closed day after updates have stopped. `totalPath` detects capped pages and confirms completion. Money uses decimal strings by default; opt into numeric input only after reviewing precision. The [configuration reference](docs/configuration.md) also covers SQL, browser sources and `validate --allow-missing-env` for CI lint steps without secrets.
|
|
189
|
+
Read the export and API at the same cutoff time: freeze a snapshot or pass the same `as_of` value to both requests if the target supports it. Downloading them one after another does not freeze changing data. Otherwise compare a closed day after updates have stopped. `totalPath` detects capped pages and confirms completion. Money uses decimal strings by default; opt into numeric input only after reviewing precision. Calendar dates use `type: date`; REST null omission can be accepted explicitly with `absentAsNull: true` or per-field `missing: null`. Reads without pagination metadata warn that completeness needs a separate count check. The [Socrata recipe](docs/recipes/socrata.md) covers these API shapes. The [configuration reference](docs/configuration.md) also covers SQL, browser sources and `validate --allow-missing-env` for CI lint steps without secrets.
|
|
182
190
|
|
|
183
191
|
The [GitHub Actions template](templates/github-actions.yml) installs a pinned CLI, downloads a staging export and retains reports for seven days. Tokens are scoped to the download and comparison steps. Shared staging checks run manually or nightly; enable pull requests only with an isolated target. A [container job](templates/github-actions-container.yml) and a [GitLab job](templates/gitlab-ci.yml) are also available.
|
|
184
192
|
|
|
@@ -208,11 +216,11 @@ The [synthetic freight day](examples/freight/README.md) covers carrier settlemen
|
|
|
208
216
|
|
|
209
217
|
## How it is tested
|
|
210
218
|
|
|
211
|
-
The automated suite passed
|
|
219
|
+
The automated suite passed 593 [unit](https://github.com/Nutts5796/logiparity/tree/main/tests/unit) and [adapter](https://github.com/Nutts5796/logiparity/tree/main/tests/adapters) tests. The 117 passing [end-to-end tests](https://github.com/Nutts5796/logiparity/tree/main/tests/e2e) exercise real Chromium, disposable PostgreSQL and synthetic HTTP services. Coverage includes core, adapters, scenarios and CLI; packages/ai is tested but excluded from this coverage figure. Unit/adapter line coverage is 91.94% (3,321/3,612), above the 80% gate: core 97.14%, adapters 91.01%, scenarios 82.29% and CLI 60.59%. Scenario runner.ts and CLI subprocess behavior are exercised by e2e and installed-package checks and do not increase unit-process coverage.
|
|
212
220
|
|
|
213
|
-
Installed-package checks cover both optional-peer profiles, public imports and declarations, installed demo/scenario commands and empty-cache offline installation.
|
|
221
|
+
Installed-package checks cover both optional-peer profiles, public imports and declarations, installed demo/scenario commands and empty-cache offline installation. The 1.2.0 package check uses npm 11.6.0 for plain npm install, npm ci, npm audit and npm audit signatures in default and optional-peer consumer profiles. The full profile uses the exact recorded tested peers (Playwright 1.64.1), matching the lockfile browser and offline kit; public peer ranges remain unchanged. Historical 1.1.0 verification also covered six npm 10/11 profiles and coexistence with older compatible peers. The clean-source quickstart executed the README commands, including plain npm ci, and checked all report formats.
|
|
214
222
|
|
|
215
|
-
The seeded property-based test [filter-contributions.test.ts](https://github.com/Nutts5796/logiparity/blob/main/tests/unit/filter-contributions.test.ts) checks reconciliation while amounts and filter fields change independently. Regression cases include one-off differences that never become patterns, DST gaps and overlaps, capped pages, duplicate keys, source filters and currency partitions, XLSX amplification, temporary-file cleanup, path escape, redirect refusal and report symlinks. Tests and fixtures are synthetic; these results are not claims about production use. Live publication, Docker builds/image execution and real staging APIs are not tested by this review. Detailed commands and review limits are in the [milestone report](https://github.com/Nutts5796/logiparity/blob/main/docs/development/MILESTONE_REPORT.md).
|
|
223
|
+
The seeded mixed-cause regression [m11-causes.test.ts](https://github.com/Nutts5796/logiparity/blob/main/tests/unit/m11-causes.test.ts) plants case, whitespace, numeric and consistent enum changes together. The seeded property-based test [filter-contributions.test.ts](https://github.com/Nutts5796/logiparity/blob/main/tests/unit/filter-contributions.test.ts) checks reconciliation while amounts and filter fields change independently. Regression cases include one-off differences that never become patterns, DST gaps and overlaps, capped pages, duplicate keys, source filters and currency partitions, XLSX amplification, temporary-file cleanup, path escape, redirect refusal and report symlinks. Tests and fixtures are synthetic; these results are not claims about production use. Live publication, Docker builds/image execution and real staging APIs are not tested by this review. Detailed commands and review limits are in the [milestone report](https://github.com/Nutts5796/logiparity/blob/main/docs/development/MILESTONE_REPORT.md).
|
|
216
224
|
|
|
217
225
|
## How it was built
|
|
218
226
|
|
|
@@ -243,7 +251,7 @@ Test and staging only. Read-only except scenario events you map yourself. No tel
|
|
|
243
251
|
|
|
244
252
|
## Status
|
|
245
253
|
|
|
246
|
-
Version 1.
|
|
254
|
+
Version 1.2.0 — 2026-11-08. Maintained by Sergei Orekhovskii.
|
|
247
255
|
|
|
248
256
|
Pilots and feedback: share a sanitized use case or defect through [Issues](https://github.com/Nutts5796/logiparity/issues).
|
|
249
257
|
|
|
@@ -254,6 +262,7 @@ Sergei Orekhovskii, the maintainer, is a QA engineer who previously ran transpor
|
|
|
254
262
|
## Reference
|
|
255
263
|
|
|
256
264
|
- [Configuration](docs/configuration.md), [scenarios](docs/scenarios.md) and [optional AI drafts](docs/ai.md)
|
|
265
|
+
- [Socrata/FMCSA recipe](docs/recipes/socrata.md)
|
|
257
266
|
- [Reports](docs/report-formats.md), [JSON schema](docs/report-schema.md) and [adapter development](docs/writing-adapters.md)
|
|
258
267
|
- [CI](docs/ci.md), [Docker](docs/docker.md), [FAQ](docs/faq.md) and [release guide](docs/release.md)
|
|
259
268
|
- [Dependencies](docs/dependencies.md), [audit notes](docs/dependency-audit.md) and [build history](https://github.com/Nutts5796/logiparity/blob/main/docs/development/milestones.md)
|
package/docs/configuration.md
CHANGED
|
@@ -45,7 +45,7 @@ Fields map logical names to paths, column headers or selectors. `{path: ..., lab
|
|
|
45
45
|
|
|
46
46
|
## Common source options and filters
|
|
47
47
|
|
|
48
|
-
Every source accepts `label`, `fields`, `where`, `caseSensitive` (false), `unique` (empty), `rollup`, `maxRows` (100,000, maximum 10,000,000), `locale`, `numberFormat`, `datetimeFormat` and `currencyField`. Row limits count physical input records before filtering; exceeding a limit yields a source error. The engine indexes the reference and one candidate in memory. With Node's default heap (about 2 GB on an 8 GB Mac), two 1,000,000-row CSV sources with six columns ran out of memory; the same check passed with `NODE_OPTIONS=--max-old-space-size=6144`. Raise `maxRows` together with the heap size.
|
|
48
|
+
Every source accepts `label`, `fields`, `where`, `caseSensitive` (false), `unique` (empty), `rollup`, `maxRows` (100,000, maximum 10,000,000), `locale`, `numberFormat`, `datetimeFormat`, `dateFormat` and `currencyField`. Row limits count physical input records before filtering; exceeding a limit yields a source error. The engine indexes the reference and one candidate in memory. With Node's default heap (about 2 GB on an 8 GB Mac), two 1,000,000-row CSV sources with six columns ran out of memory; the same check passed with `NODE_OPTIONS=--max-old-space-size=6144`. Raise `maxRows` together with the heap size.
|
|
49
49
|
|
|
50
50
|
`where` combines fields with AND. Values can be scalar equality, a nonempty list, or `{not: value}` / `{not: [values]}`. The same predicate grammar applies to source, rollup and aggregate filters. Text equality is case-insensitive by default; set source `caseSensitive: true` for exact text matching. Typed filter values use their field normalization without comparison tolerances; untyped values use canonical strings. Quote money in major units: `{pay: "10.00"}`. Source filters run before unrelated cells are parsed; an all-filtered source warns with its entity/source and applied predicates.
|
|
51
51
|
|
|
@@ -57,7 +57,7 @@ datetimeFormat: 'dd.MM.yyyy HH:mm'
|
|
|
57
57
|
maxRows: 100000
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
-
A rule's explicit locale, numberFormat or format overrides the source default. Validation checks locales and formats. `unique: [tender_id]` finds a nonblank value reused across distinct keys in one source (`duplicate_value`); with rollups, keep that field in `by` or `first`. A repeated tender alone does not prove a duplicate payment.
|
|
60
|
+
A rule's explicit locale, numberFormat or format overrides the source default. `dateFormat` is the default for calendar-date fields; `datetimeFormat` applies to datetime fields. Validation checks locales and formats. `unique: [tender_id]` finds a nonblank value reused across distinct keys in one source (`duplicate_value`); with rollups, keep that field in `by` or `first`. A repeated tender alone does not prove a duplicate payment.
|
|
61
61
|
|
|
62
62
|
## Source types
|
|
63
63
|
|
|
@@ -71,6 +71,18 @@ A rule's explicit locale, numberFormat or format overrides the source default. V
|
|
|
71
71
|
|
|
72
72
|
REST mappings use supported JSONPath property/index/wildcard selections; unsupported syntax names its source and field. File/SQL mappings use headers/column names. UI mappings are selectors relative to each row; extraction reads rendered text. UI requires Playwright and Chromium; SQL requires the three optional PostgreSQL peers. Missing-peer errors give exact `--save-exact` install commands. Playwright supports `>=1.63.0 <2.0.0`; installed-package tests cover 1.63.0 and the pinned development version 1.64.1. Other peer ranges are in [dependencies](dependencies.md).
|
|
73
73
|
|
|
74
|
+
### Absent REST properties
|
|
75
|
+
|
|
76
|
+
REST mappings are strict by default. If an API omits null-valued JSON properties, opt in for the whole REST source with `absentAsNull: true`, or just one field with `missing: null`:
|
|
77
|
+
|
|
78
|
+
```yaml
|
|
79
|
+
fields:
|
|
80
|
+
id: $.id
|
|
81
|
+
name: {path: $.name, label: Name, missing: null}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
An absent property, including an absent ancestor object, becomes null. Explicit null already maps to null. Wrong object/array shapes, multiple selected values and unreadable pages still fail; missing pagination metadata is never treated as a null field. The setting does not make a missing entity key valid. `missing: null` is a REST-only mapping option; `absentAsNull` defaults to false. The strict error names both opt-ins. Use a per-field opt-in when omitted properties might otherwise hide a misspelled mapping.
|
|
85
|
+
|
|
74
86
|
### REST completion and authentication
|
|
75
87
|
|
|
76
88
|
```yaml
|
|
@@ -84,6 +96,8 @@ pagination:
|
|
|
84
96
|
|
|
85
97
|
Page pagination also supports `start: 1`; offset pagination uses `type: offset`, `offsetParam: offset`, `sizeParam`, `size`. Both support `totalPath`, `totalPagesPath` or `hasMorePath` completion signals. A short first page without a signal fails unless `allowShortFirstPage: true` explicitly opts into an unverified result (warning). Cursor pagination uses `type: cursor`, required `next`, `cursorParam: cursor`, `sizeParam: limit`, `size: 200`; link pagination uses `type: link`. Page sizes are 1–10,000. Exhausted page limits, loops and inconsistent completion fail instead of comparing partial data. Redirects are refused immediately; selected items are counted while decoding, before large arrays materialize.
|
|
86
98
|
|
|
99
|
+
When page/offset pagination has none of those completion signals, a successful read emits one report warning: completeness is not verified. A short later page still ends the read; an empty probe page does too. A matching comparison is not proof of complete extraction. Run a separate count request with the same filter and a stable dataset version, then compare the count with the physical rows read. Cursor/Link endpoints retain their explicit next-pointer termination. The [Socrata recipe](recipes/socrata.md) shows count requests and stable ordering. There is no automatic `countUrl` option in this version.
|
|
100
|
+
|
|
87
101
|
Use `auth: {type: bearer, tokenEnv: STAGING_TOKEN}` or `auth: {type: basic, usernameEnv: STAGING_USER, passwordEnv: STAGING_PASSWORD}`. `headersEnv: {Authorization: {env: STAGING_TOKEN, prefix: 'Bearer '}}` constructs a header; a string names an environment variable containing the complete value. Literal `headers` cannot contain sensitive auth/cookie/API-key headers. Credentials must not be embedded in URLs.
|
|
88
102
|
|
|
89
103
|
### Browser and SQL restrictions
|
|
@@ -96,11 +110,12 @@ SQL accepts the restricted read-only SELECT grammar and bound `params`; the adap
|
|
|
96
110
|
|
|
97
111
|
| Type | Behavior |
|
|
98
112
|
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
-
| string | Trim text; optional `caseFold: true
|
|
100
|
-
| enum | Apply `map: {Delivered: delivered}
|
|
113
|
+
| string | Trim text; optional `caseFold: true` and `whitespaceFold: true` (collapse repeated whitespace to one space). `emptyAsNull: true` by default. |
|
|
114
|
+
| enum | Apply `map: {Delivered: delivered}`, optional case folding and `whitespaceFold`. Unknown values keep their trimmed text. |
|
|
101
115
|
| number | Finite decimals with locale or explicit separators, Latin digits, valid grouping; `tolerance: 0`. Numeric JSON/exponent input is accepted; exponent text and currency symbols are rejected. |
|
|
102
116
|
| money | Required fallback `currency: USD`; decimal text becomes bigint minor units. `allowNumber: false`, `allowCurrencySymbol: false`, `negativeParentheses: false` by default. `tolerance: 0` in integer minor units. Excess nonzero decimal places fail unless `round: half-up` or `round: half-even` is selected. |
|
|
103
117
|
| datetime | ISO 8601 becomes UTC epoch milliseconds; `toleranceSeconds: 0`. Offset-free values require `sourceTimezones`; missing zones, impossible dates, DST gaps and ambiguous wall clocks fail. |
|
|
118
|
+
| date | Calendar day normalized as `yyyy-MM-dd`, without a time zone, epoch or tolerance; see below. |
|
|
104
119
|
|
|
105
120
|
Null compares equal only to null. Empty input becomes null by default; `emptyAsNull: false` preserves empty strings only for string/enum fields. Currency digits follow Intl, not a hard-coded two decimals. Numeric sums use exact canonical decimals before returning a finite number; overflow fails.
|
|
106
121
|
|
|
@@ -120,9 +135,17 @@ Map `currency` in every source using it. A source-level `currencyField` is also
|
|
|
120
135
|
|
|
121
136
|
`allowNumber: true` converts finite numeric values through their canonical decimal text, including exponent notation. Decimal strings preserve original precision and remain preferable: conversion cannot recover precision already lost by JavaScript. For USD, 12.345 fails unless explicit rounding is configured; `0.1 + 0.2` can fail for the same reason. String inputs still use locale parsing. Rounding applies to input normalization, not to a combined total.
|
|
122
137
|
|
|
138
|
+
### Calendar dates
|
|
139
|
+
|
|
140
|
+
Use `type: date` for a calendar day, rather than an instant. The default input is strict ISO `yyyy-MM-dd`; month/year-only, timestamps and zone suffixes fail. `format: yyyyMMdd` or `format: {sms: dd-MMM-yy, census: yyyyMMdd}` selects supported calendar formats by source. The source-level `dateFormat` supplies a fallback; an explicit field format takes precedence. Formats must contain year, month and day and must not contain time or zone tokens. Impossible dates, including invalid leap days, become invalid-value findings. Null follows the normal null rules. Excel serial dates use their recorded calendar day; native JavaScript Date inputs use their UTC day.
|
|
141
|
+
|
|
142
|
+
The `yy` rule is fixed: **00–68 means 2000–2068; 69–99 means 1969–1999**. It is independent of the current year, host time zone and Luxon global settings. Prefer four-digit years when century ambiguity matters. For example, `20250107` and `07-JAN-25` normalize to `2025-01-07`. A date rule rejects time zones, display-zone fields and nonzero time/numeric tolerances. JSON and every human format retain the canonical day. Calendar-day mismatches stay individual findings rather than timezone or vocabulary patterns.
|
|
143
|
+
|
|
144
|
+
String/enum comparison still trims surrounding whitespace by default. Repeated internal whitespace remains significant unless `whitespaceFold: true`; letter case remains significant unless `caseFold: true`. Both fold settings default to false. Formatting patterns preserve the actual normalized spelling and suggest those opt-ins without automatically accepting differences.
|
|
145
|
+
|
|
123
146
|
### Datetimes and facility display
|
|
124
147
|
|
|
125
|
-
`format: 'MM/dd/yyyy HH:mm'` applies a supported Luxon format to every source; `format: {export: 'MM/dd/yyyy HH:mm'}` applies only to that source. Other sources keep ISO parsing. Literal text must be quoted within a format. Explicit Z/±hh:mm offsets are authoritative, including XLSX `t="d"` cells. Naive cells and Excel serials use `sourceTimezones: {export: America/Chicago}`; ambiguous serials require a time zone column or unambiguous text, not a guessed offset.
|
|
148
|
+
Validation rejects a datetime format without time tokens and recommends `type: date`, including a source's effective `datetimeFormat`. `format: 'MM/dd/yyyy HH:mm'` applies a supported Luxon format to every source; `format: {export: 'MM/dd/yyyy HH:mm'}` applies only to that source. Other sources keep ISO parsing. Literal text must be quoted within a format. Explicit Z/±hh:mm offsets are authoritative, including XLSX `t="d"` cells. Naive cells and Excel serials use `sourceTimezones: {export: America/Chicago}`; ambiguous serials require a time zone column or unambiguous text, not a guessed offset.
|
|
126
149
|
|
|
127
150
|
`report.displayTimezone: America/New_York` changes human displays (default UTC), never parsing. A datetime rule's optional `displayTimezoneField: facility_zone` selects a mapped per-row IANA zone from the reference record for finding values and example charts; retain it in rollup `first`. Invalid/absent display metadata falls back to the report zone. This display-only option cannot repair offset-free input; configure source parsing separately. JSON retains normalized epoch milliseconds.
|
|
128
151
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Comparing Socrata data
|
|
2
|
+
|
|
3
|
+
Socrata can omit null-valued JSON properties, return pages without completion metadata and publish calendar dates in different formats. Configure each behavior explicitly. The public FMCSA [Company Census File](https://data.transportation.gov/d/az4n-8mr2) and [SMS motor carrier census](https://data.transportation.gov/d/kjg3-diqy) illustrate these shapes.
|
|
4
|
+
|
|
5
|
+
Use `$order` on the unique key when paging with `$offset`. Order does not freeze a changing dataset: compare the same filter and dataset version, and check update metadata before/after the run. A source fragment for Company Census is:
|
|
6
|
+
|
|
7
|
+
```yaml
|
|
8
|
+
type: rest
|
|
9
|
+
baseUrl: https://data.transportation.gov
|
|
10
|
+
path: "/resource/az4n-8mr2.json?$select=dot_number,power_units,mcs150_date&$where=phy_state='IA'&$order=dot_number"
|
|
11
|
+
items: $[*]
|
|
12
|
+
pagination:
|
|
13
|
+
type: offset
|
|
14
|
+
offsetParam: $offset
|
|
15
|
+
sizeParam: $limit
|
|
16
|
+
size: 5000
|
|
17
|
+
allowShortFirstPage: true
|
|
18
|
+
fields:
|
|
19
|
+
id: $.dot_number
|
|
20
|
+
units: {path: $.power_units, missing: null}
|
|
21
|
+
filing_date: {path: $.mcs150_date, missing: null}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Map these fields in a second source and add entity rules such as `units: {type: number}` and `filing_date: {type: date, format: yyyyMMdd}`. For SMS versus Company Census, date formats can instead be `{sms: dd-MMM-yy, census: yyyyMMdd}`. Two-digit years use the fixed 1969–2068 window. Per-field `missing: null` tolerates absent properties selectively; source `absentAsNull: true` opts in for all mapped fields. Keys must still be present and valid.
|
|
25
|
+
|
|
26
|
+
No total-count metadata is returned in the owner's Socrata observations. `allowShortFirstPage` accepts a small first page but does not verify completeness; a later short page also ends the read. LogiParity emits a warning in each report. Make a separate count request with the same filter and compare it with the physical source count:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
curl --get 'https://data.transportation.gov/resource/az4n-8mr2.json' \
|
|
30
|
+
--data-urlencode '$select=count(*)' \
|
|
31
|
+
--data-urlencode "\$where=phy_state='IA'"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Use the matching `$where` for every source, including an active-record filter where applicable. A count from another dataset version cannot prove the earlier extraction complete. There is no automatic count request in LogiParity 1.2.0.
|
|
35
|
+
|
|
36
|
+
The owner's archived 2026-11-08 investigation compared Iowa and Vermont. Company Census JSON and CSV matched on all 25 selected fields for 58,074 Iowa records and 8,378 Vermont records; independent server-side counts agreed. These are historical observations, not results of the smaller example above or claims about current counts. Both representations came from the same Socrata engine, so this checks extraction consistency rather than registration accuracy.
|
|
37
|
+
|
|
38
|
+
SMS versus active Company Census produced 5,173 Iowa and 856 Vermont findings in that 1.1.0 investigation. Different populations, snapshot timing and field definitions matter: the hazmat definitions differ, and repeated power-unit changes do not imply a vocabulary mapping. Read the data dictionaries before treating one publication as authoritative. This recipe quotes only aggregates from the owner's `fmcsa_parity/findings.md`, with no carrier names or contact fields.
|
package/docs/release.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Release procedure
|
|
2
2
|
|
|
3
|
-
Release only reviewed changes. Version 1.1.0 is dated 2026-11-03; 1.0.1 was never published and its notes are folded into 1.1.0. Local preparation never pushes, tags or publishes. Use `npm run pack:artifact` for the sanitized CLI alone or `npm run release:prepare` for the complete release assets. Direct workspace `npm pack`/`npm publish` is rejected so a manifest with development fields cannot be distributed accidentally; release jobs publish the reviewed tarball.
|
|
3
|
+
Release only reviewed changes. Development/package checks allow only the current version’s exact development heading in `CHANGELOG.md`; `check:release-text -- --publication`, release preparation, release builds and publishing reject it. Replace it with the actual date only after owner approval. Version 1.1.0 is dated 2026-11-03; 1.0.1 was never published and its notes are folded into 1.1.0. Local preparation never pushes, tags or publishes. Use `npm run pack:artifact` for the sanitized CLI alone or `npm run release:prepare` for the complete release assets. Direct workspace `npm pack`/`npm publish` is rejected so a manifest with development fields cannot be distributed accidentally; release jobs publish the reviewed tarball.
|
|
4
4
|
|
|
5
5
|
## One-time setup
|
|
6
6
|
|
package/docs/report-formats.md
CHANGED
|
@@ -6,11 +6,15 @@ HTML is a self-contained offline report with embedded licensed fonts, a strict C
|
|
|
6
6
|
|
|
7
7
|
The hero leads with supported causes and individual review counts. A pattern needs at least `patterns.minCount` findings (default 3, minimum 3). A single overpayment or status mismatch stays under Needs review. Cause cards suggest a next step. Accounted for means each difference is traced to a cause above. Each cause still needs a fix. This status is neutral and does not change `failOn` or the exit code.
|
|
8
8
|
|
|
9
|
-
Money and
|
|
9
|
+
Money, count and number-sum totals show missing, extra and changed-value contributions, supported pattern deltas, uncertainty and an unexplained remainder in HTML, Markdown, console, JSON and JUnit. Number contributions use exact canonical decimal strings; numeric totals also expose `exactValues`, used for display and reconciliation. Totals fully accounted for say Follows from; a nonzero remainder needs review. Blank or duplicate lines make only the affected keys uncertain. Totals reconcile certain keys and list Uncertain lines separately. Unmapped totals say Not mapped; connection failures say Not read. HTML hides empty contribution buckets; text breakdowns retain explicit zero components.
|
|
10
|
+
|
|
11
|
+
Case-only and whitespace-only patterns retain the actual candidate text and suggest explicit normalization. Numeric pairs are repeated numeric changes, never vocabulary mappings. Consistent enum changes suggest comparing refresh timing in both sources and field definitions; a pattern alone cannot identify which system is wrong.
|
|
12
|
+
|
|
13
|
+
Page/offset reads without total-count or other completion metadata include a completeness warning even when every compared record matches. A separate count request can provide an independent check.
|
|
10
14
|
|
|
11
15
|
Charts use each source's unique key count. The rounding waffle names its source and rounds down, with 1 square = 1%. Page-boundary charts show rows kept per page. Timezone fixes name the configuration property without guessing a zone. Source headers show row filters and rolled-up line-to-key counts.
|
|
12
16
|
|
|
13
|
-
`report.displayTimezone` accepts an IANA zone and defaults to UTC. It changes human date display, including the run timestamp; it never changes normalization. Set `compare.<field>.sourceTimezones` to interpret a timezone-free source. JSON
|
|
17
|
+
`report.displayTimezone` accepts an IANA zone and defaults to UTC. It changes human date display, including the run timestamp; it never changes normalization. Set `compare.<field>.sourceTimezones` to interpret a timezone-free source. JSON retains canonical time units; calendar-date fields remain ISO days and have no display zone.
|
|
14
18
|
|
|
15
19
|
Console and Markdown prioritize individual review, then provide examples of supported causes. Markdown uses Markdown escaping, hides zero-count finding types and limits individual rows to 50, with one example per cause. JUnit groups findings by entity and human-readable type; one finding uses singular grammar. Runtime/source errors are distinct from ordinary findings in every format. JUnit `time=0` is a schema placeholder, not measured execution time.
|
|
16
20
|
|
package/docs/report-schema.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.2.0/docs/report-schema.json",
|
|
4
4
|
"title": "LogiParity parity report, schemaVersion 1",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["schemaVersion", "entities", "findings"],
|
|
@@ -281,7 +281,15 @@
|
|
|
281
281
|
"type": "string"
|
|
282
282
|
},
|
|
283
283
|
"kind": {
|
|
284
|
-
"enum": [
|
|
284
|
+
"enum": [
|
|
285
|
+
"datetime_offset",
|
|
286
|
+
"money_delta",
|
|
287
|
+
"value_mapping",
|
|
288
|
+
"missing_stride",
|
|
289
|
+
"extra_stride",
|
|
290
|
+
"formatting_difference",
|
|
291
|
+
"number_change"
|
|
292
|
+
]
|
|
285
293
|
},
|
|
286
294
|
"count": {
|
|
287
295
|
"type": "integer",
|
|
@@ -440,6 +448,13 @@
|
|
|
440
448
|
"$ref": "#/definitions/reconciliation"
|
|
441
449
|
}
|
|
442
450
|
}
|
|
451
|
+
},
|
|
452
|
+
"exactValues": {
|
|
453
|
+
"type": "object",
|
|
454
|
+
"additionalProperties": {
|
|
455
|
+
"type": "string",
|
|
456
|
+
"pattern": "^-?[0-9]+(?:\\.[0-9]+)?$"
|
|
457
|
+
}
|
|
443
458
|
}
|
|
444
459
|
}
|
|
445
460
|
},
|
|
@@ -472,7 +487,7 @@
|
|
|
472
487
|
"minimum": 0
|
|
473
488
|
},
|
|
474
489
|
"amount": {
|
|
475
|
-
"$ref": "#/definitions/
|
|
490
|
+
"$ref": "#/definitions/decimalAmount"
|
|
476
491
|
}
|
|
477
492
|
}
|
|
478
493
|
},
|
|
@@ -504,13 +519,13 @@
|
|
|
504
519
|
"minimum": 0
|
|
505
520
|
},
|
|
506
521
|
"amount": {
|
|
507
|
-
"$ref": "#/definitions/
|
|
522
|
+
"$ref": "#/definitions/decimalAmount"
|
|
508
523
|
}
|
|
509
524
|
}
|
|
510
525
|
}
|
|
511
526
|
},
|
|
512
527
|
"remainder": {
|
|
513
|
-
"$ref": "#/definitions/
|
|
528
|
+
"$ref": "#/definitions/decimalAmount"
|
|
514
529
|
},
|
|
515
530
|
"uncertain": {
|
|
516
531
|
"type": "object",
|
|
@@ -520,7 +535,7 @@
|
|
|
520
535
|
"$ref": "#/definitions/keyParts"
|
|
521
536
|
},
|
|
522
537
|
"amount": {
|
|
523
|
-
"$ref": "#/definitions/
|
|
538
|
+
"$ref": "#/definitions/decimalAmount"
|
|
524
539
|
}
|
|
525
540
|
}
|
|
526
541
|
},
|
|
@@ -531,6 +546,17 @@
|
|
|
531
546
|
}
|
|
532
547
|
}
|
|
533
548
|
}
|
|
549
|
+
},
|
|
550
|
+
"decimalAmount": {
|
|
551
|
+
"anyOf": [
|
|
552
|
+
{
|
|
553
|
+
"type": "string",
|
|
554
|
+
"pattern": "^-?[0-9]+(?:\\.[0-9]+)?$"
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
"type": "integer"
|
|
558
|
+
}
|
|
559
|
+
]
|
|
534
560
|
}
|
|
535
561
|
}
|
|
536
562
|
}
|
package/docs/report-schema.md
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
# JSON report schema, version 1
|
|
2
2
|
|
|
3
|
-
The machine-readable [JSON Schema](report-schema.json) ships in the package. New optional properties may be added to schemaVersion 1; readers must tolerate unknown fields. Required meanings and money/time units remain stable. Removing fields or changing units requires a new schema version. Readers of older reports may encounter conflict `line` properties and display-string uncertain keys; new reports use physical `row` and key-part arrays.
|
|
3
|
+
The machine-readable [JSON Schema](report-schema.json) ships in the package. New optional properties may be added to schemaVersion 1; readers must tolerate unknown fields. Required meanings and money/time units remain stable. Removing fields or changing units requires a new schema version. Readers of older reports may encounter conflict `line` properties and display-string uncertain keys; new reports use physical `row` and key-part arrays. Versions 1.1.0 and 1.2.0 use this version-1 contract.
|
|
4
4
|
|
|
5
5
|
The root requires schemaVersion, entities and findings. Optional patterns.minCount is at least 3 (default 3). groups preserve exact findingIndexes and never contain fewer members than minCount. coverage.explained counts the union of supported pattern members and accounted aggregate findings; needsReview counts the remaining findings. Accounting does not change failure policy.
|
|
6
6
|
|
|
7
7
|
Each finding has type, entity, source, key, field and message. Types are missing_in_source, extra_in_source, field_mismatch, aggregate_mismatch, duplicate_key, duplicate_value, invalid_value and source_error. expected and actual are optional normalized scalars. invalid_value includes its physical row and, when available, the valid reference value. duplicate_value has relatedKeys and optional amounts; a repeated tender alone is not evidence of duplicate payment. Rollup conflicts use values containing row and value, rather than invented expected/actual values. Composite keys retain keyParts. Entity pluralLabel is an optional display noun.
|
|
8
8
|
|
|
9
|
-
Money is a decimal integer minor-unit string. Datetimes are UTC epoch milliseconds. Optional currency identifies monetary findings. Aggregate valuesByCurrency and rawValuesByCurrency partition currencies; currencies are never converted or combined. rawValues, excludedRows, sourceStatus and uncertainKeys distinguish raw input, retained/certain inputs, unsupported mappings and failed reads. uncertainKeys contains arrays of key parts, including a one-element array for a simple key.
|
|
9
|
+
Money is a decimal integer minor-unit string. Datetimes are UTC epoch milliseconds. Calendar dates are ISO `yyyy-MM-dd` strings without a zone. Number-sum `exactValues` contains canonical exact-decimal strings alongside compatibility `values` numbers; compare and display `exactValues` when present. Optional currency identifies monetary findings. Aggregate valuesByCurrency and rawValuesByCurrency partition currencies; currencies are never converted or combined. rawValues, excludedRows, sourceStatus and uncertainKeys distinguish raw input, retained/certain inputs, unsupported mappings and failed reads. uncertainKeys contains arrays of key parts, including a one-element array for a simple key.
|
|
10
10
|
|
|
11
|
-
Reconciliation is keyed by source. Its identity is source total minus reference total equals missingRows.amount + extraRows.amount + valueDifferences.amount + sum(patterns.amount) + uncertain.amount + remainder. Each contribution is filtered by the aggregate's own where clause. Pattern amounts sum qualifying row contributions, rather than multiplying the whole pattern count. Tolerated or unsupported changes remain in the remainder. Zero remainder means the aggregate is accounted for; individual invalid values and one-off differences still require review. Mixed-currency totals remain partitioned and never receive a misleading combined reconciliation.
|
|
11
|
+
Reconciliation is keyed by source. Its identity is source total minus reference total equals missingRows.amount + extraRows.amount + valueDifferences.amount + sum(patterns.amount) + uncertain.amount + remainder. Money/count contributions use integer strings; number-sum contributions and remainders use exact decimal strings. Each contribution is filtered by the aggregate's own where clause. Pattern amounts sum qualifying row contributions, rather than multiplying the whole pattern count. Tolerated or unsupported changes remain in the remainder. Zero remainder means the aggregate is accounted for; individual invalid values and one-off differences still require review. Mixed-currency totals remain partitioned and never receive a misleading combined reconciliation.
|
|
12
|
+
|
|
13
|
+
New 1.2.0 group kinds are `formatting_difference` (cause `case_only`, `whitespace_only` or `case_whitespace`) and `number_change` (an exact repeated expected/actual pair). Both retain expected/actual and findingIndexes. `stale_value` remains a consistent-change heuristic that does not establish which side is stale or whether definitions differ.
|
|
12
14
|
|
|
13
15
|
Patterns describe observations. Timezone hints require offset-free source values and a quarter-hour delta; page-boundary hints require pagination or matching configured page-size evidence. Blank values never form mapping or delta patterns. Groups retain every underlying finding and blocking status.
|
|
14
16
|
|
|
@@ -16,7 +18,7 @@ Object properties follow configuration order; findings sort naturally by entity,
|
|
|
16
18
|
|
|
17
19
|
## Package exports and scenario reports
|
|
18
20
|
|
|
19
|
-
Import `logiparity/report-schema.json` and `logiparity/scenario-report-schema.json` (JSON import attributes depend on your runtime), or read the files under docs/ in the installed package. Their versioned raw `$id` URLs identify the 1.
|
|
21
|
+
Import `logiparity/report-schema.json` and `logiparity/scenario-report-schema.json` (JSON import attributes depend on your runtime), or read the files under docs/ in the installed package. Their versioned raw `$id` URLs identify the 1.2.0 tag; they identify the corresponding versioned release. Validators can register both local schemas without network access.
|
|
20
22
|
|
|
21
23
|
The [scenario schema](scenario-report-schema.json) describes `kind: scenario`, events, invariants, affectedOrderCount, runtime errors and optional nested Parity. Both wire schemas use version 1: consumers must tolerate additive optional fields; required meanings or money/time units cannot change without a schema version change. Runtime error reports may contain only the state collected before failure.
|
|
22
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
-
"$id": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.2.0/docs/scenario-report-schema.json",
|
|
4
4
|
"title": "LogiParity scenario report, schemaVersion 1",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"required": ["schemaVersion", "kind", "name", "target", "environment", "events", "invariants", "exitCode"],
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"type": "string"
|
|
47
47
|
},
|
|
48
48
|
"parity": {
|
|
49
|
-
"$ref": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.
|
|
49
|
+
"$ref": "https://raw.githubusercontent.com/Nutts5796/logiparity/v1.2.0/docs/report-schema.json"
|
|
50
50
|
},
|
|
51
51
|
"events": {
|
|
52
52
|
"type": "array",
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export declare function jsonPathTokens(path: string): string[];
|
|
2
|
-
export declare function selectJson(value: unknown, path: string): unknown[];
|
|
3
|
-
export declare function selectOne(value: unknown, path: string): unknown;
|
|
2
|
+
export declare function selectJson(value: unknown, path: string, absentAsNull?: boolean): unknown[];
|
|
3
|
+
export declare function selectOne(value: unknown, path: string, absentAsNull?: boolean): unknown;
|
|
@@ -16,25 +16,33 @@ export function jsonPathTokens(path) {
|
|
|
16
16
|
}
|
|
17
17
|
return tokens;
|
|
18
18
|
}
|
|
19
|
-
export function selectJson(value, path) {
|
|
19
|
+
export function selectJson(value, path, absentAsNull = false) {
|
|
20
|
+
const absent = Symbol('absent');
|
|
20
21
|
const tokens = jsonPathTokens(path);
|
|
21
22
|
let nodes = [value];
|
|
22
23
|
for (const token of tokens) {
|
|
23
24
|
nodes = nodes.flatMap((node) => {
|
|
25
|
+
if (node === absent)
|
|
26
|
+
return [absent];
|
|
24
27
|
if (token === '*') {
|
|
25
28
|
if (!Array.isArray(node))
|
|
26
29
|
throw new Error(`JSONPath ${path}: [*] requires an array; correct the extraction path.`);
|
|
27
30
|
return node;
|
|
28
31
|
}
|
|
29
|
-
if (!node || typeof node !== 'object'
|
|
30
|
-
throw new Error(`JSONPath ${path}: property ${token}
|
|
32
|
+
if (!node || typeof node !== 'object')
|
|
33
|
+
throw new Error(`JSONPath ${path}: property ${token} requires an object; correct the field mapping.`);
|
|
34
|
+
if (!Object.hasOwn(node, token)) {
|
|
35
|
+
if (absentAsNull)
|
|
36
|
+
return [absent];
|
|
37
|
+
throw new Error(`JSONPath ${path}: property ${token} is missing; correct the field mapping or opt in with absentAsNull: true (source) or missing: null (field).`);
|
|
38
|
+
}
|
|
31
39
|
return [Reflect.get(node, token)];
|
|
32
40
|
});
|
|
33
41
|
}
|
|
34
|
-
return nodes;
|
|
42
|
+
return nodes.map((node) => (node === absent ? null : node));
|
|
35
43
|
}
|
|
36
|
-
export function selectOne(value, path) {
|
|
37
|
-
const nodes = selectJson(value, path);
|
|
44
|
+
export function selectOne(value, path, absentAsNull = false) {
|
|
45
|
+
const nodes = selectJson(value, path, absentAsNull);
|
|
38
46
|
if (nodes.length !== 1)
|
|
39
47
|
throw new Error(`JSONPath ${path} selected ${nodes.length} values; map exactly one scalar.`);
|
|
40
48
|
return nodes[0];
|
|
@@ -152,16 +152,22 @@ export async function* readRest(config, context) {
|
|
|
152
152
|
if (signals.some((value) => value !== signals[0]))
|
|
153
153
|
throw new Error(`${location}; completion signals disagree. Correct the response metadata or pagination paths.`);
|
|
154
154
|
more = signals[0];
|
|
155
|
+
const unverified = 'API returned no total count or completion metadata; completeness is not verified. Compare rows read with a separate count request using the same filter, or configure totalPath, totalPagesPath or hasMorePath.';
|
|
155
156
|
if (more === undefined && page === 0 && items.length < pagination.size) {
|
|
156
157
|
const message = `${location}; no completion signal is configured. This is an unverified short first page. Set totalPath, totalPagesPath or hasMorePath to verify completeness.`;
|
|
157
158
|
if (!pagination.allowShortFirstPage)
|
|
158
159
|
throw new Error(`${message} Set size: ${items.length} to match the server cap, or allowShortFirstPage: true only to accept an unverified short first page.`);
|
|
159
|
-
(context.warn ?? console.warn)(safeConsole(message));
|
|
160
|
+
(context.warn ?? console.warn)(safeConsole(`${message} ${unverified}`));
|
|
160
161
|
}
|
|
162
|
+
else if (more === undefined && page === 0)
|
|
163
|
+
(context.warn ?? console.warn)(safeConsole(unverified));
|
|
161
164
|
}
|
|
162
165
|
for (let index = 0; index < items.length; index++) {
|
|
163
166
|
try {
|
|
164
|
-
yield Object.fromEntries(Object.entries(config.fields).map(([field, path]) => [
|
|
167
|
+
yield Object.fromEntries(Object.entries(config.fields).map(([field, path]) => [
|
|
168
|
+
field,
|
|
169
|
+
selectOne(items[index], fieldPath(path), config.absentAsNull || (typeof path !== 'string' && path.missing === null)),
|
|
170
|
+
]));
|
|
165
171
|
}
|
|
166
172
|
catch (error) {
|
|
167
173
|
throw new Error(`REST page ${page + 1}, record ${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const version = "1.
|
|
1
|
+
export declare const version = "1.2.0";
|
package/lib/cli/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by release:bump.
|
|
2
|
-
export const version = '1.
|
|
2
|
+
export const version = '1.2.0';
|
|
@@ -3,9 +3,14 @@ import type { NormalizedValue } from './types.js';
|
|
|
3
3
|
export declare class DecimalSum {
|
|
4
4
|
private units;
|
|
5
5
|
private scale;
|
|
6
|
-
add(value: number): void;
|
|
6
|
+
add(value: number | bigint | string): void;
|
|
7
|
+
text(): string;
|
|
8
|
+
percentOf(other: DecimalSum): number;
|
|
7
9
|
value(): number;
|
|
8
10
|
within(tolerance: number): boolean;
|
|
9
11
|
}
|
|
10
|
-
export declare
|
|
11
|
-
export declare function
|
|
12
|
+
export declare const negateDecimal: (value: bigint | string) => bigint | string;
|
|
13
|
+
export declare function sumDecimal(...values: (bigint | string | number)[]): string;
|
|
14
|
+
export declare function displayDecimal(value: bigint | string): string;
|
|
15
|
+
export declare function measureAggregate(aggregate: AggregateConfig, entity: EntityConfig, source: string, config: SourceConfig, rows: Map<string, Record<string, NormalizedValue>>, exactSums?: Map<string, string>): number | bigint | undefined;
|
|
16
|
+
export declare function aggregateEqual(expected: number | bigint | string, actual: number | bigint | string, config: AggregateConfig): boolean;
|
|
@@ -19,6 +19,19 @@ export class DecimalSum {
|
|
|
19
19
|
}
|
|
20
20
|
this.units += units * 10n ** BigInt(this.scale - scale);
|
|
21
21
|
}
|
|
22
|
+
text() {
|
|
23
|
+
const absolute = String(this.units < 0n ? -this.units : this.units).padStart(this.scale + 1, '0');
|
|
24
|
+
const decimal = this.scale
|
|
25
|
+
? `${absolute.slice(0, -this.scale)}.${absolute.slice(-this.scale)}`.replace(/\.?0+$/, '')
|
|
26
|
+
: absolute;
|
|
27
|
+
return `${this.units < 0n ? '-' : ''}${decimal}`;
|
|
28
|
+
}
|
|
29
|
+
percentOf(other) {
|
|
30
|
+
if (other.units === 0n)
|
|
31
|
+
return 0;
|
|
32
|
+
const scale = Math.max(this.scale, other.scale);
|
|
33
|
+
return (Number((this.units * 10n ** BigInt(scale - this.scale) * 10000n) / (other.units * 10n ** BigInt(scale - other.scale))) / 100);
|
|
34
|
+
}
|
|
22
35
|
value() {
|
|
23
36
|
const value = Number(`${this.units}e-${this.scale}`);
|
|
24
37
|
if (!Number.isFinite(value))
|
|
@@ -33,7 +46,19 @@ export class DecimalSum {
|
|
|
33
46
|
return absolute * 10n ** BigInt(scale - this.scale) <= bound.units * 10n ** BigInt(scale - bound.scale);
|
|
34
47
|
}
|
|
35
48
|
}
|
|
36
|
-
export
|
|
49
|
+
export const negateDecimal = (value) => typeof value === 'bigint' ? -value : value.startsWith('-') ? value.slice(1) : '-' + value;
|
|
50
|
+
export function sumDecimal(...values) {
|
|
51
|
+
const sum = new DecimalSum();
|
|
52
|
+
values.forEach((value) => sum.add(value));
|
|
53
|
+
return sum.text();
|
|
54
|
+
}
|
|
55
|
+
export function displayDecimal(value) {
|
|
56
|
+
const text = sumDecimal(value), [integer = '0', fraction] = text.split('.');
|
|
57
|
+
return ((text.startsWith('-') ? '−' : '') +
|
|
58
|
+
BigInt(integer.replace(/^-/, '')).toLocaleString('en-US') +
|
|
59
|
+
(fraction ? '.' + fraction : ''));
|
|
60
|
+
}
|
|
61
|
+
export function measureAggregate(aggregate, entity, source, config, rows, exactSums) {
|
|
37
62
|
const required = [...Object.keys(aggregate.where), ...(aggregate.type === 'sum' ? [aggregate.field] : [])];
|
|
38
63
|
if (required.some((field) => !Object.hasOwn(config.fields, field)))
|
|
39
64
|
return undefined;
|
|
@@ -59,15 +84,17 @@ export function measureAggregate(aggregate, entity, source, config, rows) {
|
|
|
59
84
|
}
|
|
60
85
|
if (aggregate.type === 'count')
|
|
61
86
|
return count;
|
|
87
|
+
if (entity.compare[aggregate.field]?.type === 'number')
|
|
88
|
+
exactSums?.set(aggregate.name, decimal.text());
|
|
62
89
|
return entity.compare[aggregate.field]?.type === 'money' ? money : decimal.value();
|
|
63
90
|
}
|
|
64
91
|
export function aggregateEqual(expected, actual, config) {
|
|
65
92
|
if (typeof expected === 'bigint' && typeof actual === 'bigint')
|
|
66
93
|
return (expected > actual ? expected - actual : actual - expected) <= BigInt(config.tolerance);
|
|
67
|
-
if (typeof expected
|
|
94
|
+
if (typeof expected === 'bigint' || typeof actual === 'bigint')
|
|
68
95
|
return false;
|
|
69
96
|
const difference = new DecimalSum();
|
|
70
97
|
difference.add(expected);
|
|
71
|
-
difference.add(
|
|
98
|
+
difference.add(negateDecimal(String(actual)));
|
|
72
99
|
return difference.within(config.tolerance);
|
|
73
100
|
}
|