@classytic/primitives 0.26.4 → 0.27.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 CHANGED
@@ -3,6 +3,30 @@
3
3
  Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
4
  adhering to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
5
 
6
+ ## 0.27.0
7
+
8
+ ### Added — `./csv` subpath: one CSV cell escaper for the whole fleet
9
+
10
+ `escapeCsvCell(value)` and `neutralizeCsvFormula(value)`. Six CSV producers
11
+ across ledger, the spine and the hosts each hand-rolled RFC 4180 quoting, and
12
+ not one neutralized a leading `= + - @ \t \r` — so a customer-supplied payment
13
+ reference or party name reached the accountant's spreadsheet as a live formula.
14
+ Quoting does not help: a spreadsheet strips the quotes and evaluates what is
15
+ left, which is why the two defences ship as one module.
16
+
17
+ The two functions are separable on purpose. A consumer holding a real CSV
18
+ writer (`csv-stringify`) takes `neutralizeCsvFormula` only, so its quoting is
19
+ not done twice; a hand-rolled `rows.map(...).join(',')` takes `escapeCsvCell`.
20
+
21
+ **Well-formed numbers are exempt.** Prefixing `-1500` would make every negative
22
+ amount text and break `SUM` in every finance export — a fix that quietly
23
+ corrupts the product is worse than the hole. The exemption requires the WHOLE
24
+ cell to be a number, so `-2+3+cmd|'/c calc'!A0` is still neutralized.
25
+
26
+ Not for a machine-to-machine upload contract with a fixed column spec (a
27
+ courier's bulk-import CSV): there the receiver is a parser, not a spreadsheet,
28
+ and an added `'` corrupts the payload.
29
+
6
30
  ## 0.26.4
7
31
 
8
32
  ### Added — `./price-basis` subpath: measured pricing across physical dimensions
package/dist/csv.d.mts ADDED
@@ -0,0 +1,36 @@
1
+ //#region src/serialization/csv.d.ts
2
+ /**
3
+ * `@classytic/primitives/csv` — make one value safe to put in a CSV cell.
4
+ *
5
+ * Two defences, deliberately separable because a consumer that already has a
6
+ * real CSV writer must NOT get its quoting done twice:
7
+ *
8
+ * - `neutralizeCsvFormula` — stops the cell being executed as a formula when
9
+ * the file is opened in Excel / Sheets / LibreOffice. Use this when a
10
+ * library (`csv-stringify`, `papaparse`, …) is doing the quoting.
11
+ * - `escapeCsvCell` — neutralize + RFC 4180 quoting. Use this in a
12
+ * hand-rolled `rows.map(...).join(',')` serializer.
13
+ *
14
+ * WHY quoting is not enough: a spreadsheet strips the quotes first and
15
+ * evaluates what is left, so `"=cmd|'/c calc'!A1"` still executes. Only
16
+ * changing the LEADING character stops it, which is why these are one module
17
+ * and not a quoting helper someone can use alone.
18
+ *
19
+ * Well-formed numbers are exempt. Prefixing `-1500` would make every negative
20
+ * amount text and break the reader's `SUM` — a fix that quietly corrupts every
21
+ * finance export is worse than the hole. The exemption is the WHOLE cell
22
+ * matching a number, so `-2+3+cmd|'/c calc'!A0` is still neutralized.
23
+ *
24
+ * NOT for a machine-to-machine upload contract with a fixed column spec (a
25
+ * courier's bulk-import CSV, say) — there the receiver is a parser, not a
26
+ * spreadsheet, and an added `'` corrupts the payload.
27
+ */
28
+ /**
29
+ * Prefix `'` when the cell would otherwise be evaluated as a formula.
30
+ * `null`/`undefined` become `''`. Numbers are returned unchanged.
31
+ */
32
+ declare function neutralizeCsvFormula(value: unknown): string;
33
+ /** `neutralizeCsvFormula` + RFC 4180 quoting. The one-call form. */
34
+ declare function escapeCsvCell(value: unknown): string;
35
+ //#endregion
36
+ export { escapeCsvCell, neutralizeCsvFormula };
package/dist/csv.mjs ADDED
@@ -0,0 +1,50 @@
1
+ //#region src/serialization/csv.ts
2
+ /**
3
+ * `@classytic/primitives/csv` — make one value safe to put in a CSV cell.
4
+ *
5
+ * Two defences, deliberately separable because a consumer that already has a
6
+ * real CSV writer must NOT get its quoting done twice:
7
+ *
8
+ * - `neutralizeCsvFormula` — stops the cell being executed as a formula when
9
+ * the file is opened in Excel / Sheets / LibreOffice. Use this when a
10
+ * library (`csv-stringify`, `papaparse`, …) is doing the quoting.
11
+ * - `escapeCsvCell` — neutralize + RFC 4180 quoting. Use this in a
12
+ * hand-rolled `rows.map(...).join(',')` serializer.
13
+ *
14
+ * WHY quoting is not enough: a spreadsheet strips the quotes first and
15
+ * evaluates what is left, so `"=cmd|'/c calc'!A1"` still executes. Only
16
+ * changing the LEADING character stops it, which is why these are one module
17
+ * and not a quoting helper someone can use alone.
18
+ *
19
+ * Well-formed numbers are exempt. Prefixing `-1500` would make every negative
20
+ * amount text and break the reader's `SUM` — a fix that quietly corrupts every
21
+ * finance export is worse than the hole. The exemption is the WHOLE cell
22
+ * matching a number, so `-2+3+cmd|'/c calc'!A0` is still neutralized.
23
+ *
24
+ * NOT for a machine-to-machine upload contract with a fixed column spec (a
25
+ * courier's bulk-import CSV, say) — there the receiver is a parser, not a
26
+ * spreadsheet, and an added `'` corrupts the payload.
27
+ */
28
+ /** Leading characters a spreadsheet may treat as the start of a formula or DDE call. */
29
+ const FORMULA_LEAD = /^[=+\-@\t\r]/;
30
+ /** The WHOLE cell is a plain number — no separators, no currency symbol. */
31
+ const WHOLE_NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
32
+ /** Characters that force RFC 4180 quoting. */
33
+ const NEEDS_QUOTING = /[",\r\n]/;
34
+ /**
35
+ * Prefix `'` when the cell would otherwise be evaluated as a formula.
36
+ * `null`/`undefined` become `''`. Numbers are returned unchanged.
37
+ */
38
+ function neutralizeCsvFormula(value) {
39
+ const text = value == null ? "" : String(value);
40
+ if (!FORMULA_LEAD.test(text)) return text;
41
+ if (WHOLE_NUMBER.test(text)) return text;
42
+ return `'${text}`;
43
+ }
44
+ /** `neutralizeCsvFormula` + RFC 4180 quoting. The one-call form. */
45
+ function escapeCsvCell(value) {
46
+ const text = neutralizeCsvFormula(value);
47
+ return NEEDS_QUOTING.test(text) ? `"${text.replace(/"/g, "\"\"")}"` : text;
48
+ }
49
+ //#endregion
50
+ export { escapeCsvCell, neutralizeCsvFormula };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/primitives",
3
- "version": "0.26.4",
3
+ "version": "0.27.0",
4
4
  "description": "Framework-agnostic TypeScript primitives shared across Classytic packages — Money, events, pagination, tenant config, and other building blocks.",
5
5
  "author": "Classytic",
6
6
  "license": "MIT",
@@ -205,6 +205,10 @@
205
205
  "types": "./dist/canonical.d.mts",
206
206
  "default": "./dist/canonical.mjs"
207
207
  },
208
+ "./csv": {
209
+ "types": "./dist/csv.d.mts",
210
+ "default": "./dist/csv.mjs"
211
+ },
208
212
  "./package.json": "./package.json",
209
213
  "./state-diagram": {
210
214
  "types": "./dist/state-diagram.d.mts",