Skip to content

Latest commit

 

History

History
1138 lines (936 loc) · 61.9 KB

File metadata and controls

1138 lines (936 loc) · 61.9 KB

Giulia Architecture

Document version: Build 165 · v0.3.8 · 2026-08-06

This document describes the architecture as of the build above. If the build counter in mix.exs is higher, sections may be out of date — re-audit against the codebase.

1. Overview

Giulia is a persistent, local-first code intelligence daemon built in Elixir/OTP. It runs as a persistent background daemon inside Docker, exposing an HTTP REST API on port 4000. Any client -- Claude Code, a CLI escript, an editor plugin -- talks to the daemon over plain HTTP/JSON. The daemon never restarts between terminal sessions; it keeps AST caches, property graphs, and embedding vectors warm in memory across invocations.

 +-----------+   +-----------+   +----------------+
 | Claude    |   | CLI       |   | Editor Plugin  |
 | Code      |   | (escript) |   | (future)       |
 +-----+-----+   +-----+-----+   +-------+--------+
       |               |                 |
       +-------+-------+-----------------+
               |
          HTTP / JSON  or  MCP
               |
               v
 +-----------------------------+
 |   giulia-worker  :4000      |
 |   (Bandit + Plug.Router)    |
 |   88 API endpoints          |
 |   MCP server (/mcp)         |
 +-----------------------------+

The daemon holds per-project state in ETS tables, a libgraph-based property graph, CubDB persistence for warm restarts, and an optional ArcadeDB connection for cross-build historical analysis.

2. Two-Node Model

Giulia ships as a single Docker image (giulia/core:latest). Two containers are started from that image, differentiated by the GIULIA_ROLE environment variable:

+-------------------------------------------------------------------+
|  Docker Network                                                   |
|                                                                   |
|  +-----------------------------+    +---------------------------+ |
|  | giulia-worker               |    | giulia-monitor            | |
|  | GIULIA_ROLE=worker          |    | GIULIA_ROLE=monitor       | |
|  | Port 4000 (HTTP API)        |    | Port 4001 (HTTP API)      | |
|  | Port 4369 (EPMD)            |    | Port 4369 (EPMD)          | |
|  | Ports 19100-19105 (dist)    |    | Ports 19110-19115 (dist)  | |
|  |                             |    |                           | |
|  | - AST indexing              |    | - Distributed Erlang      | |
|  | - Property Graph           |    |   connection to worker    | |
|  | - Semantic search           |    | - Burst detection         | |
|  | - EmbeddingServing          |    | - High-frequency runtime  | |
|  | - Inference engine          |    |   snapshots               | |
|  | - 88 API endpoints          |    | - Performance profiling   | |
|  | - MCP server (75 tools)     |    |                           | |
|  | - CubDB persistence         |    | Skips:                    | |
|  | - ArcadeDB L3 snapshots     |    |  EmbeddingServing (~90MB) | |
|  |                             |    |  Inference pools           | |
|  |                             |    |  SemanticIndex             | |
|  +-------------+---------------+    +-------------+-------------+ |
|                |                                  |               |
|                +---- Erlang Distribution ----------+               |
|                      (cookie-authenticated)                       |
+-------------------------------------------------------------------+
                         |
                         | HTTP (host.docker.internal:2480)
                         v
               +-------------------+
               | ArcadeDB          |
               | (standalone)      |
               | Port 2480         |
               +-------------------+

Worker (giulia-worker): The primary daemon. Runs all static analysis (AST scanning, property graph construction, semantic embeddings) and serves all 88 API endpoints. Memory limit: 4GB. (A legacy local-chat inference subsystem also loads under TIER 3 — deprecated as of v0.3.8, see Section 18 / Document History.)

Monitor (giulia-monitor): A lightweight observer node. Connects to the worker via distributed Erlang on startup (AutoConnect GenServer). Its job is runtime introspection: periodic BEAM health snapshots, burst detection (spikes in reductions/memory), and performance profiling triggered by bursts. It skips EmbeddingServing, SemanticIndex, and all Inference children to save approximately 200MB of RAM. Memory limit: 2GB.

The monitor depends_on the worker being healthy (curl health check on :4000 with 30s interval). Both containers share the same Erlang cookie (GIULIA_COOKIE, default giulia_dev) for authenticated distribution.

3. OTP Supervision Tree

Giulia.Application.start/2 detects whether it is running in client mode (thin HTTP client, empty supervision tree) or daemon mode. In daemon mode, it starts children under a single :one_for_one supervisor (Giulia.Supervisor) in five tiers:

Giulia.Supervisor (:one_for_one)
|
|-- TIER 1: Base (always started)
|   |-- Registry (Elixir.Registry, :unique, name: Giulia.Registry)
|   |-- Task.Supervisor (name: Giulia.TaskSupervisor)
|   |-- Context.Store (ETS tables for AST data)
|   |-- Persistence.Store (CubDB lifecycle, one instance per project)
|   |-- Persistence.Writer (async write-behind, 100ms debounce)
|   |-- Tools.Registry (auto-discovers tool modules on boot)
|   |-- Context.Indexer (background AST scanner, Task.async_stream)
|   |-- Knowledge.Store (libgraph in-memory directed graph)
|   |-- Persistence.WarmRestore (boot-time L2→L1 restore, non-blocking)
|   |-- Storage.Arcade.Indexer (L3 graph sync on {:graph_ready})
|   +-- Storage.Arcade.Consolidator (periodic cross-build analysis)
|
|-- TIER 2: Heavy (skipped when GIULIA_ROLE=monitor)
|   |-- Intelligence.EmbeddingServing (Bumblebee + all-MiniLM-L6-v2)
|   +-- Intelligence.SemanticIndex (cosine similarity search)
|
|-- TIER 3: Inference (DEPRECATED as of v0.3.8; skipped when GIULIA_ROLE=monitor)
|   |-- Provider.Supervisor (DynamicSupervisor for LLM connections)
|   |-- Inference.Trace (debug storage for inference runs)
|   |-- Inference.Events (SSE event broadcaster)
|   |-- Inference.Approval (interactive consent gate)
|   +-- Inference.Supervisor (pools with back-pressure)
|
|-- TIER 4: Tail (always started)
|   |-- Monitor.Store (rolling event buffer + SSE pub/sub)
|   |-- Core.ProjectSupervisor (DynamicSupervisor for per-project contexts)
|   |-- Core.ContextManager (routes requests to correct ProjectContext)
|   |-- Runtime.Collector (periodic BEAM health snapshots)
|   |-- Runtime.IngestStore (Monitor->Worker snapshot pipeline)
|   |-- Runtime.Observer (async observation controller)
|   |-- Runtime.AutoConnect (returns :ignore if GIULIA_CONNECT_NODE unset)
|   +-- Runtime.Monitor (returns :ignore unless GIULIA_ROLE=monitor)
|
|-- TIER 5: MCP (only started if GIULIA_MCP_KEY is set)
|   +-- Giulia.MCP.Server (Anubis StreamableHTTP transport)
|
+-- Bandit (HTTP endpoint, skipped in MIX_ENV=test)
    plug: Giulia.Daemon.Endpoint
    port: GIULIA_PORT (default 4000)

After the supervisor starts successfully, Giulia.Monitor.Telemetry.attach/0 hooks :telemetry handlers for the cognitive flight recording system (7 events across the inference pipeline — deprecated subsystem, see Section 18).

Restart strategy

Giulia.Supervisor uses :one_for_one: a crash in one child restarts only that child, not its siblings. A failure in Intelligence.SemanticIndex does not take down Knowledge.Store or the HTTP endpoint, so the daemon stays available through partial failures.

The trade-off is that a restarted child loses its in-memory state, and downstream consumers may briefly hold stale references. Two mechanisms close that gap, both required by the GIULIA.md "Restart-time state recovery" invariant:

  • ETS survival via EtsKeeper. ETS tables owned by Context.Store and Knowledge.Store register Giulia.EtsKeeper as their :heir. When an owner crashes, BEAM transfers the table to the keeper instead of deleting it; the restarted owner reclaims it via EtsKeeper.claim/1. Cache data survives the common case — an owner crash — without a rebuild.

  • Lost {:graph_ready} messages. Knowledge.Store notifies Storage.Arcade.Indexer of completed builds with a bare send/2 to a whereis lookup, which drops silently if the Indexer is mid-restart. Arcade.Indexer compensates with a 5-minute reconcile pass that walks every active project and re-snapshots any whose current build is missing from ArcadeDB.

If EtsKeeper itself is restarted, its inherited tables are lost; that case is covered by Persistence.WarmRestore, which rebuilds L1 from the L2 CubDB cache on boot, verifying each entry against its SHA-256 content hash.

4. Storage Architecture

Giulia uses a three-tier storage model. Each tier serves a different latency and durability requirement.

L1 -- ETS + libgraph (sub-millisecond, volatile)

The hot path. All API reads hit L1 first.

  • Context.Store: ETS table keyed by {:ast, project_path, file_path}. Stores parsed AST data, module metadata, function signatures, specs, callbacks, and struct definitions. Rebuilt on every scan.

  • Knowledge.Store: An in-memory Graph (libgraph directed graph) holding module dependency relationships. Supports queries like dependents, dependencies, centrality, impact maps, and shortest path. Rebuilt after every scan from the AST data in Context.Store.

  • Metric caches: Computed lazily and cached in Knowledge.Store's GenServer state. Five cached metrics: heatmap, change_risk, god_modules, dead_code, coupling. Warmed eagerly after graph rebuild via a background Task. Sub-10ms on warm reads (was 570-1166ms before caching).

L2 -- CubDB (warm starts, per-project)

On-disk key-value store for surviving restarts without re-scanning.

  • Location: {project}/.giulia/cache/cubdb/ (one CubDB instance per project). In test mode (MIX_ENV=test), routed to /tmp to avoid corrupting the dev daemon's data.

  • Contents: AST entries, serialized property graph, metric caches, embedding vectors (module + function).

  • Writer: Persistence.Writer batches writes with a 100ms debounce. Multiple writes within the window are coalesced into a single CubDB transaction.

  • Loader: Persistence.Loader restores L1 from L2 on startup. Detects stale files by comparing SHA-256 content hashes of source files against stored hashes. Stale entries trigger incremental re-scanning rather than a full rebuild.

  • WarmRestore: Persistence.WarmRestore is the startup driver. On boot it walks /projects/* (and GIULIA_PROJECTS_PATH if set) for directories containing the role-specific .giulia/cache/cubdb[_<role>]/ layout and calls Loader.restore_graph/1 + restore_metrics/1 for each. The work runs in handle_info(:run, _) scheduled from init/1 via send/2 so supervisor start isn't blocked on I/O. This is what keeps /api/projects populated across docker compose restart without forcing a scan.

  • Merkle tree: Persistence.Merkle builds a SHA-256 Merkle tree over all cached entries. Used for integrity verification (POST /api/index/verify) and detecting corruption (if build version mismatches, the entire L2 cache is discarded and a cold start occurs).

  • Code-digest envelope: graph and metrics are persisted wrapped in a %{digest: <12-char hex>, payload: <data>} envelope. The digest is computed by Knowledge.CodeDigest from the BEAM md5 of eleven code-tier modules (the graph/metric tier — Builder, Metrics, Behaviours, DispatchPatterns, DeadCodeClassifier, TemplateReferences — plus the enrichment tier) and the content md5 of six config files (scoring.json, dispatch_patterns.json, scan_defaults.json, enrichment_sources.json, dispatch_invariants.json, otp_checks.json). On warm-restore, the loader compares the stored digest against the current digest:

    • Match → load cache as-is
    • Mismatch → log "Code digest changed (X -> Y) — invalidating … cache", drop the cache, force a rebuild on next scan

    This automates the "I edited the builder/scoring.json, restart, expect fresh metrics" workflow without per-edit ceremony. The AST cache is intentionally NOT in the digest — re-extracting 580+ files takes seconds-to-tens-of-seconds, so AST invalidation stays under user control via ?force=true on /api/index/scan. Only the cheap downstream path (graph rebuild + metric recompute from existing ASTs) is auto-invalidated.

    See CONFIGURATION.md for the full invalidation contract and the operator workflow for tuning each config surface.

L3 -- ArcadeDB (history, consolidation)

External multi-model graph database for cross-build analysis. Not on the hot path.

  • Deployment: Standalone container (arcadedata/arcadedb:latest) on port 2480. Not managed by the Giulia docker-compose file. Worker reaches it via ARCADEDB_URL (default: http://host.docker.internal:2480).

  • Query languages: Cypher, SQL, and sqlscript. The Req-based HTTP client (Giulia.Storage.Arcade.Client) supports all three.

  • Schema:

    • Vertex types: Module, Function, File, Insight
    • Edge types: DEPENDS_ON, CALLS, DEFINED_IN
    • All records carry project, build_id, and indexed_at fields
    • Composite unique indexes on (project, name) per vertex type
  • Indexer: Giulia.Storage.Arcade.Indexer hooks into the {:graph_ready} event and snapshots the entire L1 graph into ArcadeDB after every successful build.

  • Consolidator: Giulia.Storage.Arcade.Consolidator runs on a 30-minute schedule (or on-demand via POST /api/index/compact?include=arcade). Until v0.3.7 this was a Build-137 skeleton — the timer fired but did nothing; v0.3.7 wired it to real pruning (prune_old_builds/2) and three cross-build analyses: complexity_drift, coupling_drift (fan-in/fan-out), and hotspot detection. Retention window controlled by arcade_history_builds in priv/config/scan_defaults.json (default 10, clamped ≥3 — drift / coupling / hotspot detectors require ≥3 builds of history). Analysis results stored as Insight vertices.

  • Purpose: ETS + libgraph stays L1 for real-time queries. ArcadeDB is for history -- trend analysis, regression detection, cross-build comparisons. Typical warm query latency is ~100ms, acceptable for L2/L3 but not the hot path.

5. Configuration Surface

Tunable behaviour lives in JSON, not in module attributes. The contract:

  • Edit + restart, no recompile. Daemon restart picks up changes in seconds; the BEAM and ETS state aren't rebuilt.
  • :persistent_term cache. Each loader reads its file once at boot and stores the parsed structure in :persistent_term. Reads are free (no copy, no message-passing).
  • CodeDigest envelope tracking. Config files are part of the digest that decides whether L2 caches survive a restart. Edit a config file, the next warm-start invalidates derived metrics so they're recomputed against the new values.
  • Universal defaults. The shipped values must produce correct output on every codebase, no per-project opt-in. Tightening a default that's wrong somewhere is a release-gating bug, not a "user can override" out.
  • Fail-loud on missing/malformed. Loaders raise at boot rather than silently degrade — a typo in JSON should crash the daemon at startup, not produce wrong-but-running analysis hours later.

This pattern is why v0.3.x extracted four module-attribute datasets to JSON across multiple releases (scoring constants in v0.3.0, dispatch patterns in v0.3.x, dispatch invariants and relevance buckets in v0.3.8). The architectural commitment is "configuration is data, not code."

Five JSON files in priv/config/ control behaviour that should be tunable without recompilation:

File Loader Controls
scoring.json Knowledge.ScoringConfig Heatmap weights/normalization/zones, change_risk weights, god_modules weights, unprotected_hubs thresholds
dispatch_patterns.json Knowledge.DispatchPatterns Runtime-dispatch patterns the AST walker can't see (Mix Release shell overlays, ExMachina factories, the Phoenix-style __using__/apply idiom)
scan_defaults.json Context.ScanConfig Universal source-root list for Mix projects (lib, test/support, test/test_helper.exs); ArcadeDB history retention
dispatch_invariants.json Config.DispatchInvariants Project-root markers, OTP/framework implicit functions, known external behaviour callback signatures, Phoenix HTTP verbs (v0.3.8+)
relevance.json Config.Relevance Bucket boundaries for ?relevance=high|medium|all on dead_code / conventions / duplicates (v0.3.8+)

All are loaded once at daemon startup and cached in :persistent_term. All except relevance.json are tracked by the CodeDigest envelope (see L2 section above), so edits propagate via daemon restart + automatic cache invalidation on warm-restore. relevance.json is excluded deliberately: its buckets filter a response at read time and are never baked into the cached graph or metrics, so an edit takes effect on the next request with no invalidation needed.

See CONFIGURATION.md for the per-variable reference and operator workflow.

6. AST + Runtime Fusion

Giulia merges two views of the project that most analysis tools keep separate: a static call graph extracted from source via Sourceror, and a live BEAM-runtime feed (process counts, message-queue lengths, reduction deltas, memory) collected by the monitor node. The Knowledge layer reads both and answers questions like "this hub function has 47 callers AND its process accumulated 200k reductions in the last burst" in one query — the caller-count comes from the AST graph, the burst signal comes from the runtime collector, the join happens at the function-vertex level.

Static Analysis Pipeline

.ex source files
    |
    v
Sourceror.parse_string/1        (pure Elixir AST parser)
    |
    v
Giulia.AST.Extraction           Macro.traverse/4 with enclosing-module
    |                           stack — recognizes defmodule / defprotocol
    |                           / defimpl; nested names qualified
    |                           (Outer.Inner, three levels deep);
    |                           function_info carries :module so sibling
    |                           modules don't collapse on {name, arity}
    v
Context.Store (ETS)             {:ast, project_path, file_path}
    |                           modules, functions, specs, structs
    v
Knowledge.Store (libgraph)      module dependency graph
    |                           edges from alias/import/use analysis
    v
Metric caches                   heatmap, complexity, coupling,
                                dead_code, god_modules, change_risk

Runtime Introspection Pipeline

Target BEAM node (self or remote)
    |
    v
Runtime.Inspector               :erlang.memory/0, Process.info/2,
    |                           :erlang.statistics/1, :erlang.trace/3
    v
Runtime.Collector               periodic snapshots (configurable interval)
    |
    v
Burst detection                 spike in reductions/memory triggers
    |                           high-frequency capture mode
    v
Performance profiling           function-level trace during burst window

7. External Tool Enrichment

Two sources ship today: Credo (JSON, 5-entry severity map) and Dialyzer (text, 47-entry severity map covering dialyxir's full warning catalogue). Adding a new source = parser module + JSON entry; no core changes.

External tool (Credo, Dialyzer, ...)
    |
    | mix credo --format json > /tmp/credo.json
    | ~/.mix/escripts/credo --format json --working-dir . > ...    (escript form)
    | mix dialyzer --format short > /tmp/dialyzer.out
    v
POST /api/index/enrichment        validates payload_path against allowlist
    |                              (priv/config/scan_defaults.json
    |                              :enrichment_payload_roots)
    v
Giulia.Enrichment.Ingest          dispatches to source module via Registry
    |
    v
Giulia.Enrichment.Sources.{Credo,Dialyzer,...}
    |                              parse tool output into normalized
    |                              finding/0 records. Three-path arity-
    |                              resolution waterfall (same shape across
    |                              sources):
    |                              1. line-range against function vertices
    |                                 (single match, multi-arity, ambiguous)
    |                              2. all-arities fallback for the named fn
    |                              3. module-only attach (with
    |                                 :resolution_ambiguous flag when
    |                                 multiple different-name candidates
    |                                 cover the line)
    |                              severity_map per source lives in
    |                              priv/config/enrichment_sources.json;
    |                              parser asks Registry.severity_for/2.
    v
Giulia.Enrichment.Writer          replace-on-ingest inside CubDB.transaction:
    |                              delete prior {:enrichment, tool, project, *}
    |                              keys, write new ones, stamp per-finding
    |                              provenance (tool_version, run_at,
    |                              source_digest_at_run), write sentinel
    v
CubDB                              {:enrichment, tool, project_path, target}
                                   target = "Mod.fn/N" | "Mod" | :__ingested__
                                   preserved across clear_project (source
                                   rescans don't wipe enrichments)

Read side:

GET /api/intelligence/enrichments?mfa=...
    |                                          (uncapped drill-down for agents
    |                                           wanting full per-MFA findings)
    v
Giulia.Enrichment.Reader          fetch_for_mfa / fetch_for_module:
                                   distinguishes %{} (never ingested for
                                   project) from %{credo: []} (ingested,
                                   no findings on this MFA) via sentinel
                                   key probe — different signals, agent
                                   reasons differently about each.

POST /api/knowledge/pre_impact_check
    |
    v
Knowledge.Insights.Impact         attaches :enrichments per affected_caller
                                   via Giulia.Enrichment.Consumer.attach/2

GET /api/knowledge/dead_code
    |
    v
Knowledge.Metrics.dead_code       attaches :enrichments per dead entry via
                                   the same Consumer.attach/2

(both call) Giulia.Enrichment.Consumer.apply_response_cap/1
                                   shared cap logic from priv/config/scoring.json
                                   (errors uncapped, top-3 warnings/entry,
                                   drop info, per-response cap of 30
                                   dedup'd by {check, severity}; capped
                                   responses get a project-wide
                                   :enrichments_summary on the first entry)
Module Responsibility
Enrichment.Source Behaviour contract — tool_name/0, target_granularity/0, parse/2
Enrichment.Registry Loads priv/config/enrichment_sources.json, caches in :persistent_term. Exposes sources/0, fetch_source/1, config_for/1, severity_for/2 (translates tool-emitted category/check string into canonical severity using per-source severity_map)
Enrichment.Sources.Credo JSON parser; 5-entry severity map; three-path arity resolution
Enrichment.Sources.Dialyzer Text parser for --format short; 47-entry severity map covering dialyxir's full catalogue; same three-path resolution
Enrichment.Writer Replace-on-ingest CubDB writes, provenance stamping, sentinel marker
Enrichment.Reader Per-MFA / per-module lookups with never-ingested vs no-findings distinction
Enrichment.Ingest Orchestrator; emits [:giulia, :enrichment, :ingest | :parse_error | :read] telemetry
Enrichment.Consumer Shared helper used by pre_impact_check and dead_code to attach :enrichments per entry and apply per-entry / per-response caps

Architectural commitment: tool ingest cadence is decoupled from source-extraction cadence. CI may push Credo findings on every PR; the daemon scans source on a different schedule. Findings persist independently — Persistence.Writer.clear_project/1 filters out enrichment keys precisely so re-extraction doesn't force re-ingestion.

Fusion Point

The /api/runtime/hot_spots endpoint is the fusion point. It:

  1. Reads top processes from the target BEAM node (by reductions or memory)
  2. Resolves PIDs to module names via Process.info(pid, :dictionary)
  3. Looks up each module in the Property Graph for centrality, complexity, and zone
  4. Returns a merged view: runtime activity annotated with static analysis metadata

The Observer (running on the monitor node) pushes snapshots to the worker via HTTP. The worker finalizes each snapshot with static+runtime correlation.

8. Knowledge Graph

The Knowledge layer is not a single module. Knowledge.Store is the GenServer coordinator, but the actual logic is split across purpose-built modules:

Module Responsibility
Knowledge.Builder Graph construction from AST data (11-pass pure functions — see Builder Passes below)
Knowledge.Topology Pure graph traversal: stats, centrality, reachability, cycles, paths
Knowledge.Metrics Quantitative metrics: heatmap, change_risk, god_modules, dead_code, coupling
Knowledge.Behaviours Behaviour integrity checking (callback validation, macro-aware)
Knowledge.Conventions Convention violation detection via AST (Tier 1 metadata + Tier 2 patterns)
Knowledge.Insights High-level code insights: orphan specs, logic flow, style oracle
Knowledge.Insights.Impact Pre-impact risk analysis for rename/remove/refactor operations
Knowledge.Analyzer Facade delegating to Topology, Metrics, Behaviours, Insights
Knowledge.MacroMap Static mapping of use Module to injected function signatures
Knowledge.DispatchPatterns Runtime-dispatch patterns loaded from priv/config/dispatch_patterns.json
Knowledge.ScoringConfig Heatmap/change_risk/god_modules/unprotected_hubs constants from priv/config/scoring.json
Knowledge.CodeDigest Identity hash of code-tier modules + config files for L2 cache invalidation
Knowledge.Store.Reader Direct ETS reads bypassing GenServer (concurrent bulk reads)

Builder Passes

The graph is built in 11 sequential passes over the file-keyed AST data map. Each pass is a pure function that takes a graph and returns a graph; all intermediate state lives in the function arguments. Passes 7-11 synthesize edges for runtime-dispatched call sites that the static AST walker cannot resolve directly.

Pass What it does Edge label
1. Vertices Adds module, function, struct, behaviour vertices (none — vertices)
2. Dependency / implements edges import/alias/use/require:depends_on; use/require of a project module → :implements :depends_on, :implements
3. xref call edges Optional, requires compiled BEAMs in target project {:calls, :xref}
4. Function-level call edges (AST) Walks each function body for remote and local calls; module-stack-aware so def nodes attribute to their real enclosing defmodule {:calls, :direct | :alias_resolved | :erlang_atom | :local}
5. Module edge promotion Collapses MFA → MFA :calls edges into module-level edges :calls
6. Module-reference edges Catches modules passed as atom args to framework macros (Phoenix router, plug, supervision children, struct literals) :references
7. Protocol-dispatch edges defimpl impl modules → protocol module: synthesizes function-level edges from the protocol to each impl's function vertices {:calls, :protocol_impl}
8. Behaviour-dispatch edges External-framework behaviours (GenServer, Ecto.Type, Phoenix.LiveView, etc.) → callback functions in implementer modules {:calls, :behaviour_impl}
9. Phoenix router-dispatch edges Parses get/post/put/..., resources, scoped routes; emits edges from router module to each controller action {:calls, :router_dispatch}
10. Function-reference edges Detects four runtime-reference forms: literal MFA tuples {Mod, :fn, [args]}, function captures &Mod.fn/N, apply/3, and any 3-arg call carrying MFA-shape args (Task.start_link(M, F, A), supervisor child specs, etc.) {:calls, :mfa_ref | :capture_ref | :apply_ref | :mfa_arg_ref}
11. Use-injected import edges Detects modules whose defmacro __using__/1 injects import N directives, then resolves unqualified calls in consumer files against those imports {:calls, :use_import_ref}
12. Supervision topology Parses Supervisor.start_link/2, Supervisor.init/2 and DynamicSupervisor.start_link/1,2 into the supervision tree. Every endpoint is keyed name_option || module, because supervision identity is not module identity — a root supervisor is typically a registered name with no defmodule, several children are external modules, and two {DynamicSupervisor, name: X} declarations share one module {:supervises, %{restart, order, strategy, conditional}}

Pass 12 only ever adds. It never re-labels an existing vertex, because Topology.stats/1, Topology.blast_radius/3 and Insights filter with exact equality (labels == [:module]), so a second label would silently drop the vertex from those results. The :supervisor / :process labels are minted only for vertices Pass 12 creates — registered names and external modules, which no existing filter sees.

The libgraph mechanism, verified rather than assumed:

call on a NEW vertex on an EXISTING vertex
Graph.add_vertex/3 sets the label no-op — the label is silently discarded
Graph.label_vertex/3 accumulates[:module] becomes [:module, :x]

An earlier version of this section claimed add_vertex/3 accumulates. It does not, and the distinction is load-bearing in both directions: add_vertex/3 cannot corrupt an existing vertex's labels, which is what makes Pass 12 safe — and it equally cannot add one, which is why an attempt to mark supervisors with a :children_unresolved label silently did nothing.

Consequence: the graph cannot carry supervision metadata. A supervisor whose key collides with a vertex an earlier pass already minted keeps that pass's labels, so any label-based query is blind to it. Plausible.Supervisor is the worked example — not a defmodule, appearing exactly once as a name: option, yet already a vertex by the time Pass 12 ran, which made the whole tree report supervisor_count: 0, roots: []. Whether the tree worked depended on whether an unrelated pass happened to mint the same key first.

GET /api/knowledge/supervision therefore builds from extraction declarations (Supervision.tree_from_declarations/1), which carry strategy, dynamic, children_unresolved and registered_name directly. Pass 12's edges remain for graph-shaped consumers — blast radius, traversal — where the flags do not matter.

Edges from Passes 7-11 are consumed by dead_code_with_asts/3 via a single reference_targets set (functions referenced via any of the synthesized edge labels are exempted from the dead-code report). They are also visible in verify_l3's stratified sample-identity check — the bucket names match the edge sub-labels.

Knowledge.Store orchestrates: it owns the Graph struct in its state, delegates computation to the pure modules above, and caches results in its state map. The Store.Reader module provides a fast path for bulk extraction (all_modules, all_functions, all_dependencies) that reads directly from ETS without going through the GenServer mailbox.

9. Intelligence Layer

Beyond embedding and search, the Intelligence layer provides four briefing and validation modules used by the API:

Module Responsibility
Intelligence.ArchitectBrief Single-call project briefing with topology and health metrics
Intelligence.Preflight Contract checklist pipeline (6 sections) with semantic tool ranking
Intelligence.SurgicalBriefing Layer 1+2 preprocessing: semantic search + knowledge graph enrichment
Intelligence.PlanValidator Graph-aware validation for code change plans (cycles, hub risk, blast radius)

10. MCP Layer (Build 155)

Canonical LLM integration path. Giulia is the eyes; the LLM lives in the client. External clients (Claude Code, Claude Desktop, or anything speaking MCP / REST) call Giulia for read-only data; reasoning happens client-side. This supersedes the legacy local-chat inference subsystem (Section 18 / TIER 3).

Giulia exposes a native Model Context Protocol (MCP) server alongside the REST API. MCP enables AI assistants like Claude Code to discover and call Giulia's tools directly as structured tool calls, without constructing HTTP requests.

Module Responsibility
MCP.Server Anubis MCP server — handles tools/call, tools/list, resources/read
MCP.ToolSchema Auto-generates 77 MCP tool definitions from @skill annotations on sub-routers (80 skills minus 3 non-MCP-compatible monitor endpoints: GET /api/monitor and GET /api/monitor/graph are HTML dashboards, GET /api/monitor/stream is an SSE event stream)
MCP.ResourceProvider 5 resource templates (giulia://projects/, giulia://modules/, giulia://graph/, giulia://skills/, giulia://status)
Daemon.Plugs.McpAuth Bearer token authentication via GIULIA_MCP_KEY env var (constant-time comparison)
Daemon.Plugs.McpForward Runtime forwarder to Anubis StreamableHTTP transport (defers init to avoid persistent_term race)

The MCP server is conditional — it only starts if GIULIA_MCP_KEY is set. Tool schemas are generated at boot from the same @skill annotations that power the Discovery API, ensuring REST and MCP always expose identical capabilities.

Client configuration (.mcp.json):

{
  "mcpServers": {
    "giulia": {
      "type": "http",
      "url": "http://localhost:4000/mcp",
      "headers": {
        "Authorization": "Bearer <GIULIA_MCP_KEY value>"
      }
    }
  }
}

11. Runtime Layer

The Runtime subsystem has grown beyond the core Inspector + Collector pair:

Module Responsibility
Runtime.Inspector BEAM introspection via :erlang APIs (memory, stats, processes)
Runtime.Inspector.Trace Short-lived per-module call tracing with 5-second kill switch
Runtime.Collector Periodic snapshot collector with burst detection (IDLE/CAPTURING FSM)
Runtime.Profiler Performance profile generator (template-based, offline, pure functions)
Runtime.IngestStore Buffers runtime snapshots from Monitor, fuses with static knowledge data
Runtime.Observer Observation controller for async collection sessions and HTTP push
Runtime.AutoConnect Auto-connect to target BEAM node on startup with exponential backoff
Runtime.Monitor Monitor lifecycle orchestrator (BOOT -> CONNECT -> WATCH -> PROFILING)

12. Request Flow

A typical API request follows this path:

HTTP request
    |
    v
Bandit (HTTP server)
    |
    v
Plug.Telemetry                  emits [:giulia, :http] telemetry event
    |
    v
Plug.Logger
    |
    v
Plug.Router (:match, :fetch_query_params, Plug.Parsers)
    |
    v
Endpoint.ex                     core routes + forward declarations
    |
    +-- forward "/api/index"        --> Routers.Index
    +-- forward "/api/knowledge"    --> Routers.Knowledge
    +-- forward "/api/intelligence" --> Routers.Intelligence
    +-- forward "/api/briefing"     --> Routers.Intelligence  (alias)
    +-- forward "/api/brief"        --> Routers.Intelligence  (alias)
    +-- forward "/api/plan"         --> Routers.Intelligence  (alias)
    +-- forward "/api/runtime"      --> Routers.Runtime
    +-- forward "/api/search"       --> Routers.Search
    +-- forward "/api/transaction"  --> Routers.Transaction
    +-- forward "/api/approval"     --> Routers.Approval
    +-- forward "/api/monitor"      --> Routers.Monitor
    +-- forward "/api/discovery"    --> Routers.Discovery
    +-- forward "/mcp"             --> Plugs.McpForward (MCP protocol)
    |
    v
Sub-router (e.g., Routers.Knowledge)
    |
    v
Daemon.Edge.resolve_ready/1     path resolution + scan-readiness (shared with MCP dispatch)
Knowledge.Facade / Search.Facade  string->typed coercion, defaults, response shape (shared with MCP)
    |  (the per-domain facade is the single place these live; the REST route
    |   and the MCP dispatch handler are thin renderers over the same call)
    v
GenServer.call to Knowledge.Store / Context.Store
    |
    v
ETS / libgraph lookup
    |
    v
Helpers.send_json/3   (REST renders the facade result as JSON / 4xx / 409;
                        MCP renders the same result as {:ok, _} | {:error, msg})

Core routes that remain in Endpoint.ex (not forwarded):

  • GET /health -- health check (node name, version)
  • POST /api/command -- main chat/command entry point
  • POST /api/command/stream -- SSE streaming inference
  • POST /api/ping -- lightweight path validation
  • GET /api/status -- uptime, active project count
  • GET /api/projects -- list active projects
  • POST /api/init -- initialize a project context
  • GET /api/debug/paths -- path mapping diagnostics
  • GET /api/agent/last_trace -- last inference trace
  • GET /api/approvals -- pending approval requests
  • GET /favicon.ico -- static favicon

13. Sub-Router Architecture (Build 94)

Before Build 94, Endpoint.ex was 1,331 lines containing all route handlers. The refactoring split it into 9 domain-specific sub-routers, reducing Endpoint to forwarding declarations plus core route handlers.

Each sub-router uses the Giulia.Daemon.SkillRouter macro:

defmodule Giulia.Daemon.Routers.Knowledge do
  use Giulia.Daemon.SkillRouter

  @skill %{
    intent: "Get modules that depend on a given module",
    endpoint: "GET /api/knowledge/dependents",
    params: %{module: "Elixir module name"},
    returns: "List of dependent modules",
    category: "knowledge"
  }
  get "/dependents" do
    # ...
  end
end

The use Giulia.Daemon.SkillRouter macro provides:

  • use Plug.Router with standard plugs (match, fetch_query_params, JSON parser)
  • import Giulia.Daemon.Helpers for shared response/path functions
  • @skill as an accumulate attribute for route metadata
  • __skills__/0 function generated at compile time (via @before_compile)

The __skills__/0 function powers the Discovery Engine (/api/discovery/skills, /categories, /search), which allows clients to discover available endpoints at runtime without hardcoding route tables.

Sub-routers and their domains:

Prefix Router Routes Domain
/api/index Routers.Index 10 Module/function index, scan, verify, compact, complexity
/api/knowledge Routers.Knowledge 27 Graph queries, metrics, insights, topology, conventions
/api/intelligence Routers.Intelligence 6 Briefing, preflight, architect, validate, report_rules
/api/runtime Routers.Runtime 16 BEAM introspection, trace, connect, profiles, ingest, observations
/api/search Routers.Search 3 Text search, semantic search, semantic status
/api/transaction Routers.Transaction 3 Transactional file operations
/api/approval Routers.Approval 2 Interactive consent gate
/api/monitor Routers.Monitor 7 Dashboard, Graph Explorer, SSE stream, history, observe start/stop/status
/api/discovery Routers.Discovery 4 Skill introspection, search, report rules
(core endpoint) Endpoint 10 health, command, ping, status, projects, init, debug, trace, approvals

Note: /api/briefing, /api/brief, and /api/plan all forward to Routers.Intelligence as aliases.

Edge / Facade layer (Build 163)

REST and MCP both front the same internal handlers, but originally each re-implemented param required-ness, defaults, and coercion (the MCP dispatch was meant to be a thin proxy and had drifted into a parallel implementation). That let the two protocols' param contracts diverge independently. Build 163 introduced a shared edge + per-domain facades so the contract lives once:

Module Responsibility
Daemon.Edge Domain-agnostic protocol edge: resolve_ready/1 (host->container path resolution + scan-readiness, returns plain data + actionable hint) and not_ready_message/1. Shared by every domain and both protocols.
Knowledge.Facade Per-domain coerce + Store call for knowledge endpoints (impact, style_oracle, unprotected_hubs, duplicates, pre_impact_check): owns defaults (depth/top_k/thresholds), response normalization, the schema_version stamp, and the embedding-availability check.
Search.Facade Same for the search domain (semantic): canonical result shape + top_k default.

Three layers: edge (resolve + readiness) -> domain facade (coerce + defaults + Store call) -> Store (typed, pure semantics — never learns about strings, HTTP, or defaults). A REST route body and the matching MCP dispatch handler are now thin renderers over the same facade call, each rendering the result in its own idiom (REST: JSON + 4xx/409; MCP: {:ok, _} | {:error, message} with the hint preserved as a binary string).

test/giulia/mcp/rest_mcp_parity_test.exs is the standing guarantee: for every facade-routed endpoint, the same input through REST and through MCP must agree. This also closed pre-existing divergences — MCP gained the scan-readiness and embedding-availability signals it lacked, and search/semantic converged on one canonical shape (it previously emitted raw structs via MCP and a functions-only count).

14. Semantic Search

Giulia embeds module and function descriptions into a vector space for semantic similarity search.

Model: sentence-transformers/all-MiniLM-L6-v2, loaded via Bumblebee into an Nx.Serving (Intelligence.EmbeddingServing). The model is approximately 90MB and is the primary reason the monitor node skips this child.

Indexing: On every scan, Intelligence.SemanticIndex embeds all module descriptions and function signatures. Vectors are stored in ETS and persisted to CubDB (L2) for warm restarts.

Search: Given a query string, the serving generates an embedding vector. The SemanticIndex computes cosine similarity against all stored vectors using Nx.dot, then ranks results with Nx.top_k.

Preflight integration: The /api/briefing/preflight endpoint uses semantic search to match a user's prompt against the skill intents declared across all sub-routers. The response includes a suggested_tools list ranked by cosine similarity, allowing clients to discover which API endpoints are most relevant to their current task. Graceful degradation: if EmbeddingServing is unavailable (model failed to load, or running on monitor node), suggested_tools returns an empty list.

15. Path Translation

Giulia runs inside Docker but receives file paths from clients on the host machine. Two modules handle path security and translation.

PathMapper

Giulia.Core.PathMapper translates between host paths and container paths using a prefix swap strategy.

Host:      /srv/code/MyProject/lib/foo.ex
Container: /projects/MyProject/lib/foo.ex

Mapping:   GIULIA_HOST_PROJECTS_PATH="/srv/code"
           Container prefix: /projects

The translation:

  1. Normalizes Windows backslashes to forward slashes
  2. Performs case-insensitive prefix matching for Windows drive letters only
  3. Swaps the host prefix with the mapped container prefix

GIULIA_PATH_MAPPING adds further prefixes beyond /projects, written host=container and separated by ;. Where several mappings could apply, the longest prefix wins, and a prefix matches only on a path-segment boundary, so /srv/code claims /srv/code/app but never /srv/codex.

No host path is compiled into the module: every mapping is supplied by the environment. One image therefore behaves the same under Docker Desktop on Windows, OrbStack on macOS, and native Linux, and a difference in behaviour can only follow from a difference in configuration. Case-insensitivity is confined to Windows drive-letter prefixes because /Users and /users are distinct directories on the Linux filesystem inside the container.

An unmapped path is returned unchanged. diagnostics/0 reports the active table and is surfaced on GET /health, so a misconfigured mount is visible in a single request rather than as an empty scan result later.

The reverse translation (to_host/1) inverts the same table for responses that include file paths, so clients see paths they can open locally.

PathSandbox

Giulia.Core.PathSandbox ensures Giulia can only access files under the project root -- the directory containing GIULIA.md (the project constitution). It:

  1. Expands the requested path to an absolute path (resolving .., symlinks)
  2. Verifies the expanded path starts with the sandbox root
  3. Rejects any path that escapes containment

This prevents the LLM from requesting reads of /etc/passwd, ~/.ssh/config, or any file outside the project boundary, regardless of how the path is constructed.

16. Visual Dashboards (Build 95, 151, 152)

Giulia ships two browser-based dashboards, both served as static HTML from the daemon's /api/monitor prefix.

Logic Monitor (/api/monitor)

Real-time telemetry dashboard. Every HTTP request, inference step, LLM call, and tool execution emits a :telemetry event, captured by Monitor.Telemetry handlers and pushed to Monitor.Store (a rolling 50-event buffer with SSE pub/sub).

Features:

  • SSE streaming: live event feed via /api/monitor/stream
  • Category filters: API, OODA, LLM, TOOL — toggle visibility per event type
  • Project scoping: dropdown auto-populated from events, filters by project path
  • Endpoint exclusion: right-click to exclude noisy paths (persisted in localStorage)
  • Response panel: click any API event to see its JSON response body
  • Think Stream: real-time display of LLM <think> blocks during inference
  • Cache/Graph panels: live project health (Merkle root, graph stats, top hubs)
  • Scans panel: scan event history with warm/cold/incremental badges

Events carry a project field (extracted from HTTP ?path= params via PathMapper, or from inference metadata). This enables per-project filtering when multiple codebases are being analyzed concurrently.

Graph Explorer (/api/monitor/graph)

Interactive dependency graph visualization powered by Cytoscape.js (loaded from CDN). Data source: GET /api/knowledge/topology returns the full module graph in Cytoscape-ready format (nodes with heatmap scores/centrality, edges with labels).

Four view modes:

  • Dependency: full module topology, nodes colored by heatmap zone, sized by fan-in
  • Heatmap: emphasizes red/yellow modules, dims healthy green nodes
  • Blast Radius: click any module to highlight depth-1 (orange) and depth-2 (blue) impact
  • Hub Map: highlights high-degree modules, dims low-degree periphery

Layout options: force-directed (cose), hierarchical (breadthfirst), circle, concentric. Click any node for a details panel showing score, zone, fan-in/out, complexity, and test status. Hover to highlight connected edges.

The topology endpoint combines data from three sources in a single call:

  1. Knowledge.Store.all_dependencies/1 — edge list with labels
  2. Knowledge.Store.heatmap/1 — per-module scores and zones
  3. Knowledge.Store.find_fan_in_out/1 — centrality data

Both dashboards share a navigation bar for switching between Monitor and Graph Explorer views.

17. Correctness-Floor Invariants

Giulia enforces cross-store sync and extraction-output stability through three test-surface layers that ship alongside the code. Each catches a different class of regression that property-style or example-style tests miss on their own.

L1↔L2↔L3 Verifier Endpoints

Giulia.Persistence.Verifier and Giulia.Storage.Arcade.Verifier implement round-trip integrity checks between the three storage tiers. They're exposed both as HTTP endpoints for live-daemon use and as mix-test jobs for CI:

  • GET /api/knowledge/verify_l2?path=...&check=all — L1 ETS ↔ L2 CubDB round-trip for graph, AST, and metric caches. Vertex-set parity, edge-count parity, and stratified sample identity per payload.
  • GET /api/knowledge/verify_l3?path=...&sample_per_bucket=N — L1 → L3 ArcadeDB CALLS. Stratified sample across :via buckets (:direct, :alias_resolved, :erlang_atom, :local) plus a ?/! orthogonal cross-cut, and total-count parity.
  • test/giulia/persistence/verifier_test.exs (11 tests) and test/giulia/storage/arcade/verifier_test.exs (4 tests) drive the same verifier functions from mix test, with both happy-path assertions and drift-detection cases (deliberately corrupted L2/L3 state that the verifier must classify correctly).

Filter-Accountability Regression Tests

For every filter predicate in a cross-store pipeline, both sides are tested: drop-side fixtures parametric over the filter's criteria, AND pass-through fixtures strictly larger than the drop set. Pass-through is what catches silent over-match — a predicate that rejects more than it claims to. Applied to four surfaces, this pattern caught 11 distinct silent-over-match bugs on first run (Indexer.ignored?/1 multi-segment dir, ToolSchema.mcp_ compatible?/1 /stream substring, Conventions.check_try_rescue_flow_control/ 3 source-text String.contains?, Topology.fuzzy_score/2 empty-needle String.contains?(_, "") == true).

Property Tests + Golden Fixtures

StreamData properties cover the pure-function layer: Knowledge.Builder. build_graph/1 (determinism, module vertex parity, function vertex coverage, label sanity), Topology.fuzzy_score/2 (bounded tier set, empty-needle absorbent, reflexive 100), Conventions.walk_ast/3 (determinism, no-crash on generated Elixir, violations well-formed), and the extraction passes themselves (module/function shape, per-module attribution).

Golden fixtures in test/fixtures/extraction/ freeze the full file_info output from Processor.analyze/2 for six curated source cases (predicate/ bang names, default args, moduledoc variants, framework callbacks, protocols

  • defimpl, nested modules, macros + guards). Any drift in extraction output produces a visible diff against the frozen .expected.exs that the reviewer must ratify. Regeneration: GOLDEN_UPDATE=1 mix test test/giulia/ast/golden_ fixtures_test.exs.

18. Known Blind Spots

Static analysis cannot see everything that happens at runtime. The 12-pass builder + the dispatch-patterns config close most of the gap; this section names what remains, deliberately, so consumers know where "this looks dead" is a tool limitation rather than a real finding.

What the AST passes structurally cannot resolve

Pattern Example Why it's invisible Mitigation
Variable-bound module dispatch mod = pick_provider(); mod.run(args) The callee module is a runtime value, not a literal __aliases__ AST node Out of scope — the call graph cannot be sound here
Module.concat/1,2 runtime construction apply(Module.concat([prefix, suffix]), :run, []) The atom is composed at runtime from arbitrary inputs Out of scope
Telemetry handler attachment :telemetry.attach(:id, [:event], &Mod.fn/4, _) Captures are stored in :telemetry's ETS table at runtime; the static walker sees the &Mod.fn/4 capture (Pass 10 picks it up as :capture_ref) but not the activation site Pass 10 covers the capture; activation invisible by design
Mox / dynamic mocks Mox.defmock(MockedX, for: X); MockedX.fn() Mock modules are defined at compile time of the test file, only exist in :test env, and are routed via Mox at runtime Tests are excluded from the scan; :test_only category surfaces residuals when a function is referenced from tests but called nowhere else
apply/2,3 with a computed atom argument apply(@modules, mod, args) Pass 10 handles literal apply(M, :f, [_, _]) but not when M is a variable Out of scope — covered by library_public_api / genuine classification when residuals surface
Macro-injected calls outside known behaviours A library's __using__/1 injects def x, do: ... plus calls into another library not in MacroMap The injected definition isn't in the source AST; the call site is also injected priv/config/dispatch_invariants.json (known_behaviour_callbacks) covers ~24 stdlib/ecosystem behaviours; library-specific macros need a project-side @dead_code_ignore or a dispatch_patterns.json entry
Runtime-registered routes Phoenix.Router macros are AST-visible (Pass 9 covers them); custom routers building routes from a config map at boot time are not The route table is data computed at runtime Out of scope; manual dispatch_patterns.json entry if the project uses one

Supervision topology (Pass 12)

Pattern Example Why it's invisible Mitigation
DynamicSupervisor children DynamicSupervisor.start_child(sup, spec) Children are started at runtime, never declared Supervisor vertex emitted with dynamic: true; an empty child list here is correct, not unresolved
Child list built by a function call, Enum.* or comprehension Supervisor.start_link(List.flatten(children), opts) — Plausible's actual shape Outside the bounded binding resolution (single-assignment vars, literal lists, ++ chains, enclosing function only) Supervisor emitted with children_unresolved: true and no child edges — an explicit gap, surfaced on the supervisor node in /api/knowledge/supervision. A partial list presented as complete would become a false negative in every check reasoning over the tree
Children of external supervisors {Registry, …}, {Bandit, …} Defined in dependency source, outside the scanned project Vertex emitted with external: true; the tree stops there rather than descending into deps
Registry-based via-tuples {:via, Registry, {R, id}} Process identity is runtime data Module-level resolution only; dependent checks carry a confidence flag
Process identity vs module identity one module started N times A module started repeatedly is one vertex cross_process_call_cycle reports high confidence only when every endpoint is a name: __MODULE__ singleton, medium otherwise

How residuals get classified

After detection, every dead-code candidate is routed through Giulia.Knowledge.DeadCodeClassifier. Categories in precedence order:

  1. :test_only — referenced from *_test.exs files. Tests are excluded from the scan, so the function is reachable from tests but unreached from production code.
  2. :library_public_apidef (public, not defp) on a project whose mix.exs has no application/0 :mod entry (i.e. a library, not an OTP app). Public functions in a library are exported for downstream consumers the analyzer cannot see.
  3. :genuine — none of the above. Most likely real dead code.
  4. :uncategorized — reserved. Future signals (variable-bound dispatch detectors, etc.) can land additively.

The ?relevance=high|medium|all filter (v0.3.8+) on /api/knowledge/dead_code maps directly: high = :genuine only; medium = :genuine + :uncategorized (matches the actionable rollup); all = unfiltered.

Canonical residual baseline (2026-04-29)

Reference points captured during the empirical-refactor loop. These are the residuals the tool considers irreducible against well-known codebases — useful as smoke-test signal that a new detector pass hasn't regressed:

Codebase Residuals Categorized as
Plausible CE (analytics-master) 3 2 true positives + 1 SiteEncrypt accept
AlexClaw 0
Plug 1.19.1 1 1 :library_public_api
Bandit 1.10.4 2 2 :library_public_api

A new detector or scoring change that moves these numbers warrants a slice-skeptical sanity check (per the empirical-refactor loop in GIULIA.md) before being declared an improvement.

Project-side escape hatches

  • @dead_code_ignore true module attribute — opt-out for an entire module, used when the static analyzer demonstrably cannot resolve a project-specific runtime dispatch pattern. Used sparingly — the default is to extend dispatch_patterns.json with a universal pattern rather than per-module suppression.
  • ?suppress=rule:Mod1,Mod2;rule2:Mod3 on /api/knowledge/conventions — per-rule per-module suppression for conventions that legitimately don't apply (e.g. process_dictionary:Auth.Context for code that has documented reasons to use it).
  • GIULIA.md in the project root — the project constitution. Used by the indexer for project-root detection and as a free-form notes surface for human/agent context that doesn't fit any of the above.

Deprecated subsystem: legacy local-chat inference (v0.3.8+)

Early Giulia (v0.x) shipped a self-hosted inference subsystem — Giulia itself ran an internal Observe-Orient-Decide-Act loop, calling out to LLM providers (LM Studio, Anthropic, Gemini, Groq, Ollama) and dispatching write-tools (patch_function, bulk_replace, rename_mfa) behind interactive approval gates. Entry point: POST /api/command and POST /api/command/stream.

That model is deprecated as of v0.3.8. Giulia's canonical role is the read-only data surface (REST + MCP); the LLM is somebody else's problem (Claude Code, Claude Desktop, any MCP/REST client). The subsystem still loads and the endpoints still respond — for backwards compatibility — but no new work goes there.

Modules in the deprecation set (will be removed in v0.4.0):

  • lib/giulia/inference/ — 32 modules (Inference.Pool, Inference.Approval, Inference.Events, Inference.Trace, Inference.Supervisor, Inference.ContextBuilder, Inference.ToolDispatch, Inference.Transaction, Inference.Escalation, Inference.RenameMFA, Inference.BulkReplace, Inference.Verification, …)
  • lib/giulia/provider/ — 6 LLM provider modules (Anthropic, Gemini, Groq, LM Studio, Ollama, Router)
  • lib/giulia/tools/{patch_function, bulk_replace, rename_mfa}.ex — write-tools only ever invoked through the inference dispatcher
  • HTTP endpoints POST /api/command, POST /api/command/stream, /api/approval/*, /api/transaction/*
  • MCP tools under approval_* and transaction_* prefixes
  • Compose env vars LM_STUDIO_URL, ANTHROPIC_API_KEY, GROQ_API_KEY, GEMINI_API_KEY

Why deprecated rather than removed today: the cleanup is a half-day of cross-cutting work (24 test files, supervision-tree changes, endpoint removals, env-var cleanup, CHANGELOG breaking-change entry). v0.4.0 cuts it.

Document History

One-line per release listing what changed in this document. The codebase itself is the source of truth; this is a navigation aid for readers returning after a few releases.

Doc version Date Highlights
Build 164 / v0.3.8 2026-06-09 Plug-collaudo bug-fix round (3 fixes). Conventions attribution: extracted Context.LineResolver (line→function→module) shared by the conventions analyzer and the Credo correlator, fixing multi-module files that mis-attributed every finding to the file's first module. Enrichment payload_path: project-relative paths (tmp/credo.json) now resolve against the project dir, matching the documented contract. dead_code cache: invalidated on enrichment ingest via a telemetry handler ([:giulia, :enrichment, :ingest]Knowledge.Store) — a direct call would close a 139-module dependency cycle. Report Stage 7 existence check switched from a shape test to the positive tools_ingested != [] signal (aligned with Section 10).
Build 163 / v0.3.8 2026-06-08 New Edge / Facade layer subsection (Section 13) + request-flow diagram updated: the MCP-thin-proxy refactor introduced Daemon.Edge (shared resolution + scan-readiness) and per-domain Knowledge.Facade / Search.Facade (coercion, defaults, response shape) so REST and MCP route through one contract instead of re-implementing it. Closed REST/MCP divergences (MCP gained scan-readiness + embedding-availability signals; search/semantic converged on one canonical shape + total count; schema_version single-sourced). Build 162: @skill params migrated to structured maps (discovery/MCP-inputSchema metadata).
Build 161 / v0.3.8 2026-04-29 Configuration surface table grew to 5 entries (dispatch_invariants.json, relevance.json added). Section 5 split: External Tool Enrichment, Knowledge Graph, Intelligence Layer, MCP Layer, and Runtime Layer promoted from H3 to H2. New Configuration Surface H2 (lifted from a Section 4 subsection with brief design-philosophy framing). New Known Blind Spots H2 listing what the AST passes structurally cannot see and the residual taxonomy. Consolidator framing corrected: doc now acknowledges it was a skeleton until v0.3.7. The 3 non-MCP-compatible endpoints named inline. Inference / local-chat subsystem (TIER 3, OODA-loop pipeline, POST /api/command, write-tools, LLM provider tree) marked deprecated — canonical LLM integration is now external clients calling Giulia over MCP / REST. Removal scheduled for v0.4.0.
Build 160 / v0.3.7 2026-04-29 Doc baseline alongside the 2026-04-29 trio (orchestration lift, self-scan SIGSEGV fix, verifier parity).