Engram finds files a pull request probably forgot to change.
It learns from your repository's git history which files usually change together, then checks a PR for one-sided changes. If openapi/customer.yaml and src/generated/customer-client.ts have repeatedly changed in the same commits, and a new PR changes only the OpenAPI file, Engram reports that the generated client may be missing.
Engram is useful for catching omitted companion changes such as:
- generated clients after API or schema edits
- tests after source changes
- fixtures after parser or serializer changes
- docs after user-facing behavior changes
- migrations, config, or workflow updates that historically travel together
It is deterministic and local-first. It does not call an LLM, upload source code, or use telemetry.
Engram builds a repository-memory model from git history:
- Reads recent non-merge commits.
- Ignores configured authors, paths, and very large commits.
- Counts file pairs that changed together.
- Keeps strong relationships using support, confidence, and lift thresholds.
- Compares the PR diff against those learned relationships.
When a changed file has a strong historical companion that is absent from the PR, Engram emits a finding.
Engram treats each learned relationship as a directional rule:
source file -> companion file
If openapi/customer.yaml and src/generated/customer-client.ts changed together in one commit, Engram records both directions:
openapi/customer.yaml -> src/generated/customer-client.ts
src/generated/customer-client.ts -> openapi/customer.yaml
Direction matters. A generated file might often change when a schema changes, but a generated-file-only cleanup might not imply the schema should change.
Engram reads commits from newest to oldest. Recent commits count slightly more than older commits.
If there are n learned commits, commit i has this weight, where i = 0 is the newest commit:
weight_i = 1 - (i / (n - 1)) * 0.5
So the newest commit has weight 1.0, the oldest commit has weight 0.5, and commits between them are linearly spaced. If there is only one commit, its weight is 1.0.
For every file A and file B, Engram tracks:
support(A -> B)
The unweighted number of commits where A and B changed together.
source_support(A)
The unweighted number of commits where A changed.
file_weight(A)
The sum of commit weights for commits where A changed.
pair_weight(A -> B)
The sum of commit weights for commits where A and B changed together.
total_commit_weight
The sum of all learned commit weights.
Confidence answers: "When A changes, how often does B also change?"
confidence(A -> B) = pair_weight(A -> B) / file_weight(A)
A confidence of 0.90 means that, weighted by recency, B changed in about 90% of the commits where A changed.
Lift answers: "Is B specifically associated with A, or does B just change all the time?"
Engram first computes the background probability that B changes in any learned commit:
background_probability(B) = file_weight(B) / total_commit_weight
Then it computes lift:
lift(A -> B) = confidence(A -> B) / background_probability(B)
A lift of 1.0 means B is no more likely after A changes than it is in a normal commit. A lift of 3.0 means B is three times more likely when A changes than it is in the background history.
A relationship becomes a rule only if it passes all configured thresholds:
support(A -> B) >= min_support
confidence(A -> B) >= min_confidence
lift(A -> B) >= min_lift
With the default config, that means:
support >= 4
confidence >= 0.7
lift >= 1.5
Rules that pass the thresholds get a stored model score:
model_score = confidence * lift * ln(1 + support) * model_boost
model_boost is 1.25 when either file matches boost_paths; otherwise it is 1.0.
During a scan, Engram applies the same kind of boost again when ranking candidates:
scan_score = model_score * scan_boost
scan_boost is also 1.25 when either file matches boost_paths; otherwise it is 1.0. Boosted paths do not bypass the thresholds. They only rank higher after they already pass support, confidence, and lift.
When scanning a PR, Engram looks at each changed file as a possible source. It reports a finding when:
source changed in the PR
target did not change in the PR
source -> target passed the thresholds
The PR changed-file set comes from:
git diff --name-only --diff-filter=ACMRT base...head
Ignored paths are removed before scanning. For each changed source file, Engram chooses the highest-scoring missing target for that source. It processes changed source files in path order and stops when it reaches max_findings.
Suppose your repository has many recent commits, and these are the commits involving openapi/customer.yaml:
| Commit | Files changed |
|---|---|
api client 1 |
openapi/customer.yaml, src/generated/customer-client.ts |
api client 2 |
openapi/customer.yaml, src/generated/customer-client.ts |
api client 3 |
openapi/customer.yaml, src/generated/customer-client.ts |
api client 4 |
openapi/customer.yaml, src/generated/customer-client.ts |
api client 5 |
openapi/customer.yaml, src/generated/customer-client.ts |
api server only |
openapi/customer.yaml |
Assume the generated client is otherwise rare: it appears in only a few commits outside this API history. In simplified unweighted terms:
source_support(openapi/customer.yaml) = 6
support(openapi/customer.yaml -> src/generated/customer-client.ts) = 5
confidence = 5 / 6 = 0.833
background_probability(src/generated/customer-client.ts) = 0.20
lift = 0.833 / 0.20 = 4.165
That passes the default thresholds:
support >= 4
confidence >= 0.7
lift >= 1.5
Now a PR changes only:
openapi/customer.yaml
Engram reports:
Engram found 1 finding.
1. openapi/customer.yaml changed, but src/generated/customer-client.ts did not.
Evidence: these files changed together in 5 of the last 6 relevant commits
Recommendation: Commit the companion change, regenerate derived files, or ignore this warning if the omission is intentional.That finding does not prove the PR is wrong. It points reviewers to a likely omission based on the repository's own memory.
pnpm add -D engrampnpm engram scan --base origin/main --head HEAD --config .github/engram.yml --format markdown
pnpm engram scan --base origin/main --head HEAD --format jsonYou can also precompute a model and reuse it:
pnpm engram learn --ref origin/main --since "18 months ago" --out .engram/model.json
pnpm engram scan --base origin/main --head HEAD --model .engram/model.jsonUse actions/checkout with full history so git comparisons are available.
name: Engram
on:
pull_request:
jobs:
engram:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
with:
version: 10.33.0
- uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- run: pnpm install --frozen-lockfile
- uses: Eidetic-Research/engram@v1
with:
mode: warn
comment: true
since: 18 months agoUse mode: fail when findings should block the check. In warn mode, findings are reported but the action exits successfully unless a runtime error occurs.
Engram looks for .github/engram.yml by default. The file can either contain the config directly or under an engram: key.
engram:
since: "18 months ago"
min_support: 4
min_confidence: 0.7
min_lift: 1.5
max_files_per_commit: 80
max_findings: 10
ignore_authors:
- "dependabot[bot]"
- "renovate[bot]"
ignore_paths:
- "node_modules/**"
- "dist/**"
- "coverage/**"
boost_paths:
- "db/migrations/**"
- "openapi/**"
- "schema.graphql"
- ".github/workflows/**"Important thresholds:
min_support: how many commits must show the relationship.min_confidence: how often the companion changed when the source changed.min_lift: how much stronger the relationship is than ordinary background change frequency.max_files_per_commit: skips broad commits that would create noisy relationships.max_findings: caps the number of PR findings.
{
"tool": "engram",
"version": "0.1.3",
"base": "origin/main",
"head": "HEAD",
"mode": "warn",
"summary": {
"findings": 1,
"errors": 0,
"warnings": 1
},
"findings": [
{
"id": "engram:example",
"severity": "warning",
"title": "Likely omitted companion change",
"message": "openapi/customer.yaml changed, but src/generated/customer-client.ts did not.",
"evidence": {
"source": "openapi/customer.yaml",
"target": "src/generated/customer-client.ts",
"support": 17,
"source_commits": 19,
"confidence": 0.895,
"lift": 3.214,
"summary": "these files changed together in 17 of the last 19 relevant commits"
},
"recommendation": "Commit the companion change, regenerate derived files, or ignore this warning if the omission is intentional."
}
]
}Engram found 1 finding.
1. openapi/customer.yaml changed, but src/generated/customer-client.ts did not.
Evidence: these files changed together in 17 of the last 19 relevant commits
Recommendation: Commit the companion change, regenerate derived files, or ignore this warning if the omission is intentional.- No telemetry.
- No LLM calls.
- No source-code upload.
- No external network calls except GitHub API calls for optional PR comments.
- The hidden PR comment marker is
<!-- engram-report -->.
Engram is licensed under the Business Source License 1.1. Evaluation, development, testing, security review, and use in public open-source repositories are allowed. Commercial use, including private/internal CI use, managed services, resale, hosted services, or competing products, requires a paid commercial license from Eidetic Research.
Each version converts to Apache-2.0 on the earlier of its configured Change Date or the fourth anniversary of that version's first public distribution. See LICENSE.
pnpm install
pnpm typecheck
pnpm test
pnpm build
pnpm build:actionThe action bundle is written to dist/index.js with @vercel/ncc. Marketplace publication notes are in MARKETPLACE.md.