@syncerauk/test-rules 1.0.4 → 1.0.5

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
@@ -56,7 +56,8 @@ Both in the repo's `scripts/test-rules/`:
56
56
  key, `"key": null` when the file itself is the map). The defaults are `CONFIG_DEFAULTS` in the
57
57
  script.
58
58
  - `listed.json`: the breaches the repo had on the day the check arrived. Written once by
59
- `test-rules --init`.
59
+ `test-rules --init`. An optional `moved` map, `{ "<new test file>": "<old test file>" }`,
60
+ says where a split or a rename took a listed file.
60
61
 
61
62
  ## The list only shrinks
62
63
 
@@ -64,11 +65,18 @@ Both in the repo's `scripts/test-rules/`:
64
65
  - An entry whose breach is gone is stale and fails until its line is deleted.
65
66
  - Against a base, an entry the base's list does not hold fails. The base is `--base <ref>`,
66
67
  else `origin/main` when it resolves.
68
+ - A split or a rename carries its listed breaches: the new file's entry passes when `moved`
69
+ names the old file, the base listed the same rule on it, and the old file is no longer a test
70
+ file. A `moved` line is never stale and never a breach; delete it whenever you like.
67
71
 
68
72
  A repo starts with `"fail": false`: breaches are printed and the exit is 0. One line turns it on.
69
73
 
70
74
  ## Versions
71
75
 
76
+ - **1.0.5** — A split or rename carries its listed breaches: `listed.json` may hold
77
+ `"moved": { "<new test file>": "<old test file>" }`, and a new file's entry is not
78
+ `list:grew` when the base listed the same rule on the old file and the old file is gone.
79
+ Nothing changes for a repo that does not set it.
72
80
  - **1.0.4** — `style_contract`: a repo may name one test that reads its stylesheets (`.css`,
73
81
  `.scss`), the way wording is read once from the wording list. Those files are not a
74
82
  `T4:source-text` breach in that test, and only in that test. Nothing changes for a repo that
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncerauk/test-rules",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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
@@ -16,7 +16,9 @@
16
16
  //
17
17
  // Per repo, two files:
18
18
  // scripts/test-rules/config.json what differs between repos (CONFIG_DEFAULTS below)
19
- // scripts/test-rules/listed.json { v: 1, entries: { "<test file>": ["<rule id>", …] } }
19
+ // scripts/test-rules/listed.json { v: 1, entries: { "<test file>": ["<rule id>", …] },
20
+ // moved: { "<new test file>": "<old test file>" } }
21
+ // `moved` is optional: it says where a split or a rename took a listed file (1.0.5).
20
22
  //
21
23
  // The list only shrinks. A breach on the list passes; a breach off the list fails; an entry
22
24
  // whose breach is gone is `stale` and fails until its line is deleted; with a base, an entry the
@@ -24,6 +26,10 @@
24
26
  // lines than at the base fails (`T1:hub`). The base is `--base <ref>`, else origin/main when it
25
27
  // resolves (the merge base with HEAD is what is read); `--no-base` skips those two checks. The
26
28
  // base having no list at all is the one change that may add it (`--init` writes it once).
29
+ // A split or a rename carries its listed breaches: an entry the base does not hold is not
30
+ // `list:grew` when `moved` names its old file, the base listed that same rule on the old file,
31
+ // and the old file is no longer a test file. A `moved` line is never stale and never a breach;
32
+ // it may be deleted at any time.
27
33
  // `"fail": false` in the config is report only: everything is printed, the exit is 0.
28
34
  //
29
35
  // The rules (RULES below) and how each is seen:
@@ -349,9 +355,12 @@ export function breachesOf({ files, config, register = null, tracked = null }) {
349
355
  * The verdict over the breaches and the list. `listed`: the head's entries ({ file: [rule] }).
350
356
  * `base`: null (no base read), or { listed: the base's entries or null when the base has no
351
357
  * list, lines: { file: its line count at the base } }.
358
+ * `moved`: { new file: old file } from the head's list. `present`: a Set of the head's test
359
+ * files, or null (then nothing is carried). An entry the base does not hold is carried, not
360
+ * `list:grew`, when its old file is gone and the base listed the same rule on it.
352
361
  * → { breaches (off the list, plus T1:hub and list:grew), listed (count on the list), stale }.
353
362
  */
354
- export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
363
+ export function verdict({ all, listed = {}, base = null, lineCounts = {}, moved = {}, present = null }) {
355
364
  const on = (file, rule) => (listed[file] ?? []).includes(rule) && !NEVER_LISTED.includes(rule);
356
365
  const breaches = all.filter((b) => !on(b.file, b.rule));
357
366
  const hits = all.filter((b) => on(b.file, b.rule));
@@ -360,10 +369,14 @@ export function verdict({ all, listed = {}, base = null, lineCounts = {} }) {
360
369
  for (const file of Object.keys(listed).sort()) {
361
370
  for (const rule of [...listed[file]].sort()) if (!found.has(`${file}\n${rule}`)) stale.push({ file, rule });
362
371
  }
372
+ const carried = (file, rule) => {
373
+ const old = Object.hasOwn(moved, file) ? moved[file] : null;
374
+ return typeof old === 'string' && present !== null && !present.has(old) && (base.listed?.[old] ?? []).includes(rule);
375
+ };
363
376
  if (base) {
364
377
  for (const file of Object.keys(listed).sort()) {
365
378
  for (const rule of [...listed[file]].sort()) {
366
- if (base.listed && !(base.listed[file] ?? []).includes(rule)) breaches.push({ file, rule: 'list:grew', line: 1, say: `${rule}: ${RULES['list:grew']}` });
379
+ if (base.listed && !(base.listed[file] ?? []).includes(rule) && !carried(file, rule)) breaches.push({ file, rule: 'list:grew', line: 1, say: `${rule}: ${RULES['list:grew']}` });
367
380
  }
368
381
  const was = base.lines?.[file];
369
382
  if (listed[file].includes('R7:lines') && Number.isFinite(was) && lineCounts[file] > was) {
@@ -462,6 +475,15 @@ export function entriesOf(obj, where) {
462
475
  if (!ok) throw fail(EXIT.FAILED, `${where}: must be { "v": ${VERSION}, "entries": { "<test file>": ["<rule id>", …] } }`);
463
476
  return e;
464
477
  }
478
+ /** The optional `moved` map of a list: { "<new test file>": "<old test file>" }, or {} when absent. */
479
+ export function movedOf(obj, where) {
480
+ if (obj?.moved === undefined) return {};
481
+ const m = obj.moved;
482
+ const ok = m && typeof m === 'object' && !Array.isArray(m)
483
+ && Object.entries(m).every(([to, from]) => typeof from === 'string' && from.length > 0 && from !== to);
484
+ if (!ok) throw fail(EXIT.FAILED, `${where}: "moved" must be { "<new test file>": "<old test file>", … }, each old file named and not the new one`);
485
+ return m;
486
+ }
465
487
 
466
488
  export function main(argv = [], { log = console.log, error = console.error } = {}) {
467
489
  try {
@@ -486,7 +508,9 @@ export function main(argv = [], { log = console.log, error = console.error } = {
486
508
  return EXIT.CLEAN;
487
509
  }
488
510
 
489
- const listed = existsSync(join(dir, LISTED_FILE)) ? entriesOf(readJson(dir, LISTED_FILE), LISTED_FILE) : {};
511
+ const listedJson = existsSync(join(dir, LISTED_FILE)) ? readJson(dir, LISTED_FILE) : null;
512
+ const listed = listedJson ? entriesOf(listedJson, LISTED_FILE) : {};
513
+ const moved = listedJson ? movedOf(listedJson, LISTED_FILE) : {};
490
514
  let base = null;
491
515
  let baseNote = 'no base';
492
516
  if (!opts.noBase) {
@@ -512,7 +536,7 @@ export function main(argv = [], { log = console.log, error = console.error } = {
512
536
  } else baseNote = `no base (${ref} does not resolve)`;
513
537
  }
514
538
  const lineCounts = Object.fromEntries(files.map((f) => [f.path, countLines(f.text)]));
515
- const v = verdict({ all, listed, base, lineCounts });
539
+ const v = verdict({ all, listed, base, lineCounts, moved, present: new Set(testPaths) });
516
540
 
517
541
  if (opts.list) {
518
542
  log(JSON.stringify({ v: VERSION, breaches: v.breaches, listed: v.listed, stale: v.stale }));