| name | create-evlog-map-rule |
| description | Add a new rule or a new framework adapter to `evlog map` in @evlog/cli. Use when adding a coverage check (requirement or opportunity) that scores entry points, or when extending the map scanner to a new framework. Covers rule source, registry, types, tests, docs, and the published skill. |
| metadata | {"internal":true} |
Create an evlog map Rule (or Framework Adapter)
Extend the coverage scanner in @evlog/cli. Two kinds of extension:
- A rule: a new question asked of every entry point (
packages/cli/src/lib/map/rules/). This is the common case.
- A framework adapter: teach
evlog map to find entry points in a new framework (packages/cli/src/lib/map/adapters/). Rarer and heavier; see the last section.
PR Title
feat(cli): add the {id} map rule
The cli scope already exists, so no scope registration is needed.
Requirement or opportunity? Decide first
This is the design decision everything else follows from (see apps/docs/content/3.cli/3.rules.md for the published contract):
| Requirement | Opportunity |
|---|
| Effect on the score | Costs weight points when it fails | None, ever — the type forbids a weight |
| When it appears | Whenever it applies | Only when the project already uses the feature (appliesTo.when gated on project.features / project.pairable) |
| In the report | FIX FIRST / THEN | GOING FURTHER |
Can fail a CI gate (--min-score, --baseline) | Yes | No |
Guiding principle (from error-catalog.ts): a rule that fires on perfectly good code is policing, not helping. Opportunities must be gated on a signal that makes the case on its own (duplication, an installed package used without its evlog integration), never on "you could adopt a feature you don't use".
Current requirements: wide-event (40), audit (25), structured-errors (20), page-error-handling (20), context (15), error-handling (15). Current opportunities: error-catalog, audit-coverage, ai-logging, auth-identity.
Touchpoints Checklist (rule)
| # | File | Action |
|---|
| 1 | packages/cli/src/lib/map/rules/{id}.ts | Create the rule (one exported const) |
| 2 | packages/cli/src/lib/map/rules/index.ts | Import + one line in REGISTRY |
| 3 | packages/cli/src/lib/map/types.ts | Add the id to the CheckId union (a type assert in index.ts fails the build if the registry and union drift) |
| 4 | packages/cli/test/map/rules.test.ts | Add cases (the file has an ESLint-RuleTester-style Case harness — runRuleSet exercises one rule in isolation) |
| 5 | apps/docs/content/3.cli/3.rules.md | Add a row to the Requirements or Opportunities table + a ### {title} section |
| 6 | apps/docs/content/3.cli/4.scoring.md | Requirements only: reflect the new weight in the scoring explanation |
| 7 | skills/review-logging-patterns/references/code-review.md | Add a row to the matching rules table |
| 8 | .changeset/{id}-map-rule.md | Changeset for "@evlog/cli": minor |
Important: Do NOT consider the task complete until all applicable touchpoints have been addressed.
Step 1: Rule Source
One file, one exported const satisfying MapRule (from rules/types.ts; requirements and opportunities are its two variants):
export const {camelId}Rule = {
id: '{id}',
category: 'requirement',
title: '{col}',
expects: '{concrete thing}',
question: 'Does this entry point …?',
weight: 15,
docs: '/learn/…',
fixSlot: 'body',
appliesTo: {
kinds: HANDLER_KINDS,
when: ({ project, facts }) => ,
},
suggest({ project, target }) {
return ['const log = useLogger(event)']
},
create() {
{
() {
() context.({ : , line, : })
},
}
},
}
Key rules:
- Reporting nothing means the rule passed.
context.report() only for gaps.
- Read
FileFacts first (../facts.ts). If the answer isn't there, consider extending the facts rather than writing AST listeners; facts are computed once per file for all rules.
project (ProjectFacts) is the gate for opportunities: project.features (evlog features in use), project.pairable (installed packages evlog integrates with), project.catalogs (for naming things in suggestions).
- Messages are report copy. Concrete, lowercase, pointing at the evidence (
"X is spelled out here and in 2 other files, and one catalog entry would cover them"). No exclamation marks, no advice-column tone.
- Weights are a scoring decision: look at
score.ts and the existing spread (40 down to 15) and discuss the number in the PR rather than inventing precedent.
- Every rule id is also a suppression target (
evlog-map-disable {id}) and part of the public evlog.map.json contract. Renaming later is a breaking change.
Steps 2 and 3: Registry + CheckId
Add the import and one REGISTRY line in rules/index.ts (report order matters: requirements before opportunities, heaviest first), and the id to the CheckId union in types.ts. The AssertIdsMatch type in index.ts fails the build if you forget either side.
Step 4: Tests
packages/cli/test/map/rules.test.ts has a declarative Case harness: source code in, expected check results out, with knobs for kind, framework, path (sensitivity), hasEvlog, features, pairable, dependencies, catalogs, barrels. Use runRuleSet([yourRule], run) to exercise the rule in isolation.
Cover at minimum:
- The gap fires (with the message and line you expect)
- The compliant version passes
- The
n/a boundaries: wrong kind, gated when returning false, hasEvlog: false phrasing if the rule branches on it
- Opportunity gating. Does NOT fire when the project doesn't use the feature
suggest() output when it adapts to the project (e.g. names an existing catalog)
- Suppression (
evlog-map-disable {id}) behaves like the other rules. Usually free via the shared harness
Run: pnpm --filter @evlog/cli exec vitest run test/map/rules.test.ts
Step 5 and 6: Docs
Read apps/docs/AGENTS.md before touching anything under apps/docs/. Then in apps/docs/content/3.cli/3.rules.md: add the row (column title, id, weight/fires-when, expects) and a ### {title} — {question} section following the existing ones, covering what it checks, what passes, what fails, the suggested shape. Requirements with a weight also touch the scoring narrative in 4.scoring.md.
Step 7: Published Skill
skills/review-logging-patterns/references/code-review.md mirrors the rules tables (requirements + opportunities) and maps each rule to a skill section. Add the row and, if the rule promotes a feature the skill documents elsewhere, link the section.
Step 8: Changeset
.changeset/{id}-map-rule.md with "@evlog/cli": minor, written from the user's perspective: what the rule checks, when it fires, whether it moves the score.
Verification
pnpm --filter @evlog/cli run lint
pnpm --filter @evlog/cli run typecheck
pnpm --filter @evlog/cli run test
Then sanity-check on a real project: pnpm cli:sandbox builds disposable, unevenly-instrumented apps under .sandbox/ (one per supported framework, each a git repo), and prints the commands to run against them. pnpm cli:sandbox --reset rolls an app back to pristine after an init or map run; --smoke drives the whole non-interactive feature matrix and reports what broke.
Variant: New Framework Adapter
Teaching evlog map a new framework is a different, heavier change: the adapter owns route discovery and framework capabilities.
| # | File | Action |
|---|
| 1 | packages/cli/src/lib/map/adapters/{framework}.ts | Route extraction: find entry points, classify RouteKind, declare FrameworkCapabilities (requestLogger: 'ambient' | 'explicit', evlogAutoImports) |
| 2 | packages/cli/src/lib/map/adapters/index.ts | Add the getAdapter switch case |
| 3 | packages/cli/src/lib/map/types.ts | Extend the Framework union |
| 4 | packages/cli/src/lib/map/detect.ts | Detect the framework from the project (detectFramework) |
| 5 | packages/cli/test/map/adapters.test.ts + detect.test.ts + fixtures/ | Route extraction + detection tests against a fixture tree |
| 6 | packages/cli/src/lib/init/ | Decide whether evlog init gains the framework too (separate scope of work — flag it explicitly in the PR if not) |
| 7 | apps/docs/content/3.cli/2.map.md + 0.overview.md | Update the supported-frameworks statements |
| 8 | skills/review-logging-patterns/SKILL.md | Update every "Nuxt, Nitro, Next.js, and TanStack Start" list (frontmatter description + CLI section) — same in references/code-review.md and skills/build-audit-logs/SKILL.md (Pass 2) and analyze-logs/SKILL.md (init suggestion) |
| 9 | scripts/cli-sandbox.mjs | Add the framework to APPS (reuse the map fixture) so pnpm cli:sandbox covers it and --smoke exercises every CLI command against it |
| 10 | .changeset/{framework}-map-adapter.md | Changeset for "@evlog/cli": minor |
Reference implementations: adapters/nuxt.ts (shared Nuxt/Nitro), adapters/next.ts, adapters/tanstack-start.ts.