#Data schema
The JSON contract between the extraction CLI and the SPA lives in a single file, crates/extract/src/schema.rs. There are exactly two document kinds:
| Document | File | Cost | Lifecycle |
|---|---|---|---|
Manifest |
manifest.json |
Cheap | Always regenerated on every run (see Extraction pipeline) |
ConfigData |
config/<kind>.<name>.json |
Expensive (full options eval) | Extracted on demand, cached by extractor fingerprint + flake identity + lock hash + nix version |
storePath is the universal join key: FileEntry.storePath matches the file strings in each option's declarations and definitions.
#Versioning
| Constant | Value | Gates |
|---|---|---|
SCHEMA_VERSION |
1 |
Stamped into both documents; the SPA rejects a ConfigData blob whose version mismatches (per its JSDoc in crates/extract/src/schema.rs) |
There is no manually-bumped extractor version: the cache key's code half is a content hash of the extraction sources plus the crate's resolved dependency closure (crates/extract/build.rs), so any change to the flake-explorer-extract crate — crates/extract/src/schema.rs included — or to a dependency it actually links makes every cached blob stale at once. Changes to the CLI, server and exporter in the root crate deliberately do not, which is what the crate split is for (see The extraction crate boundary). reconcile in crates/extract/src/cache.rs additionally requires the sidecar's flake identity (narHash, or the self store path when absent), resolved-input lockHash, and nixVersion to match — see Extraction pipeline.
#Type overview
classDiagram
class Manifest {
version
extractor
outputs OutputNode
outputNames
warnings
}
class FlakeInfo {
ref
path
rev
narHash
}
class InputInfo {
name
nodeKey
transitive
storePath
follows
}
class FileEntry {
id
relPath
origin FileOrigin
storePath
git
}
class ImportEdge {
from
to
}
class InputRef {
file
input
}
class InputFollow {
name
target
}
class ConfigRef {
id
kind
dataFile
status
}
class GraftInfo {
output
input
added
inherited
}
class ConfigData {
version
id
fileIndex
}
class OptionEntry {
loc
type
isDefined
highestPrio
customized
value
}
class DeclarationRef {
file
line
column
}
class DefinitionRef {
file
value
valueError
valueSkipped
prio
}
class FileOptionRefs {
defines
declares
}
Manifest --> FlakeInfo : flake
Manifest --> "many" InputInfo : inputs
Manifest --> "many" FileEntry : files
Manifest --> "many" ImportEdge : importEdges
Manifest --> "many" InputRef : inputRefs
Manifest --> "many" InputFollow : inputFollows
Manifest --> "many" ConfigRef : configurations
Manifest --> "many" GraftInfo : grafts
ConfigData --> "many" OptionEntry : options
ConfigData --> "many" FileOptionRefs : fileIndex
OptionEntry --> "many" DeclarationRef : declarations
OptionEntry --> "many" DefinitionRef : definitions
OutputNode is a recursive union (attrset | leaf | omitted | unknown) normalized from nix flake show --json; FileOrigin is self | input (optionally patched) | unknown.
#Join keys and the file id codec
- storePath join:
DeclarationRef.file/DefinitionRef.fileare absolute/nix/store/...paths (or the sentinel below). They join againstFileEntry.storePathto attribute options to files. - File id codec:
"self:<rel>"|"input:<name>:<rel>", implemented on both sides —makeFileId/parseFileIdinweb/lib/schema.ts,make_file_id_self/make_file_id_input/parse_file_idincrates/extract/src/schema.rs. This format is a client-server protocol, not just a display convention: serve's/data/file/<id>route re-derives input files from the id (re-fetching through Nix when the store path has been GC'd; see CLI reference). Every construction and parse site must go through these helpers, or the two sides drift. App-internal"unknown:…"/"inline"buckets are opaque; the parse helpers return null/Nonefor them. - fileIndex:
ConfigData.fileIndexmaps storePath → indices intooptions, split intodefines/declares(FileOptionRefs), precomputed so the SPA never scans thousands of options per click.
#Definition priorities
PRIO records the well-known lib.mkOverride values (lower wins):
| Name | Value | Meaning |
|---|---|---|
mkForce |
50 | lib.mkForce |
plain |
100 | An ordinary definition |
mkDefault |
1000 | lib.mkDefault |
optionDefault |
1500 | The option's own declared default (lib.mkOptionDefault) |
An option is customized when isDefined && highestPrio !== null && highestPrio < PRIO.optionDefault (see to_entry in crates/extract/src/options.rs) — a real definition beat the declared default. fileIndex.defines only counts customized definitions, because every defaulted option carries a mkOptionDefault definition pointing at its declaring module, which would otherwise make nixpkgs "define" everything.
Besides the option-level highestPrio, a DefinitionRef may carry its own prio, lifted by to_entry from a {mkOverride, content} envelope (scrub's rendering of a lib.mkOverride wrapper) in the raw definition value. On the real module system this rarely fires: lib.filterOverrides strips the wrapper and drops losing-priority definitions entirely before definitionsWithLocations is exposed, so every listed definition merged at the option's highestPrio and absent prio means exactly that. The lift matters on module systems that expose raw definition values (hand-rolled fixtures, older lib versions). Skipped definitions (package-typed options, degraded chunks) carry valueSkipped instead of a value.
#Declaration positions and skip flags
DeclarationRef.line/columncome from the module system'sdeclarationPositions(nixpkgs ≥ 23.11, 1-based); on older module systems onlyfileis present.valueSkipped(onOptionEntryandDefinitionRef) distinguishes "the extractor deliberately skipped this value" (package-typed/derivation-typed options, or a chunk that degraded down the ladder) from "there is no value" — previously both arrived as an absentvalue.
#Sentinels and grafts
UNKNOWN_FILE = "<unknown-file>"— the file string the module system uses for inline/anonymous modules; it appears as afileIndexkey and in declaration/definition refs.GraftInfomarks a top-level output that extends an input's same-named namespace (e.g.lib = nixpkgs.lib.extend …). The heuristic lives incrates/extract/src/extract.nix: at least 90% of the input's attr names must reappear in the output (and the input must have at least 5 names; outputs consisting only of per-system names never count). The manifest records only theaddedkeys plus aninheritedcount so the UI can hide the inherited bulk.
#Reference
The full generated API reference is at https://kris.net/flake-explorer/docs/api/ (CI-generated, site only). The source of truth is crates/extract/src/schema.rs.