@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
@@ -9,63 +9,65 @@ const DEFAULT_MAX_PARALLEL_JOBS = 4;
9
9
 
10
10
  // Pattern: Configuration Boundary - bounds workers without accepting invalid input.
11
11
  function resolveParallelJobCount(
12
- configuredJobCount,
13
- testFileCount,
14
- defaultMaximum = DEFAULT_MAX_PARALLEL_JOBS,
15
- availableJobCount = availableParallelism(),
12
+ configuredJobCount,
13
+ testFileCount,
14
+ defaultMaximum = DEFAULT_MAX_PARALLEL_JOBS,
15
+ availableJobCount = availableParallelism(),
16
16
  ) {
17
- const requestedJobCount =
18
- configuredJobCount === undefined ? Math.min(availableJobCount, defaultMaximum) : Number.parseInt(configuredJobCount, 10);
17
+ const requestedJobCount =
18
+ configuredJobCount === undefined
19
+ ? Math.min(availableJobCount, defaultMaximum)
20
+ : Number.parseInt(configuredJobCount, 10);
19
21
 
20
- if (!Number.isInteger(requestedJobCount) || requestedJobCount < 1) {
21
- throw new Error('TEST_PARALLEL_JOBS must be a positive integer.');
22
- }
23
- return Math.min(requestedJobCount, testFileCount);
22
+ if (!Number.isInteger(requestedJobCount) || requestedJobCount < 1) {
23
+ throw new Error('TEST_PARALLEL_JOBS must be a positive integer.');
24
+ }
25
+ return Math.min(requestedJobCount, testFileCount);
24
26
  }
25
27
 
26
28
  // Pattern: Command Builder - keeps Mocha worker details out of package metadata.
27
29
  function buildMochaArguments(testFiles, jobCount) {
28
- const requireFromRunner = createRequire(__filename);
29
- return [
30
- requireFromRunner.resolve('mocha/bin/mocha.js'),
31
- '--parallel',
32
- '--jobs',
33
- String(jobCount),
34
- '--require',
35
- resolve('scripts/testOrder/randomizeTestOrder.cjs'),
36
- ...testFiles,
37
- ];
30
+ const requireFromRunner = createRequire(__filename);
31
+ return [
32
+ requireFromRunner.resolve('mocha/bin/mocha.js'),
33
+ '--parallel',
34
+ '--jobs',
35
+ String(jobCount),
36
+ '--require',
37
+ resolve('scripts/testOrder/randomizeTestOrder.cjs'),
38
+ ...testFiles,
39
+ ];
38
40
  }
39
41
 
40
42
  // Pattern: Process Adapter - returns the exact test process result to the index.
41
43
  function runIndexedMochaTests(testFiles) {
42
- if (testFiles.length === 0) {
43
- throw new Error('The package test index must select at least one test file.');
44
- }
44
+ if (testFiles.length === 0) {
45
+ throw new Error('The package test index must select at least one test file.');
46
+ }
45
47
 
46
- const jobCount = resolveParallelJobCount(process.env.TEST_PARALLEL_JOBS, testFiles.length);
47
- const child = spawn(process.execPath, buildMochaArguments(testFiles, jobCount), {
48
- env: {
49
- ...process.env,
50
- NODE_ENV: 'test',
51
- },
52
- stdio: 'inherit',
53
- });
48
+ const jobCount = resolveParallelJobCount(process.env.TEST_PARALLEL_JOBS, testFiles.length);
49
+ const child = spawn(process.execPath, buildMochaArguments(testFiles, jobCount), {
50
+ env: {
51
+ ...process.env,
52
+ NODE_ENV: 'test',
53
+ },
54
+ stdio: 'inherit',
55
+ });
54
56
 
55
- return new Promise((resolveExit, rejectExit) => {
56
- child.once('error', rejectExit);
57
- child.once('exit', (code, signal) => {
58
- if (signal) {
59
- rejectExit(new Error(`Mocha exited from signal ${signal}.`));
60
- return;
61
- }
62
- resolveExit(code ?? 1);
63
- });
57
+ return new Promise((resolveExit, rejectExit) => {
58
+ child.once('error', rejectExit);
59
+ child.once('exit', (code, signal) => {
60
+ if (signal) {
61
+ rejectExit(new Error(`Mocha exited from signal ${signal}.`));
62
+ return;
63
+ }
64
+ resolveExit(code ?? 1);
64
65
  });
66
+ });
65
67
  }
66
68
 
67
69
  module.exports = {
68
- buildMochaArguments,
69
- resolveParallelJobCount,
70
- runIndexedMochaTests,
70
+ buildMochaArguments,
71
+ resolveParallelJobCount,
72
+ runIndexedMochaTests,
71
73
  };
@@ -6,16 +6,22 @@ const require = createRequire(import.meta.url);
6
6
  const { buildMochaArguments, resolveParallelJobCount } = require('./runIndexedMochaTests.cjs');
7
7
 
8
8
  test('uses bounded Mocha file workers without randomized default ordering', () => {
9
- assert.equal(resolveParallelJobCount(undefined, 8, 4, 12), 4);
10
- assert.equal(resolveParallelJobCount('2', 8, 4, 12), 2);
9
+ assert.equal(resolveParallelJobCount(undefined, 8, 4, 12), 4);
10
+ assert.equal(resolveParallelJobCount('2', 8, 4, 12), 2);
11
11
 
12
- const argumentsList = buildMochaArguments(['test/example.test.js'], 2);
12
+ const argumentsList = buildMochaArguments(['test/example.test.js'], 2);
13
13
 
14
- assert.ok(argumentsList.includes('--parallel'));
15
- assert.deepEqual(argumentsList.slice(argumentsList.indexOf('--jobs'), argumentsList.indexOf('--jobs') + 2), ['--jobs', '2']);
16
- assert.ok(argumentsList.includes('test/example.test.js'));
14
+ assert.ok(argumentsList.includes('--parallel'));
15
+ assert.deepEqual(
16
+ argumentsList.slice(argumentsList.indexOf('--jobs'), argumentsList.indexOf('--jobs') + 2),
17
+ ['--jobs', '2'],
18
+ );
19
+ assert.ok(argumentsList.includes('test/example.test.js'));
17
20
  });
18
21
 
19
22
  test('rejects invalid worker configuration instead of changing execution silently', () => {
20
- assert.throws(() => resolveParallelJobCount('0', 8, 4, 12), /TEST_PARALLEL_JOBS must be a positive integer/);
23
+ assert.throws(
24
+ () => resolveParallelJobCount('0', 8, 4, 12),
25
+ /TEST_PARALLEL_JOBS must be a positive integer/,
26
+ );
21
27
  });
@@ -1,48 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { execFileSync } from 'node:child_process';
3
- import { readFileSync } from 'node:fs';
4
- import { test } from 'node:test';
5
-
6
- const TEST_ORDER_INVARIANCE_RULE =
7
- "Non-negotiable test order invariance rule: Every test must pass independently of which tests run before or after it, and the suite must pass in every execution order. Each test must establish the state it needs, isolate mutable state, and clean up state it owns; it must never rely on another test's setup, mutations, or cleanup. Default test, CI, and Husky commands must use the test framework's ordinary ordering and must not force randomized ordering. Random-order execution is an explicit diagnostic only, and every failure it exposes must be fixed at the root cause.";
8
-
9
- function listRepositoryFiles() {
10
- return execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard'], { encoding: 'utf8' })
11
- .trim()
12
- .split('\n')
13
- .filter(Boolean);
14
- }
15
-
16
- function isRequiredTestGuidance(filePath) {
17
- return (
18
- /^readme\.md$/i.test(filePath) ||
19
- filePath === '.codex/AGENTS.md' ||
20
- filePath === '.junie/guidelines.md' ||
21
- filePath === '.agents/skills/carecard-workspace-standards/SKILL.md' ||
22
- /^\.agents\/skills\/[^/]*(?:test|testing)[^/]*\/(?:SKILL\.md|references\/[^/]*(?:test|testing|coding-principles)[^/]*\.md)$/i.test(
23
- filePath,
24
- )
25
- );
26
- }
27
-
28
- test('keeps the non-negotiable test order rule in repository guidance', () => {
29
- const guidanceFiles = listRepositoryFiles().filter(isRequiredTestGuidance);
30
- assert.ok(guidanceFiles.length > 0, 'No repository test guidance was found.');
31
-
32
- for (const guidanceFile of guidanceFiles) {
33
- const normalizedGuidance = readFileSync(guidanceFile, 'utf8').replace(/\s+/g, ' ').trim();
34
- assert.ok(
35
- normalizedGuidance.includes(TEST_ORDER_INVARIANCE_RULE),
36
- `${guidanceFile} must document the non-negotiable test order invariance rule.`,
37
- );
38
- }
39
- });
40
-
41
- test('keeps default package scripts on the test framework ordinary ordering', () => {
42
- const packageJson = JSON.parse(readFileSync('package.json', 'utf8'));
43
-
44
- for (const [scriptName, command] of Object.entries(packageJson.scripts ?? {})) {
45
- assert.equal(typeof command, 'string', `${scriptName} must be a string command.`);
46
- assert.doesNotMatch(command, /--test-randomize|--test-random-seed/, `${scriptName} must not force randomized test ordering.`);
47
- }
48
- });
@@ -1,43 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import { readdirSync, readFileSync } from 'node:fs';
3
- import { createRequire } from 'node:module';
4
- import { join, relative, resolve } from 'node:path';
5
- import test from 'node:test';
6
-
7
- const require = createRequire(import.meta.url);
8
- const repositoryRoot = resolve(import.meta.dirname, '../..');
9
- const packageJson = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
10
- const packageTaskRunnerSource = readFileSync(new URL('../runPackageTask.mjs', import.meta.url), 'utf8');
11
- const testIndexSource = readFileSync(new URL('../../test/index.test.js', import.meta.url), 'utf8');
12
- const { parallelTestFiles } = require('../../test/index.test.js');
13
-
14
- function listRuntimeTestFiles(directoryPath) {
15
- return readdirSync(directoryPath, { withFileTypes: true }).flatMap(entry => {
16
- const entryPath = join(directoryPath, entry.name);
17
- if (entry.isDirectory()) return listRuntimeTestFiles(entryPath);
18
- if (!/\.test\.(?:js|mjs)$/.test(entry.name) || entry.name === 'index.test.js') {
19
- return [];
20
- }
21
- return [relative(repositoryRoot, entryPath)];
22
- });
23
- }
24
-
25
- test('keeps runtime test selection in the index and package scripts short', () => {
26
- assert.equal(packageJson.scripts.test, 'node scripts/runPackageTask.mjs test');
27
- assert.equal(packageJson.scripts['test:coverage'], 'node scripts/runPackageTask.mjs test:coverage');
28
- assert.match(packageTaskRunnerSource, /arguments: \['run', 'test:order'\]/);
29
- assert.match(packageTaskRunnerSource, /arguments: \['test\/index\.test\.js'\]/);
30
- assert.match(packageTaskRunnerSource, /command: 'nyc'/);
31
- assert.match(testIndexSource, /parallelTestFiles/);
32
- assert.match(testIndexSource, /runIndexedMochaTests/);
33
- assert.match(testIndexSource, /if \(require\.main === module\)/);
34
- });
35
-
36
- test('runs the parallel execution contract in the test-order gate', () => {
37
- assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/parallelTestPolicy\.test\.mjs/);
38
- assert.match(packageJson.scripts['test:order'], /scripts\/testParallel\/runIndexedMochaTests\.test\.mjs/);
39
- });
40
-
41
- test('selects every runtime test file exactly once', () => {
42
- assert.deepEqual([...parallelTestFiles].sort(), listRuntimeTestFiles(resolve(repositoryRoot, 'test')).sort());
43
- });