Skip to content

Signal reference

Thiago Botelho edited this page Aug 15, 2026 · 1 revision

Signal reference

A signal is a boolean observation about a repository. Version 0.1 implements 21 of them, and a rule may only reference a signal from this list. validate rejects a profile that names anything else.

Everything here is implemented in src/core/project.mjs. If a signal surprises you, read the pattern. It is deliberately simple enough to argue with.

How detection works

Two mechanisms, and nothing else.

Path patterns test the list of relative file paths. Paths are normalized to forward slashes before matching, so the same repository gives the same answer on Linux and on Windows.

Text patterns test the contents of files that were read. Only files with a known text extension are read: .cs, .csproj, .json, .js, .jsx, .md, .mjs, .props, .sln, .toml, .ts, .tsx, .xml, .yaml, .yml. Some patterns are further restricted to .cs files only, which is noted per signal below.

A third input is derived rather than matched: package dependencies, the union of dependencies, devDependencies, and peerDependencies across every package.json in the repository. A package.json that fails to parse is treated as absent, never as something to execute.

Detection never runs a build, never invokes a package manager, and never imports a project file. See the security model.

The 21 signals

Stack and architecture

Signal Type Detected when
modularDotnet composite A .sln or .csproj exists and domainLayer, applicationLayer, and infrastructureLayer are all true
domainLayer path Any path contains a Domain/ directory (case-insensitive)
applicationLayer path Any path contains an Application/ directory (case-insensitive)
infrastructureLayer path Any path contains an Infrastructure/ directory (case-insensitive)
endpointLayer path Any path contains a Controllers/ or Endpoints/ directory (case-insensitive)

modularDotnet is the only composite signal. It exists because "this is a modular .NET solution" is the claim a rule wants to make, and spreading that across four rules would let a repository pass three of them while being nothing of the sort.

Forbidden stacks

Signal Type Detected when
forbiddenNestAbsent dependencies No dependency name starts with @nestjs/
forbiddenNextAbsent dependencies + path No dependency named next, and no file matching next.config.{js,cjs,mjs,ts,cts,mts}

Both are inverted: true means the forbidden thing is absent. That is why the rules that use them expect true like every other rule, instead of the catalog needing a mix of expected: true and expected: false that readers get wrong.

An empty repository satisfies both, which is correct. Nothing forbidden is present.

API

Signal Type Detected when
apiVersioning text, .cs only A quoted string containing /api/v1 followed by / or the closing quote
openApi text, .cs only AddOpenApi, MapOpenApi, AddSwaggerGen, or UseSwagger
problemDetails text, .cs only AddProblemDetails or ProblemDetails

apiVersioning requires the quotes. A route built by string concatenation, or declared through a versioning library that never writes the literal, will not be detected. Attribute routes such as [Route("api/v1/orders")] also miss, because the pattern expects a leading slash inside the quotes.

problemDetails matches the bare type name, so any file that mentions the ProblemDetails type satisfies it, including one that only reads a problem response. It is a weak signal and it is marked required rather than blocking for that reason.

Security

Signal Type Detected when
jwtBearer text, .cs only AddJwtBearer or JwtBearerDefaults
authorizationPolicies text, .cs only AddPolicy or IAuthorizationHandler
defaultDeny text, .cs only FallbackPolicy

defaultDeny looks for the word, not for its value. A file containing options.FallbackPolicy = null; satisfies the signal while doing the opposite of what the rule wants. Per-rule evidence in 0.2 is aimed squarely at this class of problem. Until then, treat a passing security.default-deny as "somebody thought about it", not as proof.

Database

Signal Type Detected when
postgresEfCore text, any file Npgsql.EntityFrameworkCore.PostgreSQL or UseNpgsql
migrations path Any path contains a Migrations/ directory

migrations is the one path pattern that is case-sensitive. A directory named migrations/ in lowercase will not be detected. That matches the EF Core default of Migrations/, and it is the single most common false negative people hit. If your team renamed the directory, this rule will fail on an otherwise correct repository.

postgresEfCore reads every text file rather than only .cs, so a PackageReference in a .csproj is enough.

Frontend

Signal Type Detected when
reactWeb dependencies A dependency named react exists and no dependency named next

The next exclusion is why a Next.js application fails both frontend.react and stack.no-nextjs rather than only the second. That is intentional redundancy: the profile treats Next.js as a stack decision, not as a detail.

Testing

Signal Type Detected when
unitTests path A UnitTest/ or UnitTests/ directory (case-insensitive)
integrationTests path An IntegrationTest/ or IntegrationTests/ directory (case-insensitive)
architectureTests path An ArchitectureTest(s)/ or ArchTest(s)/ directory (case-insensitive)

These are naming-convention signals, and they are the ones most likely to disagree with a healthy repository. A project with excellent tests in tests/Orders.Tests/ scores zero on all three. The profile page documents the expected layout, and 0.2's exception mechanism exists for teams that will not rename.

Operations

Signal Type Detected when
healthChecks text, .cs only AddHealthChecks or MapHealthChecks
structuredLogging text, any file Serilog or ILogger<

structuredLogging is satisfied by ILogger<T> injection, which nearly every ASP.NET Core application has by default. It detects that logging exists, not that it is structured or that correlation is propagated. Read it as a floor.

Reading your own results

inspect prints every signal with ✓ for found and · for not found:

mestre-yoda inspect --root ~/code/my-api

When a signal is unexpectedly false, work through this in order.

Check inventory.files in the JSON output. If it is far below the real file count, something is being skipped. The skipped directories are .git, .idea, .next, .vscode, bin, coverage, dist, node_modules, and obj. Note that bin and obj are skipped for .NET build output, so source that lives under a directory named bin is invisible.

Check inventory.skippedSymlinks. A symlinked source tree inspects as empty.

Check inventory.inspectedTextFiles against files. A large gap means files were skipped for size. Any single file above 512 KiB is skipped, and once the corpus reaches 8 MiB no further files are read.

Then check the pattern for that specific signal above. Most surprises are a naming convention the detector does not know about, and in 0.1 the honest answer is that the detector is narrow by design.

Adding a signal

A new signal is a code change, not a catalog edit:

  1. Implement the detection in signalMap in src/core/project.mjs.
  2. Add its name to SUPPORTED_SIGNALS in src/core/catalog.mjs, keeping the list alphabetical.
  3. Extend the fixture in tests/evaluator.test.mjs so the signal is exercised both ways.
  4. Only then reference it from a rule, which is a profile version bump.

validate fails if a rule names a signal that is not in SUPPORTED_SIGNALS, so the wrong order fails fast rather than silently evaluating nothing.

Clone this wiki locally