@stackline/stable-stringify 1.0.1 → 1.0.2

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 CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes are documented here. This project follows Semantic
4
4
  Versioning.
5
5
 
6
+ ## [1.0.2] - 2026-08-21
7
+
8
+ ### Added
9
+
10
+ - Executable examples for deterministic cache keys, RFC 8785 content digests,
11
+ and bounded cyclic logging.
12
+ - Adoption guidance for direct installs, npm aliases, safe diagnostics, and
13
+ canonical signatures.
14
+ - Reproducible benchmark methodology and Stackline package catalog link.
15
+ - First-party documentation analytics that never records serialized input.
16
+ - Trusted-publishing workflow for provenance-enabled future releases.
17
+
18
+ ### Changed
19
+
20
+ - Package tarballs now include the public guides and executable examples.
21
+
22
+ No runtime API or declaration behavior changed in this release.
23
+
6
24
  ## [1.0.1] - 2026-08-16
7
25
 
8
26
  ### Fixed
@@ -32,3 +50,4 @@ Versioning.
32
50
 
33
51
  [1.0.1]: https://github.com/alexandroit/stackline-stable-stringify/compare/v1.0.0...v1.0.1
34
52
  [1.0.0]: https://github.com/alexandroit/stackline-stable-stringify/releases/tag/v1.0.0
53
+ [1.0.2]: https://github.com/alexandroit/stackline-stable-stringify/compare/v1.0.1...v1.0.2
package/README.md CHANGED
@@ -264,6 +264,16 @@ on the same payload. Native JSON is shown only as a throughput baseline; it
264
264
  does not recursively sort object keys. Measure with representative production
265
265
  data before selecting limits or modes.
266
266
 
267
+ ## Adoption resources
268
+
269
+ - [Stable, safe, canonical, and drop-in adoption guide](docs/ADOPTION.md)
270
+ - [Reproducible benchmark methodology](docs/BENCHMARKS.md)
271
+ - [Executable examples](examples)
272
+ - [Stackline open-source catalog](https://alexandro.net/docs/open-source/)
273
+
274
+ The examples ship in the npm tarball and cover deterministic cache keys,
275
+ canonical content digests, and bounded logging of cyclic BigInt data.
276
+
267
277
  ## Verification
268
278
 
269
279
  Every release runs:
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! @stackline/stable-stringify v1.0.1 | MIT */
1
+ /*! @stackline/stable-stringify v1.0.2 | MIT */
2
2
  var StacklineStableStringifyModule = (() => {
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @stackline/stable-stringify v1.0.1 | MIT */
1
+ /*! @stackline/stable-stringify v1.0.2 | MIT */
2
2
 
3
3
  // src/index.js
4
4
  var hasOwn = Object.prototype.hasOwnProperty;
package/dist/index.min.js CHANGED
@@ -1,4 +1,4 @@
1
- /*! @stackline/stable-stringify v1.0.1 | MIT */
1
+ /*! @stackline/stable-stringify v1.0.2 | MIT */
2
2
  var StacklineStableStringifyModule=(()=>{var b=Object.defineProperty;var k=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var F=Object.prototype.hasOwnProperty;var V=(e,n)=>{for(var t in n)b(e,t,{get:n[t],enumerable:!0})},_=(e,n,t,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let o of P(n))!F.call(e,o)&&o!==t&&b(e,o,{get:()=>n[o],enumerable:!(r=k(n,o))||r.enumerable});return e};var R=e=>_(b({},"__esModule",{value:!0}),e);var ue={};V(ue,{CanonicalizationError:()=>s,StableStringifyLimitError:()=>f,canonicalize:()=>N,canonicalizeBytes:()=>I,configure:()=>C,default:()=>se,safeStringify:()=>E,stableStringify:()=>u,stringify:()=>M});var U=Object.prototype.hasOwnProperty,B=Object.prototype.propertyIsEnumerable,v=Object.prototype.toString,m=Symbol("omit"),x=null,w=Object.freeze({bigint:"string",cycleValue:"[Circular]",maxDepth:100,maxEntries:1e5,maxLength:1e6,onCycle:"marker"}),S=Object.freeze({maxDepth:1e3,maxEntries:1e5,maxLength:16*1024*1024}),f=class extends RangeError{constructor(n,t,r){let o=h(r);super(`Stable stringify ${n} limit of ${t} exceeded at ${o}`),this.name="StableStringifyLimitError",this.code="ERR_STABLE_STRINGIFY_LIMIT",this.kind=n,this.limit=t,this.path=o}},s=class extends TypeError{constructor(n,t){let r=h(t);super(`JSON canonicalization failed at ${r}: ${n}`),this.name="CanonicalizationError",this.code="ERR_JSON_CANONICALIZATION",this.path=r,this.reason=n}};function u(e,n){let t=$(n);return A(e,t,!1)}var M=u;function C(e){let n=typeof e=="function"?{cmp:e}:y(e);return $(n),function(r,o){let i=typeof o=="function"?{cmp:o}:y(o);return u(r,{...n,...i})}}function E(e,n,t,r){let o=y(r),i={...w,...o,maxDepth:o.maxDepth===void 0?o.depthLimit:o.maxDepth,maxEntries:o.maxEntries===void 0?o.edgesLimit:o.maxEntries,replacer:n===void 0?o.replacer:n,space:t===void 0?o.space:t};i.maxDepth===void 0&&(i.maxDepth=w.maxDepth),i.maxEntries===void 0&&(i.maxEntries=w.maxEntries);try{return u(e,i)}catch(c){if(o.throwOnError===!0)throw c;let l="UnknownError";try{c&&typeof c.name=="string"&&(l=c.name.slice(0,80))}catch(ae){}return JSON.stringify(`[Unable to serialize: ${l}]`)}}function N(e,n){let t=re(n);return A(e,t,!0)}function I(e,n){return ce(N(e,n))}function A(e,n,t){let r={ancestors:new WeakMap,canonical:t,chunks:[],entries:0,length:0,options:n},i=O(e,{"":e},"",x,r);if(i===m)return;let c=[{depth:0,path:x,prepared:i,type:"value"}];for(;c.length>0;){let l=c.pop();l.type==="value"?Z(l,c,r):G(l,c,r)}return r.chunks.join("")}function Z(e,n,t){if(e.prepared.kind==="primitive"){a(t,e.prepared.text,e.path);return}let r=e.prepared.value,o=t.ancestors.get(r);if(o!==void 0||t.ancestors.has(r)){ne(t,o,e.path);return}te(t,e.depth,e.path);let i=Array.isArray(r);t.canonical&&(ee(r,e.path),i||X(r,e.path)),t.ancestors.set(r,e.path);let c=W(r,i,e.depth,e.path,t);if(a(t,i?"[":"{",e.path),c.total===0){a(t,i?"]":"}",e.path),t.ancestors.delete(r);return}n.push(c)}function G(e,n,t){if(e.index>=e.total){e.emitted&&t.options.gap!==""&&a(t,`
3
3
  ${e.indent}`,e.path),a(t,e.isArray?"]":"}",e.path),t.ancestors.delete(e.value);return}if(e.isArray){let r=e.index;e.index+=1;let o=g(e.path,r);L(t,o);let i=q(e.value,r,o,t),c=O(i,e.value,String(r),o,t);c===m&&(c=p("null")),J(e,o,t),n.push(e),n.push({depth:e.depth+1,path:o,prepared:c,type:"value"});return}for(;e.index<e.total;){let r=e.keys[e.index];e.index+=1;let o=g(e.path,r);L(t,o);let i=Q(e.value,r,o,t),c=O(i,e.value,r,o,t);if(c!==m){J(e,o,t),a(t,JSON.stringify(r),o),a(t,t.options.gap===""?":":": ",o),n.push(e),n.push({depth:e.depth+1,path:o,prepared:c,type:"value"});return}}n.push(e)}function W(e,n,t,r,o){let i,c;return n?c=e.length:(i=H(e,r,o),c=i.length),{depth:t,emitted:!1,indent:o.options.gap===""?"":o.options.gap.repeat(t),index:0,isArray:n,keys:i,path:r,total:c,type:"container",value:e}}function H(e,n,t){let r;if(t.canonical||t.options.propertyList===void 0?r=Object.keys(e):r=t.options.propertyList.slice(),t.canonical){for(let o of r)if(z(o))throw new s("property names must not contain lone UTF-16 surrogates",g(n,o));return r.sort()}if(t.options.accessors!=="invoke"&&(r=r.filter(o=>{let i=Object.getOwnPropertyDescriptor(e,o);if(!i||!i.get&&!i.set)return!0;if(t.options.accessors==="throw")throw new TypeError(`Refusing to invoke accessor at ${h(g(n,o))}`);return!1})),t.options.cmp){let o=t.options.cmp;r.sort((i,c)=>o({key:i,value:T(e,i,n,t)},{key:c,value:T(e,c,n,t)}))}else r.sort();return r}function O(e,n,t,r,o){if(o.canonical)return K(e,r);let i=e;if(o.options.toJSON&&i!==null&&i!==void 0){let c=i.toJSON;typeof c=="function"&&(i=c.call(i))}if(o.options.replacerFunction&&(i=o.options.replacerFunction.call(n,t,i)),i===null)return p("null");switch(typeof i){case"string":return p(JSON.stringify(i));case"number":return p(Number.isFinite(i)?String(i):"null");case"boolean":return p(i?"true":"false");case"bigint":return Y(i,o.options.bigint,r);case"object":return{kind:"object",value:i};default:return m}}function K(e,n){if(e===null)return p("null");switch(typeof e){case"string":if(z(e))throw new s("strings must not contain lone UTF-16 surrogates",n);return p(JSON.stringify(e));case"number":if(!Number.isFinite(e))throw new s("NaN and Infinity are not valid I-JSON numbers",n);return p(JSON.stringify(e));case"boolean":return p(e?"true":"false");case"object":return{kind:"object",value:e};case"bigint":throw new s("BigInt values must be represented as JSON strings",n);default:throw new s(`values of type ${typeof e} are not valid JSON data`,n)}}function Y(e,n,t){if(n==="string")return p(JSON.stringify(String(e)));if(n==="number"){let r=Number(e);if(!Number.isSafeInteger(r))throw new RangeError(`BigInt at ${h(t)} cannot be represented as a safe JSON number`);return p(String(r))}throw new TypeError("Do not know how to serialize a BigInt")}function p(e){return{kind:"primitive",text:e}}function q(e,n,t,r){if(r.canonical){if(!U.call(e,n))throw new s("sparse arrays are not valid I-JSON data",t);return D(e,String(n),t)}return j(e,String(n),t,r.options.accessors)}function Q(e,n,t,r){return r.canonical?D(e,n,t):j(e,n,t,r.options.accessors)}function D(e,n,t){let r=Object.getOwnPropertyDescriptor(e,n);if(!r||r.get||r.set)throw new s("accessor properties are not valid canonical JSON input",t);return r.value}function j(e,n,t,r){if(r==="invoke")return e[n];let o=new WeakSet,i=e;for(;i!==null&&!o.has(i);){o.add(i);let c=Object.getOwnPropertyDescriptor(i,n);if(c){if(!c.get&&!c.set)return c.value;if(r==="throw")throw new TypeError(`Refusing to invoke accessor at ${h(t)}`);return}i=Object.getPrototypeOf(i)}}function T(e,n,t,r){return j(e,n,g(t,n),r.options.accessors)}function X(e,n){let t=Object.getPrototypeOf(e);if(t!==null&&Object.getPrototypeOf(t)!==null)throw new s("class instances must be converted to plain JSON objects",n)}function ee(e,n){if(typeof Object.getOwnPropertySymbols!="function")return;if(Object.getOwnPropertySymbols(e).some(r=>B.call(e,r)))throw new s("symbol properties are not valid JSON object members",n)}function J(e,n,t){if(t.options.gap==="")e.emitted&&a(t,",",n);else{let r=t.options.gap.repeat(e.depth+1);a(t,e.emitted?`,
4
4
  ${r}`:`
@@ -0,0 +1,37 @@
1
+ # Adoption Guide
2
+
3
+ ## Stable cache keys
4
+
5
+ Use the default export when object insertion order must not change a cache key,
6
+ snapshot, deduplication identifier, or content hash.
7
+
8
+ ```js
9
+ import stringify from '@stackline/stable-stringify';
10
+
11
+ const key = stringify({ tenant: 42, query: { status: 'open' } });
12
+ ```
13
+
14
+ ## Drop-in package alias
15
+
16
+ Applications already using `fast-json-stable-stringify` can preserve their
17
+ imports:
18
+
19
+ ```bash
20
+ npm uninstall fast-json-stable-stringify
21
+ npm install fast-json-stable-stringify@npm:@stackline/stable-stringify
22
+ ```
23
+
24
+ Commit the lockfile and run the complete application suite. The default call
25
+ shape and comparator contract are covered by differential compatibility tests.
26
+
27
+ ## Safe diagnostics
28
+
29
+ Use `safeStringify` for logs and error paths that can contain cycles, BigInt,
30
+ getters, or deeply nested data. Its bounded defaults keep diagnostic output
31
+ from becoming another failure source.
32
+
33
+ ## Cryptographic canonicalization
34
+
35
+ Use `canonicalize` or `canonicalizeBytes` only when strict RFC 8785 behavior is
36
+ required. Canonical mode intentionally rejects values outside its I-JSON
37
+ contract instead of silently applying safe-logging policies.
@@ -0,0 +1,103 @@
1
+ # Architecture
2
+
3
+ ## Scope
4
+
5
+ The runtime converts a JavaScript value into deterministic JSON text through
6
+ three policy surfaces:
7
+
8
+ - stable mode for compatibility and cache or snapshot keys;
9
+ - safe mode for bounded diagnostics and logs;
10
+ - canonical mode for RFC 8785 interoperable bytes.
11
+
12
+ All modes share one iterative serialization engine and have zero runtime
13
+ dependencies.
14
+
15
+ ## Iterative task stack
16
+
17
+ Recursive serializers fail when object depth exceeds the JavaScript call-stack
18
+ limit. This implementation prepares a root value and processes explicit
19
+ `value` and `container` tasks in a loop. A container frame stores its key list,
20
+ index, depth, indentation, path, and emission state.
21
+
22
+ The approach makes nesting depth a policy decision instead of a runtime stack
23
+ accident. Stable mode is tested with 25,000 nested arrays. Safe and canonical
24
+ modes intentionally apply finite defaults.
25
+
26
+ ## Preparation and emission
27
+
28
+ Each value goes through preparation before output:
29
+
30
+ 1. stable mode optionally invokes `toJSON` and a replacer;
31
+ 2. the value is classified as primitive, container, or omitted;
32
+ 3. object keys are selected and sorted;
33
+ 4. descriptors or accessors are read according to policy;
34
+ 5. text chunks are appended while enforcing the output budget.
35
+
36
+ Arrays preserve order. Unsupported stable-mode array items become `null`,
37
+ matching JSON semantics. Unsupported object members are omitted.
38
+
39
+ ## Cycle detection
40
+
41
+ An operation-scoped `WeakMap` tracks active ancestors and their paths. Entries
42
+ are removed when a container closes, so repeated sibling references are not
43
+ misclassified as cycles. The current ancestor path supports strict errors,
44
+ markers, null substitution, and descriptive path markers.
45
+
46
+ No graph is retained after serialization.
47
+
48
+ ## Stable mode
49
+
50
+ Stable mode sorts own enumerable string keys by UTF-16 code units. A comparator
51
+ can instead order `{ key, value }` records. Property-list replacers are
52
+ normalized once, deduplicated, and applied at every object depth.
53
+
54
+ Compatibility defaults intentionally leave depth, entry, and length budgets
55
+ unlimited. BigInt throws, accessors and `toJSON` are invoked, and cycles throw
56
+ unless configured otherwise.
57
+
58
+ ## Safe mode
59
+
60
+ Safe mode delegates to the same engine with cycle markers, BigInt strings, and
61
+ finite budgets. It catches traversal failures and returns a controlled JSON
62
+ string unless `throwOnError` is enabled. This prevents the diagnostic path from
63
+ hiding the original application error behind a second serialization error.
64
+
65
+ ## Canonical mode
66
+
67
+ Canonical mode validates I-JSON data while traversing:
68
+
69
+ - keys use recursive UTF-16 ordering required by RFC 8785;
70
+ - numbers and strings use ECMAScript JSON serialization;
71
+ - malformed Unicode and non-finite numbers are rejected;
72
+ - only arrays, plain objects, and null-prototype objects are accepted;
73
+ - data descriptors are read without invoking accessors;
74
+ - symbols, cycles, sparse arrays, and non-JSON values are rejected;
75
+ - output contains no insignificant whitespace.
76
+
77
+ `canonicalizeBytes` encodes the resulting string as UTF-8. It uses
78
+ `TextEncoder` where available and includes a small standards-compatible
79
+ fallback for older environments.
80
+
81
+ ## Resource accounting
82
+
83
+ The engine tracks:
84
+
85
+ - container depth;
86
+ - visited object keys and array positions;
87
+ - UTF-16 output length.
88
+
89
+ Limit errors include a structured kind, configured limit, and a path such as
90
+ `<root>.request.headers` or `<root>.items[4]`.
91
+
92
+ ## Distribution
93
+
94
+ One source module builds to:
95
+
96
+ - `dist/index.js` for ESM;
97
+ - `dist/index.cjs` for callable CommonJS;
98
+ - `dist/index.min.js` for the `StacklineStableStringify` browser global;
99
+ - resolver-specific `.d.mts`, `.d.cts`, and `.d.ts` declarations;
100
+ - source maps for every JavaScript artifact.
101
+
102
+ The build banner identifies package version and MIT license. Package tests load
103
+ all three JavaScript forms and execute the browser artifact in an isolated VM.
@@ -0,0 +1,25 @@
1
+ # Benchmark Methodology
2
+
3
+ The benchmark runs native `JSON.stringify`,
4
+ `fast-json-stable-stringify@2.1.0`, stable mode, safe mode, and RFC 8785 mode
5
+ against the same representative payload in one Node.js process.
6
+
7
+ ```bash
8
+ npm ci
9
+ npm run benchmark
10
+ ```
11
+
12
+ Control the sample size with `BENCHMARK_ITERATIONS`:
13
+
14
+ ```bash
15
+ BENCHMARK_ITERATIONS=250000 npm run benchmark
16
+ ```
17
+
18
+ Native JSON is a throughput baseline only. It does not recursively sort object
19
+ keys and is not behaviorally equivalent. Stable, safe, and canonical modes
20
+ also implement different contracts, so compare a mode only with alternatives
21
+ that provide the same guarantees.
22
+
23
+ When publishing results, include the command, Node.js version, CPU, complete
24
+ raw output, and payload shape. Do not present one machine's result as a
25
+ universal ranking.
@@ -0,0 +1,79 @@
1
+ # Compatibility
2
+
3
+ ## Default contract
4
+
5
+ The default export follows documented `fast-json-stable-stringify@2.1.0`
6
+ behavior:
7
+
8
+ - object keys are sorted recursively;
9
+ - arrays retain their order;
10
+ - a comparator receives `{ key, value }` records;
11
+ - comparator functions can be passed directly or as `cmp`;
12
+ - `{ cycles: true }` writes `__cycle__` for circular objects;
13
+ - `toJSON` runs before value serialization;
14
+ - JSON omission, primitive, wrapper-object, and non-finite number behavior is
15
+ preserved.
16
+
17
+ The suite performs 30,000 deterministic differential serializations across
18
+ generated JSON-compatible values, comparators, and option forms.
19
+
20
+ ## Intentional additions
21
+
22
+ | Area | Compatible default | Addition |
23
+ | :--- | :--- | :--- |
24
+ | Circular arrays | Upstream can overflow | Same policy as circular objects |
25
+ | Deep input | Recursive stack limit | Iterative traversal |
26
+ | Cycles | Throw or `__cycle__` | Path, null, and custom marker policies |
27
+ | BigInt | Throw | String or safe-number policies |
28
+ | Replacer | Not exposed | Function and JSON property list |
29
+ | Formatting | Compact | JSON-style `space` option |
30
+ | Accessors | Invoke | Omit or throw policies |
31
+ | Resources | Unbounded | Opt-in depth, entry, and length limits |
32
+ | Diagnostics | Separate ecosystem | `safeStringify` named export |
33
+ | Canonical JSON | Separate ecosystem | RFC 8785 string and byte exports |
34
+
35
+ No addition changes output unless its option or named API is used, except that
36
+ circular arrays now return the configured cycle behavior instead of causing a
37
+ call-stack failure.
38
+
39
+ ## Package forms
40
+
41
+ | Consumer | Supported form |
42
+ | :--- | :--- |
43
+ | ESM | default and named imports |
44
+ | CommonJS | callable `require()` plus static helpers |
45
+ | Browser | `StacklineStableStringify` global |
46
+ | TypeScript | resolver-specific declarations |
47
+ | npm alias | `fast-json-stable-stringify@npm:@stackline/stable-stringify` |
48
+
49
+ ## TypeScript
50
+
51
+ The public declarations use syntax accepted by TypeScript 3.9. ESM-specific
52
+ `.d.mts` resolution is validated from TypeScript 4.7 onward. The release matrix
53
+ currently covers 3.9.10, 4.7.4, 4.9.5, 5.9.3, 6.0.2, and 7.0.2.
54
+
55
+ Return types remain `string | undefined`, matching JSON root-value behavior.
56
+ Canonical APIs return `string` or `Uint8Array` because invalid values throw.
57
+
58
+ ## Migration
59
+
60
+ Direct migration:
61
+
62
+ ```js
63
+ import stringify from '@stackline/stable-stringify';
64
+ ```
65
+
66
+ No-import-change migration:
67
+
68
+ ```bash
69
+ npm install fast-json-stable-stringify@npm:@stackline/stable-stringify
70
+ ```
71
+
72
+ The clean-install test executes both names in ESM and CommonJS from the packed
73
+ tarball, rather than resolving against the development workspace.
74
+
75
+ ## Standards boundary
76
+
77
+ Stable output is deterministic for the same JavaScript data and options. It is
78
+ not automatically RFC 8785 canonical output. Use `canonicalize` when another
79
+ system, signature specification, or protocol requires JCS.
@@ -0,0 +1,100 @@
1
+ # Market Research
2
+
3
+ ## Decision
4
+
5
+ The selected opportunity is a unified deterministic JSON serializer with a
6
+ compatible stable API, safe diagnostic mode, and RFC 8785 canonical mode.
7
+
8
+ It addresses a mature, high-volume need without copying an abandoned codebase.
9
+ The runtime is original; comparison packages are development references only.
10
+
11
+ ## Evidence window
12
+
13
+ The research uses the official npm downloads and registry APIs. The fixed
14
+ 30-day window is 2026-07-17 through 2026-08-15. The comparison window is
15
+ 2026-06-17 through 2026-07-16. Fixed dates make the findings reproducible.
16
+
17
+ | Package | Recent 30 days | Prior 30 days | Change | Latest release |
18
+ | :--- | ---: | ---: | ---: | :--- |
19
+ | `fast-json-stable-stringify` | 587,334,858 | 581,507,983 | +1.00% | 2.1.0, 2019-12-14 |
20
+ | `safe-stable-stringify` | 210,997,298 | 206,370,668 | +2.24% | 2.5.0, 2024-08-24 |
21
+ | `fast-safe-stringify` | 172,544,640 | 161,813,217 | +6.63% | 2.1.1, 2021-09-08 |
22
+ | `json-stringify-safe` | 157,096,721 | 154,552,336 | +1.65% | 5.0.1, 2015-05-19 |
23
+ | **Combined package activity** | **1,127,973,517** | **1,104,244,204** | **+2.15%** | four contracts |
24
+
25
+ The same packages recorded 9,395,146,202 combined downloads from 2025-08-16
26
+ through 2026-08-15.
27
+
28
+ These sums measure package activity, not unique users. Dependency trees can
29
+ install more than one package and repeated CI installs count again. The figures
30
+ demonstrate category scale and fragmentation; they do not guarantee adoption
31
+ for a new package.
32
+
33
+ ## Primary sources
34
+
35
+ - [fast-json-stable-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/fast-json-stable-stringify)
36
+ - [safe-stable-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/safe-stable-stringify)
37
+ - [fast-safe-stringify downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/fast-safe-stringify)
38
+ - [json-stringify-safe downloads](https://api.npmjs.org/downloads/point/2026-07-17:2026-08-15/json-stringify-safe)
39
+ - [npm registry metadata](https://registry.npmjs.org/fast-json-stable-stringify)
40
+ - [RFC 8785: JSON Canonicalization Scheme](https://www.rfc-editor.org/rfc/rfc8785.html)
41
+ - [ECMAScript JSON.stringify algorithm](https://tc39.es/ecma262/multipage/structured-data.html#sec-json.stringify)
42
+
43
+ ## Product gap
44
+
45
+ The category is split by concern:
46
+
47
+ - the largest compatibility package offers stable key order but no current ESM
48
+ declaration surface, safe diagnostic mode, resource limits, or JCS API;
49
+ - safe serializers focus on cycles and logging but differ in BigInt, getter,
50
+ `toJSON`, depth, edge, and fallback behavior;
51
+ - canonical JSON implementations form another package category even though
52
+ cache keys and signatures often exist in the same systems;
53
+ - several established releases are between two and eleven years old while
54
+ download activity continues to grow.
55
+
56
+ Open issue themes in the comparison repositories include ESM, TypeScript
57
+ options, RFC 8785 support, wrapper semantics, output limits, getters, `toJSON`,
58
+ BigInt, and deep-stack behavior. The product contract directly tests those
59
+ boundaries instead of bundling unrelated utilities.
60
+
61
+ ## Why this shape can grow
62
+
63
+ The adoption path has three levels:
64
+
65
+ 1. existing users can keep the established function signature;
66
+ 2. npm alias users can keep the existing import specifier;
67
+ 3. new users gain safe and canonical modes without adding two more packages.
68
+
69
+ Zero runtime dependencies reduce supply-chain exposure. TypeScript 3.9 through
70
+ 7.0, Node.js 14.17+, browsers, Deno, and Bun widen the usable install base.
71
+
72
+ ## Rejected directions
73
+
74
+ ### Another generic utility collection
75
+
76
+ Broad utility packages compete on dozens of unrelated APIs and create a larger
77
+ maintenance and security surface. The selected package owns one data boundary.
78
+
79
+ ### Canonical JSON only
80
+
81
+ Standards compliance is valuable but is a smaller migration surface. Combining
82
+ it with the established stable call shape creates a practical entry point.
83
+
84
+ ### Forking a legacy serializer
85
+
86
+ Forking would accelerate initial code volume but inherit architecture and
87
+ maintenance constraints. Behavioral compatibility is validated through tests;
88
+ the runtime remains independently designed and licensed.
89
+
90
+ ## Success metrics
91
+
92
+ Potential is evaluated through measurable signals, not a promised download
93
+ number:
94
+
95
+ - direct and alias installs that need no code changes;
96
+ - issue resolution time and compatibility reports;
97
+ - reverse dependencies and weekly npm downloads;
98
+ - bundle size and runtime dependency count;
99
+ - regression-free releases across the supported matrix;
100
+ - adoption of safe and canonical named APIs.
@@ -0,0 +1,68 @@
1
+ # Releasing
2
+
3
+ ## Preconditions
4
+
5
+ - `main` is the only default development branch.
6
+ - Version and changelog agree.
7
+ - Runtime dependencies remain zero or the exception is documented.
8
+ - The full local suite and GitHub CI pass.
9
+ - The npm and Verdaccio package names are available or authenticated.
10
+
11
+ ## Local release gate
12
+
13
+ ```bash
14
+ npm ci
15
+ npm test
16
+ npm run test:attw
17
+ npm run audit:dependencies
18
+ npm pack --dry-run
19
+ ```
20
+
21
+ Run the benchmark for regression evidence, not as a pass/fail release gate.
22
+
23
+ ## Artifact creation
24
+
25
+ Build once and retain one immutable tarball:
26
+
27
+ ```bash
28
+ version="$(node -p "require('./package.json').version")"
29
+ mkdir -p "release/$version"
30
+ npm pack --ignore-scripts --pack-destination "release/$version"
31
+ (cd "release/$version" && sha512sum *.tgz > SHA512SUMS)
32
+ npm sbom --omit=dev --sbom-format cyclonedx > "release/$version/sbom.cdx.json"
33
+ ```
34
+
35
+ Run `scripts/smoke-install.mjs` against the retained path. The same bytes must
36
+ be published to Verdaccio and public npm.
37
+
38
+ ## Registry order
39
+
40
+ 1. publish the retained tarball to Verdaccio;
41
+ 2. install directly and through the compatibility alias;
42
+ 3. compare tarball integrity and run smoke tests;
43
+ 4. run `publish.yml` with the expected SHA-512; the trusted workflow rebuilds
44
+ the reviewed commit and stops unless its tarball is byte-identical;
45
+ 5. let the workflow publish with npm provenance, then download it and compare
46
+ SHA-512;
47
+ 6. verify `latest`, provenance metadata, dependency audit, and registry
48
+ signature where available.
49
+
50
+ Never rebuild between registry publications.
51
+
52
+ ## GitHub release
53
+
54
+ Tag the exact tested commit as `vX.Y.Z`. Attach the tarball, `SHA512SUMS`, and
55
+ CycloneDX SBOM. Confirm CI and CodeQL are green for the tagged source.
56
+
57
+ ## Documentation
58
+
59
+ Build and test the static site, deploy it under
60
+ `/docs/vanilla/stable-stringify/`, update the central sitemap, and verify HTML,
61
+ CSS, JavaScript, image, robots, sitemap, metadata, LLM references, mobile
62
+ layout, and playground behavior in production.
63
+
64
+ ## Rollback
65
+
66
+ npm versions are immutable. Do not unpublish a consumed release except for a
67
+ compelling security or legal reason. Deprecate a bad version with a precise
68
+ message and publish a patch from the last known-good source.
@@ -0,0 +1,12 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import stringify from '@stackline/stable-stringify';
4
+
5
+ const first = { query: { page: 2, status: 'open' }, tenant: 42 };
6
+ const second = { tenant: 42, query: { status: 'open', page: 2 } };
7
+
8
+ const firstKey = stringify(first);
9
+ const secondKey = stringify(second);
10
+
11
+ assert.equal(firstKey, secondKey);
12
+ console.log(firstKey);
@@ -0,0 +1,17 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+
4
+ import { canonicalizeBytes } from '@stackline/stable-stringify';
5
+
6
+ const document = {
7
+ issuedAt: '2026-08-21T00:00:00Z',
8
+ subject: 'release-manifest',
9
+ version: 1
10
+ };
11
+
12
+ const digest = createHash('sha256')
13
+ .update(canonicalizeBytes(document))
14
+ .digest('hex');
15
+
16
+ assert.equal(digest.length, 64);
17
+ console.log(digest);
@@ -0,0 +1,14 @@
1
+ import assert from 'node:assert/strict';
2
+
3
+ import { safeStringify } from '@stackline/stable-stringify';
4
+
5
+ const event = { id: 42n, name: 'checkout.failed' };
6
+ event.context = event;
7
+
8
+ const output = safeStringify(event);
9
+
10
+ assert.equal(
11
+ output,
12
+ '{"context":"[Circular]","id":"42","name":"checkout.failed"}'
13
+ );
14
+ console.log(output);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackline/stable-stringify",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Deterministic, cycle-aware JSON serialization with a fast-json-stable-stringify-compatible API and RFC 8785 canonicalization",
5
5
  "keywords": [
6
6
  "stable-stringify",
@@ -62,6 +62,8 @@
62
62
  },
63
63
  "files": [
64
64
  "dist",
65
+ "docs",
66
+ "examples",
65
67
  "CHANGELOG.md",
66
68
  "CONTRIBUTING.md",
67
69
  "LICENSE",
@@ -76,10 +78,11 @@
76
78
  "clean": "node scripts/clean.mjs",
77
79
  "build": "node scripts/build.mjs",
78
80
  "lint": "eslint . && node scripts/check-markdown.mjs",
79
- "test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:package && npm run test:install && npm run test:docs",
81
+ "test": "npm run build && npm run lint && npm run test:coverage && npm run test:types && npm run test:examples && npm run test:package && npm run test:install && npm run test:docs",
80
82
  "test:unit": "node --test --test-reporter=spec test/*.test.mjs",
81
83
  "test:coverage": "c8 --all --src src --check-coverage --lines 100 --functions 100 --statements 100 --branches 95 node --test test/core.test.mjs test/compatibility.test.mjs test/cycles-limits.test.mjs test/canonical.test.mjs test/safe.test.mjs",
82
84
  "test:types": "node scripts/test-types.mjs",
85
+ "test:examples": "node examples/cache-key.mjs && node examples/content-digest.mjs && node examples/safe-logging.mjs",
83
86
  "test:package": "node --test test/package.test.mjs && node scripts/check-dist.mjs && publint",
84
87
  "test:install": "node scripts/smoke-install.mjs",
85
88
  "test:docs": "npm run docs:build && node scripts/check-docs.mjs",