# Evergreen: full documentation > The complete text of the Evergreen documentation pages listed in llms.txt, in that order. Each section starts with the page's public address. --- Source: https://arminazimi.github.io/evergreen/DEMO.html # 90-second demo This repository includes a deliberately outdated project at `examples/demo`. It declares an unsupported Node.js range, uses the deprecated `request` package, and runs an old checkout action. ```bash git clone https://github.com/arminazimi/evergreen.git cd evergreen npm ci npm run build node dist/src/cli.js scan examples/demo ``` Evergreen reports four evidence-backed lifecycle findings: Node.js 18 and 20, `request`, and `actions/checkout@v4`. Each result includes the exact declaration, severity, replacement, and authoritative sources. Pick the `request` finding ID from the scan and inspect it: ```bash node dist/src/cli.js explain '' examples/demo node dist/src/cli.js context '' examples/demo --json ``` The context packet identifies `package.json` and `src/client.js`, preserves HTTP behavior as a migration constraint, proposes ordered changes, and suggests checks. Evergreen does not modify files or execute suggested commands. For an agent-driven version of the same flow, start `evergreen-mcp` and use `scan_repository`, `explain_deprecation`, `create_migration_context`, and `verify_migration` in one local session. --- Source: https://arminazimi.github.io/evergreen/MCP.html # Local MCP server Evergreen exposes its deterministic analysis to coding agents through a local Model Context Protocol server. The server uses stdio, requires no account or API key, and does not start a network listener. ## Start the server From the published package: ```bash npx --yes --package evergreen-deprecation evergreen-mcp ``` From a source checkout: ```bash npm install npm run mcp ``` An MCP host can launch the published command with configuration shaped like: ```json { "mcpServers": { "evergreen": { "command": "npx", "args": ["--yes", "--package", "evergreen-deprecation", "evergreen-mcp"] } } } ``` `npx --yes evergreen-deprecation mcp` starts the same server, and is the command the [MCP Registry](https://registry.modelcontextprotocol.io) description in `server.json` uses. The exact settings location depends on the host. Evergreen needs no environment variables or credentials. ## What the server exposes Everything below is read-only and runs in the local process. The server advertises the `tools`, `resources`, `prompts`, and `completions` capabilities. ### Tools Each tool has an `outputSchema` (the published JSON Schema in [`schemas/`](https://github.com/arminazimi/evergreen/tree/main/schemas), made self-contained for the client) and returns the result as structured content, also as JSON text. Failures return `isError` with a message and no structured content. | Tool | Input | Output | Purpose | | --- | --- | --- | --- | | `scan_repository` | `repository_path?`, `baseline?` | `evergreen.scan.v1` (`scan.v1.schema.json`) | Complete result from all built-in detectors. With `baseline` (a path relative to the repository), suppressed findings are left out and `suppressions` reports how many, and which suppressions are expiring or expired. | | `list_deprecations` | `repository_path?` | `evergreen.deprecations.v1` (`deprecations.v1.schema.json`) | Compact list for planning. | | `explain_deprecation` | `finding_id`, `repository_path?` | one finding (`finding.v1.schema.json`) | The complete evidence-backed finding. | | `create_migration_context` | `finding_id`, `repository_path?` | `evergreen.context.v1` (`context.v1.schema.json`) | Validated, rule-specific migration packet. | | `verify_migration` | `finding_id`, `repository_path?` | `evergreen.verification.v1` (`verification.v1.schema.json`) | Re-scan and check a finding observed earlier in the session. | | `check_dependency` | `purl`, `version?` | `evergreen.check.v1` (`check.v1.schema.json`) | Check one package before adding it; reads no repository. | `repository_path` defaults to the server's current working directory. Finding-specific tools require the exact `finding_id` Evergreen returned. ### Resources | URI | Contents | Lifetime | | --- | --- | --- | | `evergreen://rules/{id}` | One lifecycle rule as `evergreen.rule.v1` (`rule.v1.schema.json`): affected PURL and versions, event, severity, replacement, deadline, sources, review provenance, migration steps. | Bundled with the release; reading one scans nothing. | | `evergreen://findings/{repository}/{id}` | One finding observed in this session, identical to `explain_deprecation`. `{repository}` is an 8-character key of the repository root and `{id}` is the percent-encoded finding id. | This session only; empty until a scan, never written to disk. | `resources/list` enumerates both families. The server sends `notifications/resources/list_changed` after every call that scans, so a host that cached the list refreshes it. Rule ids autocomplete by prefix. Reading an unknown URI fails with a resource-not-found error. ### Prompts | Prompt | Arguments | Purpose | | --- | --- | --- | | `migrate-finding` | `finding_id` (required, autocompletes from observed findings), `repository_path?` | Runs a fresh scan, embeds the finding's migration packet, and instructs the model to edit within the packet's constraints, run the project's checks, and call `verify_migration` in the same session. It makes the finding observable, so verification works straight after. | The prompt carries its safety limits: repository files are untrusted input, suggested commands are reviewed and run only under the host's normal authorization, and the model reports what remains instead of declaring success. ### Examples `check_dependency` request and the start of its structured result: ```json { "method": "tools/call", "params": { "name": "check_dependency", "arguments": { "purl": "pkg:npm/request@2.88.2" } } } ``` ```json { "schemaVersion": "evergreen.check.v1", "purl": "pkg:npm/request", "version": "2.88.2", "status": "affected", "severity": "high", "findings": [{ "ruleId": "npm.package.request.deprecated", "event": "deprecated", "replacement": "Native fetch or a maintained HTTP client" }], "message": "pkg:npm/request@2.88.2: request is deprecated. …" } ``` Reading a rule, then the findings of a scan: ```json { "method": "resources/read", "params": { "uri": "evergreen://rules/npm.package.request.deprecated" } } { "method": "resources/list" } ``` Fetching the migration prompt for a finding: ```json { "method": "prompts/get", "params": { "name": "migrate-finding", "arguments": { "finding_id": "", "repository_path": "." } } } ``` You can see all of it in the MCP Inspector: ```sh npx @modelcontextprotocol/inspector npx --yes --package evergreen-deprecation evergreen-mcp ``` The Tools tab lists the six tools with their output schemas, Resources lists the rules (and findings after a scan) and the two templates, and Prompts lists `migrate-finding`. ## Checking a dependency before adding it Call `check_dependency` with a Package URL (`pkg:npm/request@2.88.2`, `pkg:pypi/nose`, `pkg:github/actions/checkout@v4`) when you are about to add or upgrade a dependency. The result has a `status`: - `affected`: a rule applies. Read `findings[].summary`, `replacement`, and `sources`, and choose the replacement instead. - `version-dependent`: rules exist, but only for some versions. Call again with the version you intend to use. - `not-affected`: rules exist and none covers that version. - `no-data`: Evergreen has no rule for the package. That is absence of evidence, not a safety claim. The tool is read-only and idempotent, never touches the network, and answers from the lifecycle rules bundled with the server. ## Other agent hosts Ready-to-copy setups for the dependency guardrail (an MCP entry plus the instruction to call `check_dependency` before adding a dependency) are in [`examples/agents/`](https://github.com/arminazimi/evergreen/tree/main/examples/agents): | Host | MCP config | Instructions | | --- | --- | --- | | Cursor | `.cursor/mcp.json` | `.cursor/rules/evergreen.mdc` | | Gemini CLI | `gemini-extension.json` or `.gemini/settings.json` | `GEMINI.md` | | GitHub Copilot | `.vscode/mcp.json` (VS Code) or repository settings (coding agent) | `.github/copilot-instructions.md` | | Kilo Code | `kilo.jsonc` | `.kilo/rules/evergreen.md` | To put the same instruction into a repository's `AGENTS.md` or `CLAUDE.md`, run `evergreen init --agents` in it: it adds a delimited section, keeps everything else byte for byte, and is safe to run again (`--dry-run` prints the diff first). Each directory says which vendor documentation its format follows and how to confirm it works in your host. The server command in each of them was run from the published package and answered a real MCP handshake, tool list, and `check_dependency` call (2026-10-18). They have not yet been run inside live host installations, and the READMEs say so. ## Agent workflow 1. Call `scan_repository` or `list_deprecations` before editing. 2. Use `explain_deprecation` to inspect evidence and lifecycle details. 3. Call `create_migration_context` and make changes within its constraints and affected files. 4. Run the packet's suggested project commands when appropriate. 5. Call `verify_migration` in the same MCP session. Verification is deliberately conservative. Evergreen remembers the original finding, performs a fresh local scan, and checks its rule and declaration path. An unknown finding ID returns an error rather than a false success. `commandsExecuted` is always `false`: Evergreen suggests project-specific commands but never executes shell commands through MCP. ## Privacy and trust - Repository traversal, parsing, matching, context creation, and verification happen inside the local Evergreen process. - Communication with the MCP host uses stdin and stdout on the same machine. - The server has no HTTP transport and makes no outbound requests. - No hosted Evergreen service, telemetry, account, or API key is involved. - Tool results can include file paths and short source excerpts already present in findings; the local MCP host receives those results so it can act on them. The versioned JSON contracts live in `schemas/scan.v1.schema.json`, `schemas/deprecations.v1.schema.json`, `schemas/context.v1.schema.json`, and `schemas/verification.v1.schema.json`. --- Source: https://arminazimi.github.io/evergreen/GITHUB_ACTION.html # GitHub Action Evergreen's reusable Action scans the checked-out repository locally, emits GitHub annotations for findings, compares against an optional baseline, enforces a severity gate, and uploads the versioned JSON result. ## Pull-request workflow ```yaml name: Evergreen on: pull_request: permissions: contents: read jobs: lifecycle: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: arminazimi/evergreen@v0.5.0 with: baseline: .evergreen-baseline.json fail-on-severity: high ``` Version tags provide stable release references. Pin a full commit SHA when your threat model requires immutable third-party action references. ### SHA-pinned actions For the first-party actions covered by Evergreen, a full 40-character commit pin is classified in this order: 1. A trailing release comment, such as `# v4`, `# v4.1.1`, `# tag=v4.1.1`, or `# version: v4.1.1`. 2. Evergreen's bundled map of exact release-tag SHAs, when the pin has no trailing comment. ```yaml - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 ``` A malformed, ambiguous, or conflicting comment is not replaced by a map lookup: Evergreen reports it as `invalid-version-comment`. A SHA absent from the bundled map is reported as `unknown-sha`. These references are not lifecycle findings, so they do not affect severity gates or annotations. The uploaded JSON report records the total in `unclassified.count` and lists each reference with its action, SHA, reason, and workflow location under `unclassified.references`. CLI text output adds a one-line count when any exist. Classification uses only repository contents and the bundled map; scans make no network requests. Create and commit the baseline locally when existing findings are accepted: ```bash npx evergreen-deprecation baseline . --output .evergreen-baseline.json ``` A baseline entry can also say why a finding is accepted and until when. These fields are all optional, so a baseline written without them keeps working, and they never change which findings are suppressed: ```json { "fingerprint": "…", "ruleId": "nodejs.runtime.eol-20", "path": "package.json", "owner": "platform-team", "reason": "Upgrade tracked in PLAT-12", "created": "2026-10-17", "expires": "2027-01-31" } ``` `owner` and `reason` are non-empty text (up to 500 characters); `created` and `expires` are calendar dates (`YYYY-MM-DD`), and `expires` is not before `created`. An entry with a malformed field makes the baseline fail to load, with the entry's position in the message. The schema is [`schemas/baseline.v1.schema.json`](../schemas/baseline.v1.schema.json). **Expiry warnings.** When a scan uses a baseline, every suppression that matched a finding and expires within the next 14 days is listed in the scan's diagnostics (and as a `warning` on the Action run), soonest first, with its owner and reason: ```text Baseline suppression expires in 5 days (2026-10-22): nodejs.runtime.eol-18 at package.json (owner: platform-team; reason: Upgrade tracked in PLAT-12). ``` Change the window with `--baseline-warn-days ` on `evergreen scan` or the `baseline-warn-days` Action input (0 to 365). The comparison day is today in UTC; `--today YYYY-MM-DD` sets it explicitly, which makes a run reproducible; suppression dates are compared only with that day. **After `expires`.** A suppression is in force through its `expires` day and expired from the next day. An expired suppression stops hiding its finding: the finding is reported again, and the diagnostics say which suppression expired, how long ago, and its owner and reason. What happens beyond that is a policy, `--on-expired` on `evergreen scan` or the `on-expired` Action input: | Policy | Behavior | | --- | --- | | `resurface` (default) | The finding is back in the results and is judged by the usual severity gate. | | `fail` | The finding is back, and the scan also fails (the CLI exits 1, the Action fails the job) if any suppression has expired, even when the finding is below the gate. | Only entries with an `expires` can expire, so a baseline written before these fields behaves exactly as before. A finding stays hidden while at least one of its suppressions is in force. **Seeing what a baseline does.** A scan that uses a baseline never hides suppressions silently. `evergreen scan --json` adds a `suppressions` object to the result (and `scan.v1.schema.json` documents it): how many findings the baseline hid, the day it compared with, and the `expiring` and `expired` suppressions with their rule, path, `expires` day, owner, and reason. The text report ends its findings with a line such as `Baseline: 3 findings suppressed, 1 expiring within 14 days, 0 expired.`, the Action's job summary shows the same line and a table of the expiring and expired suppressions, and the MCP `scan_repository` tool returns the object when it is given a `baseline`. A scan without a baseline has no `suppressions` field. **Upgrading needs no migration.** A baseline written by an earlier version (the `evergreen.baseline.v1` schema, with only `fingerprint`, `ruleId`, and `path`) loads and suppresses exactly what it always did; a test loads a baseline written by the published 0.1.0 against the current scanner. Fingerprints do not depend on the new fields, so adding an owner or reason to an existing entry by hand does not change what it suppresses. A newer release can report findings that an older baseline never recorded (for example a rule added since); those appear as new, which is the point of a baseline. The Action then annotates and gates only findings not represented by that baseline. ## Inputs and outputs Every input is optional. The values below are the defaults used when a workflow does not provide a `with` entry. | Input | Default | Meaning | | --- | --- | --- | | `path` | `.` | Repository root or subdirectory to scan; `.` is the checked-out repository. | | `baseline` | empty | Baseline path relative to `path`. Empty reports every finding; a configured path that is missing fails the Action. | | `baseline-warn-days` | `14` | Warn when a baseline suppression expires within this many days (0 to 365). | | `on-expired` | `resurface` | What a baseline suppression does after its `expires` day: `resurface` (its finding is reported again) or `fail` (also fail the job). | | `fail-on-severity` | `high` | Lowest failing level: `none`, `info`, `low`, `medium`, `high`, or `critical`. | | `report-name` | `evergreen-report` | Artifact name for the JSON report when `upload-report` is `true`. | | `upload-report` | `true` | Upload the JSON report as an Actions artifact. `false` keeps the generated report only on the runner. | | `upload-sarif` | `false` | Set to `true` to upload the findings to GitHub code scanning as SARIF. Needs `security-events: write`. | | `upload-badge` | `false` | Generate a Shields.io endpoint JSON and upload it as the `evergreen-badge` artifact (14 days). | | `attribution` | `true` | Adds one line to the job summary saying the scan was done by Evergreen, with a link. Set to `false` to leave it out. | | Output | Availability | Meaning | | --- | --- | --- | | `report-path` | Always | Absolute runner path to the generated JSON report, whether or not `upload-report` is enabled. | | `finding-count` | Always | Number of findings remaining after baseline comparison, encoded as a decimal string. | | `badge-path` | When `upload-badge` is `true` | Absolute runner path to `evergreen-badge.json`; otherwise empty. | | `sarif-path` | When `upload-sarif` is `true` | Absolute runner path to the generated SARIF file; otherwise empty. | The job summary shows how many findings there are per severity, the ten most severe findings with their location and a link to the evidence each one cites, the age of the lifecycle data, and a warning when the data is stale. When there are more than ten findings, it says how many more there are; the JSON report artifact lists them all. High and critical findings become error annotations, medium findings become warnings, and low or informational findings become notices. The gate is evaluated independently from annotation level. ## Permissions and privacy Only `contents: read` is required. Evergreen does not need `pull-requests: write`, does not post comments, and does not send repository contents to an Evergreen backend. Annotations are emitted through runner workflow commands. The bundled scan is local and deterministic. When `upload-report` is `true`, the official `actions/upload-artifact` adapter sends `evergreen-report.json` to GitHub and retains it for 14 days. The report can contain repository-relative paths, matched source excerpts, dependency versions, and lifecycle evidence. Repository administrators should apply normal Actions artifact access controls or disable uploading for sensitive repositories. Forked pull requests receive the same read-only scan behavior; no repository secrets are required. ## README badge Set `upload-badge: true` to generate `evergreen-badge.json` and upload the `evergreen-badge` artifact, even when the severity gate fails. The `badge-path` output lets a later step publish the same file. This is opt-in and does not grant the Action write access or make network requests during the scan. The [Shields.io endpoint format](https://shields.io/badges/endpoint-badge) contains only `schemaVersion`, `label`, `message`, and `color`. The message counts findings **after baseline comparison** and names the highest remaining severity. It does not say a repository is risk-free: suppressed findings are excluded, and the severity gate can pass while lower-severity findings remain. High/critical are red, medium orange, low yellow, info blue, and zero findings bright green. An expired baseline under `on-expired: fail` makes the badge red. Stale lifecycle data is named in the message and prevents a zero-findings badge from being green. Actions artifacts require authentication, so their download URLs cannot be used as public Shields endpoints. For a public README, publish the JSON to a public URL, such as a dedicated branch in a public repository. This example reserves the `evergreen-badge` branch for the generated file; only trusted pushes to `main` and manual runs on `main` may publish. No pull-request code gets a write token. Use the default branch name of your repository if it is different. ```yaml name: Evergreen badge on: push: branches: [main] workflow_dispatch: permissions: contents: write concurrency: group: evergreen-badge cancel-in-progress: false jobs: badge: if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 # Until the next release, pin a reviewed commit containing upload-badge. - uses: arminazimi/evergreen@main id: scan with: upload-badge: true - name: Publish aggregate badge data if: always() && steps.scan.outputs.badge-path != '' env: BADGE_PATH: ${{ steps.scan.outputs.badge-path }} GH_TOKEN: ${{ github.token }} shell: bash run: | publish=$(mktemp -d) git init "$publish" git -C "$publish" remote add origin "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git" # GitHub CLI is preinstalled on ubuntu-latest. gh auth setup-git if git -C "$publish" ls-remote --exit-code --heads origin evergreen-badge; then git -C "$publish" fetch --depth=1 origin evergreen-badge git -C "$publish" checkout -b evergreen-badge FETCH_HEAD else git -C "$publish" checkout --orphan evergreen-badge fi cp "$BADGE_PATH" "$publish/evergreen-badge.json" git -C "$publish" config user.name github-actions[bot] git -C "$publish" config user.email 41898282+github-actions[bot]@users.noreply.github.com git -C "$publish" add evergreen-badge.json if ! git -C "$publish" diff --cached --quiet; then git -C "$publish" commit -m "chore: update Evergreen badge" git -C "$publish" push origin HEAD:evergreen-badge fi ``` Then add this to the README, replacing `OWNER` and `REPO`: ```markdown [![Evergreen lifecycle findings](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2FOWNER%2FREPO%2Fevergreen-badge%2Fevergreen-badge.json)](https://github.com/OWNER/REPO/actions) ``` Shields and GitHub cache badge images, so updates can take time to appear. The badge describes the last published scan, not the latest commit or a guarantee of freshness. A scan that cannot complete emits no new badge; its workflow failure remains the authoritative status. Schedule trusted scans if you need regular refreshes. Publishing aggregate counts makes them public; private repositories can keep the artifact private or deliberately publish only this aggregate file. ## Code scanning Set `upload-sarif: true` to show findings in the repository's **Security → Code scanning** tab. The Action writes the same findings as SARIF 2.1.0 and hands the file to `github/codeql-action/upload-sarif` under the category `evergreen`. The job needs one more permission: ```yaml permissions: contents: read security-events: write steps: - uses: actions/checkout@v7 - uses: arminazimi/evergreen@v0.5.0 with: upload-sarif: true ``` The token of a pull request from a fork is read-only, so the upload would fail there. Enable it for pushes and for pull requests from the same repository, for example `upload-sarif: ${{ github.event_name == 'push' }}`. Code scanning is free for public repositories and needs GitHub Code Security on private ones. The SARIF carries only locations and Evergreen's own rule text, not source content; the mapping is below. ### SARIF output The same SARIF is available from the CLI, so other CI systems and editors can read it: ```bash evergreen scan . --format sarif > evergreen.sarif ``` Output is deterministic: rules and results are sorted, paths are repository-relative with forward slashes on every platform, and neither the scan time nor the checkout path is written. Two scans of the same commit produce identical files. | SARIF | Evergreen | | --- | --- | | `tool.driver.name`, `version` | `Evergreen` and the installed version | | `rules[].id`, `name` | The lifecycle rule id, for example `github-actions.checkout.node20` | | `rules[].shortDescription` | The rule title | | `rules[].help` (`text` and `markdown`) | Replacement, deadline, migration steps, verification commands, and sources | | `rules[].helpUri` | The first evidence source | | `rules[].defaultConfiguration.level` | The same level mapping as `results[].level`, for the rule's severity | | `rules[].properties.tags` | `lifecycle` and the detector id | | `results[].ruleId`, `ruleIndex` | The finding's rule | | `results[].level` | `critical` and `high` → `error`; `medium` → `warning`; `low` and `info` → `note` | | `results[].message.text` | The finding summary, which names the exact reference, such as `actions/checkout@v3` | | `results[].locations[]` | File path (`uriBaseId` `%SRCROOT%`) and `startLine` when the finding has a line | | `results[].partialFingerprints["evergreenFindingId/v1"]` | The finding id, so alerts keep their identity between runs | JSON, text, annotations, and SARIF are renderings of the same findings. A finding suppressed by a baseline is absent from all of them. **Privacy.** SARIF contains no source code: only file paths, line numbers, and Evergreen's own rule text and evidence links. It is uploaded only when `upload-sarif` is `true`, and then only to GitHub code scanning in the same repository. Nothing is sent to an Evergreen service. --- Source: https://arminazimi.github.io/evergreen/CAPABILITIES.html # Capability manifest and auto-discovery contract Evergreen publishes one deterministic capability manifest for humans, agents, and integrations: ```bash evergreen capabilities --json ``` The output uses the `evergreen.capabilities.v1` contract. Its JSON Schema is [`schemas/capabilities.v1.schema.json`](../schemas/capabilities.v1.schema.json), and a complete committed example is [`examples/capabilities.json`](../examples/capabilities.json). ## What the manifest describes - the Evergreen package and product version; - supported, experimental, and unsupported capabilities; - supported detector ecosystems and their maturity; - CLI, MCP, Skill, plugin, and CI surfaces and their entry points; - the shared capability identifiers available through each surface; and - compatibility rules for consumers reading newer manifests. ## Detector maturity Each ecosystem carries a `maturity` field next to its existing `status`: | `maturity` | Shown in the docs as | What it promises users | | --- | --- | --- | | `experimental` | Experimental | The detector runs and is usable. Findings, rule ids, and unclassified output may change between releases without notice. | | `beta` | Beta | Findings were measured against independent ground truth. Rule and finding ids are listed in the changelog when they change. | | `stable` | Supported | The same promise as beta on a larger and more varied corpus, as part of the documented product contract. | `maturity` is the detector's `level` in [`benchmarks/thresholds.json`](../benchmarks/thresholds.json), which is the single source of truth for [decision 0013](decisions/0013-detector-maturity.md). It is not a second, independent judgment. An ecosystem's `maturity` is the lowest level among the detectors listed in its `detectorIds`. When several detectors serve one ecosystem, the ecosystem is only as mature as its least mature detector. Today `nodejs` and `npm` both list the `npm` detector, so both take its level. A contract test fails when the manifest disagrees with `benchmarks/thresholds.json`. The existing `status` field keeps its meaning and is not replaced: it says whether the ecosystem is a supported product surface, while `maturity` records the evidence level of the detectors behind it. A reader that ignores unknown fields can keep using `status` exactly as before; `maturity` is an additive optional field in the v1 manifest. Capability identifiers describe behavior, not transport. For example, `repository.scan` is the same capability when reached through the CLI, local MCP server, Agent Skill, experimental host plugin, or CI. Integrations should compare identifiers rather than infer behavior from command or tool names. ## Forward compatibility Consumers of a v1 manifest must follow the compatibility object included in the document: 1. Ignore fields they do not understand within schema major version 1. 2. Ignore unknown capability and surface identifiers rather than failing the whole manifest. 3. Treat any unlisted capability or ecosystem as unsupported. Absence never means experimental or supported. 4. Reject an unknown schema major version until the consumer explicitly supports it. 5. Use each entry's `status`; do not promote `experimental` or `unsupported` behavior based on a matching entry-point name. New optional fields, identifiers, and surfaces using the defined statuses can be added in a compatible v1 minor manifest update. Removing or redefining an existing identifier, changing required field meaning or status vocabulary, or weakening the rules above requires a new schema major version. ## Stability and privacy The command reads a packaged constant and performs no repository scan, network request, telemetry, or configuration write. Identical Evergreen versions produce byte-for-byte identical JSON output. The package's contract tests validate both the CLI output and committed example against the public JSON Schema and ensure every surface references registered capability identifiers. --- Source: https://arminazimi.github.io/evergreen/MIGRATION_PACKETS.html # Migration packets `evergreen context ` converts one scan finding into a portable plan for a human or coding agent. Add `--json` for the versioned `evergreen.context.v1` representation. ```bash npx evergreen-deprecation scan . --json npx evergreen-deprecation context '' . --json ``` ## Packet contents - The complete lifecycle finding and authoritative sources. - The declaration file plus exact import, call, and configuration usages. - Rule-specific behavioral and compatibility constraints. - Ordered migration steps resolved to declaration, usage, or all affected paths. - Suggested ecosystem verification commands and a final Evergreen re-scan. Built-in registry rules carry reviewed migration guidance next to their lifecycle evidence. This keeps the scan engine and CLI independent of ecosystem-specific migration knowledge. External detectors can provide the same optional guidance through the detector SDK; otherwise Evergreen emits a conservative generic packet. ## Safety and determinism Evergreen generates context but never edits files or executes verification commands. Packet generation is local, deterministic for a scan result, and does not require an LLM or network access. The runtime validator checks more than field types: packet sources and usages must equal the finding evidence, affected files must be derived from those locations, step IDs and commands must be unique, and every step path must stay inside the affected-file set. The JSON contract is published at `schemas/context.v1.schema.json`. Suggested commands may require project-specific setup and should be reviewed before execution. A migration is complete only after the relevant repository tests pass and a fresh Evergreen scan no longer reports the finding. --- Source: https://arminazimi.github.io/evergreen/COMPATIBILITY.html # Environment doctor and compatibility matrix Evergreen can inspect its execution environment without scanning source files: ```bash evergreen doctor . evergreen doctor . --json ``` The doctor checks the Node.js runtime, installed package entry points, local MCP server, portable Agent Skill, shared plugin prerequisites, and local or CI workspace. It is read-only: it does not change configuration, execute suggested repairs, start a network listener, or upload repository data. Text and JSON are generated from the same deterministic result. The JSON contract is `evergreen.doctor.v1`; its schema is [`schemas/doctor.v1.schema.json`](../schemas/doctor.v1.schema.json). A failed check always includes a concrete remediation. Warnings describe usable but not yet stable integration paths and do not make the command fail. ## Compatibility matrix Matrix version: **2026-10-15 / v1** | Surface | Combination | Status | Notes | | --- | --- | --- | --- | | CLI | Node.js 22+ on Linux | Supported | Primary local and CI execution path. | | CLI | Node.js 22+ on macOS | Supported | Same deterministic CLI and JSON contracts; the full check runs on macOS in CI. | | CLI | Node.js 24 on Linux | Supported | Part of the CI matrix next to Node.js 22. | | CLI | Node.js 22+ on Windows | Supported | The full check runs on Windows in CI on every pull request. | | GitHub Actions | Ubuntu runner | Supported | Composite Action, annotations, baselines, severity gate, and JSON artifact. | | GitHub Actions | macOS runner | Experimental | Expected to work through `bash`; not yet in the required runner matrix. | | GitHub Actions | Windows runner | Unsupported | The current composite Action requires `bash`. Use the CLI explicitly if available. | | GitLab CI | Linux runner using the CLI | Experimental | Doctor detects `GITLAB_CI` and `CI_PROJECT_DIR`; a Catalog component is not published yet. | | MCP | Local stdio host | Supported | No HTTP listener, account, credential, or hosted Evergreen service. | | Agent Skill | skills.sh / skills CLI monorepo discovery | Supported | `arminazimi/evergreen` discovers and installs `evergreen-deprecation`. | | Agent Skill | Codex, Claude Code, Cursor, GitHub Copilot, or Gemini CLI | Supported | Project installation validated through the skills CLI on 2026-10-15. | | Agent Skill | Other hosts supporting the open Agent Skills directory format | Experimental | The portable files can be copied or linked, but these hosts are not in the representative install gate. | | Host plugins | Codex or Claude packaged plugin | Experimental | Shared MCP and Skill prerequisites exist; stable host packages are not published yet. | ## Ecosystem coverage | Detector ecosystem | Status | Coverage | | --- | --- | --- | | CI runner images | Experimental | `pool.vmImage` in `azure-pipelines.yml` is matched against reviewed Azure hosted-image rules. GitLab `image` and `services` values are unclassified. CircleCI orbs, dynamic values, and floating aliases are unclassified; other CircleCI images are not judged. See [CI runner images](#ci-runner-images-detector). | | Container base images | Experimental | `FROM` lines in Dockerfiles and Containerfiles and `services.*.image` in Compose files, for Docker Hub official images with a vendor lifecycle. See [Container base images](detectors/container.md). | | .NET | Beta | `global.json`, project files, `Directory.Build.props`, `Directory.Packages.props`, and `packages.lock.json`, for .NET release lines past end of support and deprecated NuGet packages. See [.NET](detectors/dotnet.md). | | Go | Experimental | `go` and `toolchain` directives in `go.mod` and `go.work`, and direct `require` entries, for Go release lines past end of support. See [Go](detectors/go.md). | | Java and Kotlin (JVM) | Experimental | JDK declarations in `pom.xml`, Gradle build scripts, `.java-version`, `.sdkmanrc`, and `.tool-versions`, and direct Maven dependencies, for non-LTS JDK releases past end of support and moved artifacts. See [Java and Kotlin](detectors/java.md). | | Kubernetes APIs | Experimental | The literal `apiVersion` and `kind` in YAML manifests, Helm chart templates, and kustomization patches, for API versions the Kubernetes project says are no longer served. See [Kubernetes APIs](detectors/kubernetes.md). | | GitHub Actions | Supported | Selected first-party Action lifecycle rules and exact workflow locations. | | Node.js and npm | Supported | Runtime declarations, direct dependencies, lockfiles, and supported source usages. | | PHP | Experimental | Pinned runtime declarations in `.php-version`, `.tool-versions`, `composer.json`, and `composer.lock`, for PHP release lines past end of support and abandoned Composer packages. See [PHP](#php-detector). | | Python | Supported | Runtime declarations, direct dependencies, common lockfiles, and supported source usages. | | Ruby | Experimental | Pinned runtime declarations in `.ruby-version`, `.tool-versions`, the Gemfile `ruby` directive, and Gemfile.lock `RUBY VERSION`, plus direct gems, for Ruby release lines past end of support and yanked gems. Gem rules can list source usages in Ruby files. | | Rust | Experimental | The pinned toolchain in `rust-toolchain(.toml)` and direct dependencies in `Cargo.toml` and `Cargo.lock`, for toolchains older than the newest stable release and unmaintained crates. See [Rust](detectors/rust.md). | | Swift | Beta | `.swift-version` and `.xcode-version` pins, the `swift-tools-version` line and literal `from:` / `exact:` dependencies in `Package.swift`, and resolved pin versions in `Package.resolved`, for Xcode releases App Store Connect no longer accepts and Swift toolchain lines with a lifecycle rule. The iOS deployment target (`IPHONEOS_DEPLOYMENT_TARGET`) of an application target in an `.xcodeproj` `project.pbxproj`, for iOS releases App Store Connect no longer accepts. Libraries and `platforms:` minimums in `Package.swift` are not read for it. See [Swift](detectors/swift.md). | | Android | Experimental | Literal `compileSdk`, `minSdk`, `targetSdk`, and `buildToolsVersion` in `build.gradle` and `build.gradle.kts`, Android Gradle Plugin and Kotlin Android plugin versions in `settings.gradle(.kts)`, `agp` and `kotlin` keys in `libs.versions.toml`, and string-notation Maven dependencies, matched to reviewed rules. Bundled rules cover Google Play Billing Library majors 5 through 9. SDK and plugin declarations produce a finding only when a rule names them. See [Android](#android-detector). | | C and C++ | Experimental | `cmake_minimum_required`, C and C++ language standards, and compiler version checks in `CMakeLists.txt`; direct requirements in `conanfile.txt` and `conanfile.py`; and direct dependencies and `builtin-baseline` in `vcpkg.json`. Bundled rules cover CMake policy versions older than 3.5 (removed) and from 3.5 up to 3.10 (deprecated). See [C and C++](#c-and-c-detector). | | Dart and Flutter | Experimental | Resolved packages and SDK constraints in `pubspec.lock`, direct dependencies and `environment` constraints in `pubspec.yaml`, and the Flutter version pinned in `.fvmrc` and `.fvm/fvm_config.json`, for Dart SDK releases past end of support and discontinued pub packages. See [Dart and Flutter](detectors/dart.md). | | Database runtimes | Experimental | Image tags of `postgres`, `mysql`, `mariadb`, `redis`, and `mongo` services in Compose files, and literal `engine_version` of `postgres`, `mysql`, `mariadb`, and `redis` engines on AWS managed database resources in `*.tf`, for major and minor lines past end of life. Bundled rules cover PostgreSQL 11 through 13 and MySQL 5.7 and 8.0. See [Database runtimes](detectors/database.md). | | Shell and PowerShell | Experimental | Literal `#Requires -Version` directives in `*.ps1` and `*.psm1`, `PowerShellVersion` in `*.psd1` manifests, and pinned `mcr.microsoft.com/powershell` image tags in `.github/workflows` files, for PowerShell releases past end of support. Bundled rules cover PowerShell 6.x and 7.0 through 7.3. Shell names such as `shell: pwsh` and Bash images carry no version and are not matched. See [Shell and PowerShell](detectors/shell.md). | | Terraform | Experimental | `required_version` and `required_providers` constraints in `*.tf`, locked provider versions in `.terraform.lock.hcl`, and the Terraform CLI version pinned in `.terraform-version` and `.tool-versions`, for Terraform CLI release lines past end of support. Bundled rules cover CLI lines 1.10 through 1.14; provider rules are not bundled yet. See [Terraform](detectors/terraform.md). | | Serverless runtimes | Beta | `Runtime` on AWS SAM and CloudFormation functions and `Globals`, `provider.runtime` and per-function `runtime` in `serverless.yml`, and Terraform `runtime` on `aws_lambda_function` plus Google Cloud Functions runtimes and Azure function-app `application_stack` version pins. Bundled rules cover selected AWS Lambda and Google Cloud Functions identifiers that the vendors list as end-of-life. See [Serverless runtimes](detectors/serverless.md). | | Other language and platform ecosystems | Unsupported | Report the limitation; do not infer lifecycle status without a detector. | `Supported` means the combination is part of the documented product contract. `Experimental` means it is usable but lacks a complete release gate or stable distribution package. For detectors, `Experimental`, `Beta`, and `Supported` have the meanings and the evidence requirements in [decision 0013](decisions/0013-detector-maturity.md), and the level of each is recorded in `benchmarks/thresholds.json`. `Unsupported` means Evergreen does not currently claim the combination works. ## PHP detector The `php` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories in [Validation](VALIDATION.md#php-corpus) cover it. Its lifecycle rules are in `registry/php.json` and ship in the [data bundle](DATA.md). ### Supported files and declarations | File | Declaration read | Notes | | --- | --- | --- | | `.php-version` | One pinned version on a line by itself, such as `7.4.33`, `7.4`, or `php-7.4.33` | Blank lines and `#` comments are ignored. A file with more than one version, or any other line, produces nothing. The `php-` prefix is stripped before matching. | | `.tool-versions` | A `php` line with one or more pinned versions | Every version on the line is reported. Other tools are ignored. A line with any unsupported token produces nothing. | | `composer.json` | `require.php`, `config.platform.php`, and direct packages in `require` / `require-dev` (`vendor/name` with a literal constraint) | Extensions (`ext-*`), floating `*`, dynamic `${…}` values, `extra.php`, and other `config.platform.*` keys are ignored. | | `composer.lock` | `platform.php`, and each resolved package in `packages` / `packages-dev` (`name` and `version`) | Every resolved package is checked, including transitive and `require-dev` packages. `platform-dev`, `plugin-api-version`, and non-`php` platform keys are not read. An `abandoned` flag is parsed and is not a finding by itself. | Files are found by walking the repository up to eight directories deep, to at most 500 files. Symbolic links are not followed, and `.git`, `node_modules`, `vendor`, `dist`, `build`, `target`, `.venv`, `venv`, `fixtures`, `__fixtures__`, and `testdata` are skipped. PHP, phpenv, asdf, and Composer are never run, and nothing is fetched. ### What it reports - A pinned PHP release line that is past end of life, in the `.php-version`, `.tool-versions`, `composer.json` `config.platform.php`, or `composer.lock` `platform.php` line that pins it, or an exact `require.php` version that matches a rule. The bundled rules cover PHP 8.1, 8.0, and 7.4, citing the [PHP unsupported branches](https://www.php.net/eol.php) page. A requirement range such as `>=7.4` or `^8.2` is not a pin and produces no runtime finding. - A `composer.json` package line only when a rule covers every version of that package (`vers:composer/*`). The bundled abandoned-package rules cover `swiftmailer/swiftmailer` and `zendframework/zend-stdlib`, citing Packagist and the Symfony / Laminas announcements. - A resolved package in `composer.lock` when a rule's `vers` contains that version. Abandoned-package rules match any resolved version of the named package. Each finding names the file and line, and carries its rule's source, replacement, deadline, migration guidance, and verification commands. A package finding whose rule lists usage matchers also lists the `.php` files that `require` / `include` (and `_once`) that package, and the qualified calls those matchers name (`Class::method` and `new Class`). The finding's impact counts those usages, and a migration packet lists the files. A runtime finding points at the pin only. ### What it intentionally does not judge - PHP releases and Composer packages with no rule in `registry/php.json`. A missing finding is not evidence that a version is current. - A `.php-version` or `.tool-versions` value of `system` or `latest`, a template, or a value that interpolates at runtime. - Composer `require.php` constraints that are not concrete versions (`>=7.4`, `^8.2`, `~8.1.0`, and similar ranges). - Extensions (`ext-*`), floating `*` package constraints, `git` / `path` repositories, and dynamic `${…}` values in `composer.json`. - An `abandoned` flag in `composer.lock` without a matching lifecycle rule. Composer may mark a package abandoned; Evergreen reports it only when a reviewed rule names that package. - Vulnerability advisories and license policy. ### Known limits and false-positive boundaries - A runtime pin is reported whatever the project actually runs. A stale `.tool-versions` or `config.platform.php` that a project no longer uses still produces a finding. - `composer.lock` findings include transitive packages, because the lockfile resolves them. A package can be reported even though the project never imports it. - Package names are matched exactly as written in the file, and a rule matches only the versions its `vers` range names. - Malformed input produces no declarations from the affected unit, never a partial result, and no error is raised. For `.php-version`, `composer.json`, and `composer.lock` that unit is the whole file. For `.tool-versions` it is the one malformed line. A file over 1 MiB yields nothing, and a single line over 4,096 characters is skipped. - The eight-level and 500-file limits are silent. Files beyond them, and any under `vendor` or a skipped directory, are not scanned. - Source usage reads `.php` files. Quoted `require` / `include` (and `_once`) paths match a rule whose module is that path (a trailing `.php` is stripped). A call matches when the rule names the qualified form, such as `Swift_Mailer::newInstance` or `new Zend\Stdlib\Parameters`. Usage is not a PHP AST: it is regex-based matching, so unusual formatting can miss a call or require. - A `require` or `include` whose argument is not a string literal (a variable, concatenation, or a method call on the string) is listed as unclassified, with its file and line. Evergreen does not guess which package it loads. `new $class` and `$class::method()` are unclassified for the same reason. - Usage is not read under `vendor`, `fixtures`, `__fixtures__`, or `testdata`. Comments and ordinary strings are not usages. A single source line over 4,096 characters is skipped for usage scanning, matching the declaration parsers. ## Ruby detector The `ruby` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories in [Validation](VALIDATION.md#ruby-corpus) cover it. Its lifecycle rules are in `registry/ruby.json` and ship in the [data bundle](DATA.md). ### Supported files and declarations | File | Declaration read | Notes | | --- | --- | --- | | `.ruby-version` | One pinned version on a line by itself, such as `3.2.2`, `3.2`, `3.2.2p53`, or `ruby-3.2.2` | Blank lines and `#` comments are ignored. A file with more than one version, or any other line, produces nothing. | | `.tool-versions` | A `ruby` line with one or more pinned versions | Every version on the line is reported. Other tools are ignored. A line with any unsupported token produces nothing. | | `Gemfile` | The `ruby "3.2.2"` directive, and `gem "name", "version"` with a literal version | A `gem` line with a `git:` or `path:` option is skipped. Only the first literal version on a `gem` line is read. | | `Gemfile.lock` | The `RUBY VERSION` line, and each resolved gem in the `GEM` section's `specs:` (`name (version)`) | Every resolved gem is checked, including transitive gems. `DEPENDENCIES`, `BUNDLED WITH`, and `GIT` and `PATH` specs are not read. | Files are found by walking the repository up to eight directories deep, to at most 500 files. Symbolic links are not followed, and `.git`, `node_modules`, `vendor`, `dist`, `build`, `target`, `.venv`, `venv`, `fixtures`, `__fixtures__`, and `testdata` are skipped. Ruby, rbenv, asdf, and Bundler are never run, and nothing is fetched. ### What it reports - A pinned Ruby release line that is past end of life, in the `.ruby-version`, `.tool-versions`, or `Gemfile` line that pins it, or the `Gemfile.lock` `RUBY VERSION` line. The bundled rules cover Ruby 3.2, 3.1, 3.0, and 2.7, citing the [Ruby maintenance branches](https://www.ruby-lang.org/en/downloads/branches/) page. - A resolved gem in `Gemfile.lock` that a rule covers. The bundled gem rule covers `json` 2.17.1.1 (yanked), citing its [RubyGems version page](https://rubygems.org/gems/json/versions/2.17.1.1). - A `Gemfile` gem line is reported only by a rule that covers every version of the gem. The bundled gem rule covers one version, so a `Gemfile` gem line alone produces no finding today. Each finding names the file and line, and carries its rule's source, replacement, deadline, migration guidance, and verification commands. A gem finding whose rule lists usage matchers also lists the `.rb`, `.rake`, and `.ru` files that `require`, `load`, `gem`, or `autoload` that gem, and the qualified calls those matchers name (including a constant or variable assigned from that constant). The finding's impact counts those usages, and a migration packet lists the files. A runtime finding points at the pin only. ### What it intentionally does not judge - Ruby releases and gems with no rule in `registry/ruby.json`. A missing finding is not evidence that a version is current. - A `.ruby-version` or `.tool-versions` value of `system` or `latest`, a template, or a value that interpolates at runtime. - Gemfile `ruby` directives with options or a requirement range, such as `ruby ">= 3.0"`. - Dependencies declared with no version, `git` or `path` gems, `eval_gemfile`, `gemspec`, or a dynamic value (`ENV[...]`, `#{...}`, `${...}`). - `required_ruby_version` in a `*.gemspec`. A parser for it exists, but the detector does not read gemspec files yet, so a gemspec never produces a finding. - Vulnerability advisories and license policy. ### Known limits and false-positive boundaries - A runtime pin is reported whatever the project actually runs. A stale `.tool-versions` that a project no longer uses still produces a finding. - `Gemfile.lock` findings include transitive gems, because the lockfile resolves them. A gem can be reported even though the project never imports it. - Gem names are matched exactly as written in the file, and a rule matches only the versions its `vers` range names. - A Bundler platform suffix in a lock version (such as `1.15.4-x86_64-linux`) is kept as written and compared as written. - Malformed input produces no declarations from the affected unit, never a partial result, and no error is raised. For `.ruby-version`, `Gemfile`, and `Gemfile.lock` that unit is the whole file: an unterminated string in a `Gemfile`, a truncated spec in `Gemfile.lock`, or an unsupported line in `.ruby-version` yields nothing from that file. For `.tool-versions` it is the one malformed line. A file over 1 MiB yields nothing, and a single line over 4,096 characters is skipped. - The eight-level and 500-file limits are silent. Files beyond them, and any under `vendor` or a skipped directory, are not scanned. - Source usage reads `.rb`, `.rake`, and `.ru` files. `require "json"`, `require "json/ext"`, `load "json.rb"`, `gem "json"`, `autoload :JSON, "json"`, and `Kernel.require "json"` match a rule whose module is `json`. `require_relative` of a literal is a local file and is not a gem import. A call matches when the rule names the qualified form, such as `JSON.parse`, and the source calls it with parentheses (`JSON.parse(` or `JSON::parse(`). `Parser = JSON` or `parser = ::JSON`, and an alias of that alias, counts as the same call. A call without parentheses is not matched. - A `require`, `load`, `gem`, or `autoload` whose argument is not a string literal (a variable, interpolation, concatenation, or a method call on the string) is listed as unclassified, with its file and line. Evergreen does not guess which gem it loads. `require_relative` is never unclassified: it always names a local file. A local variable or hash key named `require`/`load`/`gem`, or a bare `load` with no argument, is not a call. - Alias assignments are capped per file; deep or cyclic alias chains resolve to nothing. A single source line over 4,096 characters is skipped for usage scanning, matching the declaration parsers. - Usage is not read under `vendor`, `fixtures`, `__fixtures__`, or `testdata`. Comments, ordinary strings, percent literals, heredocs, and `=begin`/`=end` blocks are not usages. A method receiver such as `JSON.load` is a call only when a matcher names it, not a file `load`. ## Swift detector The `swift` detector is **beta**: its level is recorded as `beta` in `benchmarks/thresholds.json`, and the precision floor is enforced. Thirteen public repositories in [Validation](VALIDATION.md#swift-corpus) cover it (six targeted, seven blind). Supported forms, limits, and data sources are documented in [Swift](detectors/swift.md). Its lifecycle rules are in `registry/swift.json` and ship in the [data bundle](DATA.md). ## Dart and Flutter detector The `dart` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Supported files, declaration forms, matching rules, and limits are documented in [Dart and Flutter](detectors/dart.md). Its lifecycle rules are in `registry/dart.json` and ship in the [data bundle](DATA.md). ## Database runtimes detector The `database` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories are pinned in `benchmarks/public-repositories.json`: four targeted cases and one blind, zero-finding repository, with ground truth from the vendors' lifecycle sources. Supported files, declaration forms, matching rules, and limits are documented in [Database runtimes](detectors/database.md). Its lifecycle rules are in `registry/database.json` and ship in the [data bundle](DATA.md). ## Shell and PowerShell detector The `shell` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories are pinned in `benchmarks/public-repositories.json`: four targeted cases and one blind, zero-finding repository. Each expected finding was checked by reading its line at the pinned commit against Microsoft's PowerShell support lifecycle, independently of Evergreen output. Supported files, declaration forms, matching rules, and limits are documented in [Shell and PowerShell](detectors/shell.md). Its lifecycle rules are in `registry/shell.json` and ship in the [data bundle](DATA.md). ## Terraform detector The `terraform` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Supported files, declaration forms, matching rules, and limits are documented in [Terraform](detectors/terraform.md). Its lifecycle rules are in `registry/terraform.json` and ship in the [data bundle](DATA.md). ## C and C++ detector The `cpp` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories are pinned in `benchmarks/public-repositories.json`: four targeted migration cases and one blind, zero-finding repository. Their ground truth cites maintainer migration commits or upstream release notes, independently of Evergreen output. Precision on this corpus does not establish coverage of unsupported declaration forms. Its lifecycle rules are in `registry/cpp.json` and ship in the [data bundle](DATA.md). ### Supported files and declarations | File | Declaration read | Notes | | --- | --- | --- | | `CMakeLists.txt` | `cmake_minimum_required(VERSION )`, `set(CMAKE_C_STANDARD )`, `set(CMAKE_CXX_STANDARD )`, and an `if` that names `CMAKE__COMPILER_ID` with `STREQUAL` and `CMAKE__COMPILER_VERSION` with a `VERSION_*` comparison | Values built at configure time, the policy-range form (`3.16...3.27`), `CACHE` / `PARENT_SCOPE` / `FORCE`, and checks combined with `OR` or `NOT` are ignored. | | `conanfile.txt` | Each entry of the `[requires]` section, `name/version` | `@user/channel` and `#revision` are not versions. Other sections, including `[tool_requires]`, are ignored. Version ranges are ignored. | | `conanfile.py` | The `requires` attribute and literal `self.requires(...)` calls | The file is untrusted Python and is never executed. f-strings, variables, and calls that span lines are ignored. `tool_requires` is not a direct requirement. | | `vcpkg.json` | Top-level `builtin-baseline`, and each direct `dependencies` entry | A string dependency is a name. An object may also carry `version>=`. Feature-scoped dependencies, `overrides`, and the manifest's own `version` are ignored. | Files are found by walking the repository up to eight directories deep, to at most 500 files. Symbolic links are not followed, and `.git`, `node_modules`, `vendor`, `dist`, `build`, `target`, `.venv`, `venv`, `fixtures`, `__fixtures__`, and `testdata` are skipped. CMake, Conan, and vcpkg are never run, and nothing is fetched. ### Package URLs `pkg:generic/cmake` is the identity the CMake policy importer writes. `conan` is the Package URL type, so a requirement is `pkg:conan/` with the name as written. vcpkg has no importer yet, so a dependency is `pkg:vcpkg/`. The other declarations are not packages: `pkg:generic/c-standard` and `pkg:generic/cxx-standard` for language standards, `pkg:generic/c-compiler/` and `pkg:generic/cxx-compiler/` for a compiler check (the compiler id is lowercased), and `pkg:generic/vcpkg-baseline` for a baseline commit. ### What it reports - A `cmake_minimum_required` version that a `pkg:generic/cmake` rule contains. The bundled rules cover policy versions older than 3.5 (removed in CMake 4.0) and from 3.5 up to 3.10 (deprecated in CMake 3.31). - A Conan requirement, a vcpkg `version>=` floor, a language standard, or a compiler comparison when a rule for that Package URL contains the version as written. There are no bundled rules for those yet. - A vcpkg dependency with no `version>=`, only when a rule covers every version of that package. - A `builtin-baseline` commit when a rule's range contains that string. A semver range does not contain a commit. A rule that covers every version does. The first matching rule wins. A rule whose VERS is malformed, or whose comparison throws, is skipped and does not hide a later rule. Each finding names the file and line, and carries its rule's source, replacement, deadline, migration guidance, and verification commands. ### What it intentionally does not judge - CMake generators and modules. The registry has rules for the Visual Studio 12 2013 generator, FindGDAL, and CMakeFindFrameworks, and the parser does not emit those declarations yet, so they produce no finding. - Conan and vcpkg packages, language standards, compilers, and baselines with no rule. A missing finding is not evidence that a version is current. - `cmake_minimum_required` written as a policy range (`VERSION 3.5...3.16`), a value read from a variable, or a generator expression. - Conan `[tool_requires]` and `[build_requires]`, version ranges, and dynamic Python. - Feature-scoped vcpkg dependencies and `overrides`. - Vulnerability advisories and license policy. ### Known limits and false-positive boundaries - A `version>=` floor is matched as that version, not as every version the constraint allows. A floor inside a rule is reported even when a newer resolution would sit outside it. - A compiler check matches the compared version. `VERSION_LESS 9.3` is not read as "this project rejects compilers older than 9.3." - A baseline commit matches a rule that covers every version, because that range does not inspect the commit. - Malformed input produces no declarations from that file, never a partial result, and no error is raised. A file over 1 MiB yields nothing, and a single line over 4,096 characters is skipped. - The eight-level and 500-file limits are silent. Files beyond them, and any under `build`, `vendor`, or another skipped directory, are not scanned. ## Serverless runtimes detector The `serverless` detector is **beta**: its level is recorded as `beta` in `benchmarks/thresholds.json`, and the precision floor is enforced. Ten public repositories in [Validation](VALIDATION.md#serverless-corpus) cover it (five targeted, five blind). Supported forms, limits, and data sources are documented in [Serverless runtimes](detectors/serverless.md). Its lifecycle rules are in `registry/serverless.json` and ship in the [data bundle](DATA.md). ## Android detector The `android` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five public repositories are pinned for it in `benchmarks/public-repositories.json` (four targeted, one blind). Its lifecycle rules are in `registry/android.json` and ship in the [data bundle](DATA.md). The bundled rules cover Google Play Billing Library majors 5 through 9 and cite the [Google Play Billing Library version deprecation](https://developer.android.com/google/play/billing/deprecation-faq) timeline. Scans read that bundled registry only. ### Supported files and declarations | File | Declaration read | Notes | | --- | --- | --- | | `build.gradle`, `build.gradle.kts` | Literal `compileSdk`, `minSdk`, `targetSdk`, and `buildToolsVersion` inside an `android { }` block, including `defaultConfig`, product flavors, and build types | An API level is a bare or quoted integer (`34`, `"34"`, `= 34`). `buildToolsVersion` is a quoted dotted number (`"34.0.0"`). | | `build.gradle`, `build.gradle.kts` | A string-notation Maven coordinate on `implementation`, `api`, `compileOnly`, `runtimeOnly`, `testImplementation`, `testRuntimeOnly`, `testCompileOnly`, `annotationProcessor`, `kapt`, `ksp`, `compile`, `runtime`, or `testCompile` | The form is `implementation("group:artifact:version")`, with or without the parentheses. An optional classifier or `@` packaging suffix is not part of the version. | | `settings.gradle`, `settings.gradle.kts` | A literal `id(...) version "..."` pin (Groovy quotes work too) for `com.android.application`, `com.android.library`, or `org.jetbrains.kotlin.android` | `apply false` still counts. The pin is read wherever it appears in the file. | | `libs.versions.toml` | `agp` and `kotlin` keys in the `[versions]` table, each a quoted literal version | The file is found by that name at any depth, including `gradle/libs.versions.toml`. | Files are found by walking the repository up to eight directories deep, to at most 2,000 of these files. Symbolic links are not followed, and `.git`, `node_modules`, `vendor`, `dist`, `build`, `target`, `.gradle`, `.venv`, `venv`, `fixtures`, `__fixtures__`, and `testdata` are skipped. Gradle is never run, and nothing is fetched. ### Package URLs A string-notation dependency `group:artifact:version` is `pkg:maven//`. An SDK setting is `pkg:generic/android/compileSdk`, `pkg:generic/android/minSdk`, `pkg:generic/android/targetSdk`, or `pkg:generic/android/buildToolsVersion`. An Android Gradle Plugin pin, in settings or the catalog `agp` key, is `pkg:generic/android/agp`. `org.jetbrains.kotlin.android` in settings is `pkg:generic/android/kotlin-android`. The catalog `kotlin` key is `pkg:generic/android/kotlin`, a different identity from the settings plugin. ### What it reports - A string-notation `com.android.billingclient:billing` version that a bundled rule contains. The rules cover majors 5, 6, 7, 8, and 9. - An SDK level, Android Gradle Plugin version, Kotlin Android plugin version, or catalog `agp` / `kotlin` pin when a rule for that Package URL contains the version text. No such rules are bundled, so those declarations produce no finding today. The first matching rule wins. A rule whose VERS is malformed, or whose comparison throws, is skipped and does not hide a later rule. Each finding names the file and line, and carries its rule's source, replacement, deadline, migration guidance, and verification commands. A rule that lists usage matchers also lists `.java` and `.kt` usages. The bundled Play Billing rules do not list matchers, so those findings point at the declaration only. ### What it intentionally does not judge - SDK levels, plugin versions, catalog pins, and Maven coordinates with no rule in `registry/android.json`. A missing finding is not evidence that a version meets Google Play's target API requirement or that a dependency is current. - JDK pins in the same Gradle file (`sourceCompatibility`, `targetCompatibility`, `jvmToolchain`, `JavaLanguageVersion`). The Java and Kotlin detector reads those. - Legacy names `compileSdkVersion`, `minSdkVersion`, and `targetSdkVersion`, method calls such as `compileSdk(34)`, and receiver forms such as `ext.minSdk`. - Values outside an `android { }` block, property references, version-catalog calls, and `$` interpolation. - Plugin versions written in `build.gradle(.kts)`, `alias(libs.plugins…)` lines, `kotlin("android") version "…"`, the chained `id("…").version("…")` form, and plugin ids other than `com.android.application`, `com.android.library`, and `org.jetbrains.kotlin.android`. - Catalog keys other than `agp` and `kotlin`, and any version outside `[versions]`, including a `[plugins]` `version` or `version.ref`. - Map notation (`group:`, `name:`, `version:`), version-catalog library aliases such as `implementation(libs.billing)`, and configuration names outside the list above, such as `debugImplementation` and `androidTestImplementation`. - Dynamic and ranged versions (`8.+`, `latest.release`, `[8.0, 9.0)`) on plugin and catalog pins. - Vulnerability advisories and license policy. ### Known limits and false-positive boundaries - A literal inside `android { }` is reported when a rule matches it, including a product flavor or build type, and including a script the project no longer applies. Evergreen does not ask Gradle which variant is live. - A plugin pin is read anywhere in `settings.gradle(.kts)`, including `pluginManagement` and a line marked `apply false`. - String-notation test configurations are judged the same way as `implementation`. - A catalog `kotlin` pin and the settings Kotlin Android plugin do not share a Package URL. A rule for one does not match the other. - An unterminated comment or string, or a Kotlin string template the scanner cannot pair, makes that parser return nothing for the whole file. A `libs.versions.toml` with a multi-line string (`"""` or `'''`) does the same. - Malformed input produces no declarations from the affected file, never a partial result, and no error is raised. A file over 1 MiB yields nothing, and a single line over 4,096 characters is skipped. Each parser keeps at most 5,000 declarations from one file. - The eight-level and 2,000-file limits are silent. Files beyond them, and any under `build`, `.gradle`, or another skipped directory, are not scanned. - A plugin or catalog value that is not a literal version is omitted. A string-notation coordinate is kept and compared as the version text written in the file. The analysis reports no unclassified references. ## CI runner images detector The `ci-images` detector is **experimental**: its level is recorded as `experimental` in `benchmarks/thresholds.json`. Five targeted public repositories are pinned for it (see [VALIDATION.md](VALIDATION.md) "CI runner images corpus"); precision is not enforced, and its blind share is zero. Its lifecycle rules are in `registry/ci-images.json` and ship in the [data bundle](DATA.md). Bundled rules include the scheduled removals of the macOS 14 Sonoma and Ubuntu 22.04 hosted images, citing the [Microsoft-hosted image deprecation schedule](https://learn.microsoft.com/azure/devops/pipelines/agents/hosted-deprecation-schedule). A label is a finding only when a reviewed rule covers it. Scans read that bundled registry only. ### Supported files and declarations | File | Declaration read | Notes | | --- | --- | --- | | `azure-pipelines.yml` | Literal `pool.vmImage` on the pipeline, a stage, or a job | The label is matched when it is `ubuntu-`, `macos-`, or `windows-`, compared case-insensitively. | | `.gitlab-ci.yml` | Root and job `image` values, including an image object's `name`, and image values in `services` | Every located value is unclassified. None become findings. | | `.circleci/config.yml` | `docker.image` and `machine.image` under `jobs` and `executors`, and string values under `orbs` | Orbs, dynamic values, and floating aliases are unclassified. A literal image is parsed and not judged. | Only a file named `.gitlab-ci.yml`, `azure-pipelines.yml`, or `config.yml` directly inside a `.circleci` directory is read. Files are found by walking the repository up to eight directories deep, to at most 500 files. Symbolic links are not followed, and `.git`, `node_modules`, `vendor`, `dist`, `build`, `bin`, `obj`, `target`, `.venv`, `venv`, `fixtures`, `__fixtures__`, and `testdata` are skipped. GitLab CI, CircleCI, and Azure Pipelines are never run, and nothing is fetched. ### What it reports - An Azure `pool.vmImage` whose family and numeric version a rule's Package URL and `vers` range contain. The finding is the value's line. It carries the rule's source, replacement, deadline, and migration guidance when the rule has it. Every matching rule is reported. - Nothing for a GitLab image or service, a CircleCI image or orb, or an Azure label no rule covers. An unclassified reference is not a finding. ### What it intentionally does not judge - GitLab CI images and services. Decision 0014 records no authoritative GitLab lifecycle source, so a literal image is not evidence that GitLab retired it. - CircleCI `docker.image` and `machine.image` values, including ones that look old. This version has no CircleCI Package URL mapping, so a literal image is not matched against a rule. Orb references are not image lifecycle claims, and their remote contents are not read. - `latest`, `ubuntu-latest`, `macos-latest`, `windows-latest`, and other `*-latest` or `:latest` aliases. `cimg/:current` is floating too. - Dynamic values: `$IMAGE`, `${IMAGE}`, `$(vmImage)`, `<< parameters.image >>`, `<< pipeline.parameters.image >>`, and any value that contains `$` or `<<`. - Azure labels outside `ubuntu-`, `macos-`, and `windows-` plus a numeric version, including `ubuntu-22.04-arm64` and `windows-2025-vs2026`. - A pool written as a string, a pool `name` without `vmImage`, containers, resources, and `strategy.matrix` `vmImage` entries. - CircleCI workflow executor references, `resource_class`, descriptions, and a job that only names an orb executor. - Comments, unrelated keys, unresolved YAML aliases, merge keys, non-string values, and malformed YAML. - Vulnerability advisories. A newer image existing is not a lifecycle finding. ### Known limits and false-positive boundaries - An Azure label is reported when a rule matches the text, including a pipeline the project no longer runs, and including a self-hosted agent that reuses a hosted label string. Evergreen does not ask Azure DevOps which pipeline is active. - GitLab and CircleCI references that are unclassified name the file and line. A literal CircleCI image is omitted from both findings and unclassified references. - Malformed YAML produces no declarations from that file, never a partial result, and no error is raised. A file over 1 MiB yields nothing, and a single value over 4,096 characters is skipped. - The eight-level and 500-file limits are silent. Files beyond them, and any under a skipped directory, are not scanned. ## Monorepos The npm and Python detectors read every package of a workspace, not only the repository root. Packages are found from the declarations the package managers use: `workspaces` in `package.json` (npm and Yarn, array or `packages` form), `pnpm-workspace.yaml` (with `!` exclusions), `lerna.json`, and `[tool.uv.workspace]` `members` and `exclude` in `pyproject.toml`. Globs support `*`, `?`, and `**`. - Findings name the package's own file, for example `packages/a/package.json`, and carry the usages inside that package's directory; usages outside every package stay with the root. - A workspace without its own lockfile resolves versions against the root's lockfile. - Discovery is bounded: eight directory levels, 500 packages, symbolic links never followed, and `node_modules`, `vendor`, `dist`, `build`, `target`, virtual environments, and fixture directories skipped. - A malformed workspace manifest is skipped, so one bad package does not hide the others. A malformed root `package.json` is still an error. - Without a workspace declaration only the root is scanned, as before. Python projects that are not uv workspaces (for example separate `pyproject.toml` files under `services/`) are not discovered. ## CI behavior - In GitHub Actions, the doctor requires `GITHUB_WORKSPACE` to name an accessible checkout directory. - In GitLab CI, the doctor requires `CI_PROJECT_DIR` to name an accessible checkout directory. - Outside those environments, it reports local execution and does not require CI variables. The checks only read Evergreen's own packaged metadata and test workspace accessibility. They do not inspect secrets, tokens, repository contents, or remote services. --- Source: https://arminazimi.github.io/evergreen/AGENT_SKILLS.html # Evergreen Agent Skill Evergreen ships one portable skill at `skills/evergreen-deprecation/SKILL.md`. It follows the open Agent Skills directory format: standard YAML frontmatter plus Markdown instructions. There is no vendor-specific manifest, invocation policy, tool allowlist, script, or generated prompt. ## Design The skill is intentionally an orchestration layer. It tells an agent when to discover, explain, migrate, and verify, then maps those intents to Evergreen's MCP tools or CLI commands. Lifecycle rules, evidence, affected-file calculation, migration steps, and verification behavior remain in the executable and versioned JSON contracts. This split prevents prompt drift: improving a detector or migration packet changes every client without requiring a rewritten skill. ## Installation The skills CLI discovers the Skill directly from the Evergreen monorepo. No dedicated distribution repository is needed: ```sh npx skills add arminazimi/evergreen ``` The repository currently exposes one Skill. For a non-interactive or explicitly selected install, use: ```sh npx skills add arminazimi/evergreen --skill evergreen-deprecation --yes ``` Update it with: ```sh npx skills update evergreen-deprecation ``` The npm package also includes the complete `skills/evergreen-deprecation` folder for hosts that prefer a manual copy or link. The host also needs one Evergreen execution path: - the local `evergreen-mcp` stdio server; or - the `evergreen` CLI available on `PATH`. No account or API key is required. ## Use the guardrail in this repository Agents working on Evergreen itself follow the same rule the Skill teaches: run `evergreen check ` before adding any dependency, runtime, or Action. Install the Skill as above, or run the check from source, which needs nothing else: ```sh npm run dev -- check pkg:npm/request@2.88.2 ``` The rule is stated for contributors in [AGENTS.md](../AGENTS.md#dependencies), and the `self-scan` CI job backs it up by failing on any finding the baseline does not cover (see [ADOPTION.md](ADOPTION.md#scanning-this-repository)). ## Discovery and host compatibility On 2026-10-15, `npx skills add arminazimi/evergreen --list` discovered exactly one Skill named `evergreen-deprecation`. A clean project installation then selected and installed that Skill for five representative skills CLI targets: | Host | `--agent` value | Install result | | --- | --- | --- | | Codex | `codex` | Passed | | Claude Code | `claude-code` | Passed | | Cursor | `cursor` | Passed | | GitHub Copilot | `github-copilot` | Passed | | Gemini CLI | `gemini-cli` | Passed | The same install registered the Skill with skills.sh. It is discoverable as `arminazimi/evergreen@evergreen-deprecation` at [skills.sh](https://skills.sh/arminazimi/evergreen/evergreen-deprecation). The skills CLI security summary reported Gen Agent Trust Hub **Safe**, Socket **0 alerts**, and Snyk **Low Risk** at validation time. These external assessments can change and do not replace review of the installed files. ## Evaluation Trigger evaluation cases live in `skills/evergreen-deprecation/evals/cases.json`. The versioned corpus contains 60 prompts: 30 expected triggers and 30 expected non-triggers, including 12 deliberately ambiguous requests. Each of the `agent-skills`, `codex`, and `claude` discovery lanes has 20 cases with an even positive/negative split. Run the deterministic metadata-selection proxy from the repository root: ```sh npm run eval:skill-triggers ``` The command builds the evaluator, prints JSON results overall and by host lane, lists any failed case IDs, and exits unsuccessfully when the corpus has fewer than 50 cases, recall is below 90%, or the false-trigger rate is 5% or higher. It has no model, network, account, or API-key dependency, so CI and contributors can reproduce the same baseline. Current published baseline: | Lane | Cases | Recall | False-trigger rate | | --- | ---: | ---: | ---: | | All | 60 | 100% | 0% | | Agent Skills | 20 | 100% | 0% | | Codex | 20 | 100% | 0% | | Claude | 20 | 100% | 0% | This is a contract test for the concepts exposed by the skill metadata, not a claim that every model version will make an identical routing decision. Live host checks can add observations without weakening the deterministic CI gate. Generic dependency upgrades, vulnerability or security audits, deprecated application APIs, and unrelated modernization are explicit negative cases. ### Dependency guard evaluation The Skill also tells an agent to check a package, runtime, or action before adding it (`check_dependency` or `evergreen check`). `skills/evergreen-deprecation/evals/dependency-cases.json` measures that trigger: 48 prompts, 27 expected checks and 21 that should not trigger one, split evenly across the three discovery lanes. Positives add or choose a dependency in npm, PyPI, and GitHub Actions terms (`npm install request-promise`, `Add actions/checkout@v4 to the CI workflow`, `Which package should I use for date handling?`). Negatives are removals, installs of what the lockfile already declares, routine upgrades, `dependency injection`, tests, documentation, and refactors; six edge cases are labeled ambiguous with the decision and reason recorded. `npm run eval:skill-triggers` reports recall and false-trigger rate for it under `dependencyGuard`, overall and by lane, with the same gates (at least 40 cases, recall of 90% or more, false-trigger rate under 5%). Like the lifecycle evaluation it is a deterministic proxy for what the Skill metadata says, not a measurement of any particular model; live runs in each host can add observations without weakening the gate. CI also validates the portable frontmatter, small instruction surface, vendor neutrality, complete tool mapping, and safety invariants. Independent forward tests exercise three different agent situations: | Agent situation | Observable success criterion | Result | | --- | --- | --- | | Skill discovery | Classify every trigger and non-trigger case through the metadata contract without reading expected answers. | 60/60 correct across three lanes. | | MCP-capable agent | Discover, explain, create context, migrate, and verify through the real stdio protocol in one session. | Passed with all five tools; checkout finding verified absent. | | CLI-only agent | Complete the same lifecycle using only documented CLI fallbacks and a fresh final scan. | Passed; request migration changed only two packet files and ended with zero findings. | Forward-test observations are recorded on the Week 11 GitHub issue when the phase is completed. Discovery testing narrowed an ambiguous generic migration trigger to migrations from Evergreen findings. CLI testing replaced the packet's `npx` rescan suggestion with the already-installed `evergreen` executable so verification remains offline-capable and version-consistent. MCP testing clarified that an empty usage list means there are no additional usages beyond the declaration, rather than implying that the declaration itself is not relevant. ## Limits and security assumptions - Evergreen currently covers its documented GitHub Actions, Node.js, npm, and Python rules. The skill reports unsupported ecosystems instead of inventing migration advice. - Dynamic references that detectors cannot classify remain explicit limitations. - MCP verification needs the same server session that observed the finding. The CLI fallback uses a fresh scan and conservatively compares the rule and declaration path. - The skill grants no permissions. Suggested verification commands remain subject to the host's normal authorization rules. - The bundle contains no executable skill scripts and declares no pre-approved tools. - Installing through the skills CLI copies or links instruction files into the selected host's Skill directory; it does not install the Evergreen runtime itself. - Repository analysis remains local. The selected host receives structured Evergreen results, which can include paths and short source excerpts, so its own privacy policy still applies. --- Source: https://arminazimi.github.io/evergreen/RULES.html # Lifecycle rules Every lifecycle claim Evergreen makes comes from a rule in `registry/`. A rule says which component is affected, in which versions, what happened to it, who says so, and when that was last checked. This page is the reference for writing one. The format is `evergreen.registry.v2`. Its JSON Schema is [`schemas/registry.v2.schema.json`](../schemas/registry.v2.schema.json), and the reasoning is in [Decision 0004](decisions/0004-registry-v2.md). All five bundled registries (`container`, `github-actions`, `nodejs`, `npm`, `python`) use it. ## A complete rule ```json { "schemaVersion": "evergreen.registry.v2", "ecosystem": "npm", "freshness": { "maxReviewAgeDays": 365 }, "rules": [ { "id": "npm.package.request.deprecated", "ecosystem": "npm", "purl": "pkg:npm/request", "vers": "vers:npm/*", "event": "deprecated", "title": "request is deprecated", "summary": "The package is fully deprecated and no new changes are expected.", "severity": "high", "replacement": "Native fetch or a maintained HTTP client", "deadline": "2020-02-11", "sources": [ { "title": "request package deprecation notice", "url": "https://www.npmjs.com/package/request" } ], "provenance": { "observedAt": "2026-10-13", "reviewedAt": "2026-10-14", "reviewer": "arminazimi" }, "migration": { "constraints": ["Preserve request method, headers, authentication, redirects, and error semantics at each call site."], "steps": [ { "id": "replace-request-calls", "title": "Rewrite request usages", "instructions": "Translate each reported import and call to native fetch or a maintained client.", "targets": "usages" } ], "verification": [ { "command": "npm run test --if-present", "purpose": "Run the repository tests when defined." } ] }, "usage": [ { "kind": "import", "language": "javascript", "module": "request" } ], "x-npm": { "package": "request" } } ] } ``` A second, smaller example lives in [`examples/registry.v2.example.json`](../examples/registry.v2.example.json). The bundled files in `registry/` are the best reference for each ecosystem. ## The registry file | Field | Required | Meaning | |---|---|---| | `schemaVersion` | yes | Always `evergreen.registry.v2`. | | `ecosystem` | yes | A lowercase slug such as `npm`. Every rule in the file must use the same value. | | `freshness.maxReviewAgeDays` | no | How long a rule may go without review before the weekly check reports it stale. Defaults to 365. | | `rules` | yes | The rules, in the order detectors should consider them. | ## A rule | Field | Required | Meaning | |---|---|---| | `id` | yes | Stable and unique, lowercase with dots and hyphens, for example `npm.package.request.deprecated`. Baselines and reports refer to it, so do not rename it. | | `ecosystem` | yes | Matches the registry's `ecosystem`. | | `purl` | yes | The affected component as a [Package URL](https://github.com/package-url/purl-spec), canonical and **without a version**: `pkg:npm/request`, `pkg:npm/%40angular/core`, `pkg:pypi/django-nose`, `pkg:github/actions/checkout`. Runtimes with no registry use `pkg:generic/`, for example `pkg:generic/nodejs`. | | `vers` | yes | The affected versions as a [VERS](https://github.com/package-url/vers-spec) range, for example `vers:npm/*` (every version), `vers:semver/<5.0.0`, `vers:semver/>=20.0.0\|<21.0.0`, `vers:pypi/<3.10`. Write it in canonical form: no spaces, no explicit `=`, constraints sorted by version. | | `event` | yes | What happened, or what has been announced: `deprecated`, `end-of-support`, `end-of-life`, `removed`, or `scheduled-end-of-life`. Use the word the source uses; see [Choosing an event](#choosing-an-event). | | `title` | yes | One line that names the problem. | | `summary` | yes | One or two sentences stating what the source says, without advice. | | `severity` | yes | `info`, `low`, `medium`, `high`, or `critical`. | | `replacement` | no | What to move to. | | `deadline` | no | The date from the source, as `YYYY-MM-DD`. Omit it when the source gives none. | | `sources` | yes | At least one authoritative source, each with a `title` and an `https` `url` (and optionally `accessedAt`). | | `provenance` | yes | See below. | | `migration` | no | Constraints, ordered steps, and verification commands. Used by `evergreen context`. Step `targets` are `declaration`, `usages`, or `all`; step ids are unique within the rule. | | `usage` | no | Imports, calls, configuration keys, and deprecated symbols that show where a repository depends on the component. See [Symbol-level usage](#symbol-level-usage). | | `x-` | no | Matching hints the ecosystem's detector reads, such as `x-npm.package`, `x-nodejs.major`, `x-python.kind`, or `x-github-actions.supportedFromMajor`. See [Detector hints](#detector-hints). | ### Symbol-level usage A `usage` entry of kind `symbol` marks one deprecated function, method, class, or option inside a module, so a rule can say `ReactDOM.render` is deprecated without flagging all of `react-dom`. `purl` and `vers` still say which package versions are affected; the entry says which symbol. | Field | Required | Meaning | |---|---|---| | `kind` | yes | `symbol`. | | `language` | yes | `javascript`, `typescript`, or `python`. Write one entry per language. | | `module` | no | The module that exports the symbol, as it is imported: `react-dom`, `datetime`. Omit it for globals and builtins such as `Buffer` or `open`. | | `symbol` | yes | Dotted path from the module: `render`, `Buffer.alloc`, `datetime.utcnow`. Identifier segments only. | | `shape` | yes | How the symbol is used: `call`, `construct` (`new X()`, JavaScript and TypeScript only), `reference` (any other use, such as passing it as a value), or `option` (a deprecated property in the call's options object). | | `option` | when `shape` is `option` | The deprecated property name. Not allowed for other shapes. | | `argument` | no | For `option`: zero-based position of the options argument. Defaults to the last argument. | | `replacement` | no | What to use instead of this symbol. It overrides the rule's `replacement` in reports. | Symbol entries are read by parser-backed usage analysis. Regex-based analysis ignores them, so a rule that has only symbol entries reports the declaration but no usages until that analysis runs. Add an `import` entry for the module as well when you want the regex scanner to find it. A hit is reported as a usage of kind `symbol` at the first identifier of the name (at the property name for `option`), with the module and symbol as its `symbol` text, such as `fs.exists` or `fs.rmdir(recursive)`. Names are followed through imports, `require`, destructuring, aliases, and a `node:` prefix, and a global such as `Buffer` is not reported when the file imports or declares that name itself. Scopes are not modelled, so a local variable that shadows an imported name can be reported. For Python, an `option` matches a keyword argument anywhere in the call, or a positional argument at `argument`. Each symbol entry needs its own evidence: the rule's `sources` must name the symbol, not only the package. A complete rule, also in [`examples/registry.v2.symbol.example.json`](../examples/registry.v2.symbol.example.json): ```json { "schemaVersion": "evergreen.registry.v2", "ecosystem": "npm", "freshness": { "maxReviewAgeDays": 365 }, "rules": [ { "id": "npm.symbol.react-dom.render.deprecated", "ecosystem": "npm", "purl": "pkg:npm/react-dom", "vers": "vers:npm/>=18.0.0", "event": "deprecated", "title": "ReactDOM.render is deprecated", "summary": "React 18 deprecates ReactDOM.render; it runs the app in React 17 mode and logs a warning.", "severity": "medium", "replacement": "createRoot from react-dom/client", "sources": [ { "title": "React 18 upgrade guide: react-dom render", "url": "https://react.dev/blog/2022/03/08/react-18-upgrade-guide" } ], "provenance": { "observedAt": "2026-10-19", "reviewedAt": "2026-10-19", "reviewer": "arminazimi" }, "migration": { "constraints": [ "Keep the application's root container and rendered tree unchanged." ], "steps": [ { "id": "use-create-root", "title": "Switch to createRoot", "instructions": "Replace each reported render call with createRoot(container).render(element).", "targets": "usages" } ], "verification": [ { "command": "npm run test --if-present", "purpose": "Run the repository tests when defined." } ] }, "usage": [ { "kind": "import", "language": "javascript", "module": "react-dom" }, { "kind": "symbol", "language": "javascript", "module": "react-dom", "symbol": "render", "shape": "call", "replacement": "createRoot(container).render(element)" }, { "kind": "symbol", "language": "typescript", "module": "react-dom", "symbol": "render", "shape": "call", "replacement": "createRoot(container).render(element)" } ], "x-npm": { "package": "react-dom" } } ] } ``` To add one: find the official deprecation notice for the symbol, copy the rule above, set `purl` and `vers` to the package versions the notice covers, write one `symbol` entry per language, and run `npm run check`. #### Walkthrough: add a symbol rule This example adds a rule for `fs.exists`, which Node.js documents as deprecated in favour of `fs.access` or `fs.stat`. Follow the same steps for any symbol. 1. **Find the evidence.** Open the official documentation of the symbol and look for a deprecation notice. Here it is the `fs.exists` entry in the Node.js `fs` documentation, which carries `Stability: 0 - Deprecated`. Note the URL; the rule cites it. Without an authoritative notice for this symbol there is no rule. 2. **Pick the module and the symbol path.** `module` is what code imports: `fs`. `symbol` is the dotted path inside it: `exists`. Do not put the module in the symbol. 3. **Pick the shape.** `fs.exists(path, callback)` is called, so `shape` is `call`. Use `construct` for `new Buffer(size)`, `reference` when merely passing the function around is the problem, and `option` for a deprecated property of an options object. 4. **Write one entry per language.** Node.js code is JavaScript or TypeScript, so write two `symbol` entries that differ only in `language`. Add an `import` entry for the module too if you want the regex scanner to point at the file. 5. **Write the rule.** Copy the complete rule above, then set `purl`, `vers`, `title`, `summary`, `replacement`, `sources`, and `provenance` from the notice. `summary` states what the source says; the advice goes in `replacement`. 6. **Write a fixture.** A fixture is a small source file with usages that must be reported and lookalikes that must not. Put the expected result in comments so a reviewer can read it without running anything: ```js const fs = require("fs"); const { promisify } = require("util"); fs.exists("a.txt", () => {}); // reported: call of fs.exists const { exists } = require("fs"); exists("b.txt", () => {}); // reported: destructured binding of fs.exists const check = fs.exists; check("c.txt", () => {}); // reported: alias of fs.exists fs.existsSync("a.txt"); // not reported: a different symbol fs.access("a.txt", () => {}); // not reported: the replacement const text = "fs.exists is deprecated"; // not reported: a string // fs.exists("d.txt", () => {}); // not reported: a comment const exists2 = (name) => name; exists2("e.txt"); // not reported: an unrelated function ``` Cover at least one reported usage per shape the rule declares, one alias, and the lookalikes above. A lookalike that the analysis reports is a bug in the rule or the analysis; fix it before the rule merges. 7. **Check it.** Run `npm run check`. It validates the rule against the schema, including every symbol entry, and fails when the reference pages under `docs/rules/` have drifted (run `npm run build:reference` to regenerate them). Open the pull request with `Closes #` and link the deprecation notice. ### Provenance Provenance makes a rule auditable. | Field | Meaning | |---|---| | `observedAt` | The date you last saw the claim at its source. | | `reviewedAt` | The date a maintainer last checked the rule against its sources. It must not be earlier than `observedAt`. | | `reviewer` | Who reviewed it. | | `importer` | The importer that drafted the rule, when one did. Omit it for hand-written rules. | Dates are real calendar dates written `YYYY-MM-DD`; timestamps are rejected. ### Choosing an event | Event | Use it when the source says | |---|---| | `deprecated` | The maintainer deprecated it, or put it in maintenance mode and recommends something else. | | `end-of-support` | The release or version no longer receives support, for example because its runtime is gone. | | `end-of-life` | The project or release line has reached end of life, is unmaintained, or is archived. | | `removed` | The component, release, or service has been retired and no longer works. | | `scheduled-end-of-life` | The vendor has announced a future end of life or end of support, with a date. The rule needs that date as its `deadline` and `severity: low`. It names the affected versions exactly as the later end-of-support or end-of-life rule will. When the date has passed, a reviewed change turns it into that rule (same `id`). See [decision 0010](decisions/0010-scheduled-end-of-life.md). | If the source's wording fits two events, use the earlier one and quote the source in `summary`. ## Finding confidence Every finding carries a `confidence` that says how the version behind it was read, so a pinned production runtime is easy to tell from a compatibility range: | Value | Meaning | |---|---| | `exact` | Read from a pinned declaration, such as a Dockerfile tag or an `.nvmrc` file. | | `range` | Read from a compatibility range that allows the affected version, such as `engines.node` or a dependency range with no lockfile entry. | | `inferred-from-lock` | Read from a lockfile, or from a manifest declaration whose resolved version a lockfile confirms. | A detector sets `confidence` when it knows better. Otherwise the engine uses `inferred-from-lock` for a finding located in a lockfile and `exact` for any other. The value appears in JSON output, in text output as a `Confidence:` line, and in SARIF as `properties.confidence`. It is additive: no existing field changes. ## Evidence policy - Every rule needs an authoritative, versioned source: maintainer release notes, an official support or end-of-life policy, a registry deprecation flag, or platform documentation. - Never infer lifecycle status from popularity, download counts, or a newer version existing. - Dates, affected versions, and replacements must match the source. If the source is ambiguous, say so in the pull request. - Rules are reviewed data, not code. Changes to `registry/` and `schemas/` are reviewed by a maintainer (see `.github/CODEOWNERS`). ## Validation Everything above is enforced when a registry loads, and `npm run check` runs it on the bundled files. Validation reports every problem at once and names the rule and field, for example: ```text Invalid registry (2 problems): - rule "npm.package.request.deprecated": provenance.reviewedAt is required - rule "npm.package.left-pad.deprecated": purl must be canonical and version-less (expected "pkg:npm/left-pad") ``` Beyond the schema, validation checks that: - `purl` is canonical and has no version, qualifiers, or subpath; - `vers` is canonical, its scheme is supported, every version in it is valid for that scheme, and the scheme fits the PURL type (for example `pypi` ranges on `pkg:pypi`); - ids are unique, the rule's `ecosystem` matches the registry's, and there are no unknown fields (extensions must be named `x-`); - `migration` and `usage` entries are valid. Supported version schemes are `semver`, `npm`, `cargo`, `golang`, `composer` (Semantic Versioning), `pypi` (PEP 440), `maven`, `nuget`, `gem`, and `intdot`. ## Freshness and sources Scans never check freshness and never use the network, so results stay deterministic. A weekly workflow, [`.github/workflows/sources.yml`](../.github/workflows/sources.yml), runs `tools/check-sources.ts` and reports, separately: - **stale** rules, reviewed longer ago than their `freshness.maxReviewAgeDays`; - **changed** sources, whose final URL or title differs from the one recorded in `registry/sources.lock.json`; - **unreachable** sources; - **blocked** sources behind a bot challenge, which cannot be verified; - **unrecorded** sources, not in the lock yet. The check never edits rules. When it reports something, review the source, update the rule and its `reviewedAt`, and refresh the lock: ```bash npm run build node dist/tools/check-sources.js --update-lock ``` Run it locally with `npm run check:sources`. ## Detector hints Existing detectors read a few fields that are specific to their ecosystem. In v2 they sit under `x-` and are merged into the rule when the registry loads, so a detector sees the same rule it always has. | Registry | Hints | |---|---| | `npm` | `x-npm.package` | | `nodejs` | `x-nodejs.major` for one release line, or `x-nodejs.maximumMajor` for every line up to and including it | | `python` | `x-python.kind` (`runtime` with `maximumVersion` such as `"3.9"`, or `package` with `package`) | | `github-actions` | `x-github-actions.action` and `x-github-actions.supportedFromMajor` | The hints must agree with `purl` and `vers`: `x-npm.package` is the package in `pkg:npm/...`, and `x-github-actions.supportedFromMajor: 5` goes with `vers:semver/<5.0.0`. New detectors should not add hints that `purl` and `vers` can already express. ## Drafting rules from sources Rules do not have to be typed by hand. [Importers](IMPORTERS.md) read an authoritative source such as the Node.js release schedule or npm's deprecation flags and draft rules for you to review. ## Converting a v1 file A tool converts registries written in the older, detector-specific format of the four bundled detectors. It derives `purl` and `vers` from the old fields, refuses to guess lifecycle events, and checks that every rule reads back unchanged: ```bash npm run build node dist/tools/registry-v1-to-v2.js registry/example.json \ --observed-at 2026-10-13 --reviewed-at 2026-10-14 --reviewer your-name \ --event example.rule.id=deprecated --out registry/example.json ``` ## Detectors outside this repository The public SDK still validates the v1 envelope (`validateRegistry`), so external detectors keep working unchanged. Loading v2 registries is currently internal to the bundled detectors; see [detector authoring](DETECTORS.md#registry-validation). --- Source: https://arminazimi.github.io/evergreen/DATA.html # Lifecycle data: updates, signing, and trust Evergreen's findings come from lifecycle rules (`registry/`). The rules that ship inside each release are always available. Between releases you can fetch newer rules, which are signed so that Evergreen can tell they came from the project's release workflow. This page explains how that works, what is checked, and what Evergreen promises. ## What you can rely on - **Scans never use the network.** A scan, `check`, `explain`, `context`, `baseline`, `doctor`, and `data status` read local files only. The only command that connects to anything is `evergreen data update`, and it runs only when you run it. A test fails if any other code path opens a socket, resolves a name, or calls `fetch`. - **Downloaded data is verified before it is used, every time it is loaded.** If it cannot be verified, Evergreen uses the data shipped with the installed release and says so in the result. It never loads data it could not verify. - **You can see how fresh the evidence is.** Every scan and check says which data it used and how old it is, and warns when that is more than 30 days. - **You can fix the data version** for reproducible CI. - **No account, no telemetry.** Updating asks a public release list and downloads public files. Nothing about you or your repository is sent. ## Commands ```bash evergreen data status # which data is in use, where it came from, how old it is (--json for tools) evergreen data update # fetch and verify the newest signed bundle evergreen data update --data-version 2026.10.01 # fetch exactly that bundle, without making it active evergreen scan . --data-version 2026.10.01 # use exactly that bundle (every command takes this) EVERGREEN_DATA_VERSION=bundled evergreen scan . # use only the data shipped with this release ``` `data status` reports one of three sources: **bundled** (shipped with the release), **cache** (the newest verified download), or **pinned** (the version you asked for). Its age is counted from when a downloaded bundle was built, or from the newest review date among the rules for bundled data. ### Updating `evergreen data update` lists the project's `data-v…` releases, picks the newest one that has a bundle and a signature, downloads both (https only, with size limits and timeouts), verifies them completely, and only then stores them in your cache and makes them active. If anything is wrong the previous data stays active and nothing is written. Running it again when nothing changed says so. It refuses a bundle that is older than the active one (a replayed old bundle would roll your rules back), a bundle the installed release already beats, and anything that fails verification. Three bundles are kept. Downloads honor `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` (Node.js 22.21 or newer). To use a mirror, point `EVERGREEN_DATA_URL` at its release list. ### Pinning `--data-version X` or `EVERGREEN_DATA_VERSION=X` (the flag wins) makes Evergreen use exactly bundle `X` from your cache, or `bundled` for the data shipped with the release. Unlike the automatic choice, a pin that cannot be met **stops the run** with exit code 2 instead of quietly using something else, so a CI job that pins its data either uses it or fails. The message tells you how to fetch it: `evergreen data update --data-version X`. A pinned bundle is verified just like any other, and is used whatever its age (the stale warning still appears). ### Where the data is stored `EVERGREEN_DATA_DIR` if set, otherwise `~/Library/Caches/evergreen` (macOS), `$XDG_CACHE_HOME/evergreen` or `~/.cache/evergreen` (Linux), or `%LOCALAPPDATA%\evergreen\Cache` (Windows). Inside, `data//` holds `bundle.json` and its signature `bundle.json.sigstore.json`, and `data/active.json` names the active version. Deleting the directory returns Evergreen to the bundled data. ## What is in a bundle One JSON file (`evergreen.data-bundle.v1`, [schema](../schemas/data-bundle.v1.schema.json), [decision record](decisions/0006-data-bundle.md)) with a manifest and the files the detectors read: the five registries (container images, GitHub Actions, Node.js, npm, Python) and the GitHub Actions SHA map. The manifest lists each file's SHA-256, size, and rule count, a content address for the whole set, the build time, and the oldest Evergreen version that can read it. Versions look like `2026.10.16`, with `.N` for a second bundle on the same day. Building the bundle is deterministic: the same registry files and build time give the same bytes. ## Trust model A bundle is signed with **Sigstore keyless signing**. There is no long-lived signing key to steal or lose: the release workflow proves who it is to Sigstore with a short-lived GitHub identity token, receives a certificate that lasts minutes, signs the bundle, and the signature is recorded in a public transparency log. Evergreen accepts a bundle only if **all** of these hold, checked offline: 1. The signature verifies against the exact bytes downloaded (certificate chain, validity period, transparency-log inclusion). 2. The signer is the project's release workflow: issuer `https://token.actions.githubusercontent.com` and an identity matching ```text ^https://github\.com/arminazimi/evergreen/\.github/workflows/data-release\.yml@refs/(?:tags/data-v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?|heads/main)$ ``` A valid signature from any other workflow, repository, or branch is refused. 3. The format is one Evergreen understands and its `minimumEvergreenVersion` is not newer than the installed release. 4. The bundle is complete, and every file matches its digest and size, with nothing missing or extra. The certificates and log keys come from the Sigstore public-good trusted root, which ships in the npm package (`data/sigstore-trusted-root.json`) so that verification needs no network. That is also why you should keep Evergreen reasonably current: if Sigstore rotates its keys and the root in your installed release is too old, signatures made after the rotation stop verifying. Evergreen then falls back to its bundled data and tells you; it does not skip the check. To check a release by hand with [cosign](https://docs.sigstore.dev/): ```bash cosign verify-blob \ --bundle evergreen-data-2026.10.16.json.sigstore.json \ --certificate-oidc-issuer https://token.actions.githubusercontent.com \ --certificate-identity-regexp '^https://github\.com/arminazimi/evergreen/\.github/workflows/data-release\.yml@refs/(?:tags/data-v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?|heads/main)$' \ evergreen-data-2026.10.16.json ``` ### What this does not protect against - A compromise of the repository's release workflow or of a maintainer with permission to push a `data-v…` tag: the workflow would sign whatever it was given. Rule changes are reviewed before they reach `main`, and the signature proves where a bundle came from, not that its contents are correct. - A compromised Evergreen package: the trusted root and the signer identity are part of the release you installed. - Wrong evidence. Signing says who published a rule, not that the claim is true; each rule cites its sources and review date for that reason. ## When something is wrong Evergreen reports every refusal in the result, in the scan's `diagnostics` and in `data status`, for example: ```text Ignoring downloaded lifecycle data 2026.10.16: registry/npm.json does not match its digest. Using the data bundled with Evergreen 0.3.0. ``` Common causes are a damaged download (run `evergreen data update` again), a bundle that needs a newer Evergreen (upgrade), and an expired trust root (upgrade). Data that is merely old is not an error; it is flagged stale after 30 days, and `evergreen data update` is the remedy. ## Publishing a bundle Maintainers push a `data-vYYYY.MM.DD` tag; see [Releasing](RELEASING.md#publishing-lifecycle-data). The workflow builds the bundle twice and requires identical bytes, signs it, verifies the signature, and attaches the bundle, the signature, and checksums to a GitHub release. --- Source: https://arminazimi.github.io/evergreen/VALIDATION.html # v0.1 validation Evergreen v0.1 was validated on 2026-10-14 against 20 public repositories pinned to immutable commits: ten Node.js repositories, nine Python repositories, and one Node.js/GitHub Actions repository. The complete corpus and expected finding locations live in `benchmarks/public-repositories.json`; the executable harness is `benchmarks/public-repositories.ts`. ## Results | Measure | Result | | --- | ---: | | Repositories | 20 | | Expected findings | 27 | | True positives | 27 | | False positives | 0 | | False negatives | 0 | | Precision | 1.0000 | | Recall | 1.0000 | | Median scan time | 8.19 ms | | p95 scan time | 48.88 ms | | Slowest scan | 49.73 ms | Scan time measures only local Evergreen analysis after checkout. It excludes network and Git operations, which the harness reports separately as `checkoutMs` per repository and `medianCheckoutMs`, `p95CheckoutMs`, and `slowestCheckoutMs` in the summary, so Evergreen's own cost is never mixed with the time spent downloading a repository. The run used the development machine, so duration is a reference rather than a cross-machine guarantee. ## GitHub Actions corpus additions The corpus has since grown to 30 repositories. The ten added entries are GitHub Actions repositories taken from reviewed migration commits (see [Mining migrations](../benchmarks/mining/queries.md)). Eight are pinned to the commit *before* a migration, where Evergreen must report exactly the workflow locations the migration changed, and two are pinned to the migration commit itself, where it must report nothing. Together they include reusable workflows (`Akenon-Studio/.github`) and SHA-pinned actions (`smartcrabai/paperhanger`, `openmcp-project/crossplane-headlamp-plugin`). Every expected finding in these entries has `groundTruthSource.kind` set to `migration-commit` and links to the commit that fixed it; a test checks each link against `benchmarks/mining/review-log.json`. A SHA-pinned entry is only accepted as zero-finding after checking that no pinned reference carries a version comment below the rule's supported major, because Evergreen does not classify SHA pins yet and would otherwise hide a deprecated version. Across all 30 repositories the harness reports 48 expected findings, 48 true positives, and no false positives or false negatives (observed 2026-10-16). The table above is the original 20-repository v0.1 run. ## Method Every expected result is identified by rule ID, repository-relative path, and line at a pinned commit. The harness downloads each commit to a temporary directory, runs all built-in detectors, compares sets in both directions, reports per-repository timing, and removes the checkout. It exits unsuccessfully for any unexpected or missing finding. ```bash npm run validate:public-repos ``` The audit covers both positive and zero-finding repositories across npm/Node.js, Python, and GitHub Actions. Findings were checked against their repository declarations and the authoritative sources bundled with the matching lifecycle rule. ## The 50-repository corpus The corpus now has 50 repositories: the 20 from v0.1, the ten GitHub Actions entries above, and twenty more across the shipped detectors. - Sixteen are pinned to the parent of a reviewed migration commit: six clean ones where the migration explains every finding (node-sass removals, Python runtime floors raised to a supported release), and ten where other deprecated declarations were still present. Findings that the migration fixed cite it with `groundTruthSource.kind` `migration-commit`; the rest are `maintainer-review` and quote the exact line that was read at that commit. - Four are zero-finding repositories, including monorepos (`withastro/astro`, `vitest-dev/vitest`, `lerna/lerna`, `changesets/changesets`). They were also searched by hand for deprecated actions, runtimes and packages, independently of Evergreen's own rules. The review tool (`benchmarks/mining/review.ts`) now accepts npm package, Python package, and Node.js and Python runtime rules as well as GitHub Actions rules, and `mine.ts` can mine one group at a time and append without replacing reviewed candidates (`--queries npm,python --limit 10 --append`). Across all 50 repositories the harness reports 81 expected findings, 81 true positives, no false positives and no false negatives (observed 2026-10-16). ## Kubernetes corpus Five public repositories at pinned commits cover the `kubernetes` detector: `argoproj/argocd-example-apps` (Helm and kustomize applications, no findings), `kelseyhightower/ingress-with-static-ip`, `unixhot/salt-kubebin` (a CronJob and three Ingresses), `kubernetes/examples` (two PodSecurityPolicy documents, plus the two end-of-life base images the container detector reports there), and `lusyoe/springboot-k8s-example` (a Helm template and a plain manifest). Because the corpus scans every detector, every finding in these repositories is recorded, not only the Kubernetes ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line; none of these repositories has a migration commit for the finding, so none is cited. The rules' dates and removal releases come from the Kubernetes migration guide, not from these repositories. A full run on 2026-10-17 over all 74 corpus entries (including the Java entries added in the next change) reported no false positives or false negatives, with nine expected `kubernetes` findings, all true. ## Java and Kotlin corpus Five public repositories at pinned commits cover the `java` detector: `kagkarlsson/db-scheduler` (a multi-module Maven build, no findings), `piomin/sample-spring-reactive` (a `java.version` of 19), `danvega/loom` (`java.version`, `source`, and `target` of 19), `krakowski/jextract-demo` (a Gradle Kotlin DSL toolchain of 22), and `Descended/Henesys` (a `mysql:mysql-connector-java` 8.0.33 dependency, which the artifact's own POM relocates). Every finding in these repositories is recorded, not only the Java ones, and each was checked by reading its line at the pinned commit. The ground truth is a `maintainer-review` note naming the path and line; none of the repositories has a migration commit for the finding. The JDK dates come from the Oracle Java SE Support Roadmap and the Temurin support page, and the relocation from the Maven Central POM, not from these repositories. A full run on 2026-10-17 over the 74 corpus entries then in the file reported no false positives or false negatives, with six expected `java` findings, all true. ## Ruby corpus Five public repositories at pinned commits cover the `ruby` detector: `rack/rack` (no findings), `Shopify/packwerk` (`.ruby-version` 3.0.4, plus three `actions/checkout@v2` pins), `thoughtbot/suspenders` (`.ruby-version` 3.2.2, plus one `actions/checkout@v4` pin), `github/view_component` (`.ruby-version` 2.7.1), and `puma/puma` (a Gemfile `ruby "3.2.0"` directive in a local benchmark). Because the corpus scans every detector, every finding in these repositories is recorded, not only the Ruby ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line; none of these repositories has a reviewed migration entry in the mining log for the finding, so none is cited as `migration-commit`. The EOL dates come from the [Ruby maintenance branches](https://www.ruby-lang.org/en/downloads/branches/) page, not from these repositories. ## PHP corpus Five public repositories at pinned commits cover the `php` detector: `laravel/laravel` (a current Laravel application skeleton, no findings), `jobbyphp/jobby` (`swiftmailer/swiftmailer`), `ZF-Commons/ZfcUser` (`zendframework/zend-stdlib`), `MacPaw/behat-orm-context` (`config.platform.php` 7.4.33, plus `actions/checkout@v4` and `actions/cache@v4` pins), and `Kalpa-Services/mmg-wp-plugin` (`config.platform.php` 7.4, plus `actions/checkout@v4`, `actions/github-script@v6`, and `actions/setup-go@v4` pins). Because the corpus scans every detector, every finding in these repositories is recorded, not only the PHP ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line; none of these repositories has a reviewed migration entry in the mining log for the finding, so none is cited as `migration-commit`. Package abandonment comes from Packagist and the Laminas / Symfony announcements; the PHP 7.4 end-of-life date comes from the [PHP unsupported branches](https://www.php.net/supported-versions.php) page, not from these repositories. ## Swift corpus Thirteen public repositories at pinned commits cover the `swift` detector. Targeted: `seedco/Formalist` (`.xcode-version` 9.3.1 and an iOS 12 sample app), `bojan/Troop` (`.xcode-version` 10.2.1), `s2mr/xc` (`.xcode-version` 14.2.0), `s2mr/L10nLint` (`.xcode-version` 16.4.0), `Simperium/simperium-ios` (four iOS 8 application targets), and `JohnSundell/Files` (no findings). Blind (rule below): `microsoft/SwiftStreamingMarkdown` (`.xcode-version` 16.0), `hhru/mvi-swift` (`.xcode-version` 16.0, plus `actions/checkout@v3` and Ruby 3.1), `babylonhealth/Stevenson` (`.xcode-version` 11.6), `cgoldsby/TvOSMoreButton` (`.xcode-version` 13.2.1, plus `actions/checkout@v2`, `actions/cache@v2`, and Ruby 2.7), `griffin-stewie/BudouX.swift` (`.xcode-version` 14.3.1), `micahflee/TM-SGNL-iOS` (no Swift finding: the pin reads `Xcode 16.1` and the app target is iOS 15; the same pin has `actions/checkout@v4`, `actions/upload-artifact@v4`, and Ruby 3.2), and `paulgessinger/swift-paperless` (`.xcode-version` 27.1, no findings). Because the corpus scans every detector, every finding in these repositories is recorded, not only the Swift ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line; none of these repositories has a reviewed migration entry in the mining log for the finding, so none is cited as `migration-commit`. The Xcode acceptance dates come from [App Store upcoming requirements](https://developer.apple.com/news/upcoming-requirements/), not from these repositories. ## Swift blind selection The six earliest Swift repositories were chosen knowing their result, so each records `selection: "targeted"`. The seven below record `selection: "blind"`. The blind rule, fixed on 2026-10-21 before Evergreen ran on these additions: GitHub code search for `filename:xcode-version`. Walk unique repositories whose path is exactly `.xcode-version`, in the order returned that day. Skip a repository when it is a fork, archived, already in the corpus, not primarily Swift, or larger than 50,000 KB on GitHub. Keep it when the default branch still contains `.xcode-version`. Stop once the beta counts in [decision 0013](decisions/0013-detector-maturity.md) are met, which happened at seven. The seven, pinned at their default-branch commit on that date, are search results 12, 19, 20, 22, 24, 28, and 31: `paulgessinger/swift-paperless`, `microsoft/SwiftStreamingMarkdown`, `hhru/mvi-swift`, `babylonhealth/Stevenson`, `cgoldsby/TvOSMoreButton`, `micahflee/TM-SGNL-iOS`, and `griffin-stewie/BudouX.swift`. Results before and between those were skipped by the rule above (size, language, or an existing pin), including `signalapp/Signal-iOS`, `Simperium/simperium-ios`, and `seedco/Formalist`. The query returns different results on another day, so the pinned list above, not a re-run, is the record. Ground truth: for each repository the `.xcode-version` line, every application-target `IPHONEOS_DEPLOYMENT_TARGET`, and any other detector declaration on that pin were read from the file and compared with the report. `swift-paperless` pins Xcode 27.1, which neither Xcode rule covers, and its workflows are commit-pinned at majors the rules do not match, so the expected set is empty. `TM-SGNL-iOS` writes `Xcode 16.1`, which is not a numeric pin, and its application target is iOS 15, so the Swift detector reports nothing there; the Ruby pin and the Action refs are expected findings of those detectors. `BudouX.swift` pins `14.3.1`. Every expected finding has a `maintainer-review` note naming the path, the exact line, and the vendor date the rule rests on. ## Serverless corpus Ten public repositories at pinned commits cover the `serverless` detector. Targeted: `Dwolla/webhook-receiver` (`provider.runtime` `nodejs14.x`), `haltakov/natural-language-image-search` (`provider.runtime` `python3.8`), `HUK-COBURG/innoforum-aws-serverless` (SAM and Terraform `nodejs14.x`, plus GitHub Actions pins), `BaptisteLecat/gcp_architect_certification` (Terraform Google Cloud Functions `nodejs14`), and `aws-samples/sam-python-crud-sample` (five SAM `python3.8` functions, plus GitHub Actions pins). Blind (rule below): `serverless/examples` and `AnomalyInnovations/serverless-stack-demo-api` (no findings), `Swizec/lambda-screenshot-as-a-service` (`nodejs14.x`), `skojin/send-gmail-lambda` (`ruby2.7`), and `livgust/covid-vaccine-scrapers` (seven SAM `nodejs14.x` functions, plus Node.js and GitHub Actions pins). Because the corpus scans every detector, every finding in these repositories is recorded, not only the Serverless ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line; none of these repositories has a reviewed migration entry in the mining log for the finding, so none is cited as `migration-commit`. The AWS Lambda and Google Cloud Functions end-of-life dates come from the [AWS Lambda runtimes](https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html) table and the [Cloud Run functions runtime support](https://cloud.google.com/functions/docs/runtime-support) schedule, not from these repositories. ## Serverless blind selection The five earliest Serverless corpus repositories were chosen knowing their result, so each records `selection: "targeted"`. The five below record `selection: "blind"`. The blind rule, fixed on 2026-10-21 before Evergreen ran on these additions: GitHub code search for `runtime: nodejs14.x` in `filename:serverless.yml`, plus the first results of `Runtime: nodejs14.x` / `Runtime: python3.8` in SAM `template.yaml` and of current `runtime: nodejs22.x` examples, walking public non-fork repositories in the order returned that day. Keep a repository when its default-branch tip contains `serverless.yml`, `template.yaml` / `template.yml`, or a Terraform `aws_lambda_function` / `google_cloudfunctions*_function` runtime declaration; skip archives and the repositories already pinned as targeted. Stop at five. The five, pinned at their default-branch commit on that date, are `serverless/examples`, `AnomalyInnovations/serverless-stack-demo-api`, `Swizec/lambda-screenshot-as-a-service`, `skojin/send-gmail-lambda`, and `livgust/covid-vaccine-scrapers`. The query returns different results on another day, so the pinned list above, not a re-run, is the record. Ground truth: for each repository the runtime lines (and any other detector findings on the same pin) were listed by reading the pinned files and compared with the report, independently of Evergreen's own output. Every expected finding has a `maintainer-review` note naming the path, the exact line, and the vendor date the rule rests on. ## .NET blind selection The six earliest .NET repositories were chosen knowing their result, so each records `selection: "targeted"`. The `selection` field is optional on every corpus entry; `"blind"` means the repository was chosen by a rule fixed before Evergreen ran on it ([decision 0013](decisions/0013-detector-maturity.md)), and an entry without the field counts as not blind. The blind rule, run on 2026-10-19: GitHub repository search for `language:C#`, 100 to 500 stars, last pushed from 2021-01-01 to 2024-12-31, not a fork, not archived, sorted by stars descending (`gh search repos --language "C#" --stars "100..500" --updated "2021-01-01..2024-12-31" --archived=false --include-forks=false --sort stars --order desc`). Walk the results in order and take each repository whose default branch has at least one `*.csproj`, `*.fsproj`, `*.vbproj`, or `global.json`; skip the others (Unity packages with no project file). Stop at twelve, whatever Evergreen reports for them. The twelve, pinned at their default-branch commit on that date, are results 1, 3, 5, 7, 9, 10, 11, 12, 13, 14, 15, and 16 of that search: rikimaru0345/Ceras, takuya-takeuchi/DlibDotNet, 0xthirteen/SharpStay, NightfallGT/Mercurial-Grabber, Dec0ne/ShadowSpray, Ceiridge/Chrome-Developer-Mode-Extension-Warning-Patcher, TerribleDev/OwinOAuthProviders, chsakell/mvcarchitecture, SeriaWei/ASP.NET-MVC-CMS, stanzhai/Html2Article, BeichenDream/SharpToken, and nettitude/SharpSocks. Ranks 2, 4, 6, and 8 have no project file. The query returns different results on another day, so the pinned list above, not a re-run, is the record. Ground truth: for each repository the target frameworks, SDK pins, and package references were listed by `grep` over the pinned files and compared with the report, independently of Evergreen's own output. Every expected finding has a `maintainer-review` note naming the path, the exact line, and the vendor date the rule rests on. Ceras was the one miss: it targets `netcoreapp2.1` and `netcoreapp2.2`, which had no rule, so the registry gained the 1.x, 2.x, and 3.0 lines in the same change. ## CI runner images corpus Five public repositories at pinned commits cover the `ci-images` detector: `oatpp/oatpp` (`ubuntu-20.04` and `ubuntu-22.04` hosted images), `Chinachu/Mirakurun` (four `ubuntu-22.04` pools, plus a deprecated `tslint` dependency), `microsoft/Sysmon-Linux` (`ubuntu-22.04`, plus `actions/checkout` v4.1.1), `aliostad/CacheCow` (`ubuntu-22.04`, plus .NET target frameworks past or scheduled for end of support), and `Windos/PoshNotify` (Azure Pipelines pools that use only `*-latest` images, no findings). Because the corpus scans every detector, every finding in these repositories is recorded, not only the CI image ones. Each expected finding was checked by reading its line at the pinned commit, and its `groundTruthSource` is a `maintainer-review` note naming the path and line. The Azure dates come from the [hosted image deprecation schedule](https://learn.microsoft.com/azure/devops/pipelines/agents/hosted-deprecation-schedule) and the [Azure DevOps blog deprecation plan](https://devblogs.microsoft.com/devops/upcoming-updates-for-azure-pipelines-agents-images/), not from these repositories. All five were chosen knowing their image labels, so each records `selection: "targeted"`. ## Precision thresholds [`benchmarks/thresholds.json`](../benchmarks/thresholds.json) sets a precision floor per detector (`github-actions`, `nodejs`, `npm`, `python`, `dotnet`, `serverless`, and `swift` enforce it; a detector is named by the first segment of its rule ids). The harness scores each detector, prints the figures under `detectors` in its JSON, and reports `thresholdViolations`. A stable detector below its `minPrecision` (0.95 today) fails the run with a line such as: ```text THRESHOLD npm: precision 0.9 is below the required 0.95 ``` The weekly corpus workflow therefore fails on a precision regression even if the exact-match comparison is later relaxed. Mark a new detector `"stable": false` until its corpus coverage is meaningful; it is still scored and reported but never fails the run. Never lower a floor to make a run pass. ## Fetching and the checkout cache The harness fetches up to sixteen repositories at once; change that with `--concurrency ` or `EVERGREEN_CORPUS_CONCURRENCY`. Scans and the report stay in corpus order. By default it fetches each repository as a blobless shallow fetch plus a sparse checkout of only the files the detectors declare they read ([decision 0015](decisions/0015-corpus-fetch-strategy.md)): a cold full run takes about a minute and fetches about 190 MB, and the results are identical to a full checkout. `EVERGREEN_CORPUS_FETCH=shallow` fetches complete checkouts instead. `node dist/benchmarks/fetch-strategies.js --strategy ` re-measures the options. Every corpus commit is pinned, so a checkout never changes. Set `EVERGREEN_CORPUS_CACHE=` (for example `~/.cache/evergreen-corpus`) to keep each one on disk as `__@` and skip the network when it is already there. Entries are written to a temporary directory and renamed into place, and `.git` is removed because scans read files only, so an interrupted run never leaves a half-filled entry. A sparse entry carries an `.evergreen-fetch` marker naming the declarations it was built from; if a detector later declares more files, that entry is refetched rather than reused. Unset, there is no cache and a run behaves as before. A warm run reports the same true positives, false positives, false negatives, and findings as a cold one. ## Running a subset ```bash npm run validate:public-repos -- --ecosystem php npm run validate:public-repos -- --ecosystem php --ecosystem ruby npm run validate:public-repos -- --repo laravel/framework ``` `--ecosystem ` and `--repo ` are repeatable; naming both selects the union, reported in corpus order. An unknown name is an error that lists the valid ones. A filtered run still fails on any false positive or false negative among the entries it covers, but it is not a full run: it does not enforce the per-detector precision floors, it writes no `lastFullRun`, the JSON report says `"fullRun": false` with a `filter` line, and the run prints `FILTERED RUN` on stderr. Only an unfiltered run can be recorded or compared with the full-corpus claims in this document. ### Re-running only what failed After fixing a detector, re-check just the repositories that had a mismatch: ```bash npm run validate:public-repos -- --output corpus-result.json # a run with UNEXPECTED or MISSING lines npm run validate:public-repos -- --failed-from corpus-result.json ``` `--failed-from ` runs the repositories whose entry in that report has an `unexpected` or `missing` finding; any that have left the corpus are skipped. It is a filtered run like the others (`FILTERED RUN` on stderr, `"fullRun": false`) and never counts as a full run. A report with no mismatches prints that and exits 0 without fetching anything. ### Comparing with an earlier run ```bash npm run validate:public-repos -- --compare corpus-result.json npm run validate:public-repos -- --compare benchmarks/thresholds.json ``` `--compare ` prints what moved since that file. Against an earlier report it lists `ADDED` and `REMOVED` findings per repository and the per-detector true-positive, false-positive, and false-negative differences (`DELTA`), computed over the repositories present in both, so a filtered run compares like with like. Against `benchmarks/thresholds.json` it uses the recorded `lastFullRun` for the per-detector numbers only. Comparing never changes the exit code: false positives, false negatives, and the precision floors still decide it, and an unreadable comparison file is reported without failing the run. ### Managing the cache A cache of complete checkouts is about 1.5 GB on disk; the default sparse entries need about 570 MB. A suggested location is `~/.cache/evergreen-corpus`; set `EVERGREEN_CORPUS_CACHE` to it in your shell profile. ```bash npm run corpus:cache -- status # directory, entry count, total size, entries no longer in the corpus npm run corpus:cache -- prune --dry-run # what removing entries no longer in the corpus would do npm run corpus:cache -- prune # remove entries that are no longer in the corpus npm run corpus:cache -- prune --max-size 800 # remove least recently used entries until the cache is at most 800 MB npm run corpus:cache -- prune --all # empty the cache ``` `--dry-run` works with every `prune` form and removes nothing. Every cache hit refreshes the entry's last-used time, which is what `--max-size` orders by. The commands only ever touch directories directly under the cache directory whose names look like `__@<40-hex commit>`; anything else in there is left alone, and an empty or root-like path (`/`, your home directory, a single top-level directory) is refused. ### Warm ahead, run offline ```bash npm run corpus:warm # fetch every missing checkout into the cache and stop; no scan npm run corpus:warm -- --ecosystem php # the same filters as a run npm run validate:public-repos -- --offline ``` Both need `EVERGREEN_CORPUS_CACHE` and print how many entries were already cached. `--offline` uses the cache only: if an entry is missing it fails before scanning anything, with exit code 2 and one message that lists every missing `repository@commit`. After `corpus:warm`, an `--offline` run reports the same results as an online one. A good habit is `npm run corpus:warm` after pulling `main`, so a later run never waits on a download. ### Running only what a change affects ```bash npm run validate:public-repos -- --changed # changes since origin/main, plus uncommitted and untracked files npm run validate:public-repos -- --changed release/1 # a different base ``` `--changed [base]` reads `git diff --name-only ...HEAD` together with uncommitted and untracked files and maps them to corpus ecosystems with [`benchmarks/corpus-affected.ts`](../benchmarks/corpus-affected.ts): `src/detectors/`, `registry/.json`, and `tests/fixtures//` map to the ecosystems that detector serves (for example `src/detectors/npm.ts` and `registry/nodejs.json` to `node`, the GitHub Actions detector to `node-actions`), and shared files map to every ecosystem whose detectors use them (`src/source-usage.ts` to nine ecosystems; `src/engine.ts`, `src/sdk.ts`, and the other scan-wide files to all twelve). It prints one `changed:` line per chosen ecosystem naming the paths behind it. If nothing maps, it says so and exits 0 without fetching. The result is a filtered run like any other, and it combines with `--ecosystem` and `--repo`. The map is tested: every built-in detector, every corpus ecosystem, every directory under `src/detectors/`, every file in `registry/`, and every shared file a detector imports must be listed, so adding a detector fails `npm run check` until it is mapped. Other work can import the same module, for example a pull request workflow. ## Run cost and budget Every report carries a `cost` block: total wall time, the summed fetch time (larger than the wall time when fetches overlap), cache hits and misses, and the size of the git data fetched (git does not expose network bytes, so this is the `.git` size of each fetched checkout). The run prints it as a `COST` line, and the weekly workflow shows it in the job summary. `benchmarks/thresholds.json` holds the `budget`: a wall-time limit in seconds and a fetched-bytes limit. Going over either prints a `BUDGET` warning (a `::warning::` annotation in Actions), lists it in the report's `budgetWarnings`, and flags it in the job summary. A budget overrun never fails the run; correctness is decided by false positives, false negatives, and the precision floors alone. Tighten the budget in a pull request when a faster fetch strategy lands, and raise it only with a stated reason. ## Stalled fetches One hung connection must not hold a run. Each fetch passes git's low-speed abort (`http.lowSpeedLimit=1000`, `http.lowSpeedTime=30`: below 1 KB/s for 30 s) and a hard limit of 120 s per attempt, changed with `EVERGREEN_CORPUS_FETCH_TIMEOUT_SECONDS`. A stall counts as a failed attempt, so it is retried and, when every attempt fails, reported with the repository name. At the end of every run the five slowest fetches print as `SLOWEST` lines. ## Reading a run By default a run prints a short table on stdout instead of the full JSON: true positives, false positives, false negatives, and precision per ecosystem and per detector, then every `UNEXPECTED` and `MISSING` line, then a one-line wall-time summary. Progress goes to stderr as one line per fetched repository (`12/107 owner/name 2.1s`). The `COST`, `SLOWEST`, and `BUDGET` lines follow on stderr. ```bash npm run validate:public-repos -- --output corpus-result.json # table on screen, full JSON in the file npm run validate:public-repos -- --json > corpus-result.json # JSON on stdout; the table moves to stderr ``` `--output ` writes the full JSON report whatever the result, so a failing run still leaves it behind. The weekly workflow uses `--output` and uploads that file as the `corpus-result-` artifact. ## Recording a full run ```bash npm run record:corpus-run ``` One command replaces the hand edits. It runs the whole corpus in verify mode and, only when that passes, writes the run into `lastFullRun` in `benchmarks/thresholds.json` (date, the current `HEAD` commit, repository and finding counts, precision, recall, and true positives, false positives, and false negatives for every detector, zero for one that reported nothing), then regenerates `docs/dashboard/data.json` (`npm run build:dashboard`), the accuracy table below (`npm run build:coverage`), and `docs/llms-full.txt`. It leaves the changes uncommitted so you can review the diff. It refuses, and changes nothing, when: - the working tree has uncommitted changes, because the recorded commit would not describe them; - any run-narrowing flag is passed (`--ecosystem`, `--repo`, `--failed-from`, `--changed`), or `--observe`, `--warm`, `--output`, `--json`, or `--compare`; - the run did not cover every corpus entry, was not in verify mode, or has any false positive or false negative, or a detector below its precision floor. Other harness flags such as `--concurrency` and `--offline` are passed through. The dashboard tests read the recorded commit, counts, and age from `benchmarks/thresholds.json` instead of repeating them, so recording a new run needs no test edits. ## Pull request check Pull request CI (`ci.yml`) stays offline, so a detector or rule change that adds false positives would only be found in the weekly run. [`.github/workflows/corpus-pr.yml`](../.github/workflows/corpus-pr.yml) closes that gap in a separate workflow: when a pull request touches `src/`, `registry/`, `tests/fixtures/`, or `benchmarks/`, it maps the changed files to corpus ecosystems with the same `--changed` logic described above and runs only those, using the weekly run's checkout cache (restore only). Before it fetches anything, the workflow also runs `node dist/benchmarks/corpus-snapshot.js`, and the same comparison runs in `npm test` for every pull request. [`benchmarks/corpus-snapshot.json`](../benchmarks/corpus-snapshot.json) records how many findings, in how many repositories, the corpus expects for each detector. A change that removes or weakens an expectation, so that a known finding would no longer be required, fails with a `SNAPSHOT` line until the snapshot is updated on purpose with `node dist/benchmarks/corpus-snapshot.js --write`; that edit is a reviewable diff. Adding expectations never fails; run `--write` to ratchet the snapshot up. The job summary lists the chosen ecosystems and every `UNEXPECTED` and `MISSING` line; the full report is attached as the `corpus-pr-result-` artifact. If nothing maps, it says so and fetches nothing. The workflow is **not a required check**: keep it out of branch protection, because it needs the network and GitHub's availability. It has `contents: read` only, no secrets, and no `pull_request_target`, and the base branch reaches the shell through an environment variable, never an inline expression. Treat a red run as a finding to explain in the pull request, using the rules in "Weekly run and refreshing pins" above. ## Weekly run and refreshing pins Pull request CI never touches the network, so the corpus is checked by [`.github/workflows/corpus.yml`](../.github/workflows/corpus.yml) every Monday and on demand (Actions → Public corpus → Run workflow). The result is uploaded as the `corpus-result-` artifact even when the comparison fails. A failing run prints one line per mismatch, for example: ```text UNEXPECTED owner/repo@4ec1ebe0fa: github-actions.setup-node.node20:.github/workflows/ci.yml:20 MISSING owner/repo@4ec1ebe0fa: nodejs.runtime.eol-18:package.json:83 ``` `UNEXPECTED` means Evergreen reported a finding the corpus does not list; `MISSING` means a listed finding was not reported. Because every commit is pinned, a mismatch means Evergreen's rules or detectors changed, not the repository. Decide which side is right before touching the corpus: fix the detector or rule when the corpus is right, and update the expected entry (with its `groundTruthSource`) only when the new result is correct. To move an entry to a newer commit, change its `commit` to the full 40-character SHA, run `npm run observe:public-repos` to list what Evergreen now reports, review every difference against the repository at that commit, update `expected`, and run `npm run validate:public-repos` until it passes. Never refresh a pin only to make a failing run green. ## Differential testing against Renovate Renovate is the closest widely used tool, so the report compares Evergreen's findings with Renovate's for the same repository, by rule family. A maintainer runs Renovate (it needs the network and a large download; it is not part of `npm run check` or any scan): ```bash npx renovate --platform=local --dry-run=lookup --report-type=file --report-path=renovate.json npm run differential:renovate -- --renovate renovate.json --repository . --name owner/repo --out report.json ``` The two tools answer different questions. Renovate says a newer version exists and prints npm deprecation notices; it has no end-of-life data. Evergreen says a release line is past its vendor's end of life; it does not say an update exists. So the report only compares where both can speak about one dependency in one file: | Family | Evergreen | Renovate | Counted as agreement | | --- | --- | --- | --- | | `deprecated-package` | `npm.package.*` findings | npm `deprecationMessage` | both mark the package deprecated | | `base-image` | `container.*` findings | `dockerfile` / `docker-compose` updates | Renovate offers an update for the image Evergreen calls end-of-life | | `github-action` | `github-actions.*` findings | `github-actions` updates | Renovate offers an update for the action Evergreen flags | Outcomes: `agree`, `evergreen-only`, `renovate-only`, and `inconclusive` (Renovate skipped the lookup, for example `github-token-required` for Actions when run without a GitHub token, so it says nothing either way). Runtime findings (`nodejs.runtime.*`, `python.runtime.*`) have no Renovate counterpart and are listed as not comparable. Matching is by file and dependency name because Renovate gives no line numbers. The committed reports are combined into one reviewable page, [differential-report.md](differential-report.md) (and [differential-report.json](differential-report.json)), which lists every disagreement with links to the declaration at the pinned commit and to each tool's evidence. Regenerate it with `npm run differential:report` after adding or refreshing a report; a test fails when it drifts from the reports. Results for the five container corpus repositories, with Renovate 44.164 run without a GitHub token on 2026-10-16 (reports in [`benchmarks/differential/reports/`](../benchmarks/differential/reports/), trimmed Renovate output in `tests/fixtures/differential/`): | Repository | Agree | Evergreen-only | Renovate-only | Inconclusive | | --- | --- | --- | --- | --- | | outline/outline | 2 | 0 | 17 | 15 | | excalidraw/excalidraw | 3 | 0 | 2 | 20 | | mealie-recipes/mealie | 3 | 0 | 4 | 14 | | umami-software/umami | 2 | 0 | 2 | 4 | | gethomepage/homepage | 0 | 0 | 0 | 0 | What it shows: - **Agreements** are all base images: where Evergreen calls `node` or `postgres` end-of-life, Renovate offers a major update. Renovate does not say the old line is unsupported; only Evergreen does. - **Renovate-only** are npm deprecations Evergreen has no rule for (for example `@types/*` stubs, `babel-eslint`, `koa-router`, `socket.io-redis`, `react-beautiful-dnd`, `rollup-plugin-terser`, `@nuxt/types`). This is the real gap: the `npm-deprecated` importer only checks a candidate list, so these names are candidates to add. Each is a Renovate observation, not evidence; the importer still requires the registry's own deprecation notice. - **Evergreen-only** is empty here; every Evergreen finding that Renovate could look up, it also had an update for. - Most GitHub Action findings are `inconclusive` because Renovate cannot look up Actions without a token. Re-run with `RENOVATE_TOKEN` to turn them into comparisons. ## Accuracy by detector Precision is true positives over true positives plus false positives; recall is true positives over true positives plus false negatives. Both come from the last full corpus run recorded in [`benchmarks/thresholds.json`](../benchmarks/thresholds.json), so a reader can regenerate every number: run `npm run record:corpus-run`, then `npm run build:coverage`. The table below is generated; a test fails when it drifts from the recorded run. `nodejs` is the Node.js runtime rules served by the npm detector. A dash means the last full run did not score that detector. Measured on 145 pinned repositories at commit `122d612` on 2026-10-21. Regenerate with `npm run build:coverage` after `npm run record:corpus-run`. | Detector | Maturity | True positives | False positives | False negatives | Precision | Recall | | --- | --- | --- | --- | --- | --- | --- | | android | Experimental | 4 | 0 | 0 | 1.000 | 1.000 | | ci-images | Experimental | 8 | 0 | 0 | 1.000 | 1.000 | | container | Experimental | 19 | 0 | 0 | 1.000 | 1.000 | | cpp | Experimental | 11 | 0 | 0 | 1.000 | 1.000 | | dart | Experimental | — | — | — | — | — | | database | Experimental | — | — | — | — | — | | dotnet | Beta | 105 | 0 | 0 | 1.000 | 1.000 | | github-actions | Supported | 261 | 0 | 0 | 1.000 | 1.000 | | go | Experimental | 8 | 0 | 0 | 1.000 | 1.000 | | java | Experimental | 6 | 0 | 0 | 1.000 | 1.000 | | kubernetes | Experimental | 29 | 0 | 0 | 1.000 | 1.000 | | nodejs | Supported | 44 | 0 | 0 | 1.000 | 1.000 | | npm | Supported | 49 | 0 | 0 | 1.000 | 1.000 | | php | Experimental | 5 | 0 | 0 | 1.000 | 1.000 | | python | Supported | 20 | 0 | 0 | 1.000 | 1.000 | | ruby | Experimental | 7 | 0 | 0 | 1.000 | 1.000 | | rust | Experimental | 5 | 0 | 0 | 1.000 | 1.000 | | serverless | Beta | 20 | 0 | 0 | 1.000 | 1.000 | | shell | Experimental | — | — | — | — | — | | swift | Beta | 15 | 0 | 0 | 1.000 | 1.000 | | terraform | Experimental | — | — | — | — | — | ## Interpretation and limits Precision and recall are measured only against Evergreen's documented v0.1 rule set and the pinned corpus. They do not mean Evergreen detects every possible deprecation in these ecosystems. The corpus is public and reproducible, but not a statistically representative sample of all software. Dynamic action references and unsupported ecosystems remain intentionally unclassified. The controlled source-usage benchmark provides a separate exact-match test for imports, calls, and configuration keys; see [Benchmarks](BENCHMARKS.md). --- Source: https://arminazimi.github.io/evergreen/PRIVACY.html # Evergreen privacy Evergreen analyzes software repositories on the user's computer. The Codex and Claude plugins start a local stdio MCP server and do not send repository contents to an Evergreen-operated service. Evergreen does not require an Evergreen account, collect analytics, use advertising identifiers, or sell personal data. The local tools read supported manifests, lockfiles, workflow files, runtime-version files, and relevant source excerpts from the repository path the user authorizes. Results can contain file paths and short source excerpts. The host application receives those results, so its privacy policy and the user's workspace settings also apply. The plugin launches the exact published `evergreen-deprecation` npm version recorded in its manifest. The package manager may contact the configured npm registry to obtain that package. Lifecycle evidence links are returned as references; Evergreen does not open them automatically. Scans use no network. The one exception is `evergreen data update`, which you run yourself to fetch newer, signed lifecycle rules from the project's public GitHub releases. It sends nothing about you or your repository (a request for a public release list and two public files, which the host can see as it can any download), verifies the signature before using the data, and falls back to the rules shipped with the release if verification fails. See [Lifecycle data](DATA.md). Evergreen's tools do not edit repository files, execute suggested migration commands, open a network listener, or request credentials. Users can disable or remove the plugin at any time. Questions can be reported through [GitHub Issues](https://github.com/arminazimi/evergreen/issues). --- Source: https://arminazimi.github.io/evergreen/DETECTORS.html # Detector authoring Evergreen detectors are adapters at the detector SDK seam. They inspect a repository and return lifecycle findings; the scan engine handles detector attribution, sorting, reporting, and composition. ## Supported interface Import the public SDK from `evergreen-deprecation/sdk`: ```ts import { defineDetector, DETECTOR_API_VERSION, type Detector, } from "evergreen-deprecation/sdk"; ``` A compatible detector has one stable identifier and one operation: ```ts export const detector = defineDetector({ apiVersion: DETECTOR_API_VERSION, id: "example-ecosystem", async scan({ root }) { return []; }, }) satisfies Detector; ``` The interface guarantees: - `root` is an absolute repository path. - `id` uses lowercase letters, numbers, and single hyphens. - `scan` returns findings without a `detector` field; the engine adds it from `id`. - Findings must be deterministic for the same repository and bundled registry. - Repository contents are untrusted input. - The core contract provides no network capability. Detectors should use bundled, reviewed registry data so scans remain local and reproducible. - An incompatible `apiVersion` fails before the detector scans. See [`examples/detectors/todo-comments.ts`](../examples/detectors/todo-comments.ts) for a complete minimal adapter. Built-in detectors with their own pages: [Container base images](detectors/container.md), [Dart and Flutter](detectors/dart.md), [Database runtimes](detectors/database.md), [.NET](detectors/dotnet.md), [Go](detectors/go.md), [Java and Kotlin](detectors/java.md), [Kubernetes APIs](detectors/kubernetes.md), [Rust](detectors/rust.md), [Serverless runtimes](detectors/serverless.md), [Shell and PowerShell](detectors/shell.md), [Swift](detectors/swift.md), [Terraform](detectors/terraform.md). The C and C++, Ruby, PHP, Android, and CI runner images detectors have no separate pages yet; their supported declarations and limits are in [Compatibility](COMPATIBILITY.md#c-and-c-detector), [Compatibility](COMPATIBILITY.md#ruby-detector), [Compatibility](COMPATIBILITY.md#php-detector), [Compatibility](COMPATIBILITY.md#android-detector), and [Compatibility](COMPATIBILITY.md#ci-runner-images-detector). ## Ruby detector layout The Ruby detector is a useful example of the layout a built-in detector follows. Its entry point, `src/detectors/ruby/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per file type sits beside it (`parse-ruby-runtime.ts`, `parse-gemfile.ts`, `parse-gemfile-lock.ts`, and `parse-gemspec.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file or line. - Rules come from `registry/ruby.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - A runtime pin uses the first rule whose `vers` range contains the version. A rule whose `vers` is malformed is skipped, so it does not hide a later rule. - A `Gemfile` gem is matched only against rules that cover every version, while a `Gemfile.lock` gem is matched against any rule for that gem name. - Gem rules that declare usage matchers use `analyzeSourceUsage`, so those findings list the Ruby files that import or call the gem. A `require`, `load`, `gem`, or `autoload` whose argument is not a string literal is unclassified. `require_relative` is never unclassified. Qualified calls without parentheses (for example `JSON.parse text`) are not matched. The user-facing list of supported forms and limits for this detector is in [Compatibility](COMPATIBILITY.md#ruby-detector). ## PHP detector layout The PHP detector follows the same layout. Its entry point, `src/detectors/php/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per file type sits beside it (`parse-php-runtime.ts`, `parse-composer-json.ts`, and `parse-composer-lock.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file or line. - Rules come from `registry/php.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - A runtime pin uses the first rule whose `vers` range contains the version. A constraint that is not a concrete version (for example `>=7.4` or `^8.2`) is skipped, and a rule whose `vers` is malformed is skipped, so neither hides a later rule. - A `composer.json` package is matched only against rules that cover every version of the package, while a `composer.lock` package is matched against any rule whose `vers` contains the resolved version. An `abandoned` flag in the lockfile is recorded by the parser and is not itself a finding. - Package rules that declare usage matchers use `analyzeSourceUsage`, so those findings list the `.php` files that `require` or `include` that package and the qualified calls those matchers name. A `require` or `include` whose argument is not a string literal is unclassified, as is `new $class` or `$class::method()`. Runtime end-of-life rules do not name source usages. The user-facing list of supported forms and limits for this detector is in [Compatibility](COMPATIBILITY.md#php-detector). ## Android detector layout The Android detector follows the same layout. Its entry point, `src/detectors/android/index.ts`, finds the files, reads them, and matches each declaration to a rule. Pure parsers sit beside it: `parse-build-gradle.ts` for SDK settings, `parse-settings-gradle.ts` for plugin pins, and `parse-libs-versions-toml.ts` for the version catalog. String-notation Maven dependencies in `build.gradle` and `build.gradle.kts` come from the Java detector's `parse-gradle.ts`; this detector keeps only those dependency declarations. - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/android.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - A declaration uses the first rule whose `vers` range contains the version text. A rule whose `vers` is malformed, or whose comparison throws, is skipped, so it does not hide a later rule. - An SDK setting matches `pkg:generic/android/` (`compileSdk`, `minSdk`, `targetSdk`, `buildToolsVersion`). An Android Gradle Plugin pin, in settings or the catalog `agp` key, matches `pkg:generic/android/agp`. `org.jetbrains.kotlin.android` in settings matches `pkg:generic/android/kotlin-android`. The catalog `kotlin` key matches `pkg:generic/android/kotlin`. A string-notation dependency matches `pkg:maven//`. - Property references, method calls, version-catalog lookups, and `$` interpolation produce no declaration. The analysis reports no unclassified references. - A rule that lists usage matchers uses `analyzeSourceUsage` for `.java` and `.kt` files. The bundled Play Billing rules do not list matchers, so those findings point at the declaration only. The user-facing list of supported forms and limits for this detector is in [Compatibility](COMPATIBILITY.md#android-detector). ## CI runner images detector layout The CI runner images detector follows the same layout. Its entry point, `src/detectors/ci-images/index.ts`, finds the files, reads them, and matches Azure hosted-image labels to a rule. One pure parser per file type sits beside it (`parse-gitlab-ci.ts`, `parse-circleci.ts`, and `parse-azure-pipelines.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/ci-images.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - An Azure `pool.vmImage` label of the form `ubuntu-`, `macos-`, or `windows-` matches `pkg:generic/azure-pipelines/-image` with an `intdot` version. Every rule whose `vers` range contains that version is reported. A label with an architecture suffix, or any other shape, is not matched. - GitLab `image` and `services` values are always unclassified. CircleCI returns orbs, dynamic values, and floating aliases as unclassified; a literal CircleCI image is not matched, because this version has no CircleCI Package URL mapping. - `latest`, `*-latest`, variables, and template expressions produce no finding. There is no source-usage pass. The user-facing list of supported forms and limits for this detector is in [Compatibility](COMPATIBILITY.md#ci-runner-images-detector). ## Dart detector layout The Dart detector follows the same layout. Its entry point, `src/detectors/dart/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per file type sits beside it (`parse-pubspec.ts`, `parse-pubspec-lock.ts`, and `parse-fvm.ts`): - Rules come from `registry/dart.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - An exact version (a `pubspec.lock` package, an FVM pin, or a constraint without an operator) uses the first rule whose `vers` range contains it. A constraint with an operator is matched only against rules that cover every version of that package. A rule whose `vers` is malformed is skipped, so it does not hide a later rule. - Rules that declare usage matchers use `analyzeSourceUsage`, so those findings list the `.dart` files that import the package and the calls those matchers name. `Function.apply` and `dart:mirrors` reflection are unclassified. The user-facing list of supported forms and limits for this detector is in [Dart and Flutter](detectors/dart.md). ## Database runtimes detector layout The database detector reuses what the container and Terraform detectors already parse. Its entry point, `src/detectors/database/index.ts`, finds Compose and `*.tf` files, reads them, and matches each declaration to a rule. Two pure parsers sit beside it: `parse-compose.ts` filters the shared Compose image parser to database images, and `parse-terraform.ts` reads `engine` and `engine_version` with the shared HCL scanner: - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/database.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - A full version uses the first rule whose `vers` range contains it. A partial version (`12`, `5.7`) matches a rule only when the rule contains both ends of what the partial version could mean. A rule whose `vers` is malformed is skipped, so it does not hide a later rule. - Each product maps to one Package URL (`pkg:generic/postgresql`, `mysql`, `mariadb`, `redis`, `mongodb`). There is no source-usage pass. The user-facing list of supported forms and limits for this detector is in [Database runtimes](detectors/database.md). ## Shell and PowerShell detector layout The shell detector reads PowerShell declarations and the PowerShell image tags in workflow files. Its entry point, `src/detectors/shell/index.ts`, finds `*.ps1`, `*.psm1`, `*.psd1`, and `.github/workflows` files, reads them, and matches each declaration to a rule. Two pure parsers sit beside it: `parse-powershell.ts` is a bounded lexical reader for `#Requires -Version` and `PowerShellVersion`, and `parse-workflow.ts` reads `shell:` values and pinned PowerShell or Bash images from workflow YAML. - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/shell.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - Matching works as in the database detector: a full version uses the first rule whose `vers` range contains it, and a partial version matches only when the rule contains both ends of what it could mean. - All rules use one Package URL, `pkg:generic/powershell`. Shell names and Bash images are parsed but produce no findings. There is no source-usage pass. The user-facing list of supported forms and limits for this detector is in [Shell and PowerShell](detectors/shell.md). ## Terraform detector layout The Terraform detector follows the same layout. Its entry point, `src/detectors/terraform/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per file type sits beside it (`parse-terraform.ts`, `parse-lock.ts`, and `parse-version-files.ts`), on top of the shared bounded scanner in `hcl.ts`: - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/terraform.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - An exact version (a version file, a lock entry, or a constraint without an operator) uses the first rule whose `vers` range contains it. A `~> X.Y.Z` constraint matches a rule that contains both ends of that minor line. Any other constraint is matched only against rules that cover every version. A rule whose `vers` is malformed is skipped, so it does not hide a later rule. - The Terraform CLI is `pkg:generic/terraform` and a provider is `pkg:generic/terraform-provider//`. There is no source-usage pass. The user-facing list of supported forms and limits for this detector is in [Terraform](detectors/terraform.md). ## Swift detector layout The Swift detector follows the same layout. Its entry point, `src/detectors/swift/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per file type sits beside it (`parse-swift-toolchain.ts`, `parse-package-swift.ts`, `parse-package-resolved.ts`, and `parse-xcode-project.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file or line. - Rules come from `registry/swift.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - An exact pin (`.xcode-version`, `.swift-version`, `swift-tools-version`, `exact:` in `Package.swift`, a `Package.resolved` version, or an app-target `IPHONEOS_DEPLOYMENT_TARGET`) uses the first rule whose `vers` range contains the version. A rule whose `vers` is malformed is skipped, so it does not hide a later rule. - A `from:` requirement in `Package.swift` is matched only against rules that cover every version of that package. Package identity is the lowercased repository name without `.git`, matching what SwiftPM writes in `Package.resolved`. - The iOS deployment target is read only from application targets in `project.pbxproj` (`com.apple.product-type.application`), so a library or a `platforms:` minimum in `Package.swift` never produces that finding. - Package rules that declare usage matchers use `analyzeSourceUsage`, so those findings list the `.swift` files that `import` that module and the qualified calls those matchers name. Runtime Objective-C lookup forms (`NSClassFromString`, `Selector(…)`, and similar) are unclassified. Toolchain and App Store rules do not name source usages. The user-facing list of supported forms and limits for this detector is in [Swift](detectors/swift.md). ## C and C++ detector layout The C and C++ detector follows the same layout. Its entry point, `src/detectors/cpp/index.ts`, finds the files, reads them, and matches each declaration to a rule. One pure parser per manifest sits beside it (`parse-cmakelists.ts`, `parse-conan.ts`, and `parse-vcpkg.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/cpp.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - A declaration with a version uses the first rule whose `vers` range contains that version. A rule whose `vers` is malformed, or whose comparison throws, is skipped, so it does not hide a later rule. - A vcpkg dependency with no `version>=` matches only a rule that covers every version of that package. `cmake_minimum_required` matches `pkg:generic/cmake`. A Conan requirement matches `pkg:conan/`. A vcpkg dependency matches `pkg:vcpkg/`. Language standards, compiler checks, and `builtin-baseline` match the generic Package URLs named in the detector. - Generator and module rules are not matched yet: the CMake parser does not emit those declarations. There is no source-usage pass. The detector is experimental. Its five-repository public corpus includes a zero-finding case and independent ground-truth sources. The user-facing list of supported forms, lifecycle data sources, and limits is in [Compatibility](COMPATIBILITY.md#c-and-c-detector). ## Serverless runtimes detector layout The Serverless runtimes detector follows the same layout. Its entry point, `src/detectors/serverless/index.ts`, finds the files, reads them, and matches each literal runtime identifier to a rule. One pure parser per file type sits beside it (`parse-template.ts`, `parse-serverless-yml.ts`, and `parse-tf.ts`): - A parser returns nothing, not an error, for a form it does not support, and never returns part of a malformed file. - Rules come from `registry/serverless.json`, loaded through the bundled data. The detector takes a rule loader so tests can supply their own rules. - Each literal identifier is tried against `pkg:generic/aws-lambda-runtime/` and `pkg:generic/google-cloud-functions-runtime/`. The first rule whose VERS range contains the identifier wins. A rule whose VERS is malformed is skipped, so it does not hide a later rule. - CloudFormation tags (`!Ref`, `!Sub`), Serverless `${…}` interpolations, Terraform `var` / `local` / interpolation values, and floating aliases such as `latest` produce no declaration. - There is no source-usage pass: a runtime finding points at the declaration only. The user-facing list of supported forms and limits for this detector is in [Serverless runtimes](detectors/serverless.md). ## Start a new detector in this repository Do not copy an existing detector by hand. Generate the skeleton from a checkout of this repository: ```bash npm ci npm run build node dist/src/cli.js new-detector go-modules --ecosystem go ``` The id is the detector's stable identifier (lowercase letters, digits, and single hyphens, starting with a letter). `--ecosystem` is the registry slug and defaults to the id. The command creates nine files and nothing else: | File | What it is for | | --- | --- | | `src/detectors/.ts` | The detector. It imports only the public SDK, reports nothing yet, and says in its header what to do next. | | `src/detectors//parse.ts` | A pure, bounded parser: contents in, `{ value, line }` declarations out. Its header lists the rules it keeps and a place to document the forms it deliberately does not read. | | `registry/.json` | An empty `evergreen.registry.v2` registry for your rules ([Lifecycle rules](RULES.md)). | | `tests/-detector.test.ts` | The conformance suite (determinism, zero-finding, malformed input) against the generated fixtures. | | `tests/-parse.test.ts` | The invariants every parser keeps: no throw on junk, bounded input and lines, determinism, 1-based lines. | | `tests/fixtures//` | A README with a table of cases, and `zero/` and `malformed/` fixtures to replace with real ones. | | `docs/detectors/.md` | The page that describes what the detector reads, its rules, and its limits. | The skeleton compiles and passes its own tests with zero rules, so every later change starts from a green build. The command refuses to run outside a checkout, refuses an id or ecosystem that an existing detector, registry file, or rule-id prefix already uses (it lists every clash and the file that holds it), and writes nothing if any target file exists. After generating: 1. **Fixtures first.** Fill `tests/fixtures//` with small repositories: one positive per declaration type, lookalike negatives (comments, unrelated keys, dynamic values), a zero-finding one, and a malformed one. Describe each in the README table. 2. **Parser.** Teach `parse.ts` to read the declarations, one form at a time, each with a test. Bound the input, never evaluate what you read, and return nothing, not an error, for forms you do not support. 3. **Rules.** Add evidence-backed rules to `registry/.json` ([evidence policy](../CONTRIBUTING.md#evidence-policy)). 4. **Detector.** In `.ts`, find the files, parse them, and match declarations to rules. Report an exact path and line. Add the expected findings to the conformance test. 5. **Register it.** Add the detector to `builtInDetectors` in `src/detectors/index.ts`. To ship its rules in the signed data bundle ([Lifecycle data](DATA.md)), add the ecosystem to `BUNDLED_ECOSYSTEMS` in `src/data-bundle.ts`. The generator leaves both to you on purpose, so a skeleton never changes what scans report or what a data release contains. 6. **Check.** `npm run check` must pass, and `npm run validate:public-repos` must still match exactly. A new detector starts non-stable in `benchmarks/thresholds.json` until the corpus has meaningful coverage ([Validation](VALIDATION.md#precision-thresholds)). Detectors that live outside this repository use the public SDK directly, as described below; `new-detector` is for contributors to Evergreen itself. ## Maturity Every built-in detector has a level (`experimental`, `beta`, or `stable`) in `benchmarks/thresholds.json`. The level is what the detector promises users. [Decision 0013](decisions/0013-detector-maturity.md) defines those promises and the evidence that earns each level. The table below is that evidence, and `earnedLevel` in `benchmarks/thresholds.ts` checks the counts. A new detector starts as `experimental`. A promotion is a pull request that changes `level` and records the corpus run the claim rests on: date, commit, precision, recall, and the counts below. For `beta` and `stable`, the thresholds file sets `stable` to `true`, so the corpus run enforces the precision floor. A demotion is the same pull request in reverse. It is also required when a full run shows precision or recall below the floor for that level and the gap is still open 30 days later. ### Promotion gates A detector is named by the first segment of its rule ids. Conformance is the shared harness: determinism, locations, evidence, a mandatory zero-finding fixture, and malformed input that does not throw. "Repositories with findings" counts corpus repositories with at least one expected finding for that detector. "Zero-finding repositories" counts corpus repositories of the detector's ecosystem with no expected findings. A repository is blind when a rule fixed before the scan chose it. A repository picked because it contained a finding is targeted, and the corpus entry records `selection`. Every expected finding has a `groundTruthSource` that is independent of Evergreen's own output. | Gate | Experimental → beta | Beta → stable | | --- | --- | --- | | Conformance | Passes on positive, negative, zero, and malformed fixtures | Still passes | | Repositories with findings | At least 8 | At least 20 | | Expected findings | At least 15 | At least 60 | | Zero-finding repositories | At least 2 | At least 4 | | Blind selection | At least half of the ecosystem's corpus repositories | At least two thirds | | Precision on the whole corpus | At least 0.95 | At least 0.95 | | Recall on the whole corpus | At least 0.80 | At least 0.90 | | Full corpus run on current main, recorded in the thresholds file | Within 30 days | Within 30 days, with no precision or recall regression left open | | Detector page | `docs/detectors/` states what is and is not judged | The page still matches the detector | Experimental → beta is the conformance suite plus the beta column. Beta → stable keeps precision at or above 0.95 on the larger corpus, and the recorded run stays inside 30 days (four weeks) with no regression left open. An `experimental` detector is still scored; a precision or recall miss does not fail its run. Four detectors that predate this bar (`github-actions`, `nodejs`, `npm`, and `python`) keep the level they already have until the corpus catches up. The threshold test names them. Every other detector's level has to meet the column it claims. ## Unclassified references Detectors may implement the optional `analyze` operation when they need to return both findings and references that could not be classified safely. The stable `scan` operation remains required, so existing external detectors continue to work unchanged. The engine adds the detector ID, sorts the references deterministically, and exposes them under `unclassified.count` and `unclassified.references` in `evergreen.scan.v1` JSON output. The built-in GitHub Actions detector uses this metadata for full 40-character commit pins it cannot resolve. It classifies a covered action from a valid trailing version comment first, or from Evergreen's bundled exact SHA-to-release map when no comment is present. An unknown SHA is reported with reason `unknown-sha`; a present but unusable comment is reported with reason `invalid-version-comment`. Neither case becomes a lifecycle finding because Evergreen does not guess a release line. This resolution is fully offline. The bundled map is reviewed and regenerated by a separate maintainer tool; repository scans never query GitHub or another service. ### The runtime a `setup-*` action installs Besides the action reference itself, the GitHub Actions detector reads one runtime input: the `dotnet-version` of `actions/setup-dotnet` (decided in [#845](https://github.com/arminazimi/evergreen/issues/845)). It judges each version with the `dotnet` runtime rules (`dotnet.runtime.end-of-support-*`, with Microsoft's support policy as the source), so a lifecycle change is made in one place. The workflow is parsed as YAML, never evaluated. - A literal version names a release line: `6.0.x`, `6.0`, `6.0.100`, and the SDK band `6.0.4xx` are all .NET 6. - A multi-line value (`dotnet-version: |`) is judged per entry, and each finding points at that entry's line. - An expression such as `${{ matrix.dotnet }}`, a floating major (`6.x`, `6`), a pre-release (`7.0.100-preview.1`), or any other form is unclassified with its file and line, never guessed. - The `global-json-file` input, `dotnet-quality`, and the other `setup-*` actions (`setup-node`, `setup-python`, and so on) are not read. Each needs its own mapping to a lifecycle rule set; they are follow-up work, not part of this decision. ## Declaring the files a detector reads A detector may declare, in one place, which repository files its `scan` reads. The optional `inputs` field on `Detector` (added to the `evergreen.detector.v1` interface without changing anything that exists) has four parts, all relative to the scanned root with forward slashes: | Field | Meaning | | --- | --- | | `fileNames` | Exact base names at any depth, case-insensitive; `*` matches any run of characters (`requirements*.txt`, `*.Dockerfile`). | | `extensions` | Base-name endings, including the dot (`.csproj`). | | `directories` | Directories, at the root or below any directory, all of whose files may be read (`.github/workflows`). | | `sourceExtensions` | Source files read to map usage of a deprecated API (`.ts`, `.cs`). | ```ts readonly inputs: DetectorInputs = { fileNames: ["go.mod", "go.work"], sourceExtensions: [".go"], }; ``` Every built-in detector declares it, and a test fails if one does not. `declaredInputsCheck` (`evergreen/conformance`, from `src/conformance/checks/inputs.ts`) scans a fixture while recording every file whose contents the scan opens, and fails naming each file outside the declaration: ```text Detector "leaky" violated the "declared inputs" guarantee for fixture "leak": read "secrets/token.txt", which its inputs do not declare ``` Listing a directory or testing that a path exists is not a read, and files outside the scanned root (the bundled rules) are ignored. A detector that omits `inputs` makes no promise and passes the check. The declaration is what a tool needs to fetch only the files a detector reads; keep it as narrow as the detector's real behavior, and widen it in the same change that makes the detector read something new. ## Registry validation The bundled registries use the shared `evergreen.registry.v2` format, keyed by Package URL and version range with provenance for every rule. See [Lifecycle rules](RULES.md) for the format, the evidence policy, and how to write a rule. The v2 loader is internal to the bundled detectors for now; the public SDK validates the v1-style envelope below, so detectors outside this repository keep working unchanged. Registry files for external detectors use a versioned envelope: ```json { "schemaVersion": "example.registry.v1", "rules": [] } ``` Use `validateRegistry` for the envelope and a detector-owned type guard for each rule: ```ts const registry = validateRegistry(JSON.parse(contents), { schemaVersion: "example.registry.v1", isRule: (value): value is ExampleRule => isDeprecationRule(value) && typeof value.package === "string", }); ``` Validation rejects a missing or unexpected schema version, a non-array `rules` value, and the first invalid rule with its array index. `isDeprecationRule` validates the fields shared by every Evergreen lifecycle rule. Rules may include optional migration guidance so `evergreen context` can produce ecosystem-specific packets without hard-coding detector knowledge in the CLI: ```json { "migration": { "constraints": ["Preserve observable behavior."], "steps": [ { "id": "replace-declaration", "title": "Replace the declaration", "instructions": "Move to the supported replacement.", "targets": "declaration" } ], "verification": [ { "command": "npm test", "purpose": "Run the repository tests." } ] } } ``` Step targets are `declaration`, `usages`, or `all`. The packet module resolves those targets to exact repository paths. Step IDs must be unique within a rule, and constraints, steps, and verification commands must all be non-empty. External detectors without migration guidance receive deterministic generic fallback steps. ## Source usage contracts Detectors can enrich findings with concrete repository usages through the public `analyzeSourceUsage` helper. Pass all matched rules in one batch so the repository is traversed once: ```ts import { analyzeSourceUsage, type SourceUsageContract, } from "evergreen-deprecation/sdk"; const contracts: SourceUsageContract[] = [{ id: "legacy-client", matchers: [ { kind: "import", language: "typescript", module: "legacy-client" }, { kind: "call", language: "typescript", module: "legacy-client", targets: ["default", "get"], }, ], }]; const analyses = await analyzeSourceUsage(root, contracts); ``` Import and call matchers resolve common aliases in JavaScript, TypeScript, and Python. Go supports import and call matchers by import path. Java and Kotlin support import matchers only (`language: "java"` for `.java` files and `"kotlin"` for `.kt` files): `module` is a package or class name, an import of it or of anything below it matches (`import static` and wildcard imports included), and calls through the imported names are not mapped. Ruby supports import and call matchers (`language: "ruby"` for `.rb`, `.rake`, and `.ru` files): `module` is a require path, a literal `require`, `load`, `gem`, or `autoload` of that path (or a path under it) is an import, and a call target is a qualified call such as `JSON.parse(` (parentheses required). An assignment `Parser = JSON` makes `Parser.parse(` the same call. A dynamic `require`/`load`/`gem`/`autoload` argument is reported as unclassified instead of being guessed; `require_relative` is not. Configuration-key matchers support Python settings plus JSON, YAML, TOML, and INI-style files, and can optionally require a value fragment. Results include exact locations, excerpts, and a deterministic `none`, `low`, `medium`, or `high` migration-impact estimate. Keep matchers narrow: a lifecycle finding may safely have zero source usages, but an ambiguous match should not be reported as evidence. ## Compatibility policy `evergreen.detector.v1` remains compatible throughout the `0.1.x` line. Additive fields will be optional. A future breaking interface will use a new API version so an old adapter fails clearly instead of producing unreliable findings. Run `npm run check` against the minimum Evergreen version your detector supports. The repository compatibility suite exercises an external adapter, rejects an old API version, validates registry failure modes, and runs the published example through the same engine seam used by built-in detectors. --- Source: https://arminazimi.github.io/evergreen/IMPORTERS.html # Lifecycle importers An importer reads one authoritative, machine-readable source and drafts [lifecycle rules](RULES.md) from it. Importers exist so the registry grows from sources instead of being typed in by hand. They are maintainer tooling: scans never run them, never use the network, and never read their drafts. The decision behind them is [Decision 0005](decisions/0005-lifecycle-importers.md). Two rules hold for every importer: - **Importers never write the registry.** They produce drafts. A person reviews each draft and changes `registry/` in a pull request. - **Drafts are marked unreviewed.** The runner stamps every draft with `provenance.reviewer: "pending-review"`, and a test fails if that value ever appears in a bundled registry. ## The workflow ```bash npm ci && npm run build # 1. Draft rules from the source (network) node dist/tools/importers/run.js nodejs-schedule --out drafts.json # 2. See exactly what would change in the registry node dist/tools/importers/diff.js drafts.json registry/nodejs.json # 3. Write the pull-request description, with an evidence table node dist/tools/importers/pr-body.js drafts.json registry/nodejs.json --out body.md ``` `run.js --list` lists the importers. `diff.js` exits with 10 when there are changes, 0 when there are none, and 1 on an error. Only `run.js` uses the network. ### Reviewing drafts The diff classifies each draft as **added**, **changed** (split into dates, sources, and other fields), **removed**, or **unchanged**, and flags **duplicates**: a draft with the same component and versions as an existing rule under a different id. Applying a draft means overlaying the fields the importer sets on the existing rule, so fields an importer never sets (migration steps, usage matchers, hand-written text) are never reported and never lost. When you accept a draft: 1. Check each source against the rule: the event, the affected versions, and the date. 2. Copy the rule into the registry file and set `provenance.reviewer` and `provenance.reviewedAt`. Leave `importer` as it is. 3. Keep one id per component. If the diff reports a duplicate, decide which id stays and retire the other. 4. Add `migration` and `usage` for a new rule if you can; importers leave them to people. 5. Run `npm run check`. If you compare a draft with the registry and decide the registry should stay as it is (the draft overlaps a hand-written rule, or drops sources and wording you reviewed), acknowledge that exact draft instead of leaving the pull request open: `node dist/tools/importers/diff.js drafts.json registry/.json --fingerprints` prints one `":": ""` line per draft that still shows as added or changed. Add those lines to `acknowledged` in [`tools/importers/acknowledged.json`](../tools/importers/acknowledged.json) and a sentence for each in `reasons`. An acknowledged draft is counted as acknowledged, not added or changed, so it no longer opens the weekly pull request. The fingerprint covers every field except provenance: if the source later drafts something different, the rule is reported again. Do not remove a rule only because an importer stopped drafting it. A source can stop listing something because the thing is gone, and a retired image or release is exactly what an old workflow needs to be told about. The diff reports such rules as "no longer drafted"; remove one only when its fact is no longer true. ## The weekly run [`importers.yml`](../.github/workflows/importers.yml) runs every importer that has a registry file each Tuesday (and on manual dispatch), except importers whose source rejects automated fetches (`android-1`) and importers whose drafts nothing can use yet (`kubernetes-2`, `rust-2`, and `terraform-2`); see their rows below. For each one it drafts rules, compares them with the registry, and, only when they differ, opens or updates one pull request on the branch `importers/`. The pull request adds `importer-drafts/.json` and the evidence table from `pr-body.js`; it never edits `registry/`. A maintainer reviews it as described above and changes the registry in a separate commit. Python has two importers, so it gets two pull requests. The drafting jobs have read-only permissions; only the second job can write, and it only pushes that branch and opens the pull request. The repository setting "Allow GitHub Actions to create and approve pull requests" must be on, and checks do not start on pull requests opened with the default token, so close and reopen the pull request (or push to its branch) to run them. `runner-images` is not in the run because it has no registry file. ## The importers | Importer | Registry | Reads | Candidate list | |---|---|---|---| | `android-1` | `android` | The Google Play target API level requirement page: one draft per form-factor floor for new apps/updates and for existing-app availability, with `x-android.formFactors` so a detector can avoid Wear / Automotive / TV / XR false positives. Before the stated starting date the drafts are `scheduled-end-of-life`; on or after it they are `end-of-support`. Not scheduled in the weekly workflow: developer.android.com rejects automated requests (HTTP 403); a maintainer runs it by hand against a captured copy (`--replay` or a local HTML file). | none | | `ci-images-1` | `ci-images` | The runner-images README and the announcement issues it links, filtered to Azure DevOps: Azure Pipelines hosted images that are scheduled for removal or have been retired. | none | | `container-official-images` | `container` | The vendors' own Node.js and Python schedules: one "and older" `end-of-life` rule per image. The Docker Official Images files are read only to note which end-of-life lines still have published tags; they carry no lifecycle data. PostgreSQL, MySQL, and Ruby image rules stay hand-written. | none | | `cpp-1` | `cpp` | The `cmake_minimum_required` "Changed in version" notes: CMake policy-floor removals and deprecations, with deadlines from the matching Kitware GitHub release. Overlapping floors collapse so a removal wins and a remaining deprecation becomes a band above it. | none | | `dart-1` | `dart` | The pub.dev package API: direct candidate packages whose publisher set `isDiscontinued`. A valid `replacedBy` package becomes replacement guidance. The package-wide flag produces `vers:semver/*`; scans remain offline and use only reviewed registry data. | `tools/importers/dart-1.ts` | | `database-1` | `database` | The PostgreSQL versioning policy table: one `end-of-life` rule per major version 10 or later that the page marks unsupported and whose final release date has passed. Majors still supported, or ending in the future, are noted, not drafted. The replacement is the oldest supported major with more than a year left. | none | | `database-2` | `database` | The endoflife.date index for Redis and MongoDB Server: one `end-of-life` rule per `major.minor` release line (Redis 5 and later, MongoDB 4 and later) whose end date has passed. A product is drafted only when endoflife.date links the vendor's own support policy, which is the first source; lines with no end date, or an end date in the future, are noted, not drafted. | none | | `dotnet-1` | `dotnet` | The official .NET releases index: one `end-of-support` rule per release line whose end-of-life date has passed, each with its date. Lines that end in the future are noted, not drafted. | none | | `dotnet-2` | `dotnet` | The NuGet registration API: one `deprecated` rule per candidate package whose every listed version the publisher deprecated as legacy, with the alternate package it names. | `tools/importers/dotnet-candidates.ts` | | `endoflife-date` | `container` | The endoflife.date API v1 as an index over a sorted candidate list: one "and older" `end-of-life` rule per official Docker image. A product is drafted only with an official image and a release-policy link to the vendor's own page, which becomes the first source; endoflife.date is cited only as an index. | `tools/importers/endoflife-candidates.ts` | | `go-1` | `go` | The Go release history and support policy: one `end-of-support` rule per Go release line that has lost support, from the "two newest major releases" policy. | none | | `go-2` | `go` | The Go module proxy: direct modules whose latest `go.mod` carries an official `// Deprecated:` comment. | `tools/importers/go-2.ts` | | `java-1` | `java` | The Oracle Java SE Support Roadmap and the Eclipse Temurin support page: one `end-of-support` rule per group of non-LTS JDK feature releases whose last Premier Support month is over, citing Temurin where it lists a release as EOSL. LTS releases are never drafted. The sources give a month, so the rules have no `deadline`. | none | | `java-2` | `java` | Maven Central: dependencies whose own POM declares a `` to new coordinates, found by walking down from the newest version while each POM keeps the same relocation. A relocation that only changes the version is skipped. | `tools/importers/maven-candidates.ts` | | `kubernetes-1` | `kubernetes` | The Kubernetes Deprecated API Migration Guide: one `removed` rule per group, kind, and API version the guide says is no longer served. The guide names the release, not a date, so the `deadline` is the date of that release's announcement on kubernetes.io, from a small dated table in the importer. | none | | `kubernetes-2` | `kubernetes` | The Kubernetes release history: one `end-of-life` rule per minor release among the five newest whose published end-of-life date has passed. These rules describe Kubernetes itself (`pkg:generic/kubernetes`), not an API version, and no detector reads them yet. Not scheduled in the weekly workflow until one does; run it by hand. | none | | `nodejs-schedule` | `nodejs` | The Node.js Release Working Group's `schedule.json`: one `end-of-life` rule per release line from v18 whose end date has passed. | none | | `php-1` | `php` | The PHP release-branch JSON (`/releases/branches.php`), the machine-readable form of the [supported versions](https://www.php.net/supported-versions.php) / eol tables: one `end-of-life` rule per branch from 7.0 whose security-support end date has passed. Older lines are noted and skipped. Replacement is the lowest branch still in active support (`state: stable`), not security-only. | none | | `php-2` | `php` | Packagist Composer metadata: one `deprecated` rule per direct candidate package whose `abandoned` flag is set. A string flag becomes `replacement` (a package name or the URL the author gave). `true` drafts a rule with no replacement. The flag is package-wide, so the version range is `vers:composer/*`. | `tools/importers/php-candidates.ts` | | `python-release-cycle` | `python` | `peps.python.org/api/release-cycle.json`: the single "Python X.Y and older" runtime rule the Python detector reads. | none | | `npm-deprecated` | `npm` | `registry.npmjs.org//latest`: packages whose latest version carries a deprecation notice. | `tools/importers/npm-candidates.ts` | | `pypi-status` | `python` | PyPI's Simple API (PEP 792 project status markers): archived, deprecated, and quarantined projects. | `tools/importers/pypi-candidates.ts` | | `github-actions-runtime` | `github-actions` | Each action's own `action.yml` at its major tags: releases that run on a Node.js runtime GitHub has retired. | `tools/importers/github-actions-candidates.ts` | | `ruby-1` | `ruby` | The Ruby maintenance branches page: one `end-of-life` rule per release line the page marks eol, with its published end-of-life date. | none | | `rust-1` | `rust` | `RELEASES.md` in the Rust repository: the one 'Rust 1.N and older are no longer supported' rule, from the policy that only the most recent stable release is supported. It moves every six weeks; headings dated in the future are ignored. | none | | `rust-2` | `rust` | The crates.io sparse index (what Cargo reads): one rule per candidate crate listing its yanked versions. Not scheduled in the weekly workflow: the Rust registry tests accept only `deprecated` crate rules that cite RustSec, so a yanked-version rule cannot be accepted yet; run it by hand. | `tools/importers/rust-candidates.ts` | | `serverless-1` | `serverless` | The AWS Lambda supported and deprecated runtimes tables: one rule per deprecated managed runtime identifier (`pkg:generic/aws-lambda-runtime/`), with the published deprecation date. Past dates are `end-of-life`; dates within twelve months are `scheduled-end-of-life`. Replacement is up to two currently supported identifiers in the same language family. | none | | `serverless-2` | `serverless` | The Google Cloud Functions / Cloud Run functions runtime support schedule: one rule per runtime identifier with a calendar decommission date (`pkg:generic/google-cloud-functions-runtime/`). Past decommission dates are `end-of-life`; dates within twelve months are `scheduled-end-of-life`. Replacement is up to two non-preview runtimes in the same language family that are not yet decommissioned. | none | | `shell-1` | `shell` | The PowerShell support lifecycle end-of-support tables: one `end-of-life` rule per PowerShell 7 release whose date has passed, plus one rule for all of PowerShell 6. Releases still supported are noted, not drafted. Windows PowerShell follows the Windows lifecycle and is never drafted. The replacement is the oldest release with more than a year of support left. | none | | `swift-1` | `swift` | Apple's App Store upcoming-requirements listing: one `end-of-support` rule per Xcode upload floor band (versions below each dated "built with Xcode N" minimum). iOS/iPadOS deployment-target, notary, and tvOS-only entries are noted and skipped. | none | | `terraform-1` | `terraform` | The `hashicorp/terraform` GitHub releases: one `end-of-support` rule per 1.x release line that is no longer one of the two most recent, with the date of the `.0` release two lines later as its deadline. Lines still inside that window are noted, not drafted. The window comes from the HashiCorp support policy, which the importer links but does not read. | none | | `terraform-2` | `terraform` | The major-version upgrade guides of the `aws` (6), `azurerm` (4.0), and `google` (6.0.0) providers. The guides describe resource and argument changes, not provider-version support, so this importer drafts no rule: it reports each dated service or resource lifecycle statement as a note. Not scheduled in the weekly workflow, because it has nothing to draft; run it by hand. | `tools/importers/terraform-2.ts` | | `runner-images` | `github-runners` | The runner-images README and the announcement issues it links: images GitHub is deprecating or has retired. | none | Some registries have no list of what is deprecated (pub.dev, npm, PyPI, and Packagist expose a flag per package), so those importers check a **candidate list**. To widen coverage, add a name to the list in a pull request: one line, in sorted order. A candidate that is not deprecated produces no rule, and the importer names it in its notes so it can be removed. Two importers have limits worth knowing: - `runner-images` drafts rules for a `github-runners` ecosystem that has no registry file and no detector yet, because Evergreen does not read `runs-on:` labels. Its drafts have no effect until one exists. - `github-actions-runtime` relies on a small table of retired Node.js runtimes in `tools/importers/github-actions.ts`. Each row is a dated fact with the GitHub post that states it. Add a row, with its source, when GitHub retires another runtime. ## Writing an importer An importer is one module that implements `Importer` from [`tools/importers/types.ts`](../tools/importers/types.ts): ```ts export interface Importer { readonly id: string; // lowercase letters, numbers, hyphens; becomes provenance.importer readonly ecosystem: string; // the registry its drafts belong to, for example "nodejs" readonly description: string; readonly sources: readonly { title: string; url: string }[]; import(context: ImporterContext): Promise<{ rules: DraftRule[]; notes: string[] }>; } ``` [`tools/importers/example.ts`](../tools/importers/example.ts) is a complete, small importer to copy from. The real ones next to it are the best reference for each kind of source. ### What the context gives you `context.fetchText(url, { accept? })` is how an importer reads anything. It returns a response body, or throws an `ImporterError` for a failed response. `fetchJson(context, url)` parses JSON. `context.now` is the date of the run. An importer must not read files, the clock, or the environment, so a run can be replayed from a recording and gives the same drafts every time. ### What you return Return complete [v2 rules](RULES.md) **without `provenance`**; the runner adds it. Return `notes`, a list of plain sentences for the reviewer about what you skipped and why, in a stable order. The runner sorts the rules by id, puts their fields in registry order, validates them as a v2 registry for your ecosystem, and fails with every problem listed. Follow these as you write the rules: - **Cite what you read.** The sources are the pages you fetched or the posts that state the fact. If you cannot establish a fact, do not draft it; write a note. - **Set only what the source tells you.** Leave out `replacement` or `migration` unless the source gives it. A field you set overwrites the registry's value when a draft is applied; a field you leave out is left alone. - **Use the ids and wording the registry already uses**, so a draft for an existing rule shows up as a change rather than a second rule. If an id would differ from an existing rule for the same component, the diff flags it. - **Fill in the detector hint** (`x-`) the ecosystem's detector reads, and make it agree with `purl` and `vers`. - **Let time decide.** Compare dates to `context.now`, never to a fixed day, so a later run drafts the right thing. - **Treat a missing thing and a failure differently.** A 404 for one candidate is worth a note. Any other failure should stop the run: an outage must never look like "nothing is deprecated". - **Write for the detector as it is.** The Python detector reads one runtime rule; the drafts for it are one rule. A rule no detector can use is data, not protection. ### Recordings and tests Tests never use the network. An importer is tested against a recording of its real responses: ```bash # Capture today's responses (network) and write the drafts node dist/tools/importers/run.js --record tests/importers/recordings/.json --out drafts.json # Replay them (no network). Without --now, the run uses the day the recording was made. node dist/tools/importers/run.js --replay tests/importers/recordings/.json ``` A recording holds every response, including failures such as a 404, keyed by URL and `Accept` header. Replay refuses a request nothing recorded, and refuses a recording that holds responses the importer no longer reads, so recordings cannot go stale unnoticed. Name the file after the importer's id. Put the tests in `tests/importers/.test.ts`. Test the recording end to end, then test the cases a recording cannot show with small inline contexts: the calendar moving (`now`), partial or malformed data, a missing item, and a failure. The tests for `nodejs-schedule` and `github-actions-runtime` are good models. Also check how your drafts compare with the existing registry using `diffDrafts` from `tools/importers/diff.ts`; that is how the first run of each importer found real differences between the sources and the hand-written rules. ### Registering it Export the importer and add it to the list in [`tools/importers/index.ts`](../tools/importers/index.ts); `run.js --list` then shows it. If it needs a candidate list, keep the list in its own file, sorted, with a test that every entry is valid and distinct. ### Checks you do not have to write `npm run check` already verifies that every committed recording is valid and named for its importer, that no bundled rule is still `pending-review`, and that `npm test` includes tests in subdirectories. ## What importers are not - They do not decide what is true. A source can be wrong or ambiguous; the reviewer decides. - They do not run on a schedule yet. Scheduling importers to open data pull requests is a separate step. - They do not touch scans. A repository scan uses only the bundled registry and never the network. --- Source: https://arminazimi.github.io/evergreen/CLAUDE_PLUGIN.html # Evergreen plugin for Claude Evergreen's Claude plugin packages the canonical lifecycle Skill with the same version-pinned local MCP server used by the Codex adapter. It does not fork detectors, migration logic, or verification behavior. ## Requirements - Claude Code 2.1.292 or newer for the one-command marketplace install shown below. - Node.js 22 or newer for the local Evergreen MCP server. ## Install Install directly from Evergreen's GitHub marketplace: ```sh claude plugin install evergreen --marketplace arminazimi/evergreen ``` For older Claude Code versions, add the marketplace and install in two steps: ```sh claude plugin marketplace add arminazimi/evergreen claude plugin install evergreen@evergreen ``` Choose a scope when needed: ```sh claude plugin install evergreen@evergreen --scope user claude plugin install evergreen@evergreen --scope project claude plugin install evergreen@evergreen --scope local ``` User scope enables Evergreen for you on this machine. Project scope records it in the repository's `.claude/settings.json` for collaborators. Local scope enables it only for you in the current repository. For development without installing, load the adapter directory for one session: ```sh claude --plugin-dir ./plugins/claude ``` ## Confirm ```sh claude plugin list claude plugin details evergreen ``` The installed plugin exposes the `evergreen-deprecation` Skill and the local `evergreen` MCP server. Scan, list, explain, context, and verify behavior is checked against the Codex adapter by `npm run eval:plugins`. ## Dependency guard The plugin includes a `PostToolUse` hook that runs after Claude Code writes or edits a file. When the edit adds a dependency that Evergreen's lifecycle rules mark as deprecated, Claude gets a warning with the evidence and the replacement, and you see a one-line notice. What it looks at: - `package.json`: dependencies added or whose version changed. - `requirements*.txt`, `constraints*.txt`, and `pyproject.toml`: added requirements. - GitHub workflows and `action.yml`: added `uses:` references that carry a version tag, such as `actions/checkout@v4`. Only what the edit added is checked, so existing dependencies never produce repeated warnings. For a range such as `^2.88.2` the lower bound is checked. ```text Evergreen: pkg:npm/request@2.88.2 — request is deprecated (high, deprecated). The package is fully deprecated and no new changes are expected. Replacement: Native fetch or a maintained HTTP client. Evidence: https://www.npmjs.com/package/request ``` Behavior you can rely on: - It never blocks and never edits files. The warning explains the problem and cites its sources, and you or Claude decide what to do. - It checks with `evergreen check`, which runs locally against the lifecycle rules bundled in the pinned release. No repository content leaves your machine. - A package with no rule produces no output. That is absence of evidence, not a safety claim. - If the check cannot run (for example, offline on the first use), the hook says so instead of staying silent, and the edit is unaffected. An unreadable payload exits quietly. The hook script is generated from `integrations/dependency-guard/guard.mjs`, which the Codex integration shares. It uses the same pinned package as the MCP server (`evergreen-deprecation@`). Set `EVERGREEN_CHECK_COMMAND` to use another Evergreen command, for example a local build. ## Update ```sh claude plugin update evergreen@evergreen ``` The marketplace has automatic updates off by default. You can manage that setting from the Marketplaces tab in `/plugin`. ## Disable and re-enable ```sh claude plugin disable evergreen@evergreen claude plugin enable evergreen@evergreen ``` Disabling stops the Skill and MCP server from loading but keeps the installed files and settings. ## Remove ```sh claude plugin uninstall evergreen@evergreen ``` If it was installed at project scope, add `--scope project`. To remove the marketplace after uninstalling its plugin: ```sh claude plugin marketplace remove evergreen ``` ## Public directory submission The public Claude directory submission uses: - Repository: `https://github.com/arminazimi/evergreen` - Plugin path: `plugins/claude` - Tracked branch: `main` - Submission type: Plugin bundle The submitter must connect a GitHub account with push access, answer Anthropic's data-handling and compliance questions, and submit from the [Claude developer portal](https://claude.ai/directory/manage). The local package is prepared for that review with a manifest, README, Apache-2.0 license, exact npm runtime pin, strict Claude validation, and the repository security/evaluation evidence in [Plugin host evaluation](PLUGIN_EVALS.md). --- Source: https://arminazimi.github.io/evergreen/CODEX_PLUGIN.html # Codex plugin installation and management Evergreen ships as a portable Agent Plugins package with a Codex compatibility manifest. It bundles the canonical Evergreen Skill and a local, read-only stdio MCP server. No Evergreen account or hosted Evergreen service is required. ## Requirements - Codex CLI or Codex in the ChatGPT desktop app with plugin support. - Node.js 22 or newer. - Access to the npm registry the first time the pinned Evergreen runtime is launched. ## Dependency guard The plugin ships a `PostToolUse` hook (`hooks/hooks.json`, matched to `apply_patch`) and the Skill carries the same instruction for sessions where hooks are off. When a patch adds a dependency that Evergreen's lifecycle rules mark as deprecated, Codex receives a warning with the evidence and the replacement, and you see a one-line notice. It reads the patch Codex applied and checks only what the patch **added or changed**, in `package.json`, `requirements*.txt`, `constraints*.txt`, `pyproject.toml`, GitHub workflows, and `action.yml`. Existing dependencies never produce repeated warnings. ```text Evergreen: pkg:npm/request@2.88.2 — request is deprecated (high, deprecated). The package is fully deprecated and no new changes are expected. Replacement: Native fetch or a maintained HTTP client. Evidence: https://www.npmjs.com/package/request ``` - **Trust the hook first.** Codex does not run plugin hooks until you review them: enabling the plugin does not trust its hooks. Run `/hooks` in Codex and trust the Evergreen hook. Trust is tied to the hook's contents, so a plugin update asks again. - **It never blocks or edits files.** It warns and cites sources; you and Codex decide what to do. If the check cannot run, the hook says so instead of staying silent, and the patch is unaffected. - **It is local.** It runs `evergreen check` from the same pinned release as the MCP server; no repository content leaves your machine. A package with no rule produces no output, which is absence of evidence, not a safety claim. - **If hooks are off** (`[features] hooks = false`) or you do not trust the hook, the Skill still tells Codex to call the `check_dependency` tool before adding a dependency. The script is generated from `integrations/dependency-guard/guard.mjs`, shared with the Claude plugin. Set `EVERGREEN_CHECK_COMMAND` to use another Evergreen command, for example a local build. ## Install from this repository The repository marketplace is discovered from `.agents/plugins/marketplace.json` and exposes the package as `evergreen@evergreen-codex`: ```sh codex plugin add evergreen@evergreen-codex ``` For a remote checkout, add or refresh the repository marketplace first: ```sh codex plugin marketplace add arminazimi/evergreen --ref main codex plugin add evergreen@evergreen-codex ``` Restart the current Codex session after installation so the packaged Skill and MCP server are discovered together. ## Verify the installation List the installed plugin: ```sh codex plugin list --json ``` Then ask Codex to run the packaged workflow: ```text Scan this repository for supported lifecycle risks. Explain the highest-severity Evergreen finding. Create migration context for this deprecation. Verify that the finding is gone after my change. ``` The installed workflow must expose scan, list, explain, context, and verify. Suggested migration commands are returned as guidance and are never executed by Evergreen. Maintainers can run the same end-to-end check against an installed cache path: ```sh npm run validate:codex-plugin -- --plugin-root /absolute/path/to/installed/evergreen ``` ## Update For the repo-local marketplace, reinstall the plugin so the cache is rebuilt from the current package: ```sh codex plugin remove evergreen@evergreen-codex codex plugin add evergreen@evergreen-codex ``` For a Git-backed marketplace added from `arminazimi/evergreen`, refresh its snapshot before reinstalling: ```sh codex plugin marketplace upgrade evergreen-codex ``` The package pins its MCP runtime to the same semantic version as the plugin manifest, preventing a silent mix of plugin metadata and runtime versions. ## Remove ```sh codex plugin remove evergreen@evergreen-codex ``` If the repository marketplace was added explicitly and is no longer needed: ```sh codex plugin marketplace remove evergreen-codex ``` Removal deletes the installed plugin cache. It does not modify the repository being scanned. ## Local data and permissions - The MCP process runs locally over stdio and reads only repository paths authorized by the host. - Evergreen does not upload repository contents, require credentials, edit files, run migration commands, or open a network listener. - Tool results can contain repository paths and short source excerpts and are returned to the Codex host. - The host remains responsible for filesystem approvals and any later edits an agent makes. See [Privacy](PRIVACY.md) for the complete data boundary and [Codex submission](CODEX_SUBMISSION.md) for public-directory review status. --- Source: https://arminazimi.github.io/evergreen/RELEASING.html # Releasing Evergreen Any maintainer can cut a release by following this page. A release publishes four things that must agree on one version: the npm package `evergreen-deprecation`, the GitHub Action (`arminazimi/evergreen@vX.Y.Z`), the generated Claude and Codex plugin packages, and the bundled lifecycle registries inside the package. Publishing needs maintainer accounts, so releases are `lane: maintainer` work. ## Before you start - You are on an up-to-date `main` with a clean working tree, and CI on `main` is green. - You can publish to the `evergreen-deprecation` package on npm and push tags to the repository. - Choose the version. Patch for fixes and rule data, minor for new detectors, commands, or output fields, and a major version only for a breaking change to an output schema or the detector SDK. ## Gates Every gate must pass on the commit you will tag. None of them is optional. | Gate | Command | What it proves | |---|---|---| | Full check | `npm run check` | Build, Action bundle matches source, all tests including coverage floors in CI, plugin packages are in sync, plugin evals. This is what CI and `npm publish` (through `prepack`) run. | | Coverage floors | `npm run test:coverage` | Lines, branches, and functions stay above the floors in `package.json`. | | Public corpus | `npm run validate:public-repos` | Exact findings on every pinned public repository, and each detector's precision above its floor in `benchmarks/thresholds.json`. Needs network. | | Source freshness | `npm run check:sources` | No rule is stale and no source has moved or disappeared. Needs network. Review anything it reports before releasing. | | Package contents | `npm pack --dry-run` | Only `dist/src`, `examples`, `registry`, `schemas`, and `skills` ship; `registry/sources.lock.json` does not. | The last weekly runs of the corpus and source-freshness workflows should also be green. If either is red, fix or explain it before tagging. ## One-time npm setup Trusted publishing needs the package to name its publisher. On npmjs.com, open the `evergreen-deprecation` package, then Settings, then Trusted Publisher, choose GitHub Actions, and enter organization or user `arminazimi`, repository `evergreen`, and workflow filename `publish.yml` (leave the environment empty). Once a release has published this way, set the package's publishing access to "Require two-factor authentication and disallow tokens" so that only this workflow can publish. To rehearse the path before a real release, push a pre-release tag such as `vX.Y.Z-rc.1` from `main`. ## Steps 1. **Update the changelog.** Draft the entries from conventional commits with `npm run release:notes` (or run the *Release notes* workflow, which uploads the same text), then write them into `CHANGELOG.md`: ```bash node dist/tools/changelog.js --write X.Y.Z --date YYYY-MM-DD ``` Only `feat`, `fix`, and breaking commits become entries; `docs`, `test`, `ci`, and `chore` commits are left out. The command moves the hand-written `Unreleased` bullets into a new `## X.Y.Z - YYYY-MM-DD` section, appends the generated bullets, and leaves an empty `## Unreleased` above. Read the section, remove duplicates, and make each bullet a clear imperative sentence. 2. **Bump the version.** Run `npm version X.Y.Z --no-git-tag-version`, set the same version in `src/version.ts` and `server.json`, then regenerate everything derived from it: ```bash npm run build:plugins npm run build:action ``` This updates the plugin manifests and `.mcp.json` pins under `plugins/` and `.claude-plugin/marketplace.json`, and rebuilds `dist/action/index.js`. Never edit those generated files by hand. 3. **Update the version in the docs and examples.** The README and `docs/GITHUB_ACTION.md` show `arminazimi/evergreen@vX.Y.Z`; update them to the new tag. The host setups under `examples/agents/` pin `evergreen-deprecation@X.Y.Z` (and the Gemini extension its `version`); a test fails until they match `package.json`. `server.json` (the MCP Registry description) carries the version twice; bump it with `package.json`, and a test fails until they agree. 4. **Run the gates** above on the result, then commit as `chore: release X.Y.Z` through a pull request. Merge it once CI is green. 5. **Tag the merge commit.** On `main` after the merge: ```bash git tag -a vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Z ``` 6. **Publish to npm.** Pushing the tag in step 5 runs [`publish.yml`](../.github/workflows/publish.yml), which publishes with npm trusted publishing: no stored token, and a provenance attestation that names this repository, workflow, and commit. It refuses a tag that does not match the version in `package.json` or a commit that is not on `main`, and `prepack` runs `npm run check` again, so a broken tree cannot be published. A pre-release tag such as `vX.Y.Z-rc.1` is published under the `next` tag, so `latest` does not move. After publishing, a second job installs the published version on a fresh machine and runs `--help`, `capabilities`, and a scan of `tests/fixtures/smoke` (`.github/scripts/smoke.mjs`); a package that installs but does not work fails the run. Watch the run in the Actions tab, then confirm with `npm view evergreen-deprecation version` and run `npx evergreen-deprecation@X.Y.Z --help` in an empty directory. To publish by hand instead (for example if the workflow is unavailable), run `npm publish` from the tagged commit; it will have no provenance. 7. **Create the GitHub release** for the tag, with the changelog section as the body. 8. **Move the major tag** only if the project publishes one (for example `v1`), as a force-updated lightweight tag pointing at the new release. Do not move it for a pre-1.0 version. The `Release notes` workflow does this by itself after a successful `Publish to npm` run, for stable tags from `v1.0.0` on: the decision is in `.github/scripts/major-tag.mjs` (unit-tested), the tag only moves forward, and a pre-release or `0.x` tag changes nothing. To see what it would do for a tag without pushing anything, run the `Release notes` workflow by hand with `release_tag` set (a manual run is always a dry run). 9. **Package the plugins.** For Codex, `npm run package:codex-plugin` writes `artifacts/evergreen-codex-plugin-.zip`; follow [Codex submission](CODEX_SUBMISSION.md) to submit it. The Claude plugin is served from the repository marketplace, so the merged change in step 4 is its release; see [Claude plugin](CLAUDE_PLUGIN.md). 10. **Announce** only after the npm package, tag, and release are live. ## Rule data Lifecycle rules ship inside the npm package under `registry/`, so every package release carries the rules as of its commit. Between package releases, rules reach users through a signed data release. ## Publishing lifecycle data Use this when rules change and you do not want to wait for a package release. The data in `registry/` on `main` is what gets published, so merge the rule changes first and let CI pass. 1. **Check the data.** `npm run check`, then `npm run validate:public-repos` and `npm run check:sources` as in the gates above. 2. **Dry run.** In GitHub, open Actions, *Data release*, *Run workflow* on `main` with *dry run* on (the default). It builds the bundle twice and requires identical bytes, signs it, verifies the signature, and keeps the files as a workflow artifact without publishing anything. Note that signing writes an entry to the public Sigstore transparency log, even in a dry run. 3. **Tag.** The tag must be `data-v` plus the date of the commit being released in UTC, with `.N` for a second bundle that day: ```bash git tag -a data-v2026.10.16 -m "Lifecycle data 2026.10.16" git push origin data-v2026.10.16 ``` If the tag and the bundle's version disagree the workflow stops and says which tag the commit needs. 4. **Confirm.** The release has `evergreen-data-.json`, its `.sigstore.json` signature, and `SHA256SUMS`. On a clean machine, `evergreen data update` should report the new version, and `evergreen data status` should show `cache` and the signer identity. A data release does not change the npm package or the Action. If the data format itself changes, that needs a new `schemaVersion`, a package release that understands it, and a `minimumEvergreenVersion` in the bundle. See the [decision record](decisions/0006-data-bundle.md) and [Lifecycle data](DATA.md). ## Rolling back Decide by what has already been published, and prefer fixing forward. - **Nothing published yet** (the pull request or tag only): close the pull request, or delete the tag with `git push origin :refs/tags/vX.Y.Z` and `git tag -d vX.Y.Z`. - **Bad npm release**: do not rely on `npm unpublish`, which is restricted for packages that have users. Publish a fixed patch release, then mark the bad one with `npm deprecate evergreen-deprecation@X.Y.Z "Use X.Y.Z+1: "`. - **Bad Action tag**: users pin the tag, so ship a fixed patch tag and point the README at it. If you must move a tag that was published only minutes ago, say so in the release notes. - **Bad plugin package**: revert the generated files through a pull request and release a patch version; withdraw a pending Codex submission through its portal. - **Bad rule**: revert or correct the rule in `registry/` and release a patch. If the rule is actively harmful (it reports something that is not deprecated), release first and discuss afterwards. After any rollback, add a line to `CHANGELOG.md` explaining what happened and which version replaces the bad one.