lib/foundry/context/structs.ex

defmodule Foundry.Context.ModuleContext do
  @moduledoc """
  The typed output struct for `mix foundry.context <Module> --json`.

  This struct defines the JSON schema frozen at the end of Phase 1.
  Breaking changes require an ADR. The schema matches ADR-003 exactly.

  All fields must be present in every output — use nil for absent optional values,
  not missing keys. Consumers depend on key presence, not key existence checks.
  """

  @type state_machine :: %{
          present: boolean(),
          states: [String.t()],
          transitions: [%{from: String.t(), to: String.t(), action: String.t()}],
          state_attribute: String.t() | nil
        }

  @type money_attribute :: %{
          name: String.t(),
          type: String.t(),
          cldr_backend: String.t()
        }

  @type test_coverage :: %{
          property_tests: boolean(),
          scenario_tests: boolean(),
          e2e_tests: boolean()
        }

  @derive Jason.Encoder
  @enforce_keys [:module, :type, :domain, :description]
  defstruct [
    # Identity
    # String — fully qualified module name
    :module,
    # atom — :resource | :transfer | :rule | :blueprint | :adapter | :live_page | :oban_job
    :type,
    # String — parent domain name
    :domain,
    # String — @moduledoc first paragraph
    :description,

    # Relationships
    # [String] — Transfer step function names
    steps: [],
    # [String] — Rule module names applied to this Transfer
    rules: [],
    # [String] — RG-* requirement IDs
    compliance: [],
    # String | nil — path to runbook file
    runbook: nil,
    # [String] — declared spec_invariants
    invariants: [],
    # [String] — resource module names referenced
    related_resources: [],
    # [String] — ADR IDs referenced in @moduledoc
    adrs: [],

    # Metadata
    # String — ISO 8601 date
    last_modified: nil,
    # boolean
    sensitive: false,

    # Test coverage
    test_coverage: %{
      property_tests: false,
      scenario_tests: false,
      e2e_tests: false
    },

    # Data layer
    # String | nil — "ash_postgres" | "ash_ets" | nil
    data_layer: nil,
    # boolean — true if mix ash.codegen --check exits non-zero
    pending_migrations: false,

    # Extensions
    # boolean
    paper_trail: false,
    # boolean
    archival: false,

    # State machine (always present; present: false when no state machine)
    state_machine: %{
      present: false,
      states: [],
      transitions: [],
      state_attribute: nil
    },

    # API routes
    # [%{path: String, method: String, auth_required: boolean}]
    api_routes: [],

    # Observability
    # [String] — telemetry event prefix segments
    telemetry_prefix: [],

    # Money
    # [%{name, type, cldr_backend}]
    money_attributes: [],

    # Auth
    # boolean — true if this resource is an ash_authentication subject
    authentication_subject: false,

    # Background jobs
    # [String] — queue names if this is an Oban worker
    oban_queues: [],

    # Rate limiting
    # boolean — true if hammer_plug middleware declared
    rate_limited: false,

    # Feature flags
    # [String] — flag names referenced in this module
    feature_flags: [],

    # Page metadata (Phase D)
    # String | nil — route path e.g. "/games/:id"
    page_route: nil,
    # atom | nil — :player | :operator | :anonymous | :admin
    page_group: nil,
    # boolean — true if route has :param segments
    page_dynamic: false,
    # atom | nil — :sdui | nil
    page_subtype: nil,
    # [tuple] — [{resource_module, action_type}] called in this page
    calls_actions: []
  ]
end

defmodule Foundry.Context.AllContext do
  @moduledoc """
  The typed output for `mix foundry.context.all --json`.
  A map of domain name strings to lists of ModuleContext structs.

  JSON shape:
  {
    "Finance": [ <ModuleContext>, ... ],
    "Players": [ <ModuleContext>, ... ],
    ...
  }
  """

  @type t :: %{String.t() => [Foundry.Context.ModuleContext.t()]}
end

defmodule Foundry.Diagram.SystemMap do
  @moduledoc """
  The typed output for `mix foundry.diagram.generate --json`.
  A graph of nodes (modules) and edges (relationships), clustered by domain.
  Consumed by the Phase 2 System Map D3 renderer.

  Frozen at end of Phase 1. Breaking changes require an ADR.
  """

  @derive Jason.Encoder
  @enforce_keys [:nodes, :edges, :clusters, :generated_at]
  defstruct [:nodes, :edges, :clusters, :generated_at]

  defmodule Node do
    @moduledoc "A node in the system map graph. Corresponds to one Ash resource, Transfer, Rule, etc."

    @derive Jason.Encoder
    @enforce_keys [:id, :module, :type, :domain, :label]
    defstruct [
      # String — same as module name, used as D3 node id
      :id,
      # String — fully qualified module name
      :module,
      # atom — :resource | :transfer | :rule | :blueprint | :adapter
      :type,
      # String — parent domain
      :domain,
      # String — short display name (last module segment)
      :label,
      # boolean
      sensitive: false,
      # boolean — has at least one RG-* link
      has_compliance: false,
      # Derived from ModuleContext.test_coverage (three booleans → single atom):
      # :none    — property_tests: false, scenario_tests: false, e2e_tests: false
      # :partial — at least one true, not all three
      # :full    — property_tests: true, scenario_tests: true, e2e_tests: true
      test_coverage: :none,
      pending_migrations: false
    ]
  end

  defmodule Edge do
    @moduledoc "A directed edge between two nodes. Represents a relationship, rule application, or Transfer step dependency."

    @derive Jason.Encoder
    @enforce_keys [:from, :to, :kind]
    defstruct [
      # String — source node id
      :from,
      # String — target node id
      :to,
      # atom — :belongs_to | :has_many | :has_one | :many_to_many | :applies_rule | :transfer_step
      :kind
    ]
  end

  defmodule Cluster do
    @moduledoc "A named cluster of nodes — corresponds to one domain."

    @derive Jason.Encoder
    @enforce_keys [:id, :label, :node_ids]
    defstruct [:id, :label, :node_ids]
  end
end

defmodule Foundry.Compliance.CheckResult do
  @moduledoc """
  The typed output for `mix foundry.compliance.check --json`.
  Reports the implementation and test coverage status for each declared RG-* requirement.

  Frozen at end of Phase 1. Breaking changes require an ADR.
  """

  @derive Jason.Encoder
  @enforce_keys [:requirements, :summary, :generated_at]
  defstruct [:requirements, :summary, :generated_at]

  defmodule Requirement do
    @moduledoc "The status of a single RG-* compliance requirement."

    @derive Jason.Encoder
    @enforce_keys [:id, :summary, :status]
    defstruct [
      # String — e.g. "RG-UK-014"
      :id,
      # String — one-line description
      :summary,
      # atom — :implemented | :partial | :unimplemented | :planned
      :status,
      # [String] — modules that declare this requirement
      implementing_modules: [],
      # [String] — ExUnit tags linking tests to this requirement
      test_tags: [],
      # String | nil — ISO 8601 datetime of last CI run result
      last_test_run: nil,
      # boolean | nil — whether the last CI run passed
      last_test_passed: nil
    ]
  end

  defmodule Summary do
    @moduledoc "Aggregate counts across all requirements."

    @derive Jason.Encoder
    defstruct total: 0,
              implemented: 0,
              partial: 0,
              unimplemented: 0,
              planned: 0,
              passing_tests: 0,
              failing_tests: 0
  end
end

defmodule Foundry.Lint.LintReport do
  @moduledoc """
  The typed output for `mix foundry.lint.all --json`.
  Aggregates all violations from all lint rules across all modules.

  Frozen at end of Phase 1. Breaking changes require an ADR.
  """

  @derive Jason.Encoder
  @enforce_keys [:passed, :violations, :generated_at]
  defstruct [:passed, :violations, :generated_at, error_count: 0, warning_count: 0, info_count: 0]

  defmodule Violation do
    @moduledoc "A single lint violation from any lint rule."

    @derive Jason.Encoder
    @enforce_keys [:rule_id, :severity, :message]
    defstruct [
      # atom — e.g. :missing_description
      :rule_id,
      # atom — :error | :warning | :info
      :severity,
      # String — human-readable description
      :message,
      # String | nil
      module: nil,
      # String | nil
      file_path: nil,
      # integer | nil
      line: nil
    ]
  end
end

defmodule Foundry.Versions.VersionManifest do
  @moduledoc """
  The typed output for `mix foundry.versions.check --json`.
  Current dependency versions read from mix.exs.

  This is included in every LLM prompt as the first item (INV-006).
  The schema is intentionally flexible — it includes all known ecosystem
  libraries but does not fail if a library is absent (not all projects
  use all libraries).

  Frozen at end of Phase 1. Breaking changes require an ADR.
  """

  defstruct [
    # Core (always present in target platforms)
    ash: nil,
    ash_postgres: nil,
    spark: nil,
    phoenix: nil,
    phoenix_live_view: nil,
    igniter: nil,
    ecto_sql: nil,
    postgrex: nil,

    # Ash extensions
    ash_state_machine: nil,
    ash_oban: nil,
    ash_double_entry: nil,
    ash_json_api: nil,
    ash_paper_trail: nil,
    ash_archival: nil,
    ash_authentication: nil,
    ash_authentication_phoenix: nil,
    ash_money: nil,

    # Money stack
    ex_money: nil,
    ex_money_sql: nil,

    # Background jobs
    oban: nil,

    # Feature flags
    fun_with_flags: nil,

    # Rate limiting
    hammer: nil,
    hammer_plug: nil,

    # HTTP
    req: nil,
    finch: nil,
    bandit: nil,

    # Email
    swoosh: nil,

    # Caching
    nebulex: nil,

    # Clustering
    libcluster: nil,

    # Observability
    opentelemetry: nil,
    opentelemetry_exporter: nil,

    # Testing
    stream_data: nil,
    bypass: nil,
    mox: nil,
    ex_machina: nil,

    # UI
    ash_pyro: nil,

    # Metadata
    elixir_version: nil,
    otp_version: nil,
    generated_at: nil
  ]
end