The shot command

Check and build your project from the terminal, and find your way around its code, from a map of every system down to each real use of a member.

Everything goes through one command, shot. Its answers come from the same parser the compiler uses, so they are exact and carry the .shot file and line.

Running it

shot <command> --project <project folder>

Without --project it uses the SHOT_PROJECT environment variable, else the current folder.

The workflow for a change

 shot brief "<task>"with a task in hand, start here: one page about that task (COMMANDS below)
0. shot overview > overview.txtwhen you start on a project: the whole game in one read (map, Rules and Extend notes, wiring, what no code calls, then the API), about a sixth of the source. Read the file, not the output: it is longer than a terminal shows.
1. shot map [folder]what exists: one line per system (every file for a small project or one folder)
2. shot api [system]every public signature, one line each (the whole API of a system in one read)
 shot wiringhow systems talk: EventBus topics, remotes, saved data, dependencies
3. shot context Name.Memberwhat you will change: its source, the types it uses, what it calls, its callers
 shot refs Name.Memberevery real use of a member (type-checked), e.g. before renaming it
 shot trace Name.Memberhow it connects across systems: what happens after it, --up what leads to it, shot trace A B the route from A to B
 shot needs Name.Memberthe state it reads and whether the game ever runs the code that writes it
4. edit the .shot files(or shot write / shot edit / shot rename, which check right after)
5. shot checkuntil it prints 0 errors (--only <your folder> while others work elsewhere)
6. shot run <scenario>play your project's scenarios on the change, and add one for the new behaviour (Scenarios/<Name>.luau; see Scenarios)

outline (docs, with each system's Rules and Extend sections) and show (exact source of anything) fill in the details; find and search are fast name and text matches.

Read the project through these instead of opening files one by one: every answer comes from the parser and carries file:line.

Commands

shot brief "<task>"

One page about a task you are about to start: at most 12,000 characters, whatever the project's size. Give it the whole task text, in quotes, on stdin or in the file --input names. It ranks every member, field, type, every local function and script against the task: the task's words (a word few members use counts more), the names it writes as code (in backticks, A.B or CamelCase), and the ids and quoted text it mentions, matched to the code's string literals. Then each ranked member passes part of its score one step along trace's call graph, both ways, and through the events, remotes and signals it touches. The page has, in this order:

  • SYSTEMS: the systems it touches; the two top ones, and those holding one of the ten best members, with their Rules and Extend notes in full, the others with only the names of their sections. The two top systems and the folders of the task's strongest hits (members named in it as code, or holding an id or text it quotes) are what the task is about: the code near the task is theirs, and one step from it along the graph. A system after the top two is shown only when it holds such code;
  • NAMED IN THE TASK: each name written as code, where it is declared, its callers (a member) or the members that use it (a class or type), with file:line, and "new" for a name nothing declares yet. A class or module also lists its public API as you call it (wallet:Add(...), Bank.Give(...)), up to 8 members with file:line, the ones the task ranks first, then how many more and the outline that shows them; a member named alone brings the other members of its class or module that the task ranks and the page doesn't list anyway;
  • IDS AND TEXT: the string literals that match, and where the code uses them, uses near the task first (a single word no code near the task uses is left out);
  • EVENTS AND REMOTES: those of the code near the task: who sends each one, with what, and who hears it, with which parameters;
  • NOT HOOKED UP: public functions near the task that nothing calls, with what the game goes without, and state that code the game runs reads while only code it never reaches writes it;
  • MEMBERS: about 30 to read first, with file:line and the doc's first sentence, those near the task first;
  • NEXT: the commands to run next.

When the page is full, what is least needed is cut first, and each section says how much it cut; the two top systems' notes go last, after every list is down to its minimum. Same-named copies (one name and signature in several folders) are one line with "(+N copies elsewhere)", and parallel members of other owners (the same name, and a signature and body of the same shape) one line with "(+N alike: Owner, ...)". It reads the parsed project without a type check: about 1 s on a 17,000-line project. "Nothing calls it" here means no edge in trace's graph; the overview's NEVER CALLED is the type-checked list.

On the Wallet project (Overview), with a Rules section in Wallet.shot's rootdoc, shot brief with the task Players get a refund: add Bank.Refund(player, amount), which takes coins back with a new Reason, "Refund". prints:

BRIEF for: Players get a refund: add Bank.Refund(player, amount), which takes coins back with a new Reason, "Refund".
Ranked by the task's words, code names, ids and quoted text, then one step along calls, events and remotes.
One page at any project size: long lists are cut to the best few, never paged. NEXT at the end zooms in.

SYSTEMS it touches, most first, with their Rules and Extend notes (what breaks, how to add)
Wallet/  The coins one player owns.
  Rules:
    - Coins change only through `Wallet.Add`, which remembers why in the history.
Also near: Init/

NAMED IN THE TASK: each name it writes as code, where it is declared and what uses it
  Bank  module (server)  Wallet/Bank.shot:7
    used in 1: Init/Server.shot (server script) Init/Server.shot:1
    its API, 3 public members, most relevant first:
      Bank.Give(player : Player, amount : number)  Wallet/Bank.shot:16
      Bank.Start()  Wallet/Bank.shot:10
      Bank.Wallets : {[Player] : Wallet}  Wallet/Bank.shot:8
  Bank.Refund  new: Bank has no Refund yet
  public type Reason = "Pickup" | "Shop"  Wallet/Types.shot:2
    used in 2: Wallet.Add Wallet/Wallet.shot:23, Wallet._history Wallet/Wallet.shot:13

NOT HOOKED UP near it (public functions nothing calls in the call graph, and state only they write)
  Also nothing calls: Bank.Give

MEMBERS to read first, most relevant first (signature, file:line, the doc's first sentence)
  public type Reason = "Pickup" | "Shop"  Wallet/Types.shot:2  -- Why a player's coins changed
  Bank.Give(player : Player, amount : number)  Wallet/Bank.shot:16  [nothing calls it]
  Wallet.Add(amount : number, reason : Reason)  Wallet/Wallet.shot:23  -- Adds coins and remembers why
  Bank.Start()  Wallet/Bank.shot:10
  Bank.Wallets : {[Player] : Wallet} = {}  Wallet/Bank.shot:8
  Wallet.Coins : number  Wallet/Wallet.shot:12  -- Coins the player has now
  Wallet._history : {Reason}  Wallet/Wallet.shot:13
  Wallet.new(coins : number)  Wallet/Wallet.shot:15
  Wallet.Empty() -> Wallet  Wallet/Wallet.shot:28

NEXT: the commands to run
  shot outline Wallet      the API of Wallet with docs and line numbers
  shot context Bank.Give   its source, types, calls and callers
  shot context Wallet.Add  its source, types, calls and callers
  shot check               after each change, until it prints 0 errors
  shot run                 lists the scenarios; your own: shot docs Scenarios
shot overview

The whole project in one read, for planning with all of it in mind. First the game at a glance: the compact map, every system's Rules and Extend notes, shot wiring, and NEVER CALLED: every public function, method and constructor that no code calls. That list is what is declared but not hooked up; a feature that relies on one must add the call. Its hookups come first: required functions, functions named Bind, Register, Start, Init, Load, Connect, ... and Set... functions that are the only code to set a value their file reads. Each says what the game goes without, only from what is written: its doc, the line of its system's Rules or Extend that names it, and "the only code that puts entries in wallets, which Bank.Give reads" (a Bank that keeps its wallets in a local, with nothing calling Bank.Start: no player ever gets a wallet). The rest follows by system, names only. It is found by name and then by a type check (a call through a value typed any can't be seen), so the overview takes about 15 s on a 17,000-line project. Then the long part, shot api: every public signature. The whole overview is about 88,000 characters for a 17,000-line project, against 540,000 characters of source.

shot map [folder] [--all]

A project of more than 40 files: one line per folder (its entry point, how many files, the names of its rootdoc's sections like [Rules, Extend], and the first line of its rootdoc). A smaller project, a folder (shot map Utils, which includes its subfolders) or --all: every folder, its entry point, then each file's kind (class, server module, client func, types, ...), name and rootdoc line; .instance files with their class.

shot api [folder | file | Name] [--all]

Signatures only, public only, one line each, grouped by folder: public types, then each class (fields and class properties on its header line, then .constructors, .statics and :methods), module (properties, .functions), func, split module and script. The fastest way to learn what a system offers. --all adds internal members and types. Without a target: the whole project.

shot wiring

How systems connect, one page:

  • EVENTBUS TOPICS: each EventBus topic with every publish (and its arguments) and every listener (and its parameters). Topics nobody publishes or nobody listens to are marked. Topics written as a string literal in EventBus.Publish / Subscribe / Once, typed calls of declared events (CoinsGiven.Publish) and topics from data are seen. A declared event shows its owner, payload and declaration on its topic line (see Events).
  • REMOTES: each .instance remote with who fires it (FireServer, FireClient, ...) and who handles it (OnServerEvent, OnServerInvoke, ...). Unused or unhandled remotes are marked.
  • SAVED DATA SECTIONS: every Sections.Register("Name", ...).
  • SYSTEM DEPENDENCIES: which systems require or import which.
shot context <Name.Member | Folder/Name.Member>

Everything needed to change one member in one read: its source with its doc, the types in its signature (type definitions, and the api of classes it uses), the calls in its body with where each is declared, and its callers (type-checked, as refs). About 9 s on a 17,000-line project. The source ends with the member's CONNECTS, as in shot show. A call's receiver is worked out like the type checker would: self and module, locals from use and require, typed parameters and locals (wallet : Wallet), typed fields (self.Changed:Fire), and split sides (Market.Server.Start). Calls to the file's local functions are listed too. Only a receiver it can't work out falls back to a match by name, marked "by name only".

shot refs <Name.Member | Folder/Name.Member>

Every use of exactly that member: the compiler hides the member for one type check and reports every place that then fails, so only uses on that class or module are listed, never same-named members of other classes. When the member returns its own class, the rest of a chain is followed too (Receipt.new(...):Add(...):Add(...) lists every :Add). Uses through values typed any can't be seen. About 9 s on a 17,000-line project (two type checks).

When two systems declare the same name, refs and context ask for the folder:

Settings is declared 2 times (Market/Settings.shot, Wallet/Settings.shot): put its folder in front, e.g. Market/Settings
shot trace <start> [<goal>] [--up] [--depth N]

How things connect across systems, in one read. A start is Name.Member (a method, function, constructor, signal field or interface member), a func, a script file (Init/Server), event:Topic or remote:Name.

  • shot trace Bank.Give: what happens after it, as a tree (--depth N levels, 4 by default): the calls it makes, the events it publishes and who hears them, the remotes it fires and who handles them, the signals it fires and who is connected, and the overrides and implementations a call can reach. Each row is the step and the name, then its file:line two spaces after it. A node shown before says (see above); one cut by the depth says how many steps below it are not shown: (+3 more: trace it). Past 160 rows, a last line says exactly how many rows were left out.
  • shot trace Wallet.Add --up: what leads to it (who calls it, what it hears, ...). "nothing leads to it" means no code calls, publishes, fires or connects to it: it isn't hooked up.
  • shot trace Wallet/Bank.Start Wallet.new: the shortest routes from the first to the second, one step per line, each with its file:line. A name declared in several folders gets the list to pick from, as it does alone.

A Key line under it explains, once, only the marks its rows use. For the Wallet project (Overview):

shot trace Bank.Start
what happens after Bank.Start (4 levels; --up: what leads to it; shot trace A B: the route from A to B):
Bank.Start  Wallet/Bank.shot:10
  sets up Bank.Start on Players.PlayerAdded  Wallet/Bank.shot:11
    calls Wallet.Empty  Wallet/Wallet.shot:24
      calls Wallet.new  Wallet/Wallet.shot:11

Key: sets up: a callback, which runs later, not a call.

A signal it fires ("fires signal Shop.Sold") or an event it publishes leads to who listens. Going down, the signal and event libraries are not gone into: the class a signal's :Fire and :Connect belong to (any class that has both) and EventBus. The call that only delivers a fire or a publish is left out, any other call into them is a row of its own without its insides, and the Key names them; trace a library member to see inside it. --up goes through them: Signal.Connect --up lists who calls EventBus.Subscribe.

Inline listeners are nodes of their own (MarketServer.Start on BuyRequest), as are a file's local functions. A remote is one node per direction: "remote BuyRequest to server" (FireServer, InvokeServer: the server's handlers) and "remote BuyRequest to client" (FireClient, FireAllClients: the clients' handlers). A remote is its .instance file: when two folders have one of that name, each is its own, named with its folder ("remote Market/BuyRequest to server"), and code means the one in its own folder (script.Parent.BuyRequest); shot trace remote:BuyRequest then says "remote BuyRequest is in 2 folders; give the one you mean: remote:Market/BuyRequest, remote:Wallet/BuyRequest".

Calls are placed by the receiver's type the way context does, including locals typed by what they are assigned (a constructor, a method's return, a field of a record type), loop variables over typed tables, remotes held in locals and Parent.new in a subclass constructor. A class's parent and its overrides are the ones the compiler uses (a use of that name, else the class in the same folder), also when several folders declare a class of that name. A call it can't place is matched by name only when one member (or one override family) fits it by name, side (client or server) and : (a method) or . (a function), and marked "(by name only)". When several fit, the call is printed with them, each with its folder, those in the caller's folder first, three and then how many more ("may call Market/Shop.Give or Wallet/Bank.Give (thing.Give, it is typed any)", and --up "maybe called by"); a call of a function stored in data or looked up in a table is printed as "can't follow ...".

A callback runs later, so it is "sets up", not "calls": sets up Bank: local onJoin (runs when Players.PlayerAdded fires), or an inline callback as a node of its own (Bank.Start on Players.PlayerAdded). A listener whose topic is a loop over data (for _, topic in Prizes.Topics() do EventBus.Subscribe(topic, ...)) hears every topic that data lists. When there is no route, trace lists the steps after the start it could not follow, and a route through them when there is one. It reads the parsed project, no type check, so it takes about a second on a 17,000-line project.

shot needs <Name.Member | Name>

What a member needs to work: the state it reads (the file-level locals of its file, module properties, class fields), every piece of code that writes each one, and whether the game ever runs that code, with the way the game gets there (from a script, a remote or a Roblox event) or why nothing does. A module or class name alone takes all its members; a name as trace prints it works too. On the Wallet project (Overview) with Bank.Start() taken out of Init/Server.shot, shot needs Bank.Give prints:

NEEDS Bank.Give  Wallet/Bank.shot:16
never runs: nothing calls it
The state it reads (file locals, module properties, class fields), each writer, and whether the game reaches
that writer (from scripts, remotes and Roblox events), with the way it does or why it doesn't:
  Bank.Wallets  Wallet/Bank.shot:8  starts {}; read at :17
    Bank.Start on Players.PlayerAdded  :12  fills it; NOT LIVE: only reached from Bank.Start, which nothing calls
    => nothing the game runs writes it, so every reader sees it empty
Never written by code the game runs: Bank.Wallets.

With Bank.Start() back in Init/Server.shot the writer's line ends "live: Init/Server > Bank.Start > Bank.Start on Players.PlayerAdded" and the two lines after it go. "sets it" is a new value, "fills it" an entry or a field (Bank.Wallets[player] = ..., table.insert), "clears it" nil, the starting value or an empty table (not a write), "may write it" hands the state on (to a call, a table, a field or a return), where it can be filled out of sight. Values nothing writes after they are set (constants, services) are named on one line. It reads the parsed project with trace's call graph, no type check: about a second on a 17,000-line project.

shot matches <Union>

Every match on a union of string values (Match), with the values each one leaves out: MISSING "Prize", or "else covers" them. A match counts when all its cases are values of the union.

shot implements <Interface>

The interface's members and every class that implements it, directly or through a class it extends (Interfaces).

shot ids [Kind]

The id kinds (id ProductId in a Types.shot; see Id kinds) with how many ids each has and how many of its literals elsewhere are no id. With a kind: its slots, every id with the places the owning system defines it and how often other systems write it, then every use outside the owner with file:line and slot, and NOT AN ID (nearest: "...") on a literal that is none.

shot savelock [--out file]

Writes Saved.lock at the project root (or --out): per saved section and Version, the keys its Default's record type saves, older versions kept (Saved data lock). shot check compares the code with it once it exists. Run it when a version ships.

shot outline <folder | file | Name> [--all] [--full]

A file's or folder's API as you call it, one line per member: its signature, its doc's first sentence after "--" (left out when it only repeats the signature) and its .shot line, as for the Wallet project (Overview):

wallet.Coins : number  -- Coins the player has now  :8

Signatures read the way you call them:

Shape.new(name : string) -> Shapeconstructors and static functions: Class.Name()
shape:Damage(amount : number)methods: object:Name()
shape.Name : stringfields
icon.Speed : number = 10class properties, with their default
Bank.Open(player : Player) -> booleanmodule functions and properties

Under each file's header come its rootdoc's first sentence and its Rules and Extend sections in full; other sections are folded to one line. --full prints every doc whole, above its member. Internal members are hidden and counted ("-- 2 internal members not shown (--all)"); --all shows them. For a Types file: its imports and public types (internal ones with --all).

shot show <Name.Member | Folder/Name.Member | Name | folder | file | file:from-to>

The exact source with line numbers: one member with its doc (Shape.Damage, or Shapes/Shape.Damage as api and outline print it), whole files (Shape, Shape.shot, Shapes/Shape.shot), or a range of lines (Bank.shot:10-20; Bank.shot:10 gives 30 lines).

One member, or a range of lines that is one whole member (give or take 5 lines either side), ends with CONNECTS: at most 8 lines on how it meets the rest of the game, each a fact with file:line (":N" is a line of the member's own file). The events, remotes and signals it sends, each call's arguments next to every handler's parameters, e.g. "publishes CoinsGiven(player, amount) :40 -> Prizes/Prizes.shot:12 function(player, amount, reason) [2 values, 3 parameters]" (the note only when a parameter gets no value); the ones it hears, with who sends them and what; the state it reads and writes, with its other writers and readers; a route from a script, a remote or module code that reaches it, or that the search found none (it can't follow values typed any or callbacks kept in tables, so that doesn't tell whether it runs); and the Rules bullets that name it. A long list ends "+N more" (shot trace has the rest). Whole files, other line ranges and folders print no CONNECTS.

shot find <name | Name.Member> [--limit N]

"declared:" every class, module, func, type or member with that name. "used (N lines):" every line where the name appears in code (not in docs, strings or comments), labelled with the member it sits in. It matches by name only, not by type:

  • find Give lists every Give in the project.
  • find Bank.Give narrows the declarations to Bank, but its uses are still every .Give and :Give on any object.

To rename Bank.Give, use shot refs Bank.Give: it lists only the real uses. A name no code uses but some line holds as text (an EventBus topic like "CoinsGiven" lives in strings) says how many lines contain it and points at shot search and shot wiring. Shows 40 uses unless --limit says otherwise.

shot search <text> [--limit N]

Every line containing the text (case-insensitive, docs and comments included), with the member it is in. -i (grep's ignore-case) is accepted and changes nothing, since case never matters here.

shot docs [name | search <text>]

These docs: the list, one doc, or every doc line containing the text. The list opens with how many uses lines, interfaces, matches, required functions and scenarios this project has, each with its doc.

shot fix [folder | file]

Rewrites every bookmark line (─┨ Name ┠───┨) of the project, or of one folder or file, to the exact 108-wide form, keeping its indent; the -- form, any width and look-alike characters are all accepted. Write ─┨ Name ┠ and run this instead of counting the dashes yourself.

shot bookmark <Name>

Prints one finished bookmark line.

shot check [--only <folder>] [--fast] [--json]

--only takes a folder relative to the project (Wallet, Utils/Cleanup; a path that starts with the project's own folder works too). One that matches nothing is an error, never a quiet "0 errors". Compiles the project into a temporary folder and reports, at the .shot file, line and column:

  • parse errors (the ShotScript layer: declarations, members, docs, the types line, imports),
  • project errors (types files and imports that don't resolve),
  • ShotScript's rules (names, sides, internal, init(), overrides, split, diffsource, events, ids, owned fields (Owned fields), start order (Start order), saved sections against Saved.lock (Saved data lock), declared rules (Invariants)),
  • Luau type errors, from luau-lsp with Roblox's definitions (Studio's type checker).

See Rules for every message. It ends with "0 errors (5 files checked, 3.3 s)" or "N errors in M files (...)"; a 17,000-line project takes about 5 to 9 s. Above that line it prints notes, which are not errors: bookmark lines to fix, and each declared event nothing publishes or nothing hears, or whose published by lists a system that never publishes it (Events, UNUSED EVENTS and PUBLISHED BY). Type errors of a file that has parse errors are left out, since its Luau is broken anyway: fix the parse errors first.

--only <folder>only errors in files under that folder (the whole project is still checked)
--fastskip the Luau type check (about 1 s instead of 5 to 9 s on a 17,000-line project)
--jsonfor editors
shot write <file> / shot edit <file> / shot move <file> <new path> / shot delete <file>

Change the project from the command line or a script, without opening files. Paths are from the project root (Wallet/Bag.shot); anything outside the project, or not a .shot, .luau (a plain Luau module, see Files) or .instance file or a scenario (Scenarios/<Name>.luau, see Scenarios), is refused.

write takes the whole file on stdin (or in the file --input names) and turns leading spaces into the file's (else the project's) tab style, never inside long strings or comments, and says so. edit takes JSON {"old": ..., "new": ...} the same way and replaces old, which must appear exactly once. An exact match wins; else old is matched with each line's indentation ignored, only when that match is unique, and new is re-indented to the file's style (the reply says so). Line ends follow the file (a CRLF file stays CRLF).

delete keeps a copy in the temp folder (shot-trash). Each one then runs the whole check and prints its errors; --no-check skips that while making several changes in a row. A scenario is only parsed, since it is not part of the game.

Several edits in one go: shot edit with {"edits": [{"path": "Wallet/Bag.shot", "old": ..., "new": ...}, ...]} (path may be left out after shot edit <file>). They apply in order, so a later edit sees the earlier ones, in any number of files; if one doesn't apply, nothing is written and the message says which ("edit 2 of 3: old is not in ...; nothing was changed"). Then one check.

RIPPLE: with --baseline-dir <folder> (the same folder on every call; the project as it was at the first change is the baseline), a write, edit, rename, move or delete that changed a contract other code relies on prints a RIPPLE block before the check, one line per change with what depends on it: a public function's or method's signature (callers, scenarios), a member removed (what still uses it, Rules naming it), an event's arguments or a listener's parameters (the other side), a remote's payload or class (handlers or callers), a saved data section's fields and Version, a system's uses line. Each line ends with how it stands against the baseline. A change to bodies only prints nothing, and a change printed once is not repeated by later writes. New files, fields and types other than a saved section's are not followed.

shot rename <Name.Member | Folder/Name.Member> <NewName>

Renames a method, field, constructor, module function or property, and every use of it, then checks. The uses are found the way refs finds them: the member is hidden for a type check and each place that then fails is renamed, again and again until a check finds nothing new (so values returned by a renamed call, and chains, are followed). Other things with the same name are never touched. A method that subclasses override is renamed together with every override, from whichever class you name. Backticked references in docs follow (Bank.Give, and a bare Give in Bank's own docs). Refused, with nothing changed: a name the class (or a class above or below it) already has, a member that doesn't exist, or a project with parse errors. Errors that were there before stay as they were. Uses through values typed any can't be seen; shot find <old name> lists what is still called that by name. About 30 s on a 17,000-line project.

shot run [scenario | A,B | all | affected] [--log] [--new-baseline]

Builds the project into a temporary folder and plays scenarios in a mock Roblox (see Scenarios): real game flows, logged line by line, with each error's traceback. Frames in game code point at .shot lines (Wallet/Wallet.shot:20); frames in a scenario at Scenarios/<Name>.luau. Without a name it lists the scenarios with their summaries. A project's scenarios live in its Scenarios folder, which is never part of the game, and shot show Scenarios/<Name>.luau prints any of them with line numbers. Rules a Types.shot declares with invariant are checked after every frame; a broken one fails the scenario with its first frame and values (Invariants).

Pass the same --baseline-dir <folder> to every run to see only what your change did. The first run there is the baseline; later runs say SAME, CHANGED or NEW for each scenario in the summary and print only the changed lines, as hunks with no context: a line like @@ -118 +118 @@ (line numbers in the old and the new log), then -old and +new lines. A scenario with no baseline yet (one you wrote, or one whose file you changed) prints its whole log and becomes its baseline. A failed scenario always shows its errors, also when its log is SAME. --log prints the whole logs, as without a baseline; --new-baseline makes this run the baseline (with all, for the code too). shot run affected plays only the scenarios that ran code changed since the baseline (a function they ran, a file they loaded) and lists the changed members no scenario runs. Without --baseline-dir every run prints the whole logs.

shot build <output folder>

Writes the compiled Luau in Rojo layout (see Compiled).

shot graph <out.html> [--title T]

For people: one self-contained page that maps the project. Systems are boxes in layers (a system sits above what it uses), with solid "uses" arrows and dashed event arrows labelled with the topic; utils are a toolbox group at the bottom. Hovering a system shows its rules, Extend notes and connections; clicking it opens its files with their own arrows (extends, implements) and the other systems they reach into. Problems are flagged: an event listened for that nothing sends (red), one sent that nobody hears (amber), a link a system's uses line doesn't allow (red arrow), a declared system it doesn't use (faint arrow), and required functions nothing calls. It runs the same checks as the overview's NEVER CALLED, so it takes about 15 s on a 17,000-line project.

shot emit, shot parse

For editor integrations and for debugging the tool.