Incremental argus analysis as a roux query graph.
Query DAG
module_beam(module) [frontend]
│
module_extraction(module) ← Argus.Pipeline.extract, per module
│ │
module_semantic_facts module_line_table
(line_info dropped — (anchor resolution,
THE cutoff seam) consumed late by LSP)
│
module_relation_facts({module, relation}) ← per-module projection
│
relation_facts(relation) ← one relation across the project
│
stage0_facts(:all)
(shared call graph —
THE second cutoff seam)
│
analysis_facts_dir(analysis) ← content-addressed, projected to the
│ relations THIS analysis reads
souffle_solve(analysis)
│
findings(analysis)Planchette's LSP-only surface (Planchette.SupTree's supervision tree,
Planchette.Focus's flowistry slices) hangs off module_extraction
and relation_facts by query name from its own modules; nothing here
depends on it.
There is no whole-program fact node. Everything downstream of extraction is projected — per module, then per relation, then per analysis — so an edit propagates only along the relations it actually moved.
The line-shift immunity story: a whitespace/comment edit changes the
beam (Line/Dbgi chunks) → module_extraction recomputes and differs
(its line_info rows changed) → module_semantic_facts recomputes,
produces an EQUAL value → roux backdates it → every projection below it
validates green without executing. Zero Souffle runs for a comment edit.
The body-edit story: an edit that changes what a function COMPUTES but
not what it CALLS moves instruction and friends, so the analyses that
anchor at instruction sites re-solve — but supervisor, sync_call and
the rest of the structural relations backdate at
module_relation_facts, so relation_facts never executes for them and
the supervision analyses stop before Souffle.
Rule fidelity: every analysis stays in Souffle — the .dl files are
the single source of truth, and incremental findings must equal batch
Argus.Findings.run/2 exactly (the honesty principle). Fact rows are
sorted per relation so equal extractions produce equal values (roux
compares with ==); Souffle has set semantics, so ordering cannot
change results.
Purity deviation: analysis_facts_dir, stage0_facts and
souffle_solve touch the filesystem and shell out — content-addressed
and idempotent, the same pragmatic loophole as the frontend's code
loading.
Shared-layer contract
This module is consumed by BOTH planchette (LSP, in-memory compile
frontend) and scry's Mix compiler (disk-beam frontend). Roux dispatches
queries by NAME and memo keys are {query_name, key}, so query
names, key shapes, and value shapes are the ABI — rename nothing
without revisiting every consumer and its persisted manifests. The
frontend contract this module demands, by name: the queries
:module_beam, :module_map, and :file_of, and the input
:env_fingerprint (inputs are the frontend's to declare — this module
defines queries only).
Summary
Functions
The union of every built-in analysis's extractors (coverage excluded,
mirroring Argus.Findings.run/2) — one extraction serves all solves.
The always-on analyses (keynote-narrative, low-noise). The rest run on demand.
Extracts modules in parallel ahead of the query graph.
Functions
@spec all_extractors() :: [module()]
The union of every built-in analysis's extractors (coverage excluded,
mirroring Argus.Findings.run/2) — one extraction serves all solves.
@spec analysis_diagnostics(Roux.Database.t(), term()) :: {:ok, %{optional(String.t()) => [map()]}} | {:error, term()}
@spec analysis_facts_dir(Roux.Database.t(), term()) :: %{ dir: String.t(), key: String.t() }
@spec analysis_input_relations(Roux.Database.t(), term()) :: [atom()]
@spec default_analyses() :: [atom()]
The always-on analyses (keynote-narrative, low-noise). The rest run on demand.
@spec findings(Roux.Database.t(), term()) :: {:ok, [map()]} | {:error, term()}
@spec module_extraction(Roux.Database.t(), term()) :: {:ok, map()} | {:error, term()}
@spec module_line_table(Roux.Database.t(), term()) :: {:ok, map()} | {:error, term()}
@spec module_semantic_facts(Roux.Database.t(), term()) :: {:ok, map()} | {:error, term()}
@spec prewarm_extractions(%{optional(module()) => String.t()}, Roux.Database.t()) :: :ok
Extracts modules in parallel ahead of the query graph.
Queries execute one at a time on the demanding process, so a cold run
extracted every module serially. This runs the extraction for the given
module => beam_path map across the schedulers and parks each result
for module_extraction to pick up — the query still executes, records
its dependencies and memoizes as before, it just finds its answer
waiting. A result is keyed by the canonical beam's digest, so a beam
that changed between the pre-pass and the query is extracted again.
@spec program_relation_facts(Roux.Database.t(), term()) :: Argus.Facts.interned()
@spec relation_digest(Roux.Database.t(), term()) :: String.t()
@spec relation_facts(Roux.Database.t(), term()) :: [[String.t()]]
@spec relation_rows(Roux.Database.t(), term()) :: [tuple()]
@spec souffle_solve(Roux.Database.t(), term()) :: {:ok, map()} | {:error, term()}