@carecard/validate 3.18.0 → 3.20.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.
Files changed (36) hide show
  1. package/.agents/skills/carecard-workspace-standards/SKILL.md +49 -20
  2. package/.agents/skills/github-pr-create-update/SKILL.md +22 -1
  3. package/.agents/skills/github-pr-merge-cleanup/SKILL.md +22 -1
  4. package/.agents/skills/logged-in-user-profile-page/SKILL.md +22 -1
  5. package/.agents/skills/pkg-publish/SKILL.md +22 -1
  6. package/.agents/skills/pkg-validate-coding-standards-and-best-practices/SKILL.md +47 -9
  7. package/.agents/skills/pkg-validate-validation-library/SKILL.md +45 -14
  8. package/.agents/skills/software-design-patterns-and-clean-code/SKILL.md +23 -3
  9. package/.codex/AGENTS.md +41 -10
  10. package/.github/workflows/auto-draft-pr.yml +173 -151
  11. package/.github/workflows/ci.yml +35 -32
  12. package/.husky/pre-commit +4 -4
  13. package/.prettierrc.js +10 -9
  14. package/AGENTS.md +19 -0
  15. package/eslint.config.mjs +60 -8
  16. package/index.d.ts +108 -107
  17. package/index.js +6 -6
  18. package/lib/validate.js +262 -158
  19. package/lib/validateNewUserRoleRequest.js +86 -60
  20. package/lib/validateProperties.js +326 -326
  21. package/lib/validateWhitelistProperties.js +182 -142
  22. package/lint-staged.config.mjs +36 -0
  23. package/package.json +66 -60
  24. package/readme.md +22 -1
  25. package/scripts/canonicalTestCommand.test.mjs +32 -0
  26. package/scripts/packageTaskRunner.audit.mjs +37 -0
  27. package/scripts/packageTaskRunner.test.mjs +50 -72
  28. package/scripts/runPackageTask.mjs +103 -58
  29. package/scripts/testOrder/randomizeTestOrder.cjs +35 -25
  30. package/scripts/testOrder/randomizeTestOrder.test.mjs +19 -19
  31. package/scripts/testOrder/testOrderPolicy.audit.mjs +54 -0
  32. package/scripts/testParallel/parallelTestPolicy.audit.mjs +63 -0
  33. package/scripts/testParallel/runIndexedMochaTests.cjs +45 -43
  34. package/scripts/testParallel/runIndexedMochaTests.test.mjs +13 -7
  35. package/scripts/testOrder/testOrderPolicy.test.mjs +0 -48
  36. package/scripts/testParallel/parallelTestPolicy.test.mjs +0 -43
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- error: { throwBadInputError },
5
- caseConverter: { keysToSnakeCase },
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
- return /_/.test(segment) && /[A-Z]/.test(segment);
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
- return s.replace(/_([a-zA-Z0-9])/g, (_, c) => c.toUpperCase());
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
- return s.replace(/[A-Z]/g, c => `_${c.toLowerCase()}`);
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
- if (segment.indexOf('_') !== -1) return snakeToCamel(segment);
63
- if (/[A-Z]/.test(segment)) return camelToSnake(segment);
64
- return segment;
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
- const segments = String(path).split('.');
75
- if (segments.length > MAX_NESTING_DEPTH) {
76
- throwBadInputError({
77
- userMessage: `Property path "${path}" exceeds maximum nesting depth of ${MAX_NESTING_DEPTH}`,
78
- });
79
- }
80
- for (const seg of segments) {
81
- if (isMixedCaseSegment(seg)) {
82
- throwBadInputError({
83
- userMessage: `Property path "${path}" has a segment "${seg}" mixing snake_case and camelCase`,
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
- return segments;
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
- // Resolve a segment against the current node, trying its as-written form
101
- // first and then its alternate snake/camel form. Returns the actual key
102
- // present in the node, or undefined if neither form exists.
103
- function resolveKey(node, seg) {
104
- if (Object.prototype.hasOwnProperty.call(node, seg)) return seg;
105
- const alt = alternateCase(seg);
106
- if (alt !== seg && Object.prototype.hasOwnProperty.call(node, alt)) return alt;
107
- return undefined;
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
- let current = obj;
111
- for (let i = 0; i < segments.length - 1; i++) {
112
- if (current === null || typeof current !== 'object') return { found: false, value: undefined };
113
- const key = resolveKey(current, segments[i]);
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
- if (current === null || typeof current !== 'object') return { found: false, value: undefined };
118
- const leafKey = resolveKey(current, segments[segments.length - 1]);
119
- if (leafKey === undefined) return { found: false, value: undefined };
120
- return { found: true, value: current[leafKey] };
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
- let current = target;
133
- for (let i = 0; i < segments.length - 1; i++) {
134
- const key = segments[i];
135
- if (current[key] === null || typeof current[key] !== 'object') {
136
- current[key] = {};
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[segments[segments.length - 1]] = value;
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
- for (const [key, value] of Object.entries(obj)) {
156
- const path = prefix ? `${prefix}.${key}` : key;
157
- if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
158
- flattenObject(value, path, out);
159
- } else {
160
- out[path] = value;
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
- return out;
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
- for (const [key, value] of Object.entries(obj)) {
183
- if (value !== null && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
184
- flattenObjectByLeafKey(value, out, depthByKey, depth + 1);
185
- } else {
186
- if (!Object.prototype.hasOwnProperty.call(out, key) || depth < depthByKey[key]) {
187
- out[key] = value;
188
- depthByKey[key] = depth;
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
- return out;
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
- inputObject,
234
- requiredProperties = [],
235
- options = { optionalProperties: [], convertToSnakeCase: false, flattenOutput: false, flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE },
259
+ inputObject,
260
+ requiredProperties = [],
261
+ options = {
262
+ optionalProperties: [],
263
+ convertToSnakeCase: false,
264
+ flattenOutput: false,
265
+ flattenKeyStyle: DEFAULT_FLATTEN_KEY_STYLE,
266
+ },
236
267
  ) {
237
- const optionalProperties = (options && options.optionalProperties) || [];
238
- const convertToSnakeCase = !!(options && options.convertToSnakeCase);
239
- const flattenOutput = !!(options && options.flattenOutput);
240
- const flattenKeyStyle = options && options.flattenKeyStyle !== undefined ? options.flattenKeyStyle : DEFAULT_FLATTEN_KEY_STYLE;
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
- if (!VALID_FLATTEN_KEY_STYLES.has(flattenKeyStyle)) {
243
- throwBadInputError({
244
- userMessage: `Invalid flattenKeyStyle: ${String(flattenKeyStyle)}. Expected "path" or "leaf"`,
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
- // Cap the total number of paths to validate per call.
249
- const totalKeys = (requiredProperties ? requiredProperties.length : 0) + optionalProperties.length;
250
- if (totalKeys > MAX_KEYS_PER_CALL) {
251
- throwBadInputError({
252
- userMessage: `Too many properties to validate: ${totalKeys} (maximum ${MAX_KEYS_PER_CALL})`,
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
- const requiredPaths = (requiredProperties || []).map(p => ({ raw: p, segments: splitPath(p) }));
257
- const optionalPaths = optionalProperties.map(p => ({ raw: p, segments: splitPath(p) }));
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
- let validatedObject = {};
294
+ let validatedObject = {};
260
295
 
261
- // Helper: validate a single leaf value by feeding `{ [leafKey]: value }` to
262
- // `validateProperties` and checking whether the leaf key survived.
263
- //
264
- // If `value` is an array, the same per-element validation is applied to
265
- // every element; the result is an array of validated element values. The
266
- // leaf is considered valid only when every element passes validation.
267
- function validateLeafValue(leafKey, value) {
268
- if (Array.isArray(value)) {
269
- const validatedArray = [];
270
- for (const element of value) {
271
- const out = validateProperties({ [leafKey]: element });
272
- if (!Object.prototype.hasOwnProperty.call(out, leafKey)) {
273
- return { valid: false, value: undefined };
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
- return { valid: false, value: undefined };
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
- // 1 + 3. Required paths must exist and be valid.
287
- requiredPaths.forEach(({ raw, segments }) => {
288
- const { found, value } = readLeaf(inputObject, segments);
289
- if (!found) {
290
- throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
291
- }
292
- const leafKey = segments[segments.length - 1];
293
- const { valid, value: validatedValue } = validateLeafValue(leafKey, value);
294
- if (!valid) {
295
- throwBadInputError({ userMessage: `Missing or invalid property: ${raw}` });
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
- // 6. Optional flattening.
318
- if (flattenOutput) {
319
- validatedObject = flattenKeyStyle === 'leaf' ? flattenObjectByLeafKey(validatedObject) : flattenObject(validatedObject);
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
- return Promise.resolve(validatedObject);
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
- "name": "@carecard/validate",
3
- "version": "3.18.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/testOrder/testOrderPolicy.test.mjs scripts/testParallel/runIndexedMochaTests.test.mjs scripts/testParallel/parallelTestPolicy.test.mjs scripts/packageTaskRunner.test.mjs",
14
- "test:types": "node scripts/runPackageTask.mjs test:types",
15
- "test:coverage": "node scripts/runPackageTask.mjs test:coverage",
16
- "test:All": "node scripts/runPackageTask.mjs test:All",
17
- "format": "prettier --write .",
18
- "format:check": "prettier --check .",
19
- "prepare": "husky",
20
- "lint:fix": "eslint --fix",
21
- "lint": "eslint"
22
- },
23
- "keywords": [
24
- "validate",
25
- "data"
2
+ "name": "@carecard/validate",
3
+ "version": "3.20.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.20.0"
48
+ },
49
+ "nyc": {
50
+ "all": true,
51
+ "include": [
52
+ "index.js",
53
+ "lib/**/*.js"
26
54
  ],
27
- "author": "CareCard team",
28
- "license": "ISC",
29
- "devDependencies": {
30
- "@types/mocha": "10.0.10",
31
- "@types/node": "25.9.3",
32
- "eslint": "9.39.4",
33
- "husky": "9.1.7",
34
- "lint-staged": "17.0.7",
35
- "mocha": "11.7.6",
36
- "nyc": "18.0.0",
37
- "prettier": "3.8.4",
38
- "ts-node": "10.9.2",
39
- "typescript": "6.0.3"
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, add or update the relevant validation check before changing the prose.
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
+ });