Linting
rakata-lint reads a GFF resource and tells you what the engine will do with it that you probably did not intend. Not “is this valid GFF” (that is the parser’s job) but “will this crash, get silently truncated, or sit there doing nothing”.
Every rule traces back to a Ghidra audit of swkotor.exe, written up per format under formats/gff/. If a rule cannot point at a page, it does not exist yet.
The two entry points
lint(&gff) takes a parsed GFF and nothing else. It picks a schema off the file type, walks the tree against it, and runs the intra-resource rules. No filesystem, no game install, no 2DAs. Everything it knows is in the bytes you handed it.
lint_with_context(&gff, &mut LintContext) does all of that and then some. The context is what lets a rule ask a question about the world outside the file.
Both collect everything rather than stopping at the first problem. You get a Vec<LintDiagnostic>, because a mod with eleven issues should take one run to find out, not eleven.
LintContext
A LintContext holds a borrow of a GameVfs and a 2DA cache. That is the whole type, and both halves earn their place.
The VFS is how a rule answers “does this resref resolve?” against the install the user actually has, overrides and all, the same way the engine would. The cache is how twelve rules across four hundred files avoid loading baseitems.2da twelve hundred times.
The important part is whose install it reads. The linter never checks against a vanilla baseline. A range check bounds against however many rows the loaded 2DA has, not against the row count that shipped in 2003, because the engine does not know the difference either and a mod that adds appearances is not thereby broken. When a table is missing or fails to parse, the rule emits a LINT-CTX-* diagnostic and skips, so you find out a rule did not run instead of reading its silence as a pass.
The three phases
The phases are not a schedule. They are a statement about what a rule needs in order to answer.
Phase 1, intra-resource. Everything answerable from the file alone. Forced defaults, sentinel handling, truncation behaviour, dead data, and the schema walk itself. GroundPile is always forced to 1 no matter what you wrote, so writing it is decorative and worth saying so. These rules work on the raw Gff tree rather than a typed view, deliberately: a typed view drops toolset-only and dead fields, and those are exactly what several of these rules exist to find.
Phase 2, range and reference. Everything that needs a table or the resource system. Is this BaseItem a real row? Does this script exist? Is Gender inside the range the engine tolerates? These take a typed view, because by the time you are checking a value against baseitems.2da the typed view has already done the dispatch and resolution work you would otherwise write twice.
Phase 3, cross-resource. Everything that needs other files to be in the room. Does this transition trigger name an area tag that exists? Does the creature’s equipment resolve to real items? Does the conversation tree hang together? Planned rather than built.
Phases 1 and 2 are complete for all fourteen generic types. Phase 3 is the interesting one and is where the mod validation tool starts.
Reading a diagnostic
Rule ids carry their origin in the prefix.
SCHEMA-* comes from the generic walk and means something structural: wrong type, missing required field, a label the schema does not declare, a value outside a declared range, or a field the engine never reads.
A format prefix like UTI-006 or UTW-001 is a hand-written behavioural rule, numbered within that format’s set. These are functions rather than data because the logic varies too much to declare.
LINT-CTX-* means the linter could not answer, not that the answer was fine.
Severity splits three ways and the split is about the engine, not about how annoyed you should be. Error is a crash or corrupted state. Warning is the engine silently truncating, clamping, or ignoring you. Info is dead data and forced defaults, which is to say the field does nothing and you may as well know.
For the crate’s internal layout and the full rule inventory, see crates/rakata-lint/ARCHITECTURE.md in the repository.