Skip to content

Releases: thatsme/giulia

v0.3.6 — Multi-defimpl Phase 2 — function fan-out across sibling impls

28 Apr 03:24

Choose a tag to compare

Closes the Phase-2 deferral from 4136844 (multi-type defimpl extraction, v0.2.x). defimpl Proto, for: [T1, T2, T3] produces N sibling impl modules sharing one body. Phase 1 emitted N module entries (each with impl_for set), but attributed the body's functions to the first sibling only — siblings 2..N were left with zero functions and therefore invisible to Pass 7's :protocol_impl edge synthesis. The deferral was explicit in the original commit message: "Phase 2 (function-set duplication across siblings for full graph parity) deferred."

The fix is universal — fires for any project that uses the multi-type defimpl form, regardless of protocol, sibling count, or body shape. The deferral became visible after v0.3.5's topology rollup made Pass 7 edges visible at module level. It surfaced concretely while scanning analytics-master (the FOSS Plausible Analytics codebase used as Giulia's complex-shape test bed) — 5 isolated impl modules from one source file traceable to two multi-defimpl declarations:

# Found in the analytics-master test bed at extra/lib/plausible/audit/encoder.ex —
# the same shape would surface in any codebase that uses multi-type defimpl:
defimpl Plausible.Audit.Encoder, for: [Integer, BitString, Float] do
  def encode(x, _opts), do: x
end

defimpl Plausible.Audit.Encoder, for: [DateTime, Date, NaiveDateTime, Time] do
  def encode(x, _opts), do: to_string(x)
end

Integer and DateTime (first siblings) had encode/2 and Pass 7 fired. The other 5 had nothing and were silent.

Fix

Stack representation in Extraction.function_traverse_pre/2 now uses a tagged {:multi, [name1, name2, name3]} for multi-defimpl scopes. The function emitter pattern-matches on the stack head: when it's {:multi, names}, one function record is fanned out per sibling. Single-defimpl scopes unchanged. function_traverse_post/2 still pops one stack entry regardless of shape.

Multi-defimpl fixture (test/fixtures/extraction/protocols_defimpl_multi.expected.exs) updated to require all expected encode/1 records (Integer, BitString, Float, then Atom). The prior fixture only expected Integer + Atom and locked in the bug — that was the test contract that allowed Phase 1 to ship without Phase 2 noticing.

Empirical impact (analytics-master test bed)

Force-rescan after deploy:

Metric After v0.3.5 After v0.3.6
Edges 3631 3636
Project unreached 24 19

The 5 expected siblings (BitString, Float, Date, NaiveDateTime, Time) all reconnected. The remaining 19 isolates are honest static-analysis blind spots that would surface in any codebase that uses the same patterns — mox-registered mocks, mix data-migration scripts, plugins registered in config/runtime.exs, LiveView .heex callees (slice-a class), exception structs, and template helpers. None are extraction bugs.

Universality

The fix is shape-agnostic — fires for any project that uses defimpl X, for: [...] regardless of N, protocol, or body shape. Per the no-hardcoded-data rule, no library allowlist; no codebase-side opt-in. The analytics-master file just happened to be the first one that exercised the deferred path on a real codebase; the same shape is valid Elixir and could appear in any project.

Tests

59/59 pass across the focused subset (extraction + store + warm_restore). The fixture comparison verifies exact function-record parity per sibling.

v0.3.5 — Topology rolls up Pass 7-11 edges; Graph Explorer isolates panel

28 Apr 03:24

Choose a tag to compare

Visualization-honesty fix. GET /api/knowledge/topology (the data source for /api/monitor/graph) used Knowledge.Store.all_dependencies/1, which filters edges to those whose both endpoints carry the :module vertex label. That filter silently dropped every edge synthesized by Builder Passes 7-11:

Pass Synthesizes Endpoint shape
7 — protocol_impl protocol module → impl's encode/2 etc. module → function
8 — behaviour_impl behaviour module → impl's callback function module → function
9 — router_dispatch router module → controller action module → function
10 — mfa_ref / capture_ref / apply_ref caller fn → target fn function → function
11 — use_import_ref caller fn → imported fn function → function

Net effect across every analyzed codebase: defimpls of project-defined protocols, Phoenix controller actions, MFA-tuple-dispatched modules, and macro-injected import targets all rendered as isolated nodes in the Graph Explorer — even though dead_code analysis (which uses the full graph) correctly counted them as live. The visual told a different story from the analysis.

The fix is universal — applies to any Elixir project regardless of size or framework. Measured impact below uses three test beds: Giulia itself (170 files, the tool eating its own dogfood), AlexClaw (small Phoenix app), and analytics-master (FOSS Plausible Analytics — a large multi-app Phoenix codebase used as Giulia's complex-shape stress test).

Fix

New Reader.all_dependencies_with_rollup/1 walks every edge in the graph and projects function-vertex endpoints up to their owning module via Graph.vertex_labels/2. Self-loops introduced by intra-module rollup (e.g. a defimpl whose body recurses through its own protocol) are dropped. Function endpoints whose parent module isn't in the project's module set are skipped (no phantom-module invention). /api/knowledge/topology switched to the rollup variant; all_dependencies/1 kept as-is for callers that need a strict module-level filter.

Graph Explorer — isolates panel

priv/static/graph.html now pulls isolated nodes off the Cytoscape canvas into a right-side sidebar split into two sections:

  • External boundaries (purple bar, complexity == 0) — GenServer, Phoenix.Endpoint, Plug, Mix.Project, etc. The indexer records them as vertices because something in the analyzed project does use X / @behaviour X / defimpl X for: T, but never traverses their bodies (out of project scope). Meta column shows ← N where N is the metric-layer's count of project modules referencing them.
  • Project unreached (orange bar, complexity > 0) — modules in the analyzed project with no statically-resolved callers/callees. Same class as the slice-a .heex callee residuals, slice-d runtime apply/3 cases, and slice-z true positives. Meta column shows cx N (complexity).

Hide button on the panel header; "Isolates ›" button reappears top-right when the panel is hidden.

Empirical impact (measured against three test bed codebases)

Codebase Edges before Edges after Isolates before Isolates after
giulia 886 918 5 0
AlexClaw 722 771 12 4
analytics-master 3189 3631 61 31

giulia is fully connected — the 5 prior isolates (GenServer, Application, Plug, Supervisor, Mix.Project) all gained at least one rolled-up function-level edge. The big drop on the analytics-master test bed (61 → 31) surfaced a Phase-2 deferral in 4136844 (multi-defimpl extraction) — when scanning that codebase's audit-encoder file the second-and-later siblings of defimpl X, for: [T1, T2, T3] had no functions and therefore no Pass 7 edges to roll up. Closed in v0.3.6.

Tests

4 new tests in test/giulia/knowledge/store_test.exs lock the rollup contract: function-endpoint roll-up, self-loop drop, orphan-function skip, and parity with all_dependencies/1 for direct module↔module edges. 25/25 pass.

v0.3.4 — Warm-restored projects bypass the not_indexed 409 gate

28 Apr 03:24

Choose a tag to compare

Reliability fix. Persistence.WarmRestore (e8c3fa3, v0.2.x) repopulated the L1 ETS graph from the L2 CubDB cache on every boot so projects survived docker compose restart without re-scanning. But it never touched Context.Indexer's per-project state map. The scan-state gate (075816a, v0.2.x) added strict 409 not_indexed responses on every scan-dependent endpoint when Indexer.status(path) returned %{status: :idle, file_count: 0} — which is exactly what every warm-restored project looked like.

Net effect: warm-restore was a no-op for everything past /api/projects. Selecting a warm-restored project in the Graph Explorer rendered nothing; GET /api/knowledge/topology returned {"error":"project has never been scanned","state":"not_indexed",...} with HTTP 409. Surfaced when opening http://localhost:4000/api/monitor/graph after a daemon restart and noticing only the project scanned in the current daemon lifetime had any data.

The bug is universal — it affected every warm-restored project regardless of language, framework, or size. Verified against three Elixir codebases used as test beds (see "Verified" below).

Fix

New Indexer.register_warm_restored(project_path, file_count) public API + matching handle_cast clause. WarmRestore.run_for/1 calls it after each successful Loader.restore_graph/1, deriving file_count from the restored graph's vertex count via Knowledge.Store.stats(path) (any positive value satisfies the gate). last_scan stays nil — the original scan timestamp isn't recoverable from L2 metadata, and honest is better than fabricated.

Verified

Test beds: AlexClaw (small Phoenix app) and analytics-master (FOSS Plausible Analytics, a large multi-app Elixir/Phoenix codebase used as Giulia's complex-shape test target). Both were warm-restored from L2 cache after a docker compose restart.

  • Pre-fix: GET /api/knowledge/topology?path=/projects/AlexClaw returned 409 / 143 bytes. Same shape on analytics-master.
  • Post-fix on the same daemon (after warm-restore, no re-scan): 200 / 97974 bytes (AlexClaw); 200 / 450128 bytes (analytics-master).
  • 5/5 tests pass in test/giulia/persistence/warm_restore_test.exs, including a new test that asserts Indexer.status(project) reports :idle with file_count > 0 after run_for/1.

Note

file_count semantics shift slightly for warm-restored projects: previously it was the AST file count from Context.Store.stats(path).ast_files (set at scan-complete). For warm-restored projects it's now the graph's vertex count, which is bigger (modules + functions + structs + behaviours). Honest about its source — last_scan: null distinguishes warm-restored from fresh-scanned in the status payload — but a different number than a fresh scan would show.

v0.3.3 — Indexer post-scan pipeline non-blocking

27 Apr 20:14

Choose a tag to compare

Reliability fix. The scan task itself was already properly supervised, but Indexer.handle_cast({:scan_complete, ...}) ran the post-scan pipeline (mix compile + Knowledge.Store.rebuild + SemanticIndex.embed_project) inside the GenServer process. While that work ran (10-30+ seconds on a Phoenix app or on Giulia's self-scan), the Indexer mailbox was blocked. Any Indexer.status/1 GenServer.call queued behind it and hit the default 5s timeout — caller crashed, router emitted 500, docker healthcheck eventually restarted the container.

Surfaced specifically when scanning Giulia itself (170 files of meta-AST work — heavier than typical project code; previous Plausible / AlexClaw scans completed within 5s and never tripped the timeout).

Fix

handle_cast({:scan_complete, project_path}, ...) now:

  1. Computes stats (fast, in-process)
  2. Sets the project state to :building (new status atom)
  3. Spawns a Task.Supervisor.start_child(Giulia.TaskSupervisor, ...) running run_post_scan_pipeline/1
  4. Returns {:noreply, new_state} immediately

The task casts back {:scan_complete_done, project_path} when the pipeline finishes; that callback flips the status to :idle. Errors inside the pipeline are caught with rescue and logged but never crash the Indexer.

Daemon.Helpers.scan_state/1 recognizes :building as a :pending state — knowledge endpoints return 409 Conflict during the rebuild window with the same UX as :scanning.

Verified

  • Concurrent stress: 5 parallel Indexer.status/1 polls during an active force-rescan of Giulia all return instantly with {"status":"building","file_count":170}. Previously these queued in the mailbox and timed out at 5s.
  • Container survives the stress test (no docker restart).
  • 1063 unit tests + 13 properties pass; 0 regressions.

Type spec change

New atom :building added to Giulia.Context.Indexer.scan_status type union. Consumers reading the status field via the typed API should handle this state. The scan_state/1 helper already does.

v0.3.2 — Alias resolution + template scope fix

27 Apr 20:02

Choose a tag to compare

Patch release. Two fixes that together produce the cleanest dead-code state across canonical codebases since the project began.

Fixed — TestReferences alias resolution (roadmap item 2h)

Giulia.Tools.TestReferences.referenced_functions/1 now resolves alias directives before recording MFA strings. Previously, a test using alias AlexClawTest.Skills.EchoSkill followed by EchoSkill.config_help() recorded "EchoSkill.config_help/0" — a short form the dead-code classifier's :test_only predicate could never match against the real "AlexClawTest.Skills.EchoSkill.config_help/0".

Two-pass walker: first builds the file's short→full alias map (single-segment, multi-alias alias Mod.{A, B}, and alias Mod, as: Other with Sourceror keyword-key unwrapping), then walks for refs and expands head aliases via Map.fetch. Mirrors the alias-resolution pattern from Giulia.Knowledge.Metrics.collect_all_calls/1.

Fixed — TemplateReferences scoped to project source roots

Giulia.Tools.TemplateReferences.scan/1 now scopes its *.heex / *.eex walk to the project's source roots (returned by Giulia.Context.ScanConfig.absolute_roots/1 — typically lib/ plus test/support/ plus whatever mix.exs elixirc_paths/1 adds). Previously walked everything under project_path/**, including deps/ (third-party templates from phoenix_live_dashboard, phoenix codegen, etc.) and _build/, whose qualified function references coincidentally over-exempted local modules.

The over-broad scan was caught by auditing v0.3.1's verification run: AlexClaw went from 6 dead to 0 dead with the wide scan — a too-good-to-be-true result that turned out to be coincidental name overlap with phoenix_live_dashboard templates.

Empirical impact on canonical codebases

Codebase v0.3.1 dead v0.3.2 dead Notes
AlexClaw 6 0 All 6 EchoSkill.* test-only entries now correctly resolved through aliases
Plausible 10 3 7 alias-blind entries resolved; remaining 3 = canonical residual

The 3 remaining on Plausible are the documented irreducible residual:

  • Plausible.TestUtils.tmp_dir/0 — true positive (no callers anywhere)
  • PlausibleWeb.channel/0 — true positive (no use PlausibleWeb, :channel)
  • PlausibleWeb.Endpoint.app_env_config/0 — accept-as-residual (SiteEncrypt variable dispatch, not statically resolvable)

This is the cleanest residual state we've measured.

Migration

L2 caches auto-invalidate via CodeDigest. First daemon restart on v0.3.2 rebuilds graph + metrics from existing ASTs.

v0.3.1 — Slice-a: HEEx/EEx template scanner

27 Apr 19:49

Choose a tag to compare

Patch release that completes the deferred slice-a from v0.3.0. The :template_pending category was a placeholder for "we know this might be template-callable but we never wrote the parser." Slice-a builds the parser (Giulia.Tools.TemplateReferences), so template-referenced functions are now exempted from the dead-code list at detection time rather than reaching the classifier.

Changed — observable analysis output

  • /api/knowledge/dead_code no longer emits :template_pending. The category is removed from the type union. Functions called from *.heex / *.eex templates are exempted from the response entirely (matching Pass 7-11's exemption pattern). Consumers reading summary.by_category will see the field gone; if you were depending on its presence, switch to Map.get(summary.by_category, :template_pending, 0).
  • Empirical delta on Plausible: the 3 PlausibleWeb.{EmailView,LayoutView,SiteView}.plausible_url/0 entries that v0.3.0 mis-classified as :template_pending are now correctly exempted — they're called as {plausible_url()} from templates/layout/base_email.html.heex etc., resolved through the conventional templates/<view>/...<App>Web.<View>View mapping.

Added — Giulia.Tools.TemplateReferences

New scanner that walks *.heex / *.eex files and extracts function references in three syntactic surfaces:

  • HEEx curly interpolation: {Module.Sub.fn(args)}
  • Old EEx interpolation: <%= Module.Sub.fn(args) %> and <% expr %>
  • HEEx component invocation: <Module.Sub.fn args /> (qualified) and <.local_fn args /> (local)

Returns %{qualified: MapSet, local_per_file: %{path => MapSet}}. The local-per-file map is resolved to a target module via Phoenix path conventions:

  1. Strip .heex/.eex/.html; if the resulting .ex sibling exists in the project's module index, use that module (LiveView / Component / colocated templates).
  2. lib/<app>_web/templates/<view>/<file>.html.heex<App>Web.<View>View (older Phoenix layout).

Metrics.dead_code_with_asts/3 consumes both signals as additional exemption sets alongside protocol_impl_modules, router_actions, reference_targets, etc.

Known limitation surfaced (not introduced) — TestReferences alias-blindness

The cold-rescan triggered by this slice exposed a pre-existing limitation in Giulia.Tools.TestReferences.referenced_functions/1: it collects qualified Mod.fn/N strings from *_test.exs but does not resolve alias directives. A test using alias AlexClawTest.Skills.EchoSkill then EchoSkill.config_help() records "EchoSkill.config_help/0", not the fully-qualified form, so the classifier's :test_only predicate misses the match. Filed as roadmap item 2h. Fix mirrors the resolve_alias pattern from metrics.ex collect_all_calls/1.

Empirical impact: AlexClaw cold-rescan now lists 6 EchoSkill functions as :genuine dead when they're really :test_only. Plausible has similar regressions in Plausible.PromEx.Plugins.PlausibleMetrics.execute_*_metrics/0 and Plausible.InstallationSupport.Checks.*.report_progress_as/0. Out of scope for this slice; will be addressed in a follow-up patch.

Migration

  • L2 caches auto-invalidate via CodeDigest (TemplateReferences added to @tier_modules). First daemon restart on v0.3.1 rebuilds graph + metrics from existing ASTs.
  • Backward-compatible response shape — dead_code keeps :dead, :count, :total, :summary fields; only :summary.by_category.template_pending goes away.

v0.3.0 — Categorized signals + external tool enrichment

27 Apr 19:12

Choose a tag to compare

The minor bump (v0.2.x → v0.3.0) is justified by new public API surface: two new endpoints, additive fields on three existing endpoints, a new plugin behaviour, and a new persistence keyspace. Everything is backward-compatible — old consumers reading existing fields keep working.

Headline — dead-code categorization

/api/knowledge/dead_code entries now carry a :category field and the response gains a :summary map. Categorization turns the irreducible residual into honest signal: most entries on real codebases aren't bugs, they're library public API, test-only entry points, or template-only references blocked on the deferred .heex slice.

%{
  dead: [
    %{module, name, arity, type, file, line,
      category: :genuine | :test_only | :library_public_api |
                :template_pending | :uncategorized}
  ],
  count, total,
  summary: %{
    by_category: %{...counts...},
    irreducible: integer,   # test_only + library_public_api + template_pending
    actionable:  integer    # genuine + uncategorized
  }
}

Precedence: :test_only > :library_public_api > :template_pending > :genuine. Empirical results on canonical codebases:

Codebase Dead After categorization
AlexClaw 1 1 genuine
Plug 1 1 library_public_api
Bandit (post-Pass-10-ext) 2 2 library_public_api

Headline — external tool enrichment ingestion

Pluggable behaviour Giulia.Enrichment.Source plus a JSON-driven registry (priv/config/enrichment_sources.json). Two sources ship: Credo and Dialyzer. Adding Sobelow / ExDoc / Coverage is one parser module + one JSON line.

New endpoints:

  • POST /api/index/enrichment — ingest tool output. Body: {tool, project, payload_path}. Validates payload_path against an allowlist (scan_defaults.json :enrichment_payload_roots) to prevent the endpoint from becoming an arbitrary-file-read primitive. Returns {tool, ingested, targets, replaced}.
  • GET /api/intelligence/enrichments?path=X&mfa=Y (or &module=Y) — uncapped drill-down per-vertex queries. Returns {findings: %{tool => [findings]}, target}. Distinguishes %{} (never ingested for project) from %{credo: []} (ingested, no findings on this target) via a sentinel marker — different signals for consumer agents.

Two consumer endpoints surface findings inline:

  • pre_impact_checkaffected_callers[*].enrichments carries per-caller findings so refactor decisions consider type warnings + style issues alongside blast radius.
  • dead_code — entries gain :enrichments so type-warning + dead-code residual surface together.

Both apply caps (priv/config/scoring.json :enrichments): errors uncapped, top-3 warnings per entry, drop info, per-response cap of 30 deduplicated by {check, severity}. Capping logic is shared via Giulia.Enrichment.Consumer.

Replace-on-ingest, decoupled lifecycle. Tool ingest cadence (CI on every PR) is decoupled from source rescan cadence — Persistence.Writer.clear_project/1 filters enrichment keys so re-extraction doesn't force re-ingestion.

Provenance per finding: tool_version, run_at, per-file source_digest_at_run. Consumers can flag stale findings on changed files.

Severity is config-driven. Each source carries a severity_map. Credo: 5 entries ("warning" => "error" covers Credo's misleadingly-named real-bug category). Dialyzer: 47 entries covering dialyxir's full warning catalogue. Tunable without recompile.

Telemetry from day one: [:giulia, :enrichment, :ingest | :parse_error | :read].

Known limitation — function line-range precision

Per-function line ranges are derived as next_function.line - 1 after stable per-file sort. This loses precision against multi-clause definitions, defmacro bodies with quote do, multi-arity definitions interleaved with other functions, and @doc heredoc gaps. Empirical impact: most Credo findings on Plausible currently resolve to module-scope rather than function-scope. The three-path arity-resolution waterfall in each parser surfaces this honestly via scope: :module and a :resolution_ambiguous flag rather than guessing wrong arity. Roadmap item 2g — capture true :line_end during AST extraction — predicted to sharpen attribution.

Also since v0.2.2 — what shipped incrementally to users

  • Dispatch-edge synthesis (Pass 7-11) — protocol_impl, behaviour_impl, router_dispatch, mfa_ref / capture_ref / apply_ref / mfa_arg_ref, use_import_ref. Net effect across canonical codebases: −97 false-positive dead_code reports.
  • Reference-based test detection (slice E2) — replaces filename-matching has_test; 38 modules correctly reclassified yellow→green on Plausible.
  • Config externalizationscoring.json, dispatch_patterns.json, scan_defaults.json, enrichment_sources.json, all auto-invalidated via CodeDigest envelope.
  • MCP server (Build 155) — 71 tools auto-generated from @skill annotations on sub-routers. Bearer-auth gated by GIULIA_MCP_KEY.
  • OTP cleanup tier 1 + 2 — supervised SSE inference, ContextManager :exit handling, state-recovery invariant, ETS heir, reconcile paths.
  • Correctness invariantsverify_l2, verify_l3 endpoints with stratified sample-identity checks; 15 mix-test drift detectors.
  • Filter-accountability tests — 11 distinct silent-over-match bugs caught.
  • Force-rescan + path validation?force=true on /api/index/scan; 422 on missing/invalid paths or missing project marker.
  • Multi-type defimpldefimpl X, for: [T1, T2, T3] now extracts as N proper impl modules instead of Unknown.

Migration

  • L2 caches auto-invalidate via CodeDigest. First daemon restart on v0.3.0 rebuilds graph + metrics from existing ASTs (cheap; AST cache unaffected).
  • All field additions are backward-compatible. Consumers reading existing keys (dead, count, total, affected_callers, etc.) continue to work without changes.

🤖 Generated with Claude Code

v0.2.2 — MCP Server

28 Mar 09:40

Choose a tag to compare

MCP Server (Build 155)

Native Model Context Protocol server — AI assistants like Claude Code can now discover and call Giulia's analysis tools directly as structured tool calls, without constructing HTTP requests.

New: MCP Layer

  • 74 MCP tools auto-generated from @skill annotations at boot — REST and MCP always expose identical capabilities
  • 5 resource templates via giulia:// URI scheme (projects, modules, graph, skills, status)
  • Bearer token auth via GIULIA_MCP_KEY env var (constant-time comparison, MCP disabled when unset)
  • Anubis StreamableHTTP transport at /mcp with Peri schema validation for all tool parameters

New Modules

  • Giulia.MCP.Server — Anubis MCP server handling tools/call, tools/list, resources/read
  • Giulia.MCP.ToolSchema — generates tool definitions from sub-router @skill annotations
  • Giulia.MCP.ResourceProvider — resolves giulia:// resource URIs to context data
  • Giulia.Daemon.Plugs.McpAuth — bearer token authentication plug
  • Giulia.Daemon.Plugs.McpForward — deferred forwarder to Anubis transport (avoids persistent_term race)

Documentation

  • API.md: MCP section with setup, tool naming conventions, parameter mapping, LLM calling guide
  • ARCHITECTURE.md: MCP layer section, Tier 5 in supervision tree, /mcp in request flow
  • INSTALLATION.md: Full MCP setup guide (enable, configure, connect, verify)
  • SECURITY.md: MCP authentication model, GIULIA_MCP_KEY in secrets table, deployment hardening
  • SKILL.md: Access methods comparison (REST vs MCP)
  • README.md, CONTRIBUTING.md, ROADMAP.md: Updated for MCP

Dependencies

  • Added anubis_mcp ~> 1.0 (Elixir MCP framework)

Client Setup

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

v0.2.1 — Documentation Alignment

27 Mar 06:56

Choose a tag to compare

Documentation Alignment (Build 154)

Full audit and update of all project documentation against the actual codebase.

Changes

  • ARCHITECTURE.md: Added build reference header, fixed endpoint count (72→88), documented 17 missing modules (Knowledge: 10, Intelligence: 4, Runtime: 3), added TaskSupervisor to supervision tree, expanded Knowledge/Intelligence/Runtime layer sections
  • API.md: Added build reference header, updated TOC counts, added 3 missing endpoint docs (/api/knowledge/topology, /api/monitor/graph, /api/discovery/report_rules)
  • README.md: Added build reference, updated project status (Build 154, 83 endpoints)
  • CLAUDE.md: Rewrote project structure tree from ~88 files to full 119+ file coverage
  • mix.exs: Version bump 0.2.0 → 0.2.1

No code changes — documentation only.

v0.2.0 — Conventions Analyzer + Full Codebase Cleanup

26 Mar 10:44

Choose a tag to compare

What's New

Conventions Analyzer (Build 153)

New GET /api/knowledge/conventions endpoint — 12 AST-based rules that detect coding convention violations across any indexed Elixir project.

Tier 1 (Metadata — instant, ETS-only):

  • missing_moduledoc — every module needs @moduledoc
  • missing_spec — every public function needs @spec
  • missing_enforce_keys — structs should declare @enforce_keys

Tier 2 (AST Pattern — Sourceror walk):

  • try_rescue_flow_control — don't use Repo.get! / String.to_integer inside try/rescue
  • silent_rescue — never swallow errors with rescue _ -> nil
  • runtime_atom_creation — never use String.to_atom/1 on runtime strings
  • process_dictionary — avoid Process.get/put for application state
  • unsupervised_task — use Task.Supervisor instead of Task.start
  • unless_else — never use unless...else
  • single_value_pipe — don't pipe when there's no chain
  • append_in_reduce++ inside Enum.reduce is O(n²)
  • if_not — prefer unless over if not

Full Codebase Cleanup

Giulia now passes her own conventions checker with 0 violations (was 285):

  • 85 @spec annotations added across providers, tools, storage, inference
  • 165 single-value pipes converted to direct function calls
  • 16 Task.start replaced with Task.Supervisor.start_child (new Giulia.TaskSupervisor)
  • 9 if not patterns refactored
  • 6 @enforce_keys added to structs
  • 3 Process.get/put replaced with Agent in renderer
  • 1 String.to_atom secured with to_existing_atom + erlang fallback

Bug Fixes

  • Fixed stale convention cache returning ghost 0-violation results after re-indexing
  • Fixed single-value pipe false positives on chain members (two-phase meta-tracking)
  • Fixed @enforce_keys detection: reads source file instead of unpopulated ETS attributes

Stats

  • 97 files changed, 1,011 insertions, 353 deletions
  • 1,732 tests, 0 regressions
  • 0 compile warnings