@syncerauk/test-rules 1.0.0 → 1.0.2

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/README.md CHANGED
@@ -49,8 +49,10 @@ Both in the repo's `scripts/test-rules/`:
49
49
 
50
50
  - `config.json`: what differs between repos. `fail` (false is report only), `tests` and
51
51
  `ignore` (which files are test files), `fixtures` (folders a test may read), `wording` (the
52
- one list of what a person reads), `reads` (the reads register: `"comment"` or
53
- `{ "json": "<path>" }`). The defaults are `CONFIG_DEFAULTS` in the script.
52
+ one list of what a person reads), `reads` (the reads register: `"comment"`, or
53
+ `{ "json": "<path>" }` for the map under `"tests"` in that file, `"key": "<name>"` for another
54
+ key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
55
+ script.
54
56
  - `listed.json`: the breaches the repo had on the day the check arrived. Written once by
55
57
  `test-rules --init`.
56
58
 
@@ -63,6 +65,15 @@ Both in the repo's `scripts/test-rules/`:
63
65
 
64
66
  A repo starts with `"fail": false`: breaches are printed and the exit is 0. One line turns it on.
65
67
 
68
+ ## Versions
69
+
70
+ - **1.0.2** — A path that starts with a dot-folder (`.github/workflows/gate.yml`) is a repo
71
+ path; only `./` and `../` are relative to the test. A JSON reads register may be the whole
72
+ file (`"key": null`); a register with no map at its key cannot run (exit 3) instead of reading
73
+ as empty. Both make the check see reads it missed, so a repo's first run on 1.0.2 can show
74
+ breaches that are not on its list.
75
+ - **1.0.1** — The first release through the publish workflow. Same file as 1.0.0.
76
+
66
77
  ## Releases
67
78
 
68
79
  The source is `scripts/test-rules.mjs` in Syncera-UK/syncera-orchestrator, where its tests run
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncerauk/test-rules",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "The static check of Syncera's test-writing rules (T1, T4, T6 and the line limit of R7). One file, no dependency. Published to npm by the orchestrator's publish-test-rules workflow; each repo pins an exact version.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
package/test-rules.mjs CHANGED
@@ -69,7 +69,7 @@ export const CONFIG_DEFAULTS = Object.freeze({
69
69
  ignore: [], // test files the rules do not judge (a real-time tier 3 suite)
70
70
  fixtures: [], // folders a test may read: `a/fixtures/`
71
71
  wording: [], // the one list of what a person reads (a built page, a legal text): files, globs, folders
72
- reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" }
72
+ reads: null, // the reads register: null, "comment" (`// reads:` lines) or { json: "<path>", key: "tests" } (key null: the file itself is the map)
73
73
  source_ext: ['mjs', 'cjs', 'js', 'jsx', 'ts', 'tsx', 'mts', 'cts', 'sql', 'css', 'scss', 'html', 'sh', 'yml', 'yaml', 'md', 'mdx'],
74
74
  max_lines: 1500,
75
75
  task_names: ['(?:^|[^a-z0-9])w-?\\d+', '(?:^|[^a-z0-9])(?:fs|ops|trk|ci|app|leg|mig|bank|push|fun)-\\d+', '(?:^|[^a-z0-9])s\\d+(?:[^a-z0-9]|$)'],
@@ -204,7 +204,8 @@ const firstIndex = (text, re) => { const m = re.exec(text); return m ? m.index :
204
204
 
205
205
  // ---------------------------------------------------------------------------------------------
206
206
  // The reads register. Comment form: `// reads: a b` and `// runs: a b` on comment lines of their
207
- // own (the orchestrator's declared reads). JSON form: { <key>: { "<test>": ["<pattern>", …] } }.
207
+ // own (the orchestrator's declared reads). JSON form: { <key>: { "<test>": ["<pattern>", …] } },
208
+ // or the file itself is that map (`"key": null`): registerOf, below the config.
208
209
  // ---------------------------------------------------------------------------------------------
209
210
  const DECLARED = /^[ \t]*\/\/[ \t]*(reads|runs):[ \t]*(\S.*)$/gm;
210
211
  export function declaredIn(text) {
@@ -299,8 +300,9 @@ function sourceTextLine({ path, text, code, bare, strings }, { config, register,
299
300
  if (/[\s*?$]/.test(v) || !isSource(v)) continue;
300
301
  const before = code.slice(code.lastIndexOf('\n', s.start) + 1, s.start - 1);
301
302
  if (IMPORT_BEFORE.test(before) || !BUILT_HERE.test(before)) continue;
303
+ // `./x` and `../x` are relative to the test; `.github/x` is a repo path like any other.
302
304
  const here = posix.normalize(posix.join(posix.dirname(path), v));
303
- const target = v.startsWith('.') || !tracked.has(v) ? here : v;
305
+ const target = v.startsWith('./') || v.startsWith('../') || !tracked.has(v) ? here : v;
304
306
  if (target === path || !tracked.has(target) || allowed(target)) continue;
305
307
  if (said.some((p) => globRe(p).test(target))) continue;
306
308
  return lineOf(code, s.start);
@@ -394,10 +396,26 @@ export function withDefaults(raw) {
394
396
  if (!Array.isArray(config[k]) || config[k].some((x) => typeof x !== 'string')) throw fail(EXIT.FAILED, `${CONFIG_FILE}: ${k} must be a list of strings`);
395
397
  }
396
398
  const r = config.reads;
397
- if (!(r === null || r === 'comment' || (r && typeof r === 'object' && typeof r.json === 'string'))) throw fail(EXIT.FAILED, `${CONFIG_FILE}: reads must be null, "comment" or { "json": "<path>" }`);
399
+ const json = r && typeof r === 'object' && !Array.isArray(r) && typeof r.json === 'string'
400
+ && Object.keys(r).every((k) => k === 'json' || k === 'key') && (r.key === undefined || r.key === null || typeof r.key === 'string');
401
+ if (!(r === null || r === 'comment' || json)) throw fail(EXIT.FAILED, `${CONFIG_FILE}: reads must be null, "comment" or { "json": "<path>", "key": "<name>" | null }`);
398
402
  return config;
399
403
  }
400
404
 
405
+ /**
406
+ * The reads register inside a parsed JSON file: the map at `reads.key` ("tests" when no key is
407
+ * named), or the whole file when the key is null or empty. No map there cannot run (exit 3): an
408
+ * empty register would read as "no test reads anything".
409
+ */
410
+ export function registerOf(file, reads) {
411
+ const key = reads.key === undefined ? 'tests' : reads.key;
412
+ const whole = key === null || key === '';
413
+ const map = whole ? file : file?.[key];
414
+ const isMap = map && typeof map === 'object' && !Array.isArray(map) && Object.values(map).every((v) => Array.isArray(v));
415
+ if (!isMap) throw fail(EXIT.FAILED, `${reads.json}: no ${whole ? 'top-level' : `"${key}"`} map of { "<test file>": ["<pattern>", …] }${whole ? '' : ' (name it with "key", or "key": null when the file itself is the map)'}`);
416
+ return map;
417
+ }
418
+
401
419
  // ---------------------------------------------------------------------------------------------
402
420
  // The command line: the only part that touches disk and git.
403
421
  // ---------------------------------------------------------------------------------------------
@@ -446,7 +464,7 @@ export function main(argv = [], { log = console.log, error = console.error } = {
446
464
  const tracked = new Set(present);
447
465
  const testPaths = present.filter((p) => !p.split('/').includes('node_modules') && matches(config.tests, p) && !matches(config.ignore, p)).sort();
448
466
  const files = testPaths.map((path) => ({ path, text: readFileSync(join(dir, path), 'utf8') }));
449
- const register = config.reads && typeof config.reads === 'object' ? (readJson(dir, config.reads.json)[config.reads.key ?? 'tests'] ?? {}) : null;
467
+ const register = config.reads && typeof config.reads === 'object' ? registerOf(readJson(dir, config.reads.json), config.reads) : null;
450
468
  const all = breachesOf({ files, config, register, tracked });
451
469
 
452
470
  if (opts.init) {