@carecard/validate 3.18.0 → 3.19.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/.agents/skills/carecard-workspace-standards/SKILL.md +49 -20
- package/.agents/skills/github-pr-create-update/SKILL.md +22 -1
- package/.agents/skills/github-pr-merge-cleanup/SKILL.md +22 -1
- package/.agents/skills/logged-in-user-profile-page/SKILL.md +22 -1
- package/.agents/skills/pkg-publish/SKILL.md +22 -1
- package/.agents/skills/pkg-validate-coding-standards-and-best-practices/SKILL.md +47 -9
- package/.agents/skills/pkg-validate-validation-library/SKILL.md +45 -14
- package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +23 -3
- package/.codex/AGENTS.md +41 -10
- package/.github/workflows/auto-draft-pr.yml +173 -151
- package/.github/workflows/ci.yml +35 -32
- package/.husky/pre-commit +4 -4
- package/.prettierrc.js +10 -9
- package/AGENTS.md +19 -0
- package/eslint.config.mjs +60 -8
- package/index.d.ts +108 -107
- package/index.js +6 -6
- package/lib/validate.js +230 -157
- package/lib/validateNewUserRoleRequest.js +68 -60
- package/lib/validateProperties.js +326 -326
- package/lib/validateWhitelistProperties.js +182 -142
- package/lint-staged.config.mjs +36 -0
- package/package.json +66 -60
- package/readme.md +22 -1
- package/scripts/canonicalTestCommand.test.mjs +32 -0
- package/scripts/packageTaskRunner.audit.mjs +37 -0
- package/scripts/packageTaskRunner.test.mjs +50 -72
- package/scripts/runPackageTask.mjs +103 -58
- package/scripts/testOrder/randomizeTestOrder.cjs +35 -25
- package/scripts/testOrder/randomizeTestOrder.test.mjs +19 -19
- package/scripts/testOrder/testOrderPolicy.audit.mjs +54 -0
- package/scripts/testParallel/parallelTestPolicy.audit.mjs +63 -0
- package/scripts/testParallel/runIndexedMochaTests.cjs +45 -43
- package/scripts/testParallel/runIndexedMochaTests.test.mjs +13 -7
- package/scripts/testOrder/testOrderPolicy.test.mjs +0 -48
- package/scripts/testParallel/parallelTestPolicy.test.mjs +0 -43
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
error: { throwBadInputError },
|
|
5
|
+
caseConverter: { keysToSnakeCase },
|
|
6
6
|
} = require('@carecard/common-util');
|
|
7
7
|
|
|
8
8
|
const { validateProperties } = require('./validateProperties');
|
|
@@ -27,7 +27,7 @@ const VALID_FLATTEN_KEY_STYLES = new Set(['path', 'leaf']);
|
|
|
27
27
|
* @returns {boolean}
|
|
28
28
|
*/
|
|
29
29
|
function isMixedCaseSegment(segment) {
|
|
30
|
-
|
|
30
|
+
return /_/.test(segment) && /[A-Z]/.test(segment);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -37,7 +37,7 @@ function isMixedCaseSegment(segment) {
|
|
|
37
37
|
* @returns {string}
|
|
38
38
|
*/
|
|
39
39
|
function snakeToCamel(s) {
|
|
40
|
-
|
|
40
|
+
return s.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -47,7 +47,7 @@ function snakeToCamel(s) {
|
|
|
47
47
|
* @returns {string}
|
|
48
48
|
*/
|
|
49
49
|
function camelToSnake(s) {
|
|
50
|
-
|
|
50
|
+
return s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
/**
|
|
@@ -59,9 +59,13 @@ function camelToSnake(s) {
|
|
|
59
59
|
* @returns {string}
|
|
60
60
|
*/
|
|
61
61
|
function alternateCase(segment) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
if (segment.indexOf('_') !== -1) {
|
|
63
|
+
return snakeToCamel(segment);
|
|
64
|
+
}
|
|
65
|
+
if (/[A-Z]/.test(segment)) {
|
|
66
|
+
return camelToSnake(segment);
|
|
67
|
+
}
|
|
68
|
+
return segment;
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -71,20 +75,20 @@ function alternateCase(segment) {
|
|
|
71
75
|
* @returns {string[]}
|
|
72
76
|
*/
|
|
73
77
|
function splitPath(path) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
78
|
+
const segments = String(path).split('.');
|
|
79
|
+
if (segments.length > MAX_NESTING_DEPTH) {
|
|
80
|
+
throwBadInputError({
|
|
81
|
+
userMessage: `Property path "${path}" exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
for (const seg of segments) {
|
|
85
|
+
if (isMixedCaseSegment(seg)) {
|
|
86
|
+
throwBadInputError({
|
|
87
|
+
userMessage: `Property path "${path}" has a segment "${seg}" mixing snake_case and camelCase`,
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
|
-
|
|
90
|
+
}
|
|
91
|
+
return segments;
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
/**
|
|
@@ -97,27 +101,39 @@ function splitPath(path) {
|
|
|
97
101
|
* @returns {{ found: boolean, value: any }}
|
|
98
102
|
*/
|
|
99
103
|
function readLeaf(obj, segments) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
// Resolve a segment against the current node, trying its as-written form
|
|
105
|
+
// first and then its alternate snake/camel form. Returns the actual key
|
|
106
|
+
// present in the node, or undefined if neither form exists.
|
|
107
|
+
function resolveKey(node, seg) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(node, seg)) {
|
|
109
|
+
return seg;
|
|
110
|
+
}
|
|
111
|
+
const alt = alternateCase(seg);
|
|
112
|
+
if (alt !== seg && Object.prototype.hasOwnProperty.call(node, alt)) {
|
|
113
|
+
return alt;
|
|
108
114
|
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
109
117
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (key === undefined) return { found: false, value: undefined };
|
|
115
|
-
current = current[key];
|
|
118
|
+
let current = obj;
|
|
119
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
120
|
+
if (current === null || typeof current !== 'object') {
|
|
121
|
+
return { found: false, value: undefined };
|
|
116
122
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
const key = resolveKey(current, segments[i]);
|
|
124
|
+
if (key === undefined) {
|
|
125
|
+
return { found: false, value: undefined };
|
|
126
|
+
}
|
|
127
|
+
current = current[key];
|
|
128
|
+
}
|
|
129
|
+
if (current === null || typeof current !== 'object') {
|
|
130
|
+
return { found: false, value: undefined };
|
|
131
|
+
}
|
|
132
|
+
const leafKey = resolveKey(current, segments[segments.length - 1]);
|
|
133
|
+
if (leafKey === undefined) {
|
|
134
|
+
return { found: false, value: undefined };
|
|
135
|
+
}
|
|
136
|
+
return { found: true, value: current[leafKey] };
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
/**
|
|
@@ -129,15 +145,15 @@ function readLeaf(obj, segments) {
|
|
|
129
145
|
* @param {*} value
|
|
130
146
|
*/
|
|
131
147
|
function writeLeaf(target, segments, value) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
current = current[key];
|
|
148
|
+
let current = target;
|
|
149
|
+
for (let i = 0; i < segments.length - 1; i++) {
|
|
150
|
+
const key = segments[i];
|
|
151
|
+
if (current[key] === null || typeof current[key] !== 'object') {
|
|
152
|
+
current[key] = {};
|
|
139
153
|
}
|
|
140
|
-
current
|
|
154
|
+
current = current[key];
|
|
155
|
+
}
|
|
156
|
+
current[segments[segments.length - 1]] = value;
|
|
141
157
|
}
|
|
142
158
|
|
|
143
159
|
/**
|
|
@@ -152,15 +168,20 @@ function writeLeaf(target, segments, value) {
|
|
|
152
168
|
* @returns {Object}
|
|
153
169
|
*/
|
|
154
170
|
function flattenObject(obj, prefix = '', out = {}) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
171
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
172
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
173
|
+
if (
|
|
174
|
+
value !== null &&
|
|
175
|
+
typeof value === 'object' &&
|
|
176
|
+
!Array.isArray(value) &&
|
|
177
|
+
!(value instanceof Date)
|
|
178
|
+
) {
|
|
179
|
+
flattenObject(value, path, out);
|
|
180
|
+
} else {
|
|
181
|
+
out[path] = value;
|
|
162
182
|
}
|
|
163
|
-
|
|
183
|
+
}
|
|
184
|
+
return out;
|
|
164
185
|
}
|
|
165
186
|
|
|
166
187
|
/**
|
|
@@ -179,17 +200,22 @@ function flattenObject(obj, prefix = '', out = {}) {
|
|
|
179
200
|
* @returns {Object}
|
|
180
201
|
*/
|
|
181
202
|
function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
203
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
204
|
+
if (
|
|
205
|
+
value !== null &&
|
|
206
|
+
typeof value === 'object' &&
|
|
207
|
+
!Array.isArray(value) &&
|
|
208
|
+
!(value instanceof Date)
|
|
209
|
+
) {
|
|
210
|
+
flattenObjectByLeafKey(value, out, depthByKey, depth + 1);
|
|
211
|
+
} else {
|
|
212
|
+
if (!Object.prototype.hasOwnProperty.call(out, key) || depth < depthByKey[key]) {
|
|
213
|
+
out[key] = value;
|
|
214
|
+
depthByKey[key] = depth;
|
|
215
|
+
}
|
|
191
216
|
}
|
|
192
|
-
|
|
217
|
+
}
|
|
218
|
+
return out;
|
|
193
219
|
}
|
|
194
220
|
|
|
195
221
|
/**
|
|
@@ -230,96 +256,110 @@ function flattenObjectByLeafKey(obj, out = {}, depthByKey = {}, depth = 1) {
|
|
|
230
256
|
* @returns {Promise<Object>} Resolves with the validated (and possibly transformed) object.
|
|
231
257
|
*/
|
|
232
258
|
function validateWhitelistProperties(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
259
|
+
inputObject,
|
|
260
|
+
requiredProperties = [],
|
|
261
|
+
options = {
|
|
262
|
+
optionalProperties: [],
|
|
263
|
+
convertToSnakeCase: false,
|
|
264
|
+
flattenOutput: false,
|
|
265
|
+
flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE,
|
|
266
|
+
},
|
|
236
267
|
) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
268
|
+
const optionalProperties = (options && options.optionalProperties) || [];
|
|
269
|
+
const convertToSnakeCase = !!(options && options.convertToSnakeCase);
|
|
270
|
+
const flattenOutput = !!(options && options.flattenOutput);
|
|
271
|
+
const flattenKeyStyle =
|
|
272
|
+
options && options.flattenKeyStyle !== undefined
|
|
273
|
+
? options.flattenKeyStyle
|
|
274
|
+
: DEFAULT_FLATTEN_KEY_STYLE;
|
|
241
275
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
276
|
+
if (!VALID_FLATTEN_KEY_STYLES.has(flattenKeyStyle)) {
|
|
277
|
+
throwBadInputError({
|
|
278
|
+
userMessage: `Invalid flattenKeyStyle: ${String(flattenKeyStyle)}. Expected "path" or "leaf"`,
|
|
279
|
+
});
|
|
280
|
+
}
|
|
247
281
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
282
|
+
// Cap the total number of paths to validate per call.
|
|
283
|
+
const totalKeys =
|
|
284
|
+
(requiredProperties ? requiredProperties.length : 0) + optionalProperties.length;
|
|
285
|
+
if (totalKeys > MAX_KEYS_PER_CALL) {
|
|
286
|
+
throwBadInputError({
|
|
287
|
+
userMessage: `Too many properties to validate: ${totalKeys} (maximum ${MAX_KEYS_PER_CALL})`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
255
290
|
|
|
256
|
-
|
|
257
|
-
|
|
291
|
+
const requiredPaths = (requiredProperties || []).map(p => ({ raw: p, segments: splitPath(p) }));
|
|
292
|
+
const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
|
|
258
293
|
|
|
259
|
-
|
|
294
|
+
let validatedObject = {};
|
|
260
295
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
validatedArray.push(out[leafKey]);
|
|
276
|
-
}
|
|
277
|
-
return { valid: true, value: validatedArray };
|
|
278
|
-
}
|
|
279
|
-
const out = validateProperties({ [leafKey]: value });
|
|
280
|
-
if (Object.prototype.hasOwnProperty.call(out, leafKey)) {
|
|
281
|
-
return { valid: true, value: out[leafKey] };
|
|
296
|
+
// Helper: validate a single leaf value by feeding `{ [leafKey]: value }` to
|
|
297
|
+
// `validateProperties` and checking whether the leaf key survived.
|
|
298
|
+
//
|
|
299
|
+
// If `value` is an array, the same per-element validation is applied to
|
|
300
|
+
// every element; the result is an array of validated element values. The
|
|
301
|
+
// leaf is considered valid only when every element passes validation.
|
|
302
|
+
function validateLeafValue(leafKey, value) {
|
|
303
|
+
if (Array.isArray(value)) {
|
|
304
|
+
const validatedArray = [];
|
|
305
|
+
for (const element of value) {
|
|
306
|
+
const out = validateProperties({ [leafKey]: element });
|
|
307
|
+
if (!Object.prototype.hasOwnProperty.call(out, leafKey)) {
|
|
308
|
+
return { valid: false, value: undefined };
|
|
282
309
|
}
|
|
283
|
-
|
|
310
|
+
validatedArray.push(out[leafKey]);
|
|
311
|
+
}
|
|
312
|
+
return { valid: true, value: validatedArray };
|
|
284
313
|
}
|
|
314
|
+
const out = validateProperties({ [leafKey]: value });
|
|
315
|
+
if (Object.prototype.hasOwnProperty.call(out, leafKey)) {
|
|
316
|
+
return { valid: true, value: out[leafKey] };
|
|
317
|
+
}
|
|
318
|
+
return { valid: false, value: undefined };
|
|
319
|
+
}
|
|
285
320
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
writeLeaf(validatedObject, segments, validatedValue);
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
// 1 + 4. Optional paths: if provided, must be valid.
|
|
301
|
-
optionalPaths.forEach(({ raw, segments }) => {
|
|
302
|
-
const { found, value } = readLeaf(inputObject, segments);
|
|
303
|
-
if (!found) return;
|
|
304
|
-
const leafKey = segments[segments.length - 1];
|
|
305
|
-
const { valid, value: validatedValue } = validateLeafValue(leafKey, value);
|
|
306
|
-
if (!valid) {
|
|
307
|
-
throwBadInputError({ userMessage: `Invalid property value: ${raw}` });
|
|
308
|
-
}
|
|
309
|
-
writeLeaf(validatedObject, segments, validatedValue);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
// 5. Optional case transformation (recursive, handles nested keys).
|
|
313
|
-
if (convertToSnakeCase) {
|
|
314
|
-
validatedObject = keysToSnakeCase(validatedObject);
|
|
321
|
+
// 1 + 3. Required paths must exist and be valid.
|
|
322
|
+
requiredPaths.forEach(({ raw, segments }) => {
|
|
323
|
+
const { found, value } = readLeaf(inputObject, segments);
|
|
324
|
+
if (!found) {
|
|
325
|
+
throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
|
|
326
|
+
}
|
|
327
|
+
const leafKey = segments[segments.length - 1];
|
|
328
|
+
const { valid, value: validatedValue } = validateLeafValue(leafKey, value);
|
|
329
|
+
if (!valid) {
|
|
330
|
+
throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
|
|
315
331
|
}
|
|
332
|
+
writeLeaf(validatedObject, segments, validatedValue);
|
|
333
|
+
});
|
|
316
334
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
// 1 + 4. Optional paths: if provided, must be valid.
|
|
336
|
+
optionalPaths.forEach(({ raw, segments }) => {
|
|
337
|
+
const { found, value } = readLeaf(inputObject, segments);
|
|
338
|
+
if (!found) {
|
|
339
|
+
return;
|
|
320
340
|
}
|
|
341
|
+
const leafKey = segments[segments.length - 1];
|
|
342
|
+
const { valid, value: validatedValue } = validateLeafValue(leafKey, value);
|
|
343
|
+
if (!valid) {
|
|
344
|
+
throwBadInputError({ userMessage: `Invalid property value: ${raw}` });
|
|
345
|
+
}
|
|
346
|
+
writeLeaf(validatedObject, segments, validatedValue);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// 5. Optional case transformation (recursive, handles nested keys).
|
|
350
|
+
if (convertToSnakeCase) {
|
|
351
|
+
validatedObject = keysToSnakeCase(validatedObject);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 6. Optional flattening.
|
|
355
|
+
if (flattenOutput) {
|
|
356
|
+
validatedObject =
|
|
357
|
+
flattenKeyStyle === 'leaf'
|
|
358
|
+
? flattenObjectByLeafKey(validatedObject)
|
|
359
|
+
: flattenObject(validatedObject);
|
|
360
|
+
}
|
|
321
361
|
|
|
322
|
-
|
|
362
|
+
return Promise.resolve(validatedObject);
|
|
323
363
|
}
|
|
324
364
|
|
|
325
365
|
module.exports = validateWhitelistProperties;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ESLint } from 'eslint';
|
|
2
|
+
|
|
3
|
+
const eslint = new ESLint();
|
|
4
|
+
|
|
5
|
+
// Pattern: Pure Function - builds one deterministic command without shell interpolation.
|
|
6
|
+
const createCommand = (command, filePaths) =>
|
|
7
|
+
`${command} ${filePaths.map(filePath => JSON.stringify(filePath)).join(' ')}`;
|
|
8
|
+
|
|
9
|
+
// Pattern: Adapter - derives lint-staged input from ESLint's authoritative ignore rules.
|
|
10
|
+
const removeEslintIgnoredFiles = async filePaths => {
|
|
11
|
+
const ignoredFileStates = await Promise.all(
|
|
12
|
+
filePaths.map(filePath => eslint.isPathIgnored(filePath)),
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
return filePaths.flatMap((filePath, index) => (ignoredFileStates[index] ? [] : [filePath]));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Pattern: Pipeline - preserves ESLint-before-Prettier ordering for staged code.
|
|
19
|
+
const createJavaScriptTasks = async filePaths => {
|
|
20
|
+
const lintableFilePaths = await removeEslintIgnoredFiles(filePaths);
|
|
21
|
+
const tasks = [];
|
|
22
|
+
|
|
23
|
+
if (lintableFilePaths.length > 0) {
|
|
24
|
+
tasks.push(createCommand('eslint --fix --max-warnings 0', lintableFilePaths));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
tasks.push(createCommand('prettier --write', filePaths));
|
|
28
|
+
return tasks;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const lintStagedConfig = {
|
|
32
|
+
'*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}': createJavaScriptTasks,
|
|
33
|
+
'*.{json,jsonc,md,mdx,css,scss,yaml,yml}': ['prettier --write'],
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default lintStagedConfig;
|
package/package.json
CHANGED
|
@@ -1,63 +1,69 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
2
|
+
"name": "@carecard/validate",
|
|
3
|
+
"version": "3.19.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/CareCard-ca/pkg-validate.git"
|
|
7
|
+
},
|
|
8
|
+
"description": "Validate data",
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"types": "index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node scripts/runPackageTask.mjs test",
|
|
13
|
+
"test:order": "node --test scripts/testOrder/randomizeTestOrder.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/packageTaskRunner.test.mjs scripts/canonicalTestCommand.test.mjs",
|
|
14
|
+
"validate:audits": "node scripts/runPackageTask.mjs validate:audits",
|
|
15
|
+
"test:types": "node scripts/runPackageTask.mjs test:types",
|
|
16
|
+
"test:coverage": "node scripts/runPackageTask.mjs test:coverage",
|
|
17
|
+
"test:All": "node scripts/runPackageTask.mjs test:All",
|
|
18
|
+
"format": "prettier --write \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
|
|
19
|
+
"format:check": "prettier --check \"**/*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}\"",
|
|
20
|
+
"lint-staged": "lint-staged",
|
|
21
|
+
"prepare": "husky",
|
|
22
|
+
"lint:fix": "eslint . --fix --max-warnings 0",
|
|
23
|
+
"lint": "eslint . --max-warnings 0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"validate",
|
|
27
|
+
"data"
|
|
28
|
+
],
|
|
29
|
+
"author": "CareCard team",
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@eslint/js": "9.39.5",
|
|
33
|
+
"@types/mocha": "10.0.10",
|
|
34
|
+
"@types/node": "25.9.3",
|
|
35
|
+
"@typescript-eslint/parser": "8.67.0",
|
|
36
|
+
"eslint": "9.39.5",
|
|
37
|
+
"globals": "17.7.0",
|
|
38
|
+
"husky": "9.1.7",
|
|
39
|
+
"lint-staged": "17.2.0",
|
|
40
|
+
"mocha": "11.7.6",
|
|
41
|
+
"nyc": "18.0.0",
|
|
42
|
+
"prettier": "3.9.6",
|
|
43
|
+
"ts-node": "10.9.2",
|
|
44
|
+
"typescript": "6.0.3"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@carecard/common-util": "3.18.0"
|
|
48
|
+
},
|
|
49
|
+
"nyc": {
|
|
50
|
+
"all": true,
|
|
51
|
+
"include": [
|
|
52
|
+
"index.js",
|
|
53
|
+
"lib/**/*.js"
|
|
26
54
|
],
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@carecard/common-util": "3.18.0"
|
|
43
|
-
},
|
|
44
|
-
"nyc": {
|
|
45
|
-
"all": true,
|
|
46
|
-
"include": [
|
|
47
|
-
"index.js",
|
|
48
|
-
"lib/**/*.js"
|
|
49
|
-
],
|
|
50
|
-
"check-coverage": true,
|
|
51
|
-
"branches": 100,
|
|
52
|
-
"functions": 100,
|
|
53
|
-
"lines": 100,
|
|
54
|
-
"statements": 100
|
|
55
|
-
},
|
|
56
|
-
"overrides": {
|
|
57
|
-
"diff": "8.0.4",
|
|
58
|
-
"glob": "13.0.6",
|
|
59
|
-
"minimatch": "10.2.5",
|
|
60
|
-
"serialize-javascript": "7.0.5",
|
|
61
|
-
"js-yaml": "4.3.0"
|
|
62
|
-
}
|
|
55
|
+
"check-coverage": true,
|
|
56
|
+
"branches": 100,
|
|
57
|
+
"functions": 100,
|
|
58
|
+
"lines": 100,
|
|
59
|
+
"statements": 100
|
|
60
|
+
},
|
|
61
|
+
"overrides": {
|
|
62
|
+
"brace-expansion": "5.0.9",
|
|
63
|
+
"diff": "8.0.4",
|
|
64
|
+
"glob": "13.0.6",
|
|
65
|
+
"minimatch": "10.2.6",
|
|
66
|
+
"serialize-javascript": "7.0.5",
|
|
67
|
+
"js-yaml": "4.3.1"
|
|
68
|
+
}
|
|
63
69
|
}
|
package/readme.md
CHANGED
|
@@ -15,7 +15,9 @@ not pass validation.
|
|
|
15
15
|
|
|
16
16
|
## Development Rule
|
|
17
17
|
|
|
18
|
-
Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits,
|
|
18
|
+
Non-negotiable TDD rule: Always write the failing test first, run it to confirm it fails for the intended reason, then implement the code and rerun the test until it passes. Test Driven Development is required for all coding work and must not be skipped. For documentation- or skill-only edits, run the relevant focused non-test
|
|
19
|
+
validation before changing the prose; do not add automated tests that inspect
|
|
20
|
+
prose, files, or repository structure.
|
|
19
21
|
|
|
20
22
|
Non-negotiable repository isolation rule: Every repository must run its Husky hooks and tests using only files, code, fixtures, dependencies, and services contained within that repository. Tests and Husky scripts must not import, require, read, execute, or otherwise depend on sibling repositories or paths outside the repository root. app-e2e-tests is the only exception because cross-repository end-to-end testing is its explicit responsibility.
|
|
21
23
|
|
|
@@ -416,3 +418,22 @@ immediately when no helper remains, allow only a bounded 250 ms settlement
|
|
|
416
418
|
window for already-stopping helpers, fail persistent descendants, preserve
|
|
417
419
|
failures and output, use exit code `124` only for a real outer deadline, and
|
|
418
420
|
remain a final guard rather than a substitute for explicit cleanup.
|
|
421
|
+
|
|
422
|
+
## TDD And Validation
|
|
423
|
+
|
|
424
|
+
Test Driven Development is a non-negotiable requirement.
|
|
425
|
+
|
|
426
|
+
The sole purpose of automated tests is to verify observable functionality and externally visible behavior.
|
|
427
|
+
Tests must validate what the system does through its public interfaces and expected outcomes.
|
|
428
|
+
|
|
429
|
+
Tests must not assert, inspect, or depend on implementation details, including but not limited to:
|
|
430
|
+
|
|
431
|
+
- The existence of specific lines of code, statements, functions, classes, files, or modules.
|
|
432
|
+
- Specific algorithms, control flow, variable names, method calls, code snippets, or internal implementation choices.
|
|
433
|
+
- Any internal structure that can change without changing externally observable behavior.
|
|
434
|
+
|
|
435
|
+
A correct implementation may be completely rewritten or refactored without requiring changes to functional tests, provided its externally observable behavior remains unchanged.
|
|
436
|
+
|
|
437
|
+
Any test that fails solely because the implementation changed while the externally observable behavior remained correct is incorrectly designed and must be rewritten or removed.
|
|
438
|
+
|
|
439
|
+
This requirement is mandatory for all new tests and must be applied whenever existing tests are modified.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { runPackageTask } from './runPackageTask.mjs';
|
|
5
|
+
|
|
6
|
+
test('the complete test command runs each validation category exactly once', () => {
|
|
7
|
+
const executedSteps = [];
|
|
8
|
+
|
|
9
|
+
const exitCode = runPackageTask('test', taskStep => {
|
|
10
|
+
executedSteps.push([taskStep.command, ...(taskStep.arguments ?? [])].join(' '));
|
|
11
|
+
return 0;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
assert.equal(exitCode, 0);
|
|
15
|
+
assert.deepEqual(executedSteps, [
|
|
16
|
+
'npm run validate:audits',
|
|
17
|
+
'npm run test:order',
|
|
18
|
+
'tsc --noEmit',
|
|
19
|
+
'nyc node test/index.test.js',
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('the legacy aggregate command delegates to the complete test command once', () => {
|
|
24
|
+
const executedSteps = [];
|
|
25
|
+
|
|
26
|
+
runPackageTask('test:All', taskStep => {
|
|
27
|
+
executedSteps.push([taskStep.command, ...(taskStep.arguments ?? [])].join(' '));
|
|
28
|
+
return 0;
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
assert.deepEqual(executedSteps, ['npm test']);
|
|
32
|
+
});
|