SuperAPI AI Directory

create-evlog-map-rule

Agents & models

View repository

Updated 10-15 21:39

About this skill

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 apps/docs/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}',                    // kebab-case, matches CheckId
  category: 'requirement',       // or 'opportunity'
  title: '{col}',                // column header in --all, ~8 chars max
  expects: '{concrete thing}',   // e.g. 'log.audit()'
  question: 'Does this entry point …?',  // one sentence, shown by --inspect
  weight: 15,                    // requirements only — opportunities cannot have one
  docs: '/learn/…',              // docs path, no domain
  fixSlot: 'body',               // where suggest() lands: 'setup' | 'body' | … (default 'body')
  appliesTo: {
    kinds: HANDLER_KINDS,        // or a subset: 'api' | 'server-action' | 'middleware' | 'cron' | 'page'
    // frameworks: ['next'],     // optional framework gate
    when: ({ project, facts }) => /* opportunity gate — cheap, declarative */,
  },
  suggest({ project, target }) {
    // Code suggestion for `evlog map <file>`, aware of what the project already
    // has (project.catalogs, project.features…). Return lines of code.
    return ['const log = useLogger(event)']
  },
  create(context) {
    return {
      // Prefer onEnd + FileFacts — the shared AST pass already answers most
      // questions. Node-type listeners are the escape hatch.
      onEnd() {
        if (/* gap found */) context.report({ message: '…', line, snippet: true })
      },
    }
  },
} satisfies MapRule

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:

  1. The gap fires (with the message and line you expect)
  2. The compliant version passes
  3. The n/a boundaries: wrong kind, gated when returning false, hasEvlog: false phrasing if the rule branches on it
  4. Opportunity gating. Does NOT fire when the project doesn't use the feature
  5. suggest() output when it adapts to the project (e.g. names an existing catalog)
  6. 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

apps/docs/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   # catches REGISTRY/CheckId drift
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 apps/docs/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 apps/docs/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.