@nielspeter/eess
v0.8.0
Published
eess kernel — the dialect-independent rule engine (RuleBuilder, Predicate, Condition, baseline, formatters). Specifications you can run.
Maintainers
Readme
@nielspeter/eess
The dialect-independent kernel of the eess family.
This package is the engine every eess dialect runs on. It knows nothing about TypeScript, Mermaid, or any specific artifact format; it is generic over the element type being validated.
What's in here
RuleBuilder<T, P>— the fluent.that().should().check()chain, generic over element typeTand project handlePPredicate<T>,Condition<T>,Matcher<V>— the interfaces dialects implementArchViolation— the violation model; each dialect supplies its own element→violation adapter- Baseline mode, diff-aware filtering, exclusions, and the terminal/GitHub/JSON formatters
definePredicate/defineCondition— extension points for advanced users
Two entry points
@nielspeter/eess publishes two:
| Specifier | What it is |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| @nielspeter/eess | The public API. Everything below is documented here, and a change to it is a versioned change. |
| @nielspeter/eess/internal | Family plumbing the sibling dialects share — cache registries, stderr guards, suppression counters, identity hashing, glob-tree internals. Not API. |
You want the root. /internal exists so the dialects can share one engine without
each name they touch becoming a public commitment; it is published because npm has
no way to ship a package-private module, not because it is for you. Nothing there is
documented, and no dialect re-exports it as its own API. Two names — DiskSet
and OnDisk — are also on @nielspeter/eess-ts's root, where they are eess-ts's
own types that happen to share a name; that is the whole of the overlap.
Breaking something here is still a versioned change and still needs a changeset (ADR-011 clause 4). What you are not owed is migration guidance, because it is not API. See ADR-011.
If a symbol you used disappeared from the root, it moved here — the import path is the only change.
Kernel API reference
The fluent surface (RuleBuilder, Predicate, Condition, the combinators, the
formatters) is covered in the docs site. This
section covers the supporting types you will meet in signatures.
Reporting and severity
A preset never emits on its own — the caller owns emission (ADR-008).
ReportMode—'throw' | 'return' | 'warn'. What a finisher does with findings: raise anArchRuleError, hand them back, or print them as advisory and continue.ReportOptions— the emission controls:format(terminal/json/github) andreason, the rationale threaded into the output.RuleSeverity—'error' | 'warn' | 'off', the value of a per-rule override.PresetBaseOptions— what every preset accepts: anoverridesmap of rule id →RuleSeverity, plus the reporting controls. Extend it when you write a preset so{ report: 'return' }works without your own plumbing.RuleBuilderLike— the one-method shape (violations()) a preset needs from anything it dispatches, so a preset is not coupled to a dialect's builder class.Dispatchable— whatdispatchRuleaccepts: a builder that can produce violations and name itself, so the dispatcher can apply a per-rule override to it. Strictly more thanRuleBuilderLike; that one is the narrower shape used where only the findings matter.isArchRuleError(err)— use this instead ofinstanceof ArchRuleErrorwhen catching.instanceofcompares class identity, so it returnsfalsewhenever two copies of the package are on disk — a nestednode_modules, or a rule file loaded through a separate module registry. The check is structural (the stampednameplus theviolationsarray), so it survives that. The thrown shape itself —violations: ArchViolation[]— is public either way.ArchConfigError/isArchConfigError(err)— thrown when a rule is misconfigured (bad arguments to a condition, a malformed rule file), as opposed toArchRuleError, which means the architecture is wrong. Two different faults with two different audiences: one tells a rule author they mistyped something, the other tells a team their code drifted. Carriessubject— the condition, predicate or file at fault — so a caller can route the failure without parsing the message.isArchConfigErroris structural for the same two-copies-on-disk reason asisArchRuleError.ArchFix— a machine-applicable edit on a violation:file, a[start, end)character span, and thereplacementtext. This is what--fixapplies.
The JSON report
--format json emits an ArchJsonReport: a summary (total, errors, warnings, reason), then ArchJsonViolation entries, plus ArchJsonSuppression for findings silenced by an eess-exclude comment and ArchJsonUntestedAllowlist for allowlists nothing exercised. Import these to type a tool that consumes eess output rather than re-declaring the shape.
Filtering a run
BaselineFilter— decides whether an already-accepted finding is suppressed by the baseline file.DiffFilterLike— decides whether a finding falls inside the changed region, for--changedruns.
Both are interfaces so a host can supply its own; the kernel ships implementations.
Correspondence
correspondence() binds two Selections and checks they agree in both directions.
CorrespondenceOptions— its argument: theleftandrightselections, a join strategy, and optional per-sidesuggestcallbacks for the two-sided message.KeyBy— the fast join: a key extracted per element, giving an O(n+m) match. Prefer it. The fallbackmatchBypredicate is O(n×m).RelationSpec— whatpreserveRelations()takes: how to read edges from each side, so the check covers relationships and not just membership.ElementInfo— thename/file/linea dialect reports for one element, which is what the message and the violation's location are built from.
Declaring globs (for custom predicates)
A predicate written with definePredicate should declare the globs it matches on.
Without that it is opaque, so a vacuity diagnosis cannot tell "matched nothing
because the glob is dead" from "matched nothing because nothing was there" — and
any or() containing it becomes undiagnosable too.
globNode(leaf)— wrap a single declared glob into a tree. The common case.globAnyOf(globs, kind, base?)— declare several alternatives at once.DeclaredGlob— one glob with itskindandbase.GlobKindis what the glob matches against ('file-path' | 'parent-dir' | 'import-target' | 'specifier' | 'literal') andGlobBaseis what it is resolved against ('absolute' | 'tsconfig-relative' | 'normalized').GlobTree<L>— theand/anytree the declarations form;DeclaredGlobsis that tree overDeclaredGlob, andGlobLeaf<L>is one position in it.OpaqueGlob— the explicit "this branch cannot be described" marker. Honest, and it is what keeps an undeclared predicate from silently reading as a dead one.GlobSite— a declared glob plus where it was written;GlobPositionis that where ('selector' | 'discovery' | 'condition' | 'exclusion'), which is what lets a diagnosis point at the glob you actually got wrong.GlobNode— the tree overGlobSite, i.e. a fully-stamped declaration ready to diagnose.
Coverage evidence
EdgeCoverage— one rule's evidence: how manysubjectsit saw, how manyedgesit actually tested, and areasonwhen that number is zero.UntestedReason— why zero:'no-edges'(nothing to test, usually correct),'all-filtered'(edges existed and every one was filtered out),'none-matched'(edges existed and none matched the allowlist). The distinction is the point — a rule that tested nothing should say which of the three it was, per ADR-010.
Who uses it
You normally don't install this directly — you install a dialect:
@nielspeter/eess-ts— TypeScript@nielspeter/eess-mermaid— Mermaid class diagrams
Install @nielspeter/eess directly only when authoring a new dialect or a cross-dialect
tool on top of the kernel.
If that is you, /internal is your surface, not a wall. The two sentences above look
like they contradict each other — "not for you", then "install it if you are writing a
dialect" — so to be explicit: a dialect author is exactly who /internal exists for, and
the five in this repo import from it. What the root alone gives you is the rule-authoring
vocabulary, not the engine. Nothing there is documented and it changes without migration
notes, so treat it as a contract you re-check on each kernel bump; if you are building a
dialect you intend to publish, open an issue and we will work out what belongs on the root.
License
MIT
