@rtorcato/repo-tooling 3.26.0 → 3.27.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.md CHANGED
@@ -18,7 +18,7 @@ Every command supports `--json` and a non-interactive mode. Combine with `--yes`
18
18
  | `setup --config <path>` | ✅ | `--dry-run` only | Scaffold with a full `ProjectConfig` JSON file. See `setup --config-schema`. |
19
19
  | `setup --config-schema` | ✅ | ✅ (JSON Schema) | Print the JSON Schema for `ProjectConfig`. Use to validate configs before scaffolding. |
20
20
  | `setup --dry-run` | ✅ | ✅ | Print resolved config + file list without writing. Pair with `--preset` or `--config`. |
21
- | `doctor --json` | ✅ | ✅ | Audit a project. Returns `{ directory, results: [{ check, status, detail, hint? }] }`. Status: `ok` / `drift` / `missing` / `optional-missing`. |
21
+ | `doctor --json` | ✅ | ✅ | Audit a project. Returns `{ directory, results: [{ check, status, detail, hint? }] }`. Status: `ok` / `drift` / `missing` / `optional-missing` / `declared`. |
22
22
  | `fix --json --yes` | ✅ | ✅ | Walk every doctor finding, apply fixers. Returns `FixActionRecord[]` with `status: applied | dry-run | skipped | already-ok | unsupported`. |
23
23
  | `fix <target> --json --yes` | ✅ | ✅ | Apply one fixer. Targets from `list --json`. |
24
24
  | `fix --dry-run` | ✅ | ✅ | Print what each fixer would write without writing. Combine with `--json`. |
@@ -156,6 +156,40 @@ function demoteDeclined(results, lock) {
156
156
  };
157
157
  });
158
158
  }
159
+ // Declared exceptions (#558): a failing check the lock names is reported as
160
+ // `declared` with its reason — shown, never hidden, but no longer failing the
161
+ // run. An exception naming a check this run doesn't know is itself drift:
162
+ // otherwise a typo silently does nothing and a check rename silently
163
+ // un-suppresses a finding, and both are invisible.
164
+ function applyExceptions(results, lock) {
165
+ const exceptions = lock?.exceptions;
166
+ if (!exceptions)
167
+ return results;
168
+ const known = new Set(results.map((r) => r.check));
169
+ const overlaid = results.map((r) => {
170
+ const reason = exceptions[r.check];
171
+ if (!reason || r.status === 'ok')
172
+ return r;
173
+ // Hint deliberately dropped: the deviation is declared, so "how to fix it"
174
+ // is exactly the noise the exception exists to retire.
175
+ return {
176
+ check: r.check,
177
+ status: 'declared',
178
+ detail: `${r.detail} — declared exception: ${reason}`,
179
+ };
180
+ });
181
+ for (const name of Object.keys(exceptions)) {
182
+ if (known.has(name))
183
+ continue;
184
+ overlaid.push({
185
+ check: 'Declared exceptions',
186
+ status: 'drift',
187
+ detail: `.repo-tooling.json declares an exception for "${name}", which is not a check this run knows`,
188
+ hint: 'A typo, or a check that was renamed or removed — fix or delete the entry in `exceptions`',
189
+ });
190
+ }
191
+ return overlaid;
192
+ }
159
193
  // The language-agnostic checks (src/base): repo hygiene, git hooks, CI,
160
194
  // security, and GitHub repo-settings that apply to any repo regardless of
161
195
  // language. Declared once and run for every project — a language module layers
@@ -238,7 +272,7 @@ export async function runDoctor(dir, skillsDir) {
238
272
  skillsDir,
239
273
  })),
240
274
  ];
241
- return demoteDeclined(results, lock);
275
+ return applyExceptions(demoteDeclined(results, lock), lock);
242
276
  }
243
277
  // Swift suite (#286): base checks plus the module's own. Swift repos have no
244
278
  // package.json, so nothing JS-shaped runs.
@@ -262,7 +296,7 @@ export async function runDoctor(dir, skillsDir) {
262
296
  })),
263
297
  ...(await runSwiftChecks(targetDir)),
264
298
  ];
265
- return demoteDeclined(results, lock);
299
+ return applyExceptions(demoteDeclined(results, lock), lock);
266
300
  }
267
301
  // Python suite (#290): same shape as Swift — base checks plus the module's
268
302
  // own, and nothing JS-shaped, because a Python repo has no package.json.
@@ -286,7 +320,7 @@ export async function runDoctor(dir, skillsDir) {
286
320
  })),
287
321
  ...(await runPythonChecks(targetDir)),
288
322
  ];
289
- return demoteDeclined(results, lock);
323
+ return applyExceptions(demoteDeclined(results, lock), lock);
290
324
  }
291
325
  // Perl suite (#289): same shape as Swift and Python — base checks plus the
292
326
  // module's own, and nothing JS-shaped, because a distribution has no
@@ -312,7 +346,7 @@ export async function runDoctor(dir, skillsDir) {
312
346
  })),
313
347
  ...(await runPerlChecks(targetDir)),
314
348
  ];
315
- return demoteDeclined(results, lock);
349
+ return applyExceptions(demoteDeclined(results, lock), lock);
316
350
  }
317
351
  // JS suite: the module's own checks, then the shared base ones. Only the
318
352
  // JS-shaped checks are listed here — re-listing the base suite is what made
@@ -371,13 +405,14 @@ export async function runDoctor(dir, skillsDir) {
371
405
  codeqlLanguages: languageModule.codeqlLanguages,
372
406
  skillsDir,
373
407
  })));
374
- return demoteDeclined(results, lock);
408
+ return applyExceptions(demoteDeclined(results, lock), lock);
375
409
  }
376
410
  const STATUS_ICONS = {
377
411
  ok: chalk.green('✅'),
378
412
  drift: chalk.yellow('⚠️ '),
379
413
  missing: chalk.red('❌'),
380
414
  'optional-missing': chalk.gray('➖'),
415
+ declared: chalk.blue('📝'),
381
416
  };
382
417
  function statusLabel(status) {
383
418
  switch (status) {
@@ -389,6 +424,8 @@ function statusLabel(status) {
389
424
  return chalk.red('missing');
390
425
  case 'optional-missing':
391
426
  return chalk.gray('not configured');
427
+ case 'declared':
428
+ return chalk.blue('declared');
392
429
  }
393
430
  }
394
431
  const MAX_NEXT_STEP_SUGGESTIONS = 8;
@@ -421,6 +458,7 @@ export function summarize(results) {
421
458
  drift: results.filter((r) => r.status === 'drift').length,
422
459
  missing: results.filter((r) => r.status === 'missing').length,
423
460
  optionalMissing: results.filter((r) => r.status === 'optional-missing').length,
461
+ declared: results.filter((r) => r.status === 'declared').length,
424
462
  };
425
463
  }
426
464
  export async function doctorCommand(options = {}) {
@@ -440,7 +478,7 @@ export async function doctorCommand(options = {}) {
440
478
  }
441
479
  const summary = summarize(results);
442
480
  console.log();
443
- console.log(` Summary: ${chalk.green(`${summary.ok} ok`)}, ${chalk.yellow(`${summary.drift} drift`)}, ${chalk.red(`${summary.missing} missing`)}, ${chalk.gray(`${summary.optionalMissing} not configured`)}\n`);
481
+ console.log(` Summary: ${chalk.green(`${summary.ok} ok`)}, ${chalk.yellow(`${summary.drift} drift`)}, ${chalk.red(`${summary.missing} missing`)}, ${chalk.gray(`${summary.optionalMissing} not configured`)}, ${chalk.blue(`${summary.declared} declared`)}\n`);
444
482
  const suggestions = nextStepSuggestions(results, await detectLanguage(dir));
445
483
  if (suggestions.length > 0) {
446
484
  console.log(chalk.bold(' Next steps:'));
@@ -436,7 +436,10 @@ export async function fixCommand(target, options = {}) {
436
436
  console.log();
437
437
  return;
438
438
  }
439
- const fixable = results.filter((r) => r.status !== 'ok');
439
+ // `declared` is a deviation the lockfile records on purpose (#558) — a bulk
440
+ // fix must not "repair" it. A targeted `fix <target>` still can: naming the
441
+ // fixer is the same explicit override the declined-in-lock path gets.
442
+ const fixable = results.filter((r) => r.status !== 'ok' && r.status !== 'declared');
440
443
  if (fixable.length === 0) {
441
444
  if (json)
442
445
  return emitJson(null);
@@ -117,6 +117,11 @@ export function lockfileSchema() {
117
117
  },
118
118
  },
119
119
  },
120
+ exceptions: {
121
+ type: 'object',
122
+ additionalProperties: { type: 'string', minLength: 1 },
123
+ description: 'Declared exceptions: doctor check name → the reason this repo deliberately deviates. The reason is mandatory and non-empty — doctor shows the check as `declared` with it (never hidden) and stops failing the run for it. An entry naming a check doctor does not run is itself reported as drift.',
124
+ },
120
125
  writtenBy: {
121
126
  type: 'string',
122
127
  description: 'Package name and version that last wrote this file.',
@@ -193,6 +198,7 @@ export async function writeLockfile(dir, config, assets) {
193
198
  ...(existing?.aiLoop ? { aiLoop: existing.aiLoop } : {}),
194
199
  ...(existing?.requiredSkills ? { requiredSkills: existing.requiredSkills } : {}),
195
200
  ...(existing?.mcp ? { mcp: existing.mcp } : {}),
201
+ ...(existing?.exceptions ? { exceptions: existing.exceptions } : {}),
196
202
  writtenBy: `@rtorcato/repo-tooling@${packageJson.version}`,
197
203
  writtenAt: new Date().toISOString(),
198
204
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.26.0",
3
+ "version": "3.27.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -30,6 +30,8 @@ npx @rtorcato/repo-tooling doctor --json # confirm clean
30
30
  prompt to **No**; `--yes` is required to overwrite. Show `fix <target> --diff` first.
31
31
  - `missing` — required and absent → fix it.
32
32
  - `optional-missing` — opt-in tool not configured. Only fix if the user wants that tool.
33
+ - `declared` — a real deviation the repo's `.repo-tooling.json` `exceptions` records on
34
+ purpose, with its reason. Leave it alone; it doesn't fail the run.
33
35
 
34
36
  `fix` returns `FixActionRecord[]` with `status: applied | dry-run | skipped | already-ok | unsupported`.
35
37