{"id":35425203,"url":"https://github.com/jimmy-guzman/mejora","last_synced_at":"2026-02-18T21:11:07.144Z","repository":{"id":331390259,"uuid":"1118589307","full_name":"jimmy-guzman/mejora","owner":"jimmy-guzman","description":"Prevent regressions. Allow improvement.","archived":false,"fork":false,"pushed_at":"2026-02-12T03:29:48.000Z","size":325,"stargazers_count":1,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-02-12T13:42:53.716Z","etag":null,"topics":["baseline","cli","eslint","improvement","incremental","regression","testing","typescript"],"latest_commit_sha":null,"homepage":"https://jimmy.codes/blog/prevent-regressions-allow-improvement","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jimmy-guzman.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-18T01:51:42.000Z","updated_at":"2026-02-12T03:29:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jimmy-guzman/mejora","commit_stats":null,"previous_names":["jimmy-guzman/mejora"],"tags_count":38,"template":false,"template_full_name":null,"purl":"pkg:github/jimmy-guzman/mejora","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jimmy-guzman%2Fmejora","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jimmy-guzman%2Fmejora/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jimmy-guzman%2Fmejora/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jimmy-guzman%2Fmejora/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jimmy-guzman","download_url":"https://codeload.github.com/jimmy-guzman/mejora/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jimmy-guzman%2Fmejora/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29596186,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-18T20:59:56.587Z","status":"ssl_error","status_checked_at":"2026-02-18T20:58:41.434Z","response_time":162,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["baseline","cli","eslint","improvement","incremental","regression","testing","typescript"],"created_at":"2026-01-02T18:12:28.310Z","updated_at":"2026-02-18T21:11:07.138Z","avatar_url":"https://github.com/jimmy-guzman.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# mejora\n\n\u003e Prevent regressions. Allow improvement.\n\u003e _mejora_ (Spanish for \"improvement\")\n\n![actions][actions-badge]\n[![version][version-badge]][package]\n[![downloads][downloads-badge]][npmtrends]\n[![Install Size][install-size-badge]][packagephobia]\n\n`mejora` runs [checks](#supported-checks), compares them to a stored baseline, and fails only when things get worse.\n\n## Behavior\n\nEach check produces a snapshot.\n\nSnapshots are compared against a baseline.\n\n- New items are regressions and fail the run\n- Removed items are improvements and pass the run\n\nSnapshots use the `items` format to represent issues:\n\n```json\n{\n  \"checks\": {\n    \"eslint\": {\n      \"type\": \"items\",\n      \"items\": [\n        {\n          \"id\": \"a1b2c3d4...\",\n          \"file\": \"src/example.ts\",\n          \"line\": 12,\n          \"column\": 5,\n          \"rule\": \"no-unused-vars\",\n          \"message\": \"'foo' is declared but never used\"\n        }\n      ]\n    }\n  }\n}\n```\n\n\u003e [!NOTE]\n\u003e Issue identifiers (`id`) are stable across runs and generally insensitive to code movement, while remaining unique for repeated issues.\n\nThe baseline represents the last accepted state and should be committed to the repository.\n\nDefault location:\n\n```txt\n.mejora/baseline.json\n```\n\nWhen a run produces fewer items than the baseline, the run passes and the baseline is updated automatically.\n\nRegressions fail the run.\n\n`mejora --force` updates the baseline even when regressions are present.\n\n### Output\n\nOutput is non-interactive and deterministic.\n\n- Plain text by default\n- Markdown output for human-friendly review and navigation\n- `--json` produces structured output for CI and automation\n\n### Exit Codes\n\n- `0` pass or improvement\n- `1` regression detected or baseline out of sync\n- `2` configuration or runtime error\n\n## Installation\n\n```bash\npnpm add -D mejora\n```\n\n\u003e [!NOTE]\n\u003e `mejora` requires Node.js 22.18.0 or later.\n\n## Usage\n\nRun checks:\n\n```bash\npnpm mejora\n```\n\nForce the baseline to accept regressions:\n\n```bash\npnpm mejora --force\n```\n\nJSON output for CI and automation:\n\n```bash\npnpm mejora --json\n```\n\nRun only a subset of checks:\n\n```bash\npnpm mejora --only \"eslint \u003e *\"\n```\n\nSkip checks:\n\n```bash\npnpm mejora --skip typescript\n```\n\n## Configuration\n\nCreate one of:\n\n- `mejora.config.ts`\n- `mejora.config.js`\n- `mejora.config.mjs`\n- `mejora.config.mts`\n\n```ts\nimport { defineConfig, eslint, regex, typescript } from \"mejora\";\n\nexport default defineConfig({\n  checks: [\n    eslint({\n      name: \"no-nested-ternary\",\n      files: [\"src/**/*.{ts,tsx,js,jsx}\"],\n      rules: {\n        \"no-nested-ternary\": \"error\",\n      },\n    }),\n    typescript({\n      name: \"no-implicit-any\",\n      compilerOptions: {\n        noImplicitAny: true,\n      },\n    }),\n    regex({\n      name: \"no-todos\",\n      files: [\"src/**/*\"],\n      patterns: [\n        {\n          pattern: /\\/\\/\\s*TODO(?:\\((?\u003cowner\u003e[^)]+)\\))?:\\s*(?\u003ctask\u003e.*)/gi,\n          message: (match) =\u003e {\n            const task = match.groups?.task?.trim() || \"no description\";\n            const owner = match.groups?.owner;\n            const truncated =\n              task.length \u003e 80 ? `${task.slice(0, 80)}...` : task;\n\n            return owner ? `[${owner}] ${truncated}` : truncated;\n          },\n          rule: \"todo\",\n        },\n      ],\n    }),\n  ],\n});\n```\n\nEach check’s `name` is used as its ID in the baseline and output.\n\n## Supported Checks\n\n### ESLint\n\n- Snapshot type: `\"items\"`\n- Each lint message is treated as an issue\n- Regressions are new issues\n\n\u003e [!NOTE]\n\u003e `eslint` (^9.34.0) is required as a peer dependency when using the ESLint check\n\n### TypeScript\n\n- Snapshot type: `\"items\"`\n- Each compiler diagnostic is treated as an issue\n- Regressions are new issues\n- Uses the nearest `tsconfig.json` by default, or an explicit one if provided\n\n\u003e [!NOTE]\n\u003e `typescript` (^5.0.0) is required as a peer dependency when using the TypeScript check\n\n### Regex\n\n- Snapshot type: `\"items\"`\n- Each pattern match is treated as an issue\n- Regressions are new matches\n- Works on any file type (not just code)\n\n### Custom Checks\n\nDefine custom checks using `defineCheck()`:\n\n```ts\nimport { defineConfig, defineCheck } from \"mejora\";\nimport { glob, readFile } from \"node:fs/promises\";\n\nconst noHardcodedUrls = defineCheck\u003c{ files: string[] }\u003e({\n  type: \"no-hardcoded-urls\",\n  async run(config) {\n    const violations = [];\n\n    for await (const file of glob(config.files, { cwd: process.cwd() })) {\n      const content = await readFile(file, \"utf-8\");\n      const lines = content.split(\"\\n\");\n\n      for (let i = 0; i \u003c lines.length; i++) {\n        const line = lines[i];\n        const matches = line.matchAll(/https?:\\/\\/[^\\s'\"]+/g);\n\n        for (const match of matches) {\n          violations.push({\n            file,\n            line: i + 1,\n            column: match.index + 1,\n            rule: \"no-hardcoded-urls\",\n            message: `Hardcoded URL found: ${match[0]}`,\n          });\n        }\n      }\n    }\n\n    return violations;\n  },\n});\n\nexport default defineConfig({\n  checks: [\n    noHardcodedUrls({\n      name: \"urls-in-src\",\n      files: [\"src/**/*.ts\"],\n    }),\n    noHardcodedUrls({\n      name: \"urls-in-lib\",\n      files: [\"lib/**/*.ts\"],\n    }),\n  ],\n});\n```\n\n## CI\n\nWhen running in CI, `mejora` does not write the baseline.\n\nInstead, it compares the committed baseline to the expected results from the current codebase.\n\nIf there is any difference between the committed baseline and the expected results, the run fails.\n\n## Merge Conflicts\n\n`mejora` can automatically resolve merge conflicts in both `baseline.json` and `baseline.md`.\n\nAfter merging branches, you may see conflicts like:\n\n```bash\n$ git status\nboth modified:   .mejora/baseline.json\nboth modified:   .mejora/baseline.md\n```\n\nInstead of resolving these by hand, simply run `mejora`:\n\n```bash\n$ mejora\nMerge conflict detected in baseline, auto-resolving...\n✔ Baseline conflict resolved\n```\n\n`mejora` reconciles both sides of the conflict and regenerates a consistent baseline.\n\n## Credits\n\n- `mejora` is inspired by [betterer](https://phenomnomnominal.github.io/betterer/).\n\n[actions-badge]: https://img.shields.io/github/actions/workflow/status/jimmy-guzman/mejora/cd.yml?style=flat-square\u0026logo=github-actions\n[version-badge]: https://img.shields.io/npm/v/mejora?style=flat-square\u0026logo=npm\n[package]: https://www.npmjs.com/package/mejora\n[downloads-badge]: https://img.shields.io/npm/dm/mejora?style=flat-square\u0026logo=npm\n[npmtrends]: https://www.npmtrends.com/mejora\n[packagephobia]: https://packagephobia.com/result?p=mejora\n[install-size-badge]: https://img.shields.io/badge/dynamic/json?url=https://packagephobia.com/v2/api.json%3Fp=mejora\u0026query=$.install.pretty\u0026label=install%20size\u0026style=flat-square\u0026logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDggMTA4Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImEiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzAwNjgzOCIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzMyZGU4NSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGZpbGw9InVybCgjYSkiIGQ9Ik0yMS42NjcgNzMuODA5VjMzLjg2N2wyOC4zMy0xNi4xODggMjguMzM3IDE2LjE4OFY2Ni4xM0w0OS45OTcgODIuMzIxIDM1IDczLjc1VjQxLjYwNGwxNC45OTctOC41N0w2NSA0MS42MDR2MTYuNzg4bC0xNS4wMDMgOC41NzEtMS42NjMtLjk1di0xNi42NzJsOC4zODItNC43OTItNi43MTktMy44MzgtOC4zMyA0Ljc2M1Y2OS44OGw4LjMzIDQuNzYyIDIxLjY3LTEyLjM4M1YzNy43MzdsLTIxLjY3LTEyLjM3OS0yMS42NjMgMTIuMzc5djM5Ljg4TDQ5Ljk5NyA5MCA4NSA3MFYzMEw0OS45OTcgMTAgMTUgMzB2NDB6Ii8+PC9zdmc+\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjimmy-guzman%2Fmejora","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjimmy-guzman%2Fmejora","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjimmy-guzman%2Fmejora/lists"}