Add delta workflow helpers and comparison serialization#383
Conversation
Port workflow helpers from json-delta-py PR #2, adapted to TypeScript functional style: - squashDeltas: compact multiple deltas into net-effect delta - deltaMap / deltaStamp / deltaGroupBy: immutable delta transformations - operationSpecDict / operationExtensions: spec vs extension key partitioning - deltaSpecDict / deltaExtensions: same for delta envelopes - leafProperty: terminal property name from operation path - comparisonToDict: recursive comparison tree serialization - comparisonToFlatList: flatten comparison to leaf changes with paths
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
Adds TypeScript “delta workflow helpers” (ported from json-delta-py) and new serialization helpers for enriched comparisons, plus exports/docs/tests to support the new APIs.
Changes:
- Introduces
src/deltaHelpers.tswith delta transformation, introspection, grouping, and squashing utilities. - Adds comparison serialization helpers (
comparisonToDict,comparisonToFlatList) tosrc/jsonCompare.ts. - Updates public exports, docs, tests, and bumps package version to
5.0.0-alpha.2.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/deltaHelpers.ts |
New helper APIs for delta workflows, including squashDeltas and spec/extension utilities. |
src/jsonCompare.ts |
Adds comparison tree serialization to dict and flat change-list forms. |
src/index.ts |
Re-exports new delta helpers from the package entrypoint. |
tests/deltaHelpers.test.ts |
New test suite covering delta helper behaviors. |
tests/jsonCompare.test.ts |
Adds tests for the new comparison serialization helpers. |
README.md |
Documents the new APIs and adds release notes. |
package.json |
Version bump to 5.0.0-alpha.2. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…t name - Use diffDelta instead of JSON.stringify for target verification (handles key ordering differences correctly) - Guard against undefined children in comparisonToDict and flattenNode (enrich() returns undefined for Function values) - Fix IDENT_RE to match spec's SIMPLE_PROPERTY_RE (no $ in identifiers) - Rename misleading test name to 'omits absent keys from result'
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- comparisonToDict: preserve array indices with null placeholders instead of filtering (which collapsed indices) - operationExtensions/deltaExtensions/deltaGroupBy: use Object.create(null) to prevent prototype pollution from untrusted keys
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } else if (node.value && typeof node.value === 'object') { | ||
| const obj: Record<string, IComparisonDict> = {}; | ||
| for (const [key, child] of Object.entries( | ||
| node.value as Record<string, IComparisonEnrichedNode> | ||
| )) { | ||
| if (child == null) continue; | ||
| obj[key] = comparisonToDict(child); | ||
| } | ||
| result.value = obj; |
There was a problem hiding this comment.
comparisonToDict builds obj with {} and then assigns arbitrary keys from the compared data. If a key is __proto__ (or similar), obj[key] = ... will invoke the __proto__ setter, changing the prototype and not creating an own enumerable property, which both breaks serialization (the key can disappear from JSON output) and can create prototype-pollution risks for consumers. Use a null-prototype accumulator (e.g., Object.create(null)) or Object.defineProperty to ensure these keys are treated as data properties.
There was a problem hiding this comment.
Fixed in 9a15233 — changed to Object.create(null), consistent with the other extension accumulators.
| // Build envelope without operations | ||
| const envelope: Record<string, any> = {}; | ||
| for (const key of Object.keys(delta)) { | ||
| if (key !== 'operations') { | ||
| envelope[key] = delta[key]; | ||
| } | ||
| } |
There was a problem hiding this comment.
deltaGroupBy builds the envelope with a normal {} and copies arbitrary keys from delta. If an extension key is __proto__/constructor, envelope[key] = ... can trigger special-key behavior (prototype mutation) and the property may be dropped when spreading into the grouped deltas. Use a null-prototype accumulator (e.g., Object.create(null)) for envelope (consistent with the other extension helpers) to avoid prototype pollution and preserve all keys.
There was a problem hiding this comment.
Good catch — fixed in 9a15233. envelope now uses Object.create(null), consistent with the groups and result accumulators fixed in the previous commit.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/deltaHelpers.ts
Outdated
| for (const d of deltas) { | ||
| for (const key of Object.keys(d)) { | ||
| if (!DELTA_SPEC_KEYS.has(key)) { | ||
| result[key] = d[key]; |
There was a problem hiding this comment.
In squashDeltas, merging envelope extensions via result[key] = d[key] can trigger special __proto__/constructor behaviors on plain objects (prototype mutation), reintroducing the prototype-pollution/serialization risks you already mitigated elsewhere with Object.create(null). Consider defining these extension keys with Object.defineProperty (data property) or collecting extensions into a null-prototype object and merging via object spread to ensure __proto__ is treated as a normal key.
| result[key] = d[key]; | |
| Object.defineProperty(result, key, { | |
| value: d[key], | |
| writable: true, | |
| enumerable: true, | |
| configurable: true, | |
| }); |
There was a problem hiding this comment.
Fixed in 45adaf0 — now uses Object.defineProperty with enumerable/writable/configurable to safely assign extension keys onto the diffDelta result, avoiding __proto__ setter behavior.
| export interface IComparisonDict { | ||
| type: string; | ||
| value?: any; | ||
| oldValue?: any; | ||
| } |
There was a problem hiding this comment.
IComparisonDict.type is currently string, but the implementation only ever emits Operation | CompareOperation (both string enums). Exporting the narrower union type would improve consumer type-safety without affecting JSON-serializability.
There was a problem hiding this comment.
Intentionally keeping string for now. During alpha, the public interface types are still settling — keeping string avoids coupling consumers to our enum imports and keeps the types JSON-serialization-friendly. We can narrow this when the API stabilizes for the stable v5 release.
| export interface IFlatChange { | ||
| path: string; | ||
| type: string; | ||
| value?: any; | ||
| oldValue?: any; | ||
| } |
There was a problem hiding this comment.
IFlatChange.type is typed as string, but values originate from Operation | CompareOperation. Narrowing this to Operation | CompareOperation (or a dedicated union) would prevent invalid type strings in consumer code while keeping the serialized output unchanged.
There was a problem hiding this comment.
Same reasoning as IComparisonDict.type — keeping string during alpha to avoid coupling consumers to enum imports. Will revisit for the stable v5 release.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|



Summary
Port of json-delta-py PR #2 workflow helpers to TypeScript, adapted from Python class methods to standalone functions matching the existing functional style.
New functions (11 total):
squashDeltas,deltaMap,deltaStamp,deltaGroupBy— all immutableoperationSpecDict,operationExtensions,deltaSpecDict,deltaExtensions,leafPropertycomparisonToDict,comparisonToFlatListFiles changed:
src/deltaHelpers.tssrc/jsonCompare.tscomparisonToDict,comparisonToFlatList+ typessrc/index.tstests/deltaHelpers.test.tstests/jsonCompare.test.tsREADME.mdpackage.jsonTest plan
npx tsc --noEmit— type check passesnpm test— 327/327 tests pass (all existing + 50 new)npm run lint— cleannpm run build— tsup ESM+CJS+DTS build succeeds