deciduous 0.9.0

Decision graph tooling for AI-assisted development. Track every goal, decision, and outcome. Survive context loss. Query your reasoning.
Documentation
{
  description = "Deciduous - Decision graph tooling for AI-assisted development";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";

    rust-overlay = {
      url = "github:oxalica/rust-overlay";
      inputs.nixpkgs.follows = "nixpkgs";
    };

    crane = {
      url = "github:ipetkov/crane";
    };
  };

  outputs = { self, nixpkgs, flake-utils, rust-overlay, crane }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        overlays = [ (import rust-overlay) ];
        pkgs = import nixpkgs {
          inherit system overlays;
        };

        # Rust toolchain - use stable with minimum version from Cargo.toml (1.70)
        rustToolchain = pkgs.rust-bin.stable.latest.default.override {
          extensions = [ "rust-src" "rust-analyzer" "clippy" ];
        };

        # Initialize crane with our Rust toolchain
        craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain;

        # Common source filtering for Rust builds
        src = pkgs.lib.cleanSourceWith {
          src = ./.;
          filter = path: type:
            # Include Cargo files
            (pkgs.lib.hasSuffix "Cargo.toml" path) ||
            (pkgs.lib.hasSuffix "Cargo.lock" path) ||
            # Include Rust source
            (pkgs.lib.hasInfix "/src/" path) ||
            (pkgs.lib.hasInfix "/bin/" path) ||
            (pkgs.lib.hasSuffix ".rs" path) ||
            # Include migrations for diesel
            (pkgs.lib.hasInfix "/migrations/" path) ||
            # Include the viewer HTML (embedded in binary)
            (pkgs.lib.hasSuffix "viewer.html" path) ||
            # Default crane filter (for build.rs, etc.)
            (craneLib.filterCargoSources path type);
        };

        # Platform-specific dependencies for macOS
        # libiconv is needed for diesel/sqlite bindings
        darwinDeps = pkgs.lib.optionals pkgs.stdenv.isDarwin [
          pkgs.libiconv
        ];

        # Common build inputs for Rust
        commonBuildInputs = [
          pkgs.sqlite
          pkgs.openssl
        ] ++ darwinDeps;

        # Common native build inputs
        commonNativeBuildInputs = [
          pkgs.pkg-config
        ];

        # Common environment variables for builds
        commonEnv = {
          # Use bundled SQLite (matches Cargo.toml libsqlite3-sys bundled feature)
          SQLITE3_STATIC = "1";
        } // pkgs.lib.optionalAttrs pkgs.stdenv.isDarwin {
          # macOS needs libiconv in library path
          LIBRARY_PATH = "${pkgs.libiconv}/lib";
        };

        # Build trace-interceptor (Node.js)
        traceInterceptor = pkgs.buildNpmPackage {
          pname = "deciduous-trace-interceptor";
          version = "0.1.0";
          src = ./trace-interceptor;
          npmDepsHash = "sha256-Vq918124VdB1h+NzqD1bTiNe2k7c+xcjg01KIlU0cdM=";

          # Skip default npm build
          dontNpmBuild = true;

          buildPhase = ''
            runHook preBuild
            npm run build
            npm run bundle
            runHook postBuild
          '';

          installPhase = ''
            runHook preInstall
            mkdir -p $out/dist
            cp -r dist/* $out/dist/
            runHook postInstall
          '';
        };

        # Build web viewer (Node.js + Vite)
        webViewer = pkgs.buildNpmPackage {
          pname = "deciduous-viewer";
          version = "0.1.0";
          src = ./web;
          npmDepsHash = "sha256-2+OTxPgKsLI8uH3NOO3ebWi6QsIRvfnivWZa24DRPcQ=";

          buildPhase = ''
            runHook preBuild
            npm run build
            runHook postBuild
          '';

          installPhase = ''
            runHook preInstall
            mkdir -p $out/dist
            cp -r dist/* $out/dist/
            runHook postInstall
          '';
        };

        # Read Cargo.toml contents for crane (needed when source is a derivation)
        cargoTomlContents = builtins.readFile ./Cargo.toml;

        # Minimal source with trace-interceptor (no web viewer)
        # The Rust code requires trace-interceptor/dist/bundle.js at compile time
        srcMinimal = pkgs.runCommand "deciduous-src-minimal" { } ''
          cp -r ${src} $out
          chmod -R u+w $out
          mkdir -p $out/trace-interceptor/dist
          cp ${traceInterceptor}/dist/bundle.js $out/trace-interceptor/dist/bundle.js
        '';

        # Cargo artifacts (dependencies only) - speeds up rebuilds
        cargoArtifacts = craneLib.buildDepsOnly ({
          pname = "deciduous";
          version = "0.8.15";
          src = srcMinimal;
          inherit cargoTomlContents;
          buildInputs = commonBuildInputs;
          nativeBuildInputs = commonNativeBuildInputs;
        } // commonEnv);

        # Main deciduous binary (minimal - no embedded web viewer)
        deciduous = craneLib.buildPackage ({
          pname = "deciduous";
          version = "0.8.15";
          src = srcMinimal;
          inherit cargoTomlContents cargoArtifacts;
          buildInputs = commonBuildInputs;
          nativeBuildInputs = commonNativeBuildInputs;

          meta = with pkgs.lib; {
            description = "Decision graph tooling for AI-assisted development";
            homepage = "https://github.com/notactuallytreyanastasio/deciduous";
            license = licenses.mit;
            maintainers = [ ];
            mainProgram = "deciduous";
          };
        } // commonEnv);

        # Full release source with embedded web viewer and trace interceptor
        # Uses the filtered source as base, patches in the built artifacts
        srcFull = pkgs.runCommand "deciduous-src-full" { } ''
          cp -r ${src} $out
          chmod -R u+w $out
          cp ${webViewer}/dist/index.html $out/src/viewer.html
          mkdir -p $out/trace-interceptor/dist
          cp ${traceInterceptor}/dist/bundle.js $out/trace-interceptor/dist/bundle.js
        '';

        # Cargo artifacts for full build
        # Must provide cargoTomlContents since srcFull is a derivation
        cargoArtifactsFull = craneLib.buildDepsOnly ({
          pname = "deciduous";
          version = "0.8.15";
          src = srcFull;
          inherit cargoTomlContents;
          buildInputs = commonBuildInputs;
          nativeBuildInputs = commonNativeBuildInputs;
        } // commonEnv);

        # Full release binary with embedded web viewer (equivalent to make release-full)
        deciduousFull = craneLib.buildPackage ({
          pname = "deciduous";
          version = "0.8.15";
          src = srcFull;
          inherit cargoTomlContents;
          cargoArtifacts = cargoArtifactsFull;
          buildInputs = commonBuildInputs;
          nativeBuildInputs = commonNativeBuildInputs;

          meta = with pkgs.lib; {
            description = "Decision graph tooling for AI-assisted development (with embedded web viewer)";
            homepage = "https://github.com/notactuallytreyanastasio/deciduous";
            license = licenses.mit;
            maintainers = [ ];
            mainProgram = "deciduous";
          };
        } // commonEnv);

        # Helper script: menu
        menu = pkgs.writeShellScriptBin "menu" ''
          echo "========================================"
          echo "  Deciduous Development Shell (Nix)"
          echo "========================================"
          echo ""
          echo "Cargo commands:"
          echo "  cargo build --release    Build release binary (uses existing viewer.html)"
          echo "  cargo test               Run tests"
          echo "  cargo clippy             Run linter"
          echo "  cargo fmt                Format code"
          echo ""
          echo "Nix build commands:"
          echo "  nix build                Full build with embedded web viewer (default)"
          echo "  nix build .#minimal      Minimal build without rebuilding web viewer"
          echo "  nix build .#webViewer    Build web viewer only"
          echo "  nix build .#traceInterceptor  Build trace interceptor only"
          echo ""
          echo "Nix run/check commands:"
          echo "  nix run                  Run deciduous (full build)"
          echo "  nix flake check          Run all checks (build, clippy, test, fmt)"
          echo "  nix run .#test           Run tests only"
          echo "  nix run .#clippy         Run clippy lints only"
          echo "  nix run .#fmt-check      Check formatting only"
          echo ""
          echo "Dev workflow (impure, modifies source tree):"
          echo "  nix-build-full           Build everything locally (like make release-full)"
          echo ""
          echo "Help:"
          echo "  menu                     Show this menu"
          echo ""
        '';

        # Helper script: nix-build-full (equivalent to make release-full)
        nixBuildFull = pkgs.writeShellScriptBin "nix-build-full" ''
          set -e
          echo "Building trace-interceptor..."
          (cd trace-interceptor && npm install && npm run build && npm run bundle)

          echo "Building web viewer..."
          (cd web && npm install && npm run build)

          echo "Copying viewer to src/..."
          cp web/dist/index.html src/viewer.html
          cp web/dist/index.html docs/demo/index.html

          echo "Clearing trace interceptor cache..."
          rm -rf ~/.deciduous/trace-interceptor

          echo "Building Rust binary..."
          cargo build --release

          echo ""
          echo "Full release build complete: target/release/deciduous"
        '';

      in
      {
        # Packages
        packages = {
          # Default is the full build with embedded web viewer
          default = deciduousFull;
          full = deciduousFull;
          # Minimal build without web viewer (faster, smaller)
          minimal = deciduous;
          inherit deciduous deciduousFull traceInterceptor webViewer;
        };

        # Checks (run with `nix flake check`)
        # Note: checks use srcMinimal which includes trace-interceptor bundle
        checks = {
          # Build the package
          inherit deciduous;

          # Run clippy
          deciduous-clippy = craneLib.cargoClippy ({
            pname = "deciduous";
            version = "0.8.15";
            src = srcMinimal;
            inherit cargoTomlContents cargoArtifacts;
            buildInputs = commonBuildInputs;
            nativeBuildInputs = commonNativeBuildInputs;
            cargoClippyExtraArgs = "--all-targets -- -D warnings";
          } // commonEnv);

          # Run tests
          deciduous-test = craneLib.cargoTest ({
            pname = "deciduous";
            version = "0.8.15";
            src = srcMinimal;
            inherit cargoTomlContents cargoArtifacts;
            buildInputs = commonBuildInputs;
            nativeBuildInputs = commonNativeBuildInputs;
          } // commonEnv);

          # Check formatting (uses original src since formatting doesn't need trace-interceptor)
          deciduous-fmt = craneLib.cargoFmt {
            pname = "deciduous";
            version = "0.8.15";
            src = srcMinimal;
            inherit cargoTomlContents;
          };
        };

        # Apps (run with `nix run`)
        apps =
          let
            mkAppWithMeta = drv: description: {
              type = "app";
              program = "${drv}/bin/deciduous";
              meta = {
                inherit description;
                homepage = "https://github.com/notactuallytreyanastasio/deciduous";
                license = pkgs.lib.licenses.mit;
                mainProgram = "deciduous";
              };
            };
            mkCheckApp = name: check: description: {
              type = "app";
              program = toString (pkgs.writeShellScript "run-${name}" ''
                echo "Running ${name}..."
                nix build .#checks.${system}.${check} --print-build-logs
                echo "✓ ${name} passed"
              '');
              meta = {
                inherit description;
                homepage = "https://github.com/notactuallytreyanastasio/deciduous";
              };
            };
          in
          {
            default = mkAppWithMeta deciduousFull "Decision graph tooling for AI-assisted development";
            deciduous = mkAppWithMeta deciduousFull "Decision graph tooling for AI-assisted development";
            minimal = mkAppWithMeta deciduous "Decision graph tooling (minimal build without embedded web viewer)";

            # Development/CI apps
            test = mkCheckApp "test" "deciduous-test" "Run cargo tests";
            clippy = mkCheckApp "clippy" "deciduous-clippy" "Run clippy lints";
            fmt-check = mkCheckApp "fmt-check" "deciduous-fmt" "Check code formatting";
          };

        # Development shell
        devShells.default = craneLib.devShell {
          # Include checks to get build inputs
          checks = self.checks.${system};

          # Additional packages for development
          packages = [
            # DevShell helper scripts
            menu
            nixBuildFull

            # Node.js for web viewer and trace-interceptor
            pkgs.nodejs_20
            pkgs.nodePackages.npm
            pkgs.nodePackages.typescript

            # SQLite tools
            pkgs.sqlite

            # Optional: graphviz for DOT -> PNG conversion
            pkgs.graphviz

            # Optional: diesel CLI for database migrations
            pkgs.diesel-cli

            # Git (usually already available, but explicit)
            pkgs.git

            # Useful development tools
            pkgs.cargo-watch
            pkgs.cargo-edit
          ] ++ darwinDeps;

          # Environment variables for development
          shellHook = ''
            # Ensure Rust tools are available
            export RUST_SRC_PATH="${rustToolchain}/lib/rustlib/src/rust/library"

            ${pkgs.lib.optionalString pkgs.stdenv.isDarwin ''
              # macOS: Set library path for libiconv (needed by diesel/sqlite)
              export LIBRARY_PATH="${pkgs.libiconv}/lib:''${LIBRARY_PATH:-}"
            ''}

            # Show menu on shell entry
            menu
          '';

          # Set environment variables
          SQLITE3_STATIC = "1";
        };

        # Formatter for `nix fmt`
        formatter = pkgs.nixpkgs-fmt;
      }
    );
}