@classytic/primitives 0.26.0 → 0.26.1
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 +4 -0
- package/dist/testing/stock-key-seam.d.mts +47 -0
- package/dist/testing/stock-key-seam.mjs +109 -0
- package/package.json +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
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.26.1
|
|
7
|
+
|
|
8
|
+
- `./testing/stock-key-seam` — new subpath; `scanStockKeySeam`, `assertStockKeySeam`, `takesStockKeyFromLabel`, `STOCK_KEY_SEAM_FIXTURES` for conformance-scanning packages that resolve stock keys
|
|
9
|
+
|
|
6
10
|
## [0.26.0] — 2026-08-27
|
|
7
11
|
|
|
8
12
|
### Added
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
//#region src/testing/stock-key-seam.d.ts
|
|
2
|
+
interface StockKeyViolation {
|
|
3
|
+
/** Path relative to the scanned root. */
|
|
4
|
+
file: string;
|
|
5
|
+
/** 1-indexed line number. */
|
|
6
|
+
line: number;
|
|
7
|
+
/** The offending source line, trimmed. */
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Does this line take a stock key from a label?
|
|
12
|
+
*
|
|
13
|
+
* The class is "assigns `skuRef` from an expression that reads `.sku` and never
|
|
14
|
+
* reads `.skuRef`". Matching `.sku` alone flags the CORRECT idiom
|
|
15
|
+
* (`line.skuRef ?? line.sku`), and a gate that cries wolf is a gate someone
|
|
16
|
+
* deletes — so the ref-read is what distinguishes a fallback from a label grab.
|
|
17
|
+
*/
|
|
18
|
+
declare function takesStockKeyFromLabel(line: string): boolean;
|
|
19
|
+
/** @deprecated Kept for callers that want the raw predicate; prefer {@link takesStockKeyFromLabel}. */
|
|
20
|
+
declare const STOCK_KEY_FROM_LABEL: {
|
|
21
|
+
test: typeof takesStockKeyFromLabel;
|
|
22
|
+
};
|
|
23
|
+
interface StockKeySeamScan {
|
|
24
|
+
/** How many source files were read — assert a floor, or the gate can pass on an empty tree. */
|
|
25
|
+
scanned: number;
|
|
26
|
+
violations: StockKeyViolation[];
|
|
27
|
+
}
|
|
28
|
+
/** Scan a package's `src` root. Pure: returns findings, throws nothing. */
|
|
29
|
+
declare function scanStockKeySeam(srcRoot: string): StockKeySeamScan;
|
|
30
|
+
/**
|
|
31
|
+
* Throwing form for a one-line test. `minFiles` is the scan-volume floor: a gate
|
|
32
|
+
* that quietly scanned nothing passes forever, so state what "ran" means.
|
|
33
|
+
*/
|
|
34
|
+
declare function assertStockKeySeam(srcRoot: string, opts: {
|
|
35
|
+
minFiles: number;
|
|
36
|
+
}): void;
|
|
37
|
+
/**
|
|
38
|
+
* The fixtures the matcher must and must not fire on — exported so every
|
|
39
|
+
* adopting package falsifies the DETECTOR, not just its own current cleanliness.
|
|
40
|
+
* Both live defects are here verbatim.
|
|
41
|
+
*/
|
|
42
|
+
declare const STOCK_KEY_SEAM_FIXTURES: {
|
|
43
|
+
readonly mustMatch: readonly [" skuRef: (snapshot.sku as string | undefined) ?? '',", " skuRef: snap?.sku ?? '',", " const skuRef = line.snapshot?.sku ?? line.offerId;", " skuRef: (line.snapshot?.sku ?? line.offerId) as string,"];
|
|
44
|
+
readonly mustNotMatch: readonly [" skuRef: stockKeyOf(snapshot) ?? '',", " skuRef: line.skuRef,", " sku: (snapshot.sku as string | undefined) ?? '',", " const label = variant.sku ?? variant.skuRef;", " const skuRef = line.skuRef ?? line.sku;", " skuRef: item.skuRef ?? item.sku,"];
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { STOCK_KEY_FROM_LABEL, STOCK_KEY_SEAM_FIXTURES, StockKeySeamScan, StockKeyViolation, assertStockKeySeam, scanStockKeySeam, takesStockKeyFromLabel };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
//#region src/testing/stock-key-seam.ts
|
|
4
|
+
/**
|
|
5
|
+
* Conformance scan: a STOCK KEY is never taken from a merchandising label.
|
|
6
|
+
*
|
|
7
|
+
* `skuRef: snapshot.sku` type-checks (both are `string`), reads plausibly, and
|
|
8
|
+
* ships. Downstream it decrements a quant nothing is stocked under — the row
|
|
9
|
+
* goes NEGATIVE while the real units sit under the stamped ref, and nothing
|
|
10
|
+
* throws because a move carrying a reservation may go negative. The rule and
|
|
11
|
+
* `stockKeyOf` already existed when that shipped; what was missing was a gate.
|
|
12
|
+
*
|
|
13
|
+
* Every package that resolves a stock key runs this over its own `src`, the way
|
|
14
|
+
* `@classytic/arc/testing`'s store contracts are run by their implementors. The
|
|
15
|
+
* matcher lives HERE so a fix to it reaches every consumer at once — a regex
|
|
16
|
+
* copied per package is the same drift one layer up.
|
|
17
|
+
*
|
|
18
|
+
* Node-only (`node:fs`), so it is a `/testing` subpath and never reachable from
|
|
19
|
+
* the isomorphic value-object entries.
|
|
20
|
+
*/
|
|
21
|
+
/** `skuRef` being assigned — property (`skuRef:`) or binding (`const skuRef =`). Both are the same defect. */
|
|
22
|
+
const ASSIGNS_STOCK_KEY = /\bskuRef\s*[:=]\s*([^,;{}\n]*)/;
|
|
23
|
+
/** The right-hand side reads a `.sku` label. `(?!Ref)` so `.skuRef` is not a `.sku` hit. */
|
|
24
|
+
const READS_LABEL = /\.sku\b(?!Ref)/;
|
|
25
|
+
/** The right-hand side already reads the stamped ref, i.e. it IS the `skuRef ?? sku` fallback. */
|
|
26
|
+
const READS_REF = /\.skuRef\b/;
|
|
27
|
+
/**
|
|
28
|
+
* Does this line take a stock key from a label?
|
|
29
|
+
*
|
|
30
|
+
* The class is "assigns `skuRef` from an expression that reads `.sku` and never
|
|
31
|
+
* reads `.skuRef`". Matching `.sku` alone flags the CORRECT idiom
|
|
32
|
+
* (`line.skuRef ?? line.sku`), and a gate that cries wolf is a gate someone
|
|
33
|
+
* deletes — so the ref-read is what distinguishes a fallback from a label grab.
|
|
34
|
+
*/
|
|
35
|
+
function takesStockKeyFromLabel(line) {
|
|
36
|
+
const rhs = ASSIGNS_STOCK_KEY.exec(line)?.[1];
|
|
37
|
+
if (rhs === void 0) return false;
|
|
38
|
+
return READS_LABEL.test(rhs) && !READS_REF.test(rhs);
|
|
39
|
+
}
|
|
40
|
+
/** @deprecated Kept for callers that want the raw predicate; prefer {@link takesStockKeyFromLabel}. */
|
|
41
|
+
const STOCK_KEY_FROM_LABEL = { test: takesStockKeyFromLabel };
|
|
42
|
+
/** Lines that are prose, not code. */
|
|
43
|
+
const isComment = (trimmed) => trimmed.startsWith("*") || trimmed.startsWith("//") || trimmed.startsWith("/*");
|
|
44
|
+
function tsFilesIn(dir, acc = []) {
|
|
45
|
+
for (const entry of readdirSync(dir)) {
|
|
46
|
+
if (entry === "node_modules" || entry === "dist") continue;
|
|
47
|
+
const full = join(dir, entry);
|
|
48
|
+
if (statSync(full).isDirectory()) tsFilesIn(full, acc);
|
|
49
|
+
else if (entry.endsWith(".ts") && !entry.endsWith(".d.ts")) acc.push(full);
|
|
50
|
+
}
|
|
51
|
+
return acc;
|
|
52
|
+
}
|
|
53
|
+
/** Scan a package's `src` root. Pure: returns findings, throws nothing. */
|
|
54
|
+
function scanStockKeySeam(srcRoot) {
|
|
55
|
+
const files = tsFilesIn(srcRoot);
|
|
56
|
+
const violations = [];
|
|
57
|
+
for (const file of files) {
|
|
58
|
+
const lines = readFileSync(file, "utf8").split("\n");
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const raw = lines[i] ?? "";
|
|
61
|
+
const trimmed = raw.trim();
|
|
62
|
+
if (isComment(trimmed)) continue;
|
|
63
|
+
if (takesStockKeyFromLabel(raw)) violations.push({
|
|
64
|
+
file: relative(srcRoot, file),
|
|
65
|
+
line: i + 1,
|
|
66
|
+
text: trimmed.slice(0, 120)
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
scanned: files.length,
|
|
72
|
+
violations
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Throwing form for a one-line test. `minFiles` is the scan-volume floor: a gate
|
|
77
|
+
* that quietly scanned nothing passes forever, so state what "ran" means.
|
|
78
|
+
*/
|
|
79
|
+
function assertStockKeySeam(srcRoot, opts) {
|
|
80
|
+
const { scanned, violations } = scanStockKeySeam(srcRoot);
|
|
81
|
+
if (scanned < opts.minFiles) throw new Error(`stock-key seam REFUSING to pass: scanned only ${scanned} file(s) under ${srcRoot}, expected >= ${opts.minFiles}`);
|
|
82
|
+
if (violations.length > 0) {
|
|
83
|
+
const list = violations.map((v) => ` ${v.file}:${v.line} ${v.text}`).join("\n");
|
|
84
|
+
throw new Error(`A stock key must come from stockKeyOf() (skuRef ?? sku), never from the .sku label:\n${list}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The fixtures the matcher must and must not fire on — exported so every
|
|
89
|
+
* adopting package falsifies the DETECTOR, not just its own current cleanliness.
|
|
90
|
+
* Both live defects are here verbatim.
|
|
91
|
+
*/
|
|
92
|
+
const STOCK_KEY_SEAM_FIXTURES = {
|
|
93
|
+
mustMatch: [
|
|
94
|
+
" skuRef: (snapshot.sku as string | undefined) ?? '',",
|
|
95
|
+
" skuRef: snap?.sku ?? '',",
|
|
96
|
+
" const skuRef = line.snapshot?.sku ?? line.offerId;",
|
|
97
|
+
" skuRef: (line.snapshot?.sku ?? line.offerId) as string,"
|
|
98
|
+
],
|
|
99
|
+
mustNotMatch: [
|
|
100
|
+
" skuRef: stockKeyOf(snapshot) ?? '',",
|
|
101
|
+
" skuRef: line.skuRef,",
|
|
102
|
+
" sku: (snapshot.sku as string | undefined) ?? '',",
|
|
103
|
+
" const label = variant.sku ?? variant.skuRef;",
|
|
104
|
+
" const skuRef = line.skuRef ?? line.sku;",
|
|
105
|
+
" skuRef: item.skuRef ?? item.sku,"
|
|
106
|
+
]
|
|
107
|
+
};
|
|
108
|
+
//#endregion
|
|
109
|
+
export { STOCK_KEY_FROM_LABEL, STOCK_KEY_SEAM_FIXTURES, assertStockKeySeam, scanStockKeySeam, takesStockKeyFromLabel };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/primitives",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.1",
|
|
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",
|
|
@@ -225,6 +225,10 @@
|
|
|
225
225
|
"./item-facts": {
|
|
226
226
|
"types": "./dist/item-facts.d.mts",
|
|
227
227
|
"default": "./dist/item-facts.mjs"
|
|
228
|
+
},
|
|
229
|
+
"./testing/stock-key-seam": {
|
|
230
|
+
"types": "./dist/testing/stock-key-seam.d.mts",
|
|
231
|
+
"default": "./dist/testing/stock-key-seam.mjs"
|
|
228
232
|
}
|
|
229
233
|
},
|
|
230
234
|
"files": [
|