@viberails/config 0.3.3 → 0.5.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/dist/index.cjs CHANGED
@@ -30,27 +30,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ BUILTIN_IGNORE: () => BUILTIN_IGNORE,
33
34
  DEFAULT_IGNORE: () => DEFAULT_IGNORE,
34
35
  DEFAULT_RULES: () => DEFAULT_RULES,
35
36
  VERSION: () => VERSION,
37
+ compactConfig: () => compactConfig,
36
38
  configSchema: () => configSchema,
39
+ expandDefaults: () => expandDefaults,
37
40
  generateConfig: () => generateConfig,
41
+ inferCoverageCommand: () => inferCoverageCommand,
38
42
  loadConfig: () => loadConfig,
39
43
  loadConfigSafe: () => loadConfigSafe,
40
44
  mergeConfig: () => mergeConfig
41
45
  });
42
46
  module.exports = __toCommonJS(index_exports);
43
47
 
48
+ // src/generate-config.ts
49
+ var path = __toESM(require("path"), 1);
50
+
44
51
  // src/defaults.ts
45
52
  var DEFAULT_RULES = {
46
53
  maxFileLines: 300,
47
54
  maxTestFileLines: 0,
48
- maxFunctionLines: 50,
49
- requireTests: true,
55
+ testCoverage: 80,
50
56
  enforceNaming: true,
51
- enforceBoundaries: false
57
+ enforceBoundaries: false,
58
+ enforceMissingTests: true
52
59
  };
53
- var DEFAULT_IGNORE = [
60
+ var DEFAULT_IGNORE = [];
61
+ var BUILTIN_IGNORE = [
54
62
  "**/*.d.ts",
55
63
  "**/*.min.js",
56
64
  "**/*.min.cjs",
@@ -72,70 +80,68 @@ var DEFAULT_IGNORE = [
72
80
  "**/__generated__/**"
73
81
  ];
74
82
 
75
- // src/generate-config.ts
76
- var path = __toESM(require("path"), 1);
77
-
78
- // src/generate-overrides.ts
79
- function conventionsDiffer(pkgConventions, globalConventions) {
80
- const overrides = {};
81
- let hasDiff = false;
83
+ // src/generate-packages.ts
84
+ function buildPackageStack(pkg) {
85
+ const config = {
86
+ language: formatStackItem(pkg.stack.language),
87
+ packageManager: formatStackItem(pkg.stack.packageManager)
88
+ };
89
+ if (pkg.stack.framework) config.framework = formatStackItem(pkg.stack.framework);
90
+ if (pkg.stack.styling) config.styling = formatStackItem(pkg.stack.styling);
91
+ if (pkg.stack.backend) config.backend = formatStackItem(pkg.stack.backend);
92
+ if (pkg.stack.orm) config.orm = formatStackItem(pkg.stack.orm);
93
+ if (pkg.stack.linter) config.linter = formatStackItem(pkg.stack.linter);
94
+ if (pkg.stack.formatter) config.formatter = formatStackItem(pkg.stack.formatter);
95
+ if (pkg.stack.testRunner) config.testRunner = formatStackItem(pkg.stack.testRunner);
96
+ return config;
97
+ }
98
+ function buildPackageConventions(pkgConventions) {
99
+ const config = {};
82
100
  for (const key of CONVENTION_KEYS) {
83
101
  const detected = pkgConventions[key];
84
- if (!detected) continue;
85
- const mapped = mapConvention(detected);
86
- if (mapped === void 0) continue;
87
- const globalValue = globalConventions[key];
88
- const globalStr = typeof globalValue === "string" ? globalValue : globalValue?.value;
89
- if (detected.value !== globalStr) {
90
- overrides[key] = mapped;
91
- hasDiff = true;
102
+ if (detected && detected.confidence !== "low") {
103
+ config[key] = detected.value;
92
104
  }
93
105
  }
94
- return hasDiff ? overrides : void 0;
106
+ return config;
95
107
  }
96
- function generatePackageOverrides(scanResult, globalConfig) {
108
+ function generatePackages(scanResult) {
97
109
  if (!scanResult.packages || scanResult.packages.length <= 1) return void 0;
98
- const overrides = [];
110
+ const packages = [];
99
111
  for (const pkg of scanResult.packages) {
100
- const override = {
112
+ const pkgScanResult = {
113
+ root: pkg.root,
114
+ stack: pkg.stack,
115
+ structure: pkg.structure,
116
+ conventions: pkg.conventions,
117
+ statistics: pkg.statistics
118
+ };
119
+ const packageConfig = {
101
120
  name: pkg.name,
102
- path: pkg.relativePath
121
+ path: pkg.relativePath,
122
+ stack: buildPackageStack(pkg),
123
+ structure: mapStructure(pkgScanResult),
124
+ conventions: buildPackageConventions(pkg.conventions)
103
125
  };
104
- let hasDiff = false;
105
- const stackOverride = {};
106
- let hasStackDiff = false;
107
- const optionalStackFields = [
108
- "framework",
109
- "styling",
110
- "backend",
111
- "orm",
112
- "linter",
113
- "formatter",
114
- "testRunner"
115
- ];
116
- for (const field of optionalStackFields) {
117
- const pkgItem = pkg.stack[field];
118
- if (!pkgItem) continue;
119
- const pkgValue = formatStackItem(pkgItem);
120
- if (pkgValue !== globalConfig.stack[field]) {
121
- stackOverride[field] = pkgValue;
122
- hasStackDiff = true;
123
- }
124
- }
125
- if (hasStackDiff) {
126
- override.stack = stackOverride;
127
- hasDiff = true;
128
- }
129
- const conventionOverrides = conventionsDiffer(pkg.conventions, globalConfig.conventions);
130
- if (conventionOverrides) {
131
- override.conventions = conventionOverrides;
132
- hasDiff = true;
133
- }
134
- if (hasDiff) {
135
- overrides.push(override);
126
+ if (pkg.typesOnly) {
127
+ packageConfig.rules = { testCoverage: 0 };
136
128
  }
129
+ packages.push(packageConfig);
130
+ }
131
+ return packages.length > 0 ? packages : void 0;
132
+ }
133
+
134
+ // src/infer-coverage-command.ts
135
+ function inferCoverageCommand(testRunner) {
136
+ if (!testRunner) return void 0;
137
+ const runner = testRunner.split("@")[0];
138
+ if (runner === "vitest") {
139
+ return "npx vitest run --coverage --coverage.reporter=json-summary";
140
+ }
141
+ if (runner === "jest") {
142
+ return "npx jest --coverage --coverageReporters=json-summary";
137
143
  }
138
- return overrides.length > 0 ? overrides : void 0;
144
+ return void 0;
139
145
  }
140
146
 
141
147
  // src/generate-config.ts
@@ -182,16 +188,6 @@ function mapStructure(scanResult) {
182
188
  }
183
189
  return config;
184
190
  }
185
- function mapConvention(convention) {
186
- if (convention.confidence === "low") {
187
- return void 0;
188
- }
189
- return {
190
- value: convention.value,
191
- _confidence: convention.confidence,
192
- _consistency: convention.consistency
193
- };
194
- }
195
191
  var CONVENTION_KEYS = [
196
192
  "fileNaming",
197
193
  "componentNaming",
@@ -199,67 +195,308 @@ var CONVENTION_KEYS = [
199
195
  "importAlias"
200
196
  ];
201
197
  function mapConventions(scanResult) {
202
- const config = {};
198
+ const conventions = {};
199
+ const meta = {};
203
200
  for (const key of CONVENTION_KEYS) {
204
201
  const detected = scanResult.conventions[key];
205
- if (detected) {
206
- const value = mapConvention(detected);
207
- if (value !== void 0) {
208
- config[key] = value;
209
- }
202
+ if (detected && detected.confidence !== "low") {
203
+ conventions[key] = detected.value;
204
+ meta[key] = {
205
+ value: detected.value,
206
+ confidence: detected.confidence,
207
+ consistency: detected.consistency
208
+ };
210
209
  }
211
210
  }
212
- return config;
211
+ return { conventions, meta };
212
+ }
213
+ function buildConventionMeta(conventions) {
214
+ const meta = {};
215
+ for (const key of CONVENTION_KEYS) {
216
+ const detected = conventions[key];
217
+ if (detected && detected.confidence !== "low") {
218
+ meta[key] = {
219
+ value: detected.value,
220
+ confidence: detected.confidence,
221
+ consistency: detected.consistency
222
+ };
223
+ }
224
+ }
225
+ return meta;
213
226
  }
214
227
  function generateConfig(scanResult) {
228
+ const projectName = path.basename(scanResult.root);
229
+ const { conventions, meta } = mapConventions(scanResult);
230
+ const rootPackage = {
231
+ name: projectName,
232
+ path: ".",
233
+ stack: mapStack(scanResult),
234
+ structure: mapStructure(scanResult),
235
+ conventions
236
+ };
237
+ const _meta = {
238
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
239
+ packages: {
240
+ ".": { conventions: Object.keys(meta).length > 0 ? meta : void 0 }
241
+ }
242
+ };
215
243
  const config = {
216
244
  $schema: "https://viberails.sh/schema/v1.json",
217
245
  version: 1,
218
- name: path.basename(scanResult.root),
219
- enforcement: "warn",
220
- stack: mapStack(scanResult),
221
- structure: mapStructure(scanResult),
222
- conventions: mapConventions(scanResult),
246
+ name: projectName,
223
247
  rules: { ...DEFAULT_RULES },
224
- ignore: [...DEFAULT_IGNORE]
248
+ ignore: [...DEFAULT_IGNORE],
249
+ packages: [rootPackage],
250
+ _meta
225
251
  };
252
+ const testRunner = scanResult.stack.testRunner;
253
+ const coverageCommand = inferCoverageCommand(
254
+ testRunner ? formatStackItem(testRunner) : void 0
255
+ );
256
+ if (coverageCommand) {
257
+ config.defaults = { coverage: { command: coverageCommand } };
258
+ }
226
259
  if (scanResult.workspace) {
227
- config.workspace = {
228
- packages: scanResult.workspace.packages.map((p) => p.relativePath),
229
- isMonorepo: true
230
- };
260
+ const packages = generatePackages(scanResult);
261
+ if (packages) {
262
+ config.packages = packages;
263
+ const pkgMeta = {};
264
+ for (const pkg of scanResult.packages) {
265
+ const convMeta = buildConventionMeta(pkg.conventions);
266
+ if (Object.keys(convMeta).length > 0) {
267
+ pkgMeta[pkg.relativePath] = { conventions: convMeta };
268
+ }
269
+ }
270
+ if (Object.keys(pkgMeta).length > 0) {
271
+ _meta.packages = pkgMeta;
272
+ }
273
+ }
231
274
  config.boundaries = { deny: {} };
232
275
  }
233
- const packageOverrides = generatePackageOverrides(scanResult, config);
234
- if (packageOverrides) {
235
- config.packages = packageOverrides;
236
- }
237
276
  return config;
238
277
  }
239
278
 
279
+ // src/compact-config.ts
280
+ var STACK_KEYS = [
281
+ "language",
282
+ "packageManager",
283
+ "framework",
284
+ "styling",
285
+ "backend",
286
+ "orm",
287
+ "linter",
288
+ "formatter",
289
+ "testRunner"
290
+ ];
291
+ var STRUCTURE_KEYS = [
292
+ "srcDir",
293
+ "pages",
294
+ "components",
295
+ "hooks",
296
+ "utils",
297
+ "types",
298
+ "tests",
299
+ "testPattern"
300
+ ];
301
+ function compactConfig(config) {
302
+ const pkgs = config.packages ?? [];
303
+ const defaults = {};
304
+ const packages = pkgs.map((p) => {
305
+ const copy = { ...p };
306
+ if (config.defaults?.coverage) {
307
+ copy.coverage = { ...config.defaults.coverage, ...copy.coverage ?? {} };
308
+ }
309
+ if (config.defaults?.stack) {
310
+ copy.stack = { ...config.defaults.stack, ...copy.stack ?? {} };
311
+ }
312
+ if (config.defaults?.structure) {
313
+ copy.structure = { ...config.defaults.structure, ...copy.structure ?? {} };
314
+ }
315
+ if (config.defaults?.conventions) {
316
+ copy.conventions = { ...config.defaults.conventions, ...copy.conventions ?? {} };
317
+ }
318
+ return copy;
319
+ });
320
+ if (packages.length <= 1) {
321
+ for (const pkg of packages) {
322
+ if (pkg.stack && Object.keys(pkg.stack).length === 0) delete pkg.stack;
323
+ if (pkg.structure && Object.keys(pkg.structure).length === 0) delete pkg.structure;
324
+ if (pkg.conventions && Object.keys(pkg.conventions).length === 0) delete pkg.conventions;
325
+ if (pkg.coverage && Object.keys(pkg.coverage).length === 0) delete pkg.coverage;
326
+ }
327
+ const { defaults: _d, ...rest } = config;
328
+ return { ...rest, packages };
329
+ }
330
+ const sharedStack = {};
331
+ for (const key of STACK_KEYS) {
332
+ const values = packages.map((p) => (p.stack ?? {})[key]);
333
+ if (values[0] !== void 0 && values.every((v) => v === values[0])) {
334
+ sharedStack[key] = values[0];
335
+ }
336
+ }
337
+ if (Object.keys(sharedStack).length > 0) {
338
+ defaults.stack = sharedStack;
339
+ for (const pkg of packages) {
340
+ const pkgStack = pkg.stack ?? {};
341
+ const sparse = {};
342
+ for (const key of STACK_KEYS) {
343
+ if (pkgStack[key] !== void 0 && pkgStack[key] !== sharedStack[key]) {
344
+ sparse[key] = pkgStack[key];
345
+ }
346
+ }
347
+ pkg.stack = sparse;
348
+ }
349
+ }
350
+ const sharedStructure = {};
351
+ for (const key of STRUCTURE_KEYS) {
352
+ const values = packages.map((p) => p.structure?.[key]);
353
+ const first = values[0];
354
+ if (first !== void 0 && values.every((v) => JSON.stringify(v) === JSON.stringify(first))) {
355
+ sharedStructure[key] = first;
356
+ }
357
+ }
358
+ if (Object.keys(sharedStructure).length > 0) {
359
+ defaults.structure = sharedStructure;
360
+ for (const pkg of packages) {
361
+ const pkgStructure = pkg.structure ?? {};
362
+ const sparse = {};
363
+ for (const key of STRUCTURE_KEYS) {
364
+ const val = pkgStructure[key];
365
+ if (val !== void 0 && JSON.stringify(val) !== JSON.stringify(sharedStructure[key])) {
366
+ sparse[key] = val;
367
+ }
368
+ }
369
+ pkg.structure = sparse;
370
+ }
371
+ }
372
+ const sharedConventions = {};
373
+ for (const key of CONVENTION_KEYS) {
374
+ const values = packages.map((p) => p.conventions?.[key]);
375
+ if (values[0] !== void 0 && values.every((v) => v === values[0])) {
376
+ sharedConventions[key] = values[0];
377
+ }
378
+ }
379
+ const sharedCoverage = {};
380
+ const coverageKeys = ["command", "summaryPath"];
381
+ for (const key of coverageKeys) {
382
+ const values = packages.map((p) => p.coverage?.[key]);
383
+ if (values[0] !== void 0 && values.every((v) => v === values[0])) {
384
+ sharedCoverage[key] = values[0];
385
+ }
386
+ }
387
+ if (Object.keys(sharedCoverage).length > 0) {
388
+ defaults.coverage = sharedCoverage;
389
+ for (const pkg of packages) {
390
+ const pkgCoverage = pkg.coverage ?? {};
391
+ const sparse = {};
392
+ for (const key of coverageKeys) {
393
+ if (pkgCoverage[key] !== void 0 && pkgCoverage[key] !== sharedCoverage[key]) {
394
+ sparse[key] = pkgCoverage[key];
395
+ }
396
+ }
397
+ pkg.coverage = sparse;
398
+ }
399
+ }
400
+ if (Object.keys(sharedConventions).length > 0) {
401
+ defaults.conventions = sharedConventions;
402
+ for (const pkg of packages) {
403
+ const pkgConventions = pkg.conventions ?? {};
404
+ const sparse = {};
405
+ for (const key of CONVENTION_KEYS) {
406
+ if (pkgConventions[key] !== void 0 && pkgConventions[key] !== sharedConventions[key]) {
407
+ sparse[key] = pkgConventions[key];
408
+ }
409
+ }
410
+ pkg.conventions = sparse;
411
+ }
412
+ }
413
+ for (const pkg of packages) {
414
+ if (pkg.stack && Object.keys(pkg.stack).length === 0) {
415
+ delete pkg.stack;
416
+ }
417
+ if (pkg.structure && Object.keys(pkg.structure).length === 0) {
418
+ delete pkg.structure;
419
+ }
420
+ if (pkg.conventions && Object.keys(pkg.conventions).length === 0) {
421
+ delete pkg.conventions;
422
+ }
423
+ if (pkg.coverage && Object.keys(pkg.coverage).length === 0) {
424
+ delete pkg.coverage;
425
+ }
426
+ }
427
+ const result = {
428
+ ...config.$schema ? { $schema: config.$schema } : {},
429
+ version: config.version,
430
+ name: config.name,
431
+ rules: config.rules,
432
+ ...config.ignore && config.ignore.length > 0 ? { ignore: config.ignore } : {},
433
+ ...config.boundaries ? { boundaries: config.boundaries } : {},
434
+ ...Object.keys(defaults).length > 0 ? { defaults } : {},
435
+ packages,
436
+ ...config._meta ? { _meta: config._meta } : {}
437
+ };
438
+ return result;
439
+ }
440
+ function expandDefaults(config) {
441
+ const defaults = config.defaults;
442
+ const packages = config.packages.map((pkg) => {
443
+ const expanded = { ...pkg };
444
+ expanded.stack = { ...defaults?.stack ?? {}, ...pkg.stack ?? {} };
445
+ expanded.structure = { ...defaults?.structure ?? {}, ...pkg.structure ?? {} };
446
+ expanded.conventions = { ...defaults?.conventions ?? {}, ...pkg.conventions ?? {} };
447
+ const mergedCoverage = { ...defaults?.coverage ?? {}, ...pkg.coverage ?? {} };
448
+ if (Object.keys(mergedCoverage).length > 0) {
449
+ expanded.coverage = mergedCoverage;
450
+ } else {
451
+ delete expanded.coverage;
452
+ }
453
+ return expanded;
454
+ });
455
+ const ignore = config.ignore ?? [];
456
+ const { defaults: _d, ...rest } = config;
457
+ return { ...rest, ignore, packages };
458
+ }
459
+
240
460
  // src/load-config.ts
241
461
  var fs = __toESM(require("fs/promises"), 1);
242
462
  function validateConfig(parsed, configPath) {
243
463
  const errors = [];
244
- const required = ["version", "name", "stack", "rules"];
464
+ const required = ["version", "name", "packages", "rules"];
245
465
  const missing = required.filter((field) => parsed[field] === void 0);
246
466
  if (missing.length > 0) {
247
467
  throw new Error(
248
468
  `Invalid viberails config at ${configPath}: missing required field(s): ${missing.join(", ")}`
249
469
  );
250
470
  }
251
- if (typeof parsed.version !== "number") errors.push('"version" must be a number');
252
- if (typeof parsed.name !== "string") errors.push('"name" must be a string');
253
- if (parsed.enforcement !== void 0 && parsed.enforcement !== "warn" && parsed.enforcement !== "enforce") {
254
- errors.push('"enforcement" must be "warn" or "enforce"');
471
+ if (typeof parsed.version !== "number") {
472
+ errors.push('"version" must be a number');
473
+ } else if (parsed.version !== 1) {
474
+ errors.push('"version" must be 1');
255
475
  }
256
- if (typeof parsed.stack !== "object" || parsed.stack === null) {
257
- errors.push('"stack" must be an object');
476
+ if (typeof parsed.name !== "string") errors.push('"name" must be a string');
477
+ if (!Array.isArray(parsed.packages)) {
478
+ errors.push('"packages" must be an array');
479
+ } else if (parsed.packages.length === 0) {
480
+ errors.push('"packages" must contain at least one package');
258
481
  } else {
259
- const stack = parsed.stack;
260
- if (typeof stack.language !== "string") errors.push('"stack.language" must be a string');
261
- if (typeof stack.packageManager !== "string")
262
- errors.push('"stack.packageManager" must be a string');
482
+ for (let i = 0; i < parsed.packages.length; i++) {
483
+ const pkg = parsed.packages[i];
484
+ if (typeof pkg.name !== "string") errors.push(`"packages[${i}].name" must be a string`);
485
+ if (typeof pkg.path !== "string") errors.push(`"packages[${i}].path" must be a string`);
486
+ if (pkg.coverage !== void 0) {
487
+ if (typeof pkg.coverage !== "object" || pkg.coverage === null) {
488
+ errors.push(`"packages[${i}].coverage" must be an object`);
489
+ } else {
490
+ const coverage = pkg.coverage;
491
+ if (coverage.command !== void 0 && typeof coverage.command !== "string") {
492
+ errors.push(`"packages[${i}].coverage.command" must be a string`);
493
+ }
494
+ if (coverage.summaryPath !== void 0 && typeof coverage.summaryPath !== "string") {
495
+ errors.push(`"packages[${i}].coverage.summaryPath" must be a string`);
496
+ }
497
+ }
498
+ }
499
+ }
263
500
  }
264
501
  if (typeof parsed.rules !== "object" || parsed.rules === null) {
265
502
  errors.push('"rules" must be an object');
@@ -267,10 +504,18 @@ function validateConfig(parsed, configPath) {
267
504
  const rules = parsed.rules;
268
505
  if (typeof rules.maxFileLines !== "number")
269
506
  errors.push('"rules.maxFileLines" must be a number');
270
- if (typeof rules.maxFunctionLines !== "number")
271
- errors.push('"rules.maxFunctionLines" must be a number');
272
- if (typeof rules.requireTests !== "boolean")
273
- errors.push('"rules.requireTests" must be a boolean');
507
+ else if (rules.maxFileLines < 0) errors.push('"rules.maxFileLines" must be >= 0');
508
+ if (rules.maxTestFileLines !== void 0) {
509
+ if (typeof rules.maxTestFileLines !== "number") {
510
+ errors.push('"rules.maxTestFileLines" must be a number');
511
+ } else if (rules.maxTestFileLines < 0) {
512
+ errors.push('"rules.maxTestFileLines" must be >= 0');
513
+ }
514
+ }
515
+ if (typeof rules.testCoverage !== "number")
516
+ errors.push('"rules.testCoverage" must be a number');
517
+ else if (rules.testCoverage < 0 || rules.testCoverage > 100)
518
+ errors.push('"rules.testCoverage" must be between 0 and 100');
274
519
  if (typeof rules.enforceNaming !== "boolean")
275
520
  errors.push('"rules.enforceNaming" must be a boolean');
276
521
  if (typeof rules.enforceBoundaries !== "boolean")
@@ -279,6 +524,26 @@ function validateConfig(parsed, configPath) {
279
524
  if (parsed.ignore !== void 0 && !Array.isArray(parsed.ignore)) {
280
525
  errors.push('"ignore" must be an array');
281
526
  }
527
+ if (parsed.defaults !== void 0) {
528
+ if (typeof parsed.defaults !== "object" || parsed.defaults === null) {
529
+ errors.push('"defaults" must be an object');
530
+ } else {
531
+ const defaults = parsed.defaults;
532
+ if (defaults.coverage !== void 0) {
533
+ if (typeof defaults.coverage !== "object" || defaults.coverage === null) {
534
+ errors.push('"defaults.coverage" must be an object');
535
+ } else {
536
+ const coverage = defaults.coverage;
537
+ if (coverage.command !== void 0 && typeof coverage.command !== "string") {
538
+ errors.push('"defaults.coverage.command" must be a string');
539
+ }
540
+ if (coverage.summaryPath !== void 0 && typeof coverage.summaryPath !== "string") {
541
+ errors.push('"defaults.coverage.summaryPath" must be a string');
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
282
547
  if (errors.length > 0) {
283
548
  throw new Error(`Invalid viberails config at ${configPath}: ${errors.join("; ")}`);
284
549
  }
@@ -305,7 +570,15 @@ async function loadConfig(configPath) {
305
570
  if (rules.maxTestFileLines === void 0) {
306
571
  rules.maxTestFileLines = 0;
307
572
  }
308
- return parsed;
573
+ if (rules.enforceMissingTests === void 0) {
574
+ rules.enforceMissingTests = true;
575
+ }
576
+ if (parsed.ignore === void 0) {
577
+ parsed.ignore = [];
578
+ }
579
+ let config = parsed;
580
+ config = expandDefaults(config);
581
+ return config;
309
582
  }
310
583
  async function loadConfigSafe(configPath) {
311
584
  try {
@@ -341,43 +614,79 @@ function mergeStructure(existing, fresh) {
341
614
  testPattern: existing.testPattern ?? fresh.testPattern
342
615
  };
343
616
  }
344
- function hasConvention(conventions, key) {
345
- return conventions[key] !== void 0;
346
- }
347
- function markAsDetected(value) {
348
- if (typeof value === "string") {
349
- return { value, _confidence: "high", _consistency: 100, _detected: true };
350
- }
351
- return { ...value, _detected: true };
352
- }
353
- function mergeConventions(existing, fresh) {
354
- const merged = { ...existing };
617
+ function mergeConventions(existing, fresh, existingMeta, freshMeta) {
618
+ const conventions = { ...existing };
619
+ const meta = { ...existingMeta ?? {} };
355
620
  for (const key of CONVENTION_KEYS) {
356
- if (!hasConvention(existing, key) && fresh[key] !== void 0) {
357
- const freshValue = fresh[key];
358
- if (freshValue !== void 0) {
359
- merged[key] = markAsDetected(freshValue);
621
+ if (existing[key] === void 0 && fresh[key] !== void 0) {
622
+ conventions[key] = fresh[key];
623
+ if (freshMeta?.[key]) {
624
+ meta[key] = { ...freshMeta[key], detected: true };
360
625
  }
626
+ } else if (existing[key] !== void 0 && freshMeta?.[key]) {
627
+ meta[key] = { ...freshMeta[key], value: freshMeta[key].value };
361
628
  }
362
629
  }
363
- return merged;
630
+ return { conventions, meta };
631
+ }
632
+ function mergePackage(existing, fresh, existingMeta, freshMeta) {
633
+ const { conventions, meta } = mergeConventions(
634
+ existing.conventions ?? {},
635
+ fresh.conventions ?? {},
636
+ existingMeta,
637
+ freshMeta
638
+ );
639
+ return {
640
+ pkg: {
641
+ ...existing,
642
+ stack: mergeStack(existing.stack ?? {}, fresh.stack ?? {}),
643
+ structure: mergeStructure(existing.structure ?? {}, fresh.structure ?? {}),
644
+ conventions
645
+ },
646
+ meta
647
+ };
364
648
  }
365
649
  function mergeConfig(existing, scanResult) {
366
650
  const fresh = generateConfig(scanResult);
651
+ const existingByPath = new Map(existing.packages.map((p) => [p.path, p]));
652
+ const freshByPath = new Map(fresh.packages.map((p) => [p.path, p]));
653
+ const mergedPackages = [];
654
+ const mergedPkgMeta = {};
655
+ for (const existingPkg of existing.packages) {
656
+ const freshPkg = freshByPath.get(existingPkg.path);
657
+ if (freshPkg) {
658
+ const existingConvMeta = existing._meta?.packages?.[existingPkg.path]?.conventions;
659
+ const freshConvMeta = fresh._meta?.packages?.[existingPkg.path]?.conventions;
660
+ const { pkg, meta } = mergePackage(existingPkg, freshPkg, existingConvMeta, freshConvMeta);
661
+ mergedPackages.push(pkg);
662
+ if (Object.keys(meta).length > 0) {
663
+ mergedPkgMeta[pkg.path] = { conventions: meta };
664
+ }
665
+ } else {
666
+ mergedPackages.push(existingPkg);
667
+ }
668
+ }
669
+ for (const freshPkg of fresh.packages) {
670
+ if (!existingByPath.has(freshPkg.path)) {
671
+ mergedPackages.push(freshPkg);
672
+ const freshConvMeta = fresh._meta?.packages?.[freshPkg.path]?.conventions;
673
+ if (freshConvMeta && Object.keys(freshConvMeta).length > 0) {
674
+ mergedPkgMeta[freshPkg.path] = { conventions: freshConvMeta };
675
+ }
676
+ }
677
+ }
367
678
  const merged = {
368
679
  $schema: existing.$schema ?? fresh.$schema,
369
680
  version: existing.version,
370
681
  name: existing.name,
371
- enforcement: existing.enforcement,
372
- stack: mergeStack(existing.stack, fresh.stack),
373
- structure: mergeStructure(existing.structure, fresh.structure),
374
- conventions: mergeConventions(existing.conventions, fresh.conventions),
375
682
  rules: { ...existing.rules },
376
- ignore: [...existing.ignore]
683
+ ignore: [...existing.ignore ?? []],
684
+ packages: mergedPackages,
685
+ _meta: {
686
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
687
+ ...Object.keys(mergedPkgMeta).length > 0 ? { packages: mergedPkgMeta } : {}
688
+ }
377
689
  };
378
- if (fresh.workspace) {
379
- merged.workspace = fresh.workspace;
380
- }
381
690
  if (existing.boundaries) {
382
691
  merged.boundaries = {
383
692
  deny: { ...existing.boundaries.deny },
@@ -389,57 +698,10 @@ function mergeConfig(existing, scanResult) {
389
698
  ...fresh.boundaries.ignore ? { ignore: [...fresh.boundaries.ignore] } : {}
390
699
  };
391
700
  }
392
- if (existing.packages || fresh.packages) {
393
- merged.packages = mergePackageOverrides(existing.packages, fresh.packages);
394
- }
395
701
  return merged;
396
702
  }
397
- function mergePackageOverrides(existing, fresh) {
398
- if (!fresh || fresh.length === 0) return existing;
399
- if (!existing || existing.length === 0) return fresh;
400
- const existingByPath = new Map(existing.map((p) => [p.path, p]));
401
- const merged = [...existing];
402
- for (const freshPkg of fresh) {
403
- if (!existingByPath.has(freshPkg.path)) {
404
- merged.push(freshPkg);
405
- }
406
- }
407
- return merged.length > 0 ? merged : void 0;
408
- }
409
703
 
410
704
  // src/schema-parts.ts
411
- var conventionValueDef = {
412
- description: "A convention value \u2014 either a plain string (user-confirmed) or an object with scanner metadata.",
413
- oneOf: [
414
- { type: "string" },
415
- {
416
- type: "object",
417
- required: ["value", "_confidence", "_consistency"],
418
- properties: {
419
- value: {
420
- type: "string",
421
- description: "The convention value."
422
- },
423
- _confidence: {
424
- type: "string",
425
- enum: ["high", "medium", "low"],
426
- description: "Scanner confidence level."
427
- },
428
- _consistency: {
429
- type: "number",
430
- minimum: 0,
431
- maximum: 100,
432
- description: "Scanner consistency percentage."
433
- },
434
- _detected: {
435
- type: "boolean",
436
- description: "Set by mergeConfig when a convention is newly detected during sync."
437
- }
438
- },
439
- additionalProperties: false
440
- }
441
- ]
442
- };
443
705
  var boundarySchema = {
444
706
  type: "object",
445
707
  required: ["deny"],
@@ -460,44 +722,112 @@ var boundarySchema = {
460
722
  },
461
723
  additionalProperties: false
462
724
  };
725
+ var stackSchema = {
726
+ type: "object",
727
+ properties: {
728
+ framework: { type: "string", description: 'Primary framework (e.g. "nextjs@15").' },
729
+ language: { type: "string", description: 'Primary language (e.g. "typescript").' },
730
+ styling: { type: "string", description: 'Styling solution (e.g. "tailwindcss@4").' },
731
+ backend: { type: "string", description: 'Backend framework (e.g. "express@5").' },
732
+ orm: { type: "string", description: 'ORM or database client (e.g. "prisma").' },
733
+ packageManager: { type: "string", description: 'Package manager (e.g. "pnpm").' },
734
+ linter: { type: "string", description: 'Linter (e.g. "eslint@9").' },
735
+ formatter: { type: "string", description: 'Formatter (e.g. "prettier").' },
736
+ testRunner: { type: "string", description: 'Test runner (e.g. "vitest").' }
737
+ },
738
+ additionalProperties: false
739
+ };
740
+ var structureSchema = {
741
+ type: "object",
742
+ properties: {
743
+ srcDir: { type: "string", description: 'Source directory (e.g. "src").' },
744
+ pages: { type: "string", description: "Pages or routes directory." },
745
+ components: { type: "string", description: "Components directory." },
746
+ hooks: { type: "string", description: "Hooks directory." },
747
+ utils: { type: "string", description: "Utilities directory." },
748
+ types: { type: "string", description: "Type definitions directory." },
749
+ tests: { type: "string", description: "Tests directory." },
750
+ testPattern: { type: "string", description: 'Test file naming pattern (e.g. "*.test.ts").' }
751
+ },
752
+ additionalProperties: false
753
+ };
754
+ var conventionsSchema = {
755
+ type: "object",
756
+ properties: {
757
+ fileNaming: { type: "string", description: 'File naming convention (e.g. "kebab-case").' },
758
+ componentNaming: { type: "string", description: "Component naming convention." },
759
+ hookNaming: { type: "string", description: "Hook naming convention." },
760
+ importAlias: { type: "string", description: 'Import alias pattern (e.g. "@/*").' }
761
+ },
762
+ additionalProperties: false
763
+ };
764
+ var coverageSchema = {
765
+ type: "object",
766
+ properties: {
767
+ command: {
768
+ type: "string",
769
+ description: "Command to generate coverage summary data for this package."
770
+ },
771
+ summaryPath: {
772
+ type: "string",
773
+ description: "Path to coverage summary JSON relative to package root."
774
+ }
775
+ },
776
+ additionalProperties: false
777
+ };
463
778
  var packageItemSchema = {
464
779
  type: "object",
465
780
  required: ["name", "path"],
466
781
  properties: {
467
782
  name: { type: "string", description: "Package name from package.json." },
468
- path: { type: "string", description: "Relative path to the package." },
469
- stack: {
470
- type: "object",
471
- properties: {
472
- framework: { type: "string" },
473
- language: { type: "string" },
474
- styling: { type: "string" },
475
- backend: { type: "string" },
476
- orm: { type: "string" },
477
- packageManager: { type: "string" },
478
- linter: { type: "string" },
479
- formatter: { type: "string" },
480
- testRunner: { type: "string" }
481
- },
482
- additionalProperties: false
783
+ path: { type: "string", description: 'Relative path to the package ("." for root).' },
784
+ stack: { ...stackSchema, description: "Technology stack for this package." },
785
+ structure: { ...structureSchema, description: "Directory structure for this package." },
786
+ conventions: { ...conventionsSchema, description: "Coding conventions for this package." },
787
+ coverage: {
788
+ ...coverageSchema,
789
+ description: "Coverage generation and summary settings for this package."
483
790
  },
484
- conventions: { $ref: "#/properties/conventions" },
485
791
  rules: {
486
792
  type: "object",
487
793
  properties: {
488
794
  maxFileLines: { type: "number" },
489
795
  maxTestFileLines: { type: "number" },
490
- maxFunctionLines: { type: "number" },
491
- requireTests: { type: "boolean" },
796
+ testCoverage: { type: "number" },
492
797
  enforceNaming: { type: "boolean" },
493
798
  enforceBoundaries: { type: "boolean" }
494
799
  },
495
800
  additionalProperties: false
496
801
  },
497
- ignore: { type: "array", items: { type: "string" } }
802
+ ignore: { type: "array", items: { type: "string" } },
803
+ boundaries: {
804
+ type: "object",
805
+ properties: {
806
+ deny: { type: "array", items: { type: "string" } },
807
+ ignore: { type: "array", items: { type: "string" } }
808
+ },
809
+ additionalProperties: false
810
+ }
498
811
  },
499
812
  additionalProperties: false
500
813
  };
814
+ var defaultsSchema = {
815
+ type: "object",
816
+ properties: {
817
+ stack: { ...stackSchema, description: "Default stack inherited by all packages." },
818
+ structure: { ...structureSchema, description: "Default structure inherited by all packages." },
819
+ conventions: {
820
+ ...conventionsSchema,
821
+ description: "Default conventions inherited by all packages."
822
+ },
823
+ coverage: {
824
+ ...coverageSchema,
825
+ description: "Default coverage settings inherited by all packages."
826
+ }
827
+ },
828
+ additionalProperties: false,
829
+ description: "Shared defaults for all packages. Packages inherit and can override."
830
+ };
501
831
 
502
832
  // src/schema.ts
503
833
  var configSchema = {
@@ -506,7 +836,7 @@ var configSchema = {
506
836
  title: "viberails configuration",
507
837
  description: "Configuration file for viberails \u2014 guardrails for vibe coding.",
508
838
  type: "object",
509
- required: ["version", "name", "stack", "rules"],
839
+ required: ["version", "name", "packages", "rules"],
510
840
  properties: {
511
841
  $schema: {
512
842
  type: "string",
@@ -515,120 +845,20 @@ var configSchema = {
515
845
  version: {
516
846
  type: "number",
517
847
  const: 1,
518
- description: "Config format version. Always 1 for V1.0."
848
+ description: "Config format version. Always 1."
519
849
  },
520
850
  name: {
521
851
  type: "string",
522
852
  description: "Project name, typically from package.json."
523
853
  },
524
- enforcement: {
525
- type: "string",
526
- enum: ["warn", "enforce"],
527
- default: "warn",
528
- description: "Whether conventions are warned about or enforced as errors."
529
- },
530
- stack: {
531
- type: "object",
532
- required: ["language", "packageManager"],
533
- properties: {
534
- framework: {
535
- type: "string",
536
- description: 'Primary framework identifier (e.g. "nextjs@15", "remix@2").'
537
- },
538
- language: {
539
- type: "string",
540
- description: 'Primary language (e.g. "typescript", "javascript").'
541
- },
542
- styling: {
543
- type: "string",
544
- description: 'Styling solution (e.g. "tailwindcss@4", "css-modules").'
545
- },
546
- backend: {
547
- type: "string",
548
- description: 'Backend framework (e.g. "express@5", "fastify").'
549
- },
550
- orm: {
551
- type: "string",
552
- description: 'ORM or database client (e.g. "prisma", "drizzle", "typeorm").'
553
- },
554
- packageManager: {
555
- type: "string",
556
- description: 'Package manager (e.g. "pnpm", "npm", "yarn").'
557
- },
558
- linter: {
559
- type: "string",
560
- description: 'Linter (e.g. "eslint@9", "biome").'
561
- },
562
- formatter: {
563
- type: "string",
564
- description: 'Formatter (e.g. "prettier", "biome").'
565
- },
566
- testRunner: {
567
- type: "string",
568
- description: 'Test runner (e.g. "vitest", "jest").'
569
- }
570
- },
571
- additionalProperties: false,
572
- description: "Detected or configured technology stack."
573
- },
574
- structure: {
575
- type: "object",
576
- properties: {
577
- srcDir: {
578
- type: "string",
579
- description: 'Source directory (e.g. "src"), or omit for flat structure.'
580
- },
581
- pages: {
582
- type: "string",
583
- description: 'Pages or routes directory (e.g. "src/app").'
584
- },
585
- components: {
586
- type: "string",
587
- description: 'Components directory (e.g. "src/components").'
588
- },
589
- hooks: {
590
- type: "string",
591
- description: 'Hooks directory (e.g. "src/hooks").'
592
- },
593
- utils: {
594
- type: "string",
595
- description: 'Utilities directory (e.g. "src/utils", "src/lib").'
596
- },
597
- types: {
598
- type: "string",
599
- description: 'Type definitions directory (e.g. "src/types").'
600
- },
601
- tests: {
602
- type: "string",
603
- description: 'Tests directory (e.g. "tests", "__tests__").'
604
- },
605
- testPattern: {
606
- type: "string",
607
- description: 'Test file naming pattern (e.g. "*.test.ts", "*.spec.ts").'
608
- }
609
- },
610
- additionalProperties: false,
611
- description: "Detected or configured directory structure."
612
- },
613
- conventions: {
614
- type: "object",
615
- properties: {
616
- fileNaming: { $ref: "#/definitions/conventionValue" },
617
- componentNaming: { $ref: "#/definitions/conventionValue" },
618
- hookNaming: { $ref: "#/definitions/conventionValue" },
619
- importAlias: { $ref: "#/definitions/conventionValue" }
620
- },
621
- additionalProperties: false,
622
- description: "Detected or configured coding conventions."
623
- },
624
854
  rules: {
625
855
  type: "object",
626
856
  required: [
627
857
  "maxFileLines",
628
- "maxFunctionLines",
629
- "requireTests",
858
+ "testCoverage",
630
859
  "enforceNaming",
631
- "enforceBoundaries"
860
+ "enforceBoundaries",
861
+ "enforceMissingTests"
632
862
  ],
633
863
  properties: {
634
864
  maxFileLines: {
@@ -639,17 +869,12 @@ var configSchema = {
639
869
  maxTestFileLines: {
640
870
  type: "number",
641
871
  default: 0,
642
- description: "Maximum number of lines allowed per test file. Set to 0 to exempt test files from size checks."
872
+ description: "Maximum number of lines allowed per test file. Set to 0 to exempt test files."
643
873
  },
644
- maxFunctionLines: {
874
+ testCoverage: {
645
875
  type: "number",
646
- default: 50,
647
- description: "Maximum number of lines allowed per function."
648
- },
649
- requireTests: {
650
- type: "boolean",
651
- default: true,
652
- description: "Whether to require test files for source modules."
876
+ default: 80,
877
+ description: "Minimum line coverage target percentage. 0 disables coverage threshold checks."
653
878
  },
654
879
  enforceNaming: {
655
880
  type: "boolean",
@@ -660,6 +885,11 @@ var configSchema = {
660
885
  type: "boolean",
661
886
  default: false,
662
887
  description: "Whether to enforce module boundary rules."
888
+ },
889
+ enforceMissingTests: {
890
+ type: "boolean",
891
+ default: true,
892
+ description: "Whether to enforce that every source file has a corresponding test file."
663
893
  }
664
894
  },
665
895
  additionalProperties: false,
@@ -668,50 +898,62 @@ var configSchema = {
668
898
  ignore: {
669
899
  type: "array",
670
900
  items: { type: "string" },
671
- description: "Glob patterns for files and directories to ignore."
901
+ description: "Project-specific glob patterns to ignore (universal patterns are built-in)."
672
902
  },
673
903
  boundaries: {
674
904
  ...boundarySchema,
675
905
  description: "Module boundary rules for import enforcement."
676
906
  },
677
- workspace: {
907
+ defaults: defaultsSchema,
908
+ packages: {
909
+ type: "array",
910
+ items: packageItemSchema,
911
+ description: 'Per-package configs. Single projects use path ".".'
912
+ },
913
+ _meta: {
678
914
  type: "object",
679
- required: ["packages", "isMonorepo"],
680
915
  properties: {
916
+ lastSync: { type: "string", description: "ISO timestamp of last sync." },
681
917
  packages: {
682
- type: "array",
683
- items: { type: "string" },
684
- description: "Relative paths to workspace packages."
685
- },
686
- isMonorepo: {
687
- type: "boolean",
688
- description: "Whether this project is a monorepo with multiple packages."
918
+ type: "object",
919
+ additionalProperties: {
920
+ type: "object",
921
+ properties: {
922
+ conventions: {
923
+ type: "object",
924
+ additionalProperties: {
925
+ type: "object",
926
+ properties: {
927
+ value: { type: "string" },
928
+ confidence: { type: "string", enum: ["high", "medium", "low"] },
929
+ consistency: { type: "number" },
930
+ detected: { type: "boolean" }
931
+ }
932
+ }
933
+ }
934
+ }
935
+ }
689
936
  }
690
937
  },
691
- additionalProperties: false,
692
- description: "Workspace configuration for monorepo projects."
693
- },
694
- packages: {
695
- type: "array",
696
- items: packageItemSchema,
697
- description: "Per-package overrides for monorepo projects."
938
+ description: "Scanner metadata. Regenerated on every sync \u2014 not user-editable."
698
939
  }
699
940
  },
700
- additionalProperties: false,
701
- definitions: {
702
- conventionValue: conventionValueDef
703
- }
941
+ additionalProperties: false
704
942
  };
705
943
 
706
944
  // src/index.ts
707
- var VERSION = "0.3.3";
945
+ var VERSION = "0.5.0";
708
946
  // Annotate the CommonJS export names for ESM import in node:
709
947
  0 && (module.exports = {
948
+ BUILTIN_IGNORE,
710
949
  DEFAULT_IGNORE,
711
950
  DEFAULT_RULES,
712
951
  VERSION,
952
+ compactConfig,
713
953
  configSchema,
954
+ expandDefaults,
714
955
  generateConfig,
956
+ inferCoverageCommand,
715
957
  loadConfig,
716
958
  loadConfigSafe,
717
959
  mergeConfig