Skip to content

Add delta workflow helpers and comparison serialization#383

Merged
ltwlf merged 7 commits intomasterfrom
feature/delta-helpers
Mar 11, 2026
Merged

Add delta workflow helpers and comparison serialization#383
ltwlf merged 7 commits intomasterfrom
feature/delta-helpers

Conversation

@ltwlf
Copy link
Owner

@ltwlf ltwlf commented Mar 11, 2026

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):

  • Delta transformations: squashDeltas, deltaMap, deltaStamp, deltaGroupBy — all immutable
  • Spec introspection: operationSpecDict, operationExtensions, deltaSpecDict, deltaExtensions, leafProperty
  • Comparison serialization: comparisonToDict, comparisonToFlatList

Files changed:

File Change
src/deltaHelpers.ts New — all delta workflow helpers
src/jsonCompare.ts Added comparisonToDict, comparisonToFlatList + types
src/index.ts Re-export deltaHelpers
tests/deltaHelpers.test.ts New — 33 tests
tests/jsonCompare.test.ts Added 17 tests for comparison serialization
README.md New sections, API reference updates, v5.0.0-alpha.2 release notes
package.json Version bump to 5.0.0-alpha.2

Test plan

  • npx tsc --noEmit — type check passes
  • npm test — 327/327 tests pass (all existing + 50 new)
  • npm run lint — clean
  • npm run build — tsup ESM+CJS+DTS build succeeds

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
Copilot AI review requested due to automatic review settings March 11, 2026 11:09
@codecov
Copy link

codecov bot commented Mar 11, 2026

Codecov Report

❌ Patch coverage is 99.28058% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/deltaHelpers.ts 98.64% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts with delta transformation, introspection, grouping, and squashing utilities.
  • Adds comparison serialization helpers (comparisonToDict, comparisonToFlatList) to src/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.

ltwlf added 2 commits March 11, 2026 15:23
…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'
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +153 to +161
} 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;
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9a15233 — changed to Object.create(null), consistent with the other extension accumulators.

Comment on lines +114 to +120
// Build envelope without operations
const envelope: Record<string, any> = {};
for (const key of Object.keys(delta)) {
if (key !== 'operations') {
envelope[key] = delta[key];
}
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 9a15233. envelope now uses Object.create(null), consistent with the groups and result accumulators fixed in the previous commit.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

for (const d of deltas) {
for (const key of Object.keys(d)) {
if (!DELTA_SPEC_KEYS.has(key)) {
result[key] = d[key];
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
result[key] = d[key];
Object.defineProperty(result, key, {
value: d[key],
writable: true,
enumerable: true,
configurable: true,
});

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 45adaf0 — now uses Object.defineProperty with enumerable/writable/configurable to safely assign extension keys onto the diffDelta result, avoiding __proto__ setter behavior.

Comment on lines +123 to +127
export interface IComparisonDict {
type: string;
value?: any;
oldValue?: any;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +129 to +134
export interface IFlatChange {
path: string;
type: string;
value?: any;
oldValue?: any;
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reasoning as IComparisonDict.type — keeping string during alpha to avoid coupling consumers to enum imports. Will revisit for the stable v5 release.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@sonarqubecloud
Copy link

@ltwlf ltwlf merged commit ed4410a into master Mar 11, 2026
6 checks passed
@ltwlf ltwlf deleted the feature/delta-helpers branch March 11, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants