Teams using AI coding assistants ship design debt faster than ever. Strata is the automated design reviewer that catches the patterns AI reliably gets wrong — shallow modules, speculative abstractions, pass-through layers — before they compound into architectural problems.
Grounded in Ousterhout's A Philosophy of Software Design and the information-hiding tradition, strata finds high-recall candidates for human or AI review in TypeScript codebases. Every finding is emitted as severity: "candidate" — a signal to inspect, not a verdict to enforce.
Published package:
bun add -g @andrezzoid/strata
strata --helpThis first distribution channel is Bun-native and requires bun on PATH. Standalone binaries, mise, and Homebrew are deferred until binary packaging is proven.
From a local checkout:
bun install
bun link
strata --helpstrata [PATH]
strata [PATH] --touched-since <git-ref>
strata [PATH] --new-since <git-ref>
strata [PATH] --format json
strata [PATH] --format text
strata [PATH] --format sarif
strata [PATH] --only passThroughMethod,duplicateSymbol
strata [PATH] --exclude wideSignature
strata [PATH] --fail-on-findings
strata --versionDefaults:
PATHdefaults to the current directory.--formatdefaults totext, the local review report.--format sarifemits SARIF 2.1.0 for GitHub code scanning and other CI consumers.- Scan scope modes are mutually exclusive review questions: touched files, new candidate identities, future worsened existing candidates, or their future union.
--touched-sinceanalyzes the full project graph, then filters findings to files touched since the git ref so cross-file detectors keep correct context.--new-sincescans the current target and the base ref, then reports only current candidates whose stablefingerprintwas absent from the base scan.--onlyand--excludeaccept comma-separated detector IDs from the table below. They filter which detectors run, not how findings are judged; every emitted finding remains a review candidate.--fail-on-findingsexits non-zero when candidates are emitted, which is intended for CI gates; default scans remain report-only.--versionprints the installedstratapackage version.--helpalso starts with the same version line.
Project resolution:
stratareadstsconfig.jsonfrom the scan root for cross-file analysis.- Supported today: direct
compilerOptions.baseUrland exact or wildcardcompilerOptions.pathsmappings. - Aliases resolve only to scanned
.tsand.tsxfiles; package imports stay external. - First-version limit: no monorepo tsconfig selection, project references, or
extendschain evaluation.
Text output is the default because it is the local human/agent review interface:
strata .
strata . --format textExample with candidates present:
strata complexity candidates
Mode: full scan
Target: .
Found 2 review candidates.
These are candidate signals, not automated design verdicts. Review whether
each finding actually makes the system harder to understand or modify.
By detector:
passThroughMethod 2
Top files:
2 case.ts
passThroughMethod
Suspicious when a method only forwards same-order args to a collaborator; the layer may add API surface without hiding useful complexity.
case.ts:7
class method delegates to instance state with same args - layer without logic
evidence: 2/3 public methods in UserService are pass-through (67%)
case.ts:11
class method delegates to instance state with same args - layer without logic
evidence: 2/3 public methods in UserService are pass-through (67%)
Introduced-only text answers a different review question: which candidate identities did this change create?
strata . --new-since origin/main --format textstrata complexity candidates
Mode: introduced candidates
Target: .
Base ref: origin/main
Found 1 review candidate introduced since origin/main.
These are candidate signals, not automated design verdicts. Inherited
candidates are omitted by fingerprint; omitted does not mean approved.
By detector:
passThroughMethod 1
Top files:
1 src/new-service.ts
passThroughMethod
Suspicious when a method only forwards same-order args to a collaborator; the layer may add API surface without hiding useful complexity.
src/new-service.ts:3
class method delegates to instance state with same args - layer without logic
Example when no enabled detector matches the selected scope:
strata complexity candidates
Mode: introduced candidates
Target: .
Base ref: origin/main
No review candidates were emitted for this scan.
This is not a verdict that the design is clean. It only means no enabled
detector matched the selected scope.
Text output teaches the review model and stays compact: detector groups include a short explanation, findings include locations and human evidence, and empty sections are omitted. It intentionally omits fixed severity, raw metadata, and finding fingerprints. Use JSON or SARIF when another tool needs stable machine identity.
Scanner or detector failures are not zero-candidate scans. If strata cannot compute the requested scope or a detector crashes, it writes a failure report to stderr, exits non-zero, and does not emit text, JSON, or SARIF candidate output on stdout:
strata scan failed
Mode: introduced candidates
Target: .
Base ref: missing-ref
Reason: invalid git ref: missing-ref
No trustworthy candidate report was produced.
For --format json and --format sarif, stdout is reserved for completed scan results. Operational failures still use the stderr report above so tool consumers do not accidentally parse partial findings as trustworthy output.
Use JSON when another tool needs the stable structured result, including finding fingerprints:
strata . --format json{
"summary": {
"totalFindings": 1,
"byFlag": { "passThroughMethod": 1 },
"topFiles": [{ "file": "src/user-service.ts", "count": 1 }]
},
"findings": [
{
"flag": "passThroughMethod",
"severity": "candidate",
"fingerprint": "strata:v1:1c5zo4q",
"file": "src/user-service.ts",
"line": 8,
"message": "class method delegates to instance state with same args — layer without logic",
"metadata": {
"className": "UserService",
"methodName": "getUser",
"receiver": "this.repo",
"callee": "this.repo.getUser",
"passThroughMethodCount": 1,
"publicMethodCount": 2,
"passThroughRatio": 0.5,
"concentrated": false
}
}
]
}Findings are sorted by (flag, file, line) for deterministic review and diffing. Each finding has a versioned fingerprint so CI systems, agents, and future baselines can match the same candidate across harmless line shifts. Fingerprints are stable identifiers for review workflow state; they are not judgments and are not promised across file renames or detector semantic changes.
Recommended non-blocking pull request workflow:
name: strata
on:
pull_request:
permissions:
contents: read
jobs:
complexity-candidates:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: andrezzoid/strata@v0.1.1The action emits native GitHub warning annotations plus a job summary. It also prints the same scan report shape as strata --format text to the live console, followed by whether the GitHub job summary was written. It does not need write permissions because it uses workflow commands and GITHUB_STEP_SUMMARY, not PR comments.
Defaults:
pathdefaults to..- On pull requests, the action fetches the base branch and runs introduced-only scanning with
--new-since origin/<base>. - Outside pull request contexts, no base ref is available, so the action runs a normal scan.
- Findings do not fail the job unless
fail-on-findingsis enabled. - Scanner or detector failures print the
strata scan failedreport and exit non-zero; they do not look like zero-candidate scans or candidate gates.
Inputs:
| Input | Default | Description |
|---|---|---|
path |
. |
File or directory to scan. |
base-ref |
PR base | Git branch or ref for introduced-only comparison. |
only |
Comma-separated detector IDs to run. | |
exclude |
Comma-separated detector IDs to skip. | |
fail-on-findings |
false |
Set to true to fail after annotations and summary write. |
Blocking gate example:
- uses: andrezzoid/strata@v0.1.1
with:
only: passThroughMethod,duplicateSymbol
fail-on-findings: "true"SARIF upload and Reviewdog are optional advanced integrations. Prefer the action above first; use SARIF when your team already relies on GitHub code scanning, or feed JSON/SARIF into Reviewdog if you want richer check-run behavior from your existing Reviewdog setup.
Agents can retrieve the same GitHub check annotations through gh without scraping logs:
gh pr checks <pr> --json name,state,bucket,link
gh run view <run-id> --json jobs
gh api repos/<owner>/<repo>/check-runs/<check-run-id>/annotations \
--jq '.[] | select(.title | startswith("strata:")) | {path,start_line,title,message}'GitHub exposes these as check annotations, not commentable PR review comments. The annotation path is repository-relative even when the action scans a subdirectory with path: src.
Generate a SARIF log for GitHub code scanning:
strata . --format sarif > strata.sarifTouched-file SARIF keeps full-project graph analysis, then reports only findings that touch files changed since a git ref:
strata . --touched-since origin/main --format sarif > strata.sarifIntroduced-only SARIF answers a different PR-review question: which candidate identities did this change create?
strata . --new-since origin/main --format sarif > strata.sarifUse --new-since for CI annotations or gates that should focus on newly introduced review candidates rather than inherited design debt in files the PR happened to edit. "New" means a new finding fingerprint identity; if an existing same-fingerprint candidate merely worsens its metadata, such as a wide module gaining another export, it is not reported as introduced by this mode.
Focus CI annotations or gates on detector families your team is ready to review:
strata . --new-since origin/main --only passThroughMethod,duplicateSymbol --format sarif > strata.sarif
strata . --new-since origin/main --exclude wideSignature --fail-on-findingsExample GitHub Actions upload step:
- run: bunx @andrezzoid/strata . --format sarif > strata.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: strata.sarifSARIF is for annotations and review workflow integration. If you want a blocking quality gate, run strata . --fail-on-findings as a separate CI step so candidates fail the job deliberately rather than being confused with SARIF upload behavior.
SARIF partialFingerprints.primaryLocationLineHash uses the same value as JSON finding.fingerprint, based on detector-owned semantic anchors where available rather than raw file:line identity.
Each detector targets a design failure that AI-assisted workflows reliably introduce and that cyclomatic-complexity or style tools do not see.
| Flag | Scope | Signal |
|---|---|---|
wideSignature |
file | Exported function or public exported-class member has too many required parameters. |
passThroughMethod |
file | Public class method only forwards same-order args to a collaborator. |
passThroughExport |
file | Exported function only forwards same-order args to another callable. |
forcedRareOption |
project | Most callers pass the same literal, placeholder, or default-like option. |
duplicateSymbol |
project | Named declarations with identical structure are repeated. |
uniqueImplementation |
project | Interface or abstract class has no real polymorphism payoff. |
Notably absent: long-function detection, cyclomatic complexity scoring. Both are well-served by existing tools. Strata occupies the gap they leave — the design layer between "this function is complex" and "this module is not earning its abstraction."
bun install
bun run hooks:install
bun run format:check
bun run lint
bun run typecheck
bun run test
bun run test:coverage
bun run package:check
bun run scan:ci
bun run scan:sarif
bun run scan -- test/fixtures/pass-through-method --format textTests use Bun's test runner rather than the old shell harness. Fixture tests compare exact (flag, file, line) triples for each detector's primary fixture while allowing incidental cross-detector findings.
Lefthook installs the repo's Git hooks during local dependency installation; run bun run hooks:install if you need to repair or reinstall them manually. The pre-commit hook formats staged JS/TS, JSON, Markdown, and YAML files with Oxfmt, re-stages fixes, then lints staged JS/TS files with Oxlint. It intentionally skips full tests and coverage so commits stay fast; GitHub Actions still runs the complete gate before merge.
GitHub Actions runs the local gate scripts before merge. bun run test:coverage gates LCOV line coverage for src/ at 85%, which matches the current machine-readable aggregate rather than Bun's human table. bun run package:check runs npm pack --dry-run --json to validate package contents without publishing.
bun run scan:ci runs strata src --fail-on-findings. A failing self-scan means the source now contains review candidates that should be fixed or intentionally redesigned; it is still a candidate signal, not an automated final verdict. bun run scan:sarif emits the same source scan as SARIF for CI smoke tests or upload workflows. Publish automation is intentionally deferred until release credentials and side effects are handled in a separate change.
- Preserve the scanner's contract: candidates, not verdicts.
- New detectors should target design failures that metric-based tools miss; avoid duplicating what ESLint, SonarQube, or similar tools already cover.
- Prefer deeper modules over more modules. Split by owned knowledge, not by execution order.
- Add or update a fixture whenever detector behavior changes.
- Keep detector defaults conservative enough that findings focus the review instead of burying it.
- Update
CHANGELOG.mdfor release-worthy changes andBACKLOG.mdfor deferred ideas.