Imported from kevinmettias/nomos (
.claude/skills/nomos-add-plugin/SKILL.md). Install upstream withnpx skills add kevinmettias/nomos --skill nomos-add-plugin. Copyright stays with the author.
Adding a language provider, package format, or rule
This is the mechanical recipe OD-HOST-004 and the two real precedents in this workspace
(nomos-lang-rust-scan as a second provider, nomos-package/OD-PACKAGE-007 as the
language-agnostic manifest split) already establish. It does not re-argue any of their
reasoning — read the cited record if you want the why. This skill exists so the four or
five mechanical steps do not have to be reverse-engineered from a git diff each time.
Follow nomos-task's claim/implement/verify/finish/commit loop for the ledger item this
work needs; this skill only covers what that item's territory and done_when should
contain.
1. A second provider of a capability that already exists
The real precedent is nomos-lang-rust-scan (crates/languages/nomos-lang-rust-scan), the
second offer against nomos.cap.syntax.items.
A provider crate exports exactly four things, by convention — there is no Provider
trait, so nothing enforces this beyond the composition root failing to compile without it:
pub const PROVIDER: &str— this provider's ownProviderIdstring, owned here and pulled by reference everywhere else it is needed (a package'sKNOWN_PROVIDERS, a composition root'sRegistry::Offer) rather than retyped.pub const fn Declared_Guarantee() -> Guarantee— the four-axis promise (FactVariant, soundnessAssurance, completenessAssurance,IncrementalGranularity) this provider's method actually supports. State it honestly on every axis your method is weaker on;Assurance::Unsound(established false) andAssurance::Unknown(nobody has established it) are not interchangeable —crates/languages/nomos-lang-rust-scan/src/guarantee.rs's own tests are the worked example of proving a guarantee is neither vacuously satisfying nor vacuously failing.pub fn Provider_Offer() -> ProviderOffer—{ provider: ProviderId::New(PROVIDER), capability: Capability(), version: CONTRACT_VERSION, guarantee: Declared_Guarantee() }, whereCapability()andCONTRACT_VERSIONcome from the capability contract crate (nomos-cap-syntaxfor syntax), never redeclared here — a capability contract is owned by the crate below every provider that offers against it, not by any one provider.pub fn Materialize(...) -> MaterializedFact(or whatever the capability's own fact shape is) — the actual work: turning a subject into a fact this provider's guarantee describes.
Band: the same band as the peer it contends with (25, for a second syntax provider) —
never a band that could let one provider name the other. tests/contract/tests/boundaries/ graph.rs's downward-only rule forbids a same-band edge, which is what stops a second
provider's answer from being derived from the first's.
Composition: Registry::Offer (or Declare_And_Offer for a genuinely new capability —
see §3) is one more hand-written line in crates/orchestration/nomos-check-orchestration/ src/composition.rs::Registered(). OD-HOST-004 already decided this needs no selection
mechanism at any count: the registry ranks, the caller states a Requirement, one more
Offer call is composition, not choice. Check Registered() as it stands before adding
your line — as of commit e669e7f, both nomos_lang_rust::Provider_Offer() and
nomos_lang_rust_scan::Provider_Offer() reach the production composition root, so a third
offer follows an already-proven pattern rather than an untested one. Confirm this yourself
against crates/orchestration/nomos-check-orchestration/src/composition.rs rather than
trusting this sentence — it goes stale the moment a new provider lands and nobody re-reads
it, which is exactly how it went stale once already.
Package registration: if this provider is meant to be selectable through a
LanguagePackage-shaped manifest, its KNOWN_PROVIDERS array (nomos-lang-rust-package's
today) needs your PROVIDER constant added — see §4 for whether that means editing an
existing package crate or writing a new one.
Reserve in the ledger item's territory: the new provider crate's directory,
Cargo.toml (workspace members and [workspace.dependencies]), README.md's band table,
tests/contract/tests/boundaries/bands.rs, the composition root file you edit, and
tests/contract/surface (a new snapshot for the crate — NOMOS_SURFACE_BLESS=<crate> cargo test -p nomos-contract-tests --test public_surface).
2. A capability contract that does not exist yet
If the fact your provider produces is not shaped like any existing capability
(nomos.cap.syntax.items, nomos.cap.module.index, nomos.cap.dependency.edges and
nomos.cap.controlflow.reachability are the four in this workspace today), the contract
itself needs a home first — its own crate, below every provider that
will offer against it, the way nomos-cap-syntax sits below nomos-lang-rust and
nomos-lang-rust-scan. Do not define a capability contract inside the first provider that
needs it: crates/languages/nomos-lang-rust-scan/src/guarantee.rs's module doc is the
worked cautionary tale of what happens when a contract starts out living with one party to
it (nomos-cap-syntax was
extracted from nomos-lang-rust for exactly this reason, once a second provider needed to
agree with it independently). Pull the contract out at the moment a second party is
expected to offer against it, not only once one actually has.
3. A second rule
Much smaller than a provider. crates/rules/nomos-rules holds every rule this workspace
ships. How many that is, and which, is RULE_COUNT and the ComposedRule table in
crates/orchestration/nomos-check-orchestration/src/run_context.rs — read it rather than
trusting a count written here, which goes stale the moment a rule lands.
A rule is a free function taking the sources it judges and, if it reads facts, a
&mut dyn FactReader, returning Vec<Finding>. A new rule is one more function of that
shape inside the same crate, not a new crate and not a new band. There is no Rule trait;
match the signature of the rule nearest what yours does.
Which module inside that crate it joins is decided — read OD-RULES-033 before you pick a
file. That record answers which module a new rule belongs to, and what to do when none of
the existing ones fits. It is deliberately not summarised here: a summary of a checked file is
an unchecked copy of it, which is what OD-AGENT-001 decided and why this skill routes rather
than restates. Note that the sentence above is about matching a signature; which file the
function lands in is a separate question and proximity is not the answer to it.
Wiring it in is two lists, not one. This is the step that most often lands half done, and the failure is silent in the crate you edited and loud in one you did not.
The run. crates/orchestration/nomos-check-orchestration/src/run_context.rs holds a
ComposedRule table of fixed length RULE_COUNT. Add your entry — its RuleId and a
closure calling your function — and raise RULE_COUNT by one. This decides what a run
actually judges.
The plan. crates/orchestration/nomos-gate-orchestration/src/composition.rs holds
OFFERINGS, one row per rule of (id, authority, authority version). Add your row there
too. This decides what nomos gate plan reports and what nomos gate explain can cite.
A rule ported from code-standards with no record behind it cites PORTED_STANDARD and
NO_VERSIONED_RECORD, the way most rows do; a rule backed by a governing record cites
that record and its version, the way COMPLETENESS_MIRROR and DEPENDENCY_DIRECTION do.
Two tests assert the lists agree — Test_Registered_Should_Offer_Every_Composed_Rule and
Test_Registered_Should_Compose_Every_Rule_A_Check_Run_Composes, both in
nomos-gate-orchestration. They read nomos_check_orchestration::Composed_Rules(), so
they are the authority on the pairing rather than a second hand-written copy of it. They
live in a crate a rules-and-check predicate never runs, which is exactly how a composed
rule with no OFFERINGS row left the workspace red at commit d12a60b0.
OD-HOST-004 decided the composition root stays hand-written, and one more entry beside
the existing ones is composition, not the accretion that record warns about.
This stops being true the moment your rule is meant to run only for some invocations
(a per-language rule, an opt-in, a subset) — at that point read OD-HOST-004 in full
before writing an if; it names the declared selection mechanism that case needs instead.
State your own floor. nomos_rules::Syntax_Requirement is Check_Completeness_Mirrors's
own stated Requirement against the capability it reads — not Registered()'s and not
Run()'s. Your rule states its own the same way; do not let the composition root decide
what your rule needs.
Reserve in the ledger item's territory: crates/rules/nomos-rules (your new function
and its own test module), crates/orchestration/nomos-check-orchestration/src/run_context.rs
(the ComposedRule entry), crates/orchestration/nomos-gate-orchestration/src/composition.rs
(the OFFERINGS row), and tests/contract/surface/nomos-rules.txt if the crate's public
surface grows a new export.
The item's verification predicate must run nomos-gate-orchestration. A predicate over
nomos-rules, nomos-check-orchestration and nomos-contract-tests alone passes while the
parity tests fail, so the item finishes green and the workspace test step is red for every
other session until somebody else notices.
4. A second language's package manifest
Depend on nomos-package (band 24) directly — never on nomos-lang-rust-package, which is the
Rust-specific wrapper, not a generic base a second language extends. OD-PACKAGE-007 is
why the split exists and what it does and does not provide: nomos_package::PackageManifest
carries language_versions: Vec<String> as raw, unresolved labels (there is no typed
version-domain type to reuse — invent your own, the way nomos-lang-rust-package::RustEdition
is Rust's own), and nomos_package::Parse_Manifest/Read_Manifest take a known_providers: &[&str] parameter rather than a hardcoded list. Your package crate supplies that list (its
own providers' PROVIDER constants, pulled by reference the same way
nomos-lang-rust-package::KNOWN_PROVIDERS pulls Rust's) and resolves each raw
language_versions label against whatever typed domain your language's own version scheme
actually needs, reusing nomos-lang-rust-package's reader.rs::Resolved_Editions as the shape
to follow rather than as code to call.
Band: above your language's own provider crates (so it can name them), the way
nomos-lang-rust-package sits at 26, above nomos-lang-rust/nomos-lang-rust-scan at 25. It
must never need to be below nomos-package (24) or above it in a way that would create a
same-band or upward edge.
Reserve in the ledger item's territory: the new package crate's directory, Cargo.toml,
README.md, tests/contract/tests/boundaries/bands.rs, and tests/contract/surface.
What this skill does not cover
OD-PACKAGE-006 is accepted, not open: nomos_package::KnownProviders
(crates/packages/nomos-package/src/known_providers.rs) is the generic
provider-registration base type the resolution decided, and
crates/packages/nomos-lang-rust-package/src/known_providers.rs is its first real consumer, building
KNOWN_PROVIDERS through KnownProviders::New(...).As_Slice() rather than as a bespoke
array of its own. §1's "Package registration" step and §4 both describe that shape as it
stands today.
What the resolution explicitly does not reach is OD-PACKAGE-006's own original
trigger: a third same-language (Rust) provider crate joining this workspace has still not
happened, and whether KnownProviders needs anything beyond "one more line, pulled by
reference" at that point is a question the record's own text says this resolution does not
answer. Read OD-PACKAGE-006 directly before assuming that question is settled — the
generic base type resolves how a package crate builds its allowlist, not what a third
same-language provider would demand of it.
