packages feed

synapse-cc (empty) → 0.1.0.0

raw patch · 14 files changed

+2607/−0 lines, 14 filesdep +QuickCheckdep +aesondep +ansi-terminal

Dependencies added: QuickCheck, aeson, ansi-terminal, async, base, bytestring, containers, cryptohash-sha256, directory, filepath, hspec, optparse-applicative, process, synapse-cc, text, time

Files

+ PLAN.md view
@@ -0,0 +1,411 @@+# synapse-cc: Unified Compiler Toolchain++## Overview++`synapse-cc` (synapse compiler collection) is a unified toolchain that orchestrates the complete pipeline from backend schema discovery to compiled, ready-to-use client libraries.++## Goals++1. **Unified Interface**: Single command to generate clients from any backend+2. **Tool Discovery**: Find and use local development tools or installed versions+3. **Smart Caching**: Avoid regeneration when schemas haven't changed+4. **Language Integration**: Handle dependency installation and compilation+5. **Developer Experience**: Clear errors, progress indicators, helpful messages++## Architecture++### Tool Chain++```+synapse-cc (Haskell)+    ↓+    ├─→ synapse (Haskell) → IR (JSON)+    ↓+    ├─→ hub-codegen (Rust) → Generated Code+    ↓+    └─→ Language Tools (npm, tsc, etc.) → Compiled Artifact+```++### Command Structure++```bash+synapse-cc <target> <backend> <url> [OPTIONS]++Arguments:+  <target>   Target language (typescript, python, rust, etc.)+  <backend>  Backend identifier (substrate, plexus, synapse, etc.)+  <url>      Backend WebSocket URL (e.g., ws://localhost:4444)++Options:+  -o, --output <DIR>           Output directory (default: ./generated)+  --bundle-transport BOOL      Bundle transport code (default: true)+  --no-install                 Skip dependency installation+  --no-build                   Skip compilation step+  --cache-dir <DIR>            Cache directory (default: ~/.plexus/cache)+  --force                      Force regeneration (ignore cache)+  --watch                      Watch backend and regenerate on changes+  --debug                      Enable debug logging+  -h, --help                   Show help message+```++## Implementation Phases++### Phase 1: MVP Pipeline (Core Functionality)++**Goal**: Working end-to-end pipeline without bells and whistles++**Tasks**:+1. ✅ Project scaffolding (Cabal, module structure)+2. ✅ CLI argument parsing with `optparse-applicative`+3. ✅ Tool discovery system:+   - Check local dev paths+   - Check $PATH+   - Check ~/.plexus/bin/+   - Fail with helpful error+4. ✅ Pipeline orchestration:+   - Run synapse to generate IR+   - Run hub-codegen on IR+   - Write output to directory+5. ✅ Basic error handling and logging+6. ✅ End-to-end test with substrate++**Deliverable**: Command that works for TypeScript generation from substrate++### Phase 2: Language Integration++**Goal**: Handle language-specific tooling automatically++**Tasks**:+1. Detect package manager (npm, pnpm, yarn, cargo, pip, etc.)+2. Run dependency installation+3. Run compilation/build commands+4. Validate output artifacts+5. Handle language-specific errors gracefully++**Deliverable**: Fully compiled, ready-to-use client libraries++### Phase 3: Smart Caching++**Goal**: Only regenerate when necessary++**Tasks**:+1. Read IR hash from generated IR+2. Cache structure in ~/.plexus/cache/+3. Compare schema hashes to detect changes+4. Reuse cached IR and generated code when possible+5. --force flag to bypass cache++**Cache Structure**:+```+~/.plexus/cache/+├── ir/+│   └── <hash>.json              # Cached IR by hash+└── generated/+    └── <hash>/+        ├── typescript/           # Cached TypeScript client+        ├── python/               # Cached Python client+        └── ...+```++**Deliverable**: Near-instant regeneration when schemas unchanged++### Phase 4: Watch Mode & Intelligence++**Goal**: Developer-friendly features++**Tasks**:+1. Watch backend schema for changes (periodic polling)+2. Regenerate automatically on detection+3. Parallel generation for multiple targets+4. Progress indicators and spinners+5. Colorized output+6. Auto-install missing tools (synapse, hub-codegen)++**Deliverable**: Production-ready developer tool++## Module Structure++```+synapse-cc/+├── synapse-cc.cabal+├── PLAN.md                      # This file+├── README.md                    # User-facing documentation+├── app/+│   └── Main.hs                  # Entry point, wires everything together+└── src/+    └── SynapseCC/+        ├── Types.hs             # Core types (Config, Target, Backend, etc.)+        ├── CLI.hs               # Command-line argument parsing+        ├── Discover.hs          # Tool discovery (find synapse, hub-codegen)+        ├── Pipeline.hs          # Pipeline orchestration+        ├── Process.hs           # Subprocess execution helpers+        ├── Cache.hs             # IR/code caching by hash+        ├── Language.hs          # Language-specific integrations+        └── Logging.hs           # Pretty logging and progress indicators+```++## Key Design Decisions++### 1. Subprocess Execution (Not FFI)++**Decision**: Call synapse and hub-codegen as separate processes++**Rationale**:+- Clean separation of concerns+- No need to link Rust into Haskell+- Easier to version tools independently+- Standard Unix philosophy: compose tools+- Each tool can be developed/tested independently++**Implementation**:+```haskell+runSynapse :: Backend -> URL -> FilePath -> IO (Either Error IRPath)+runHubCodegen :: IRPath -> Target -> Options -> FilePath -> IO (Either Error GeneratedPath)+```++### 2. Tool Discovery Strategy++**Priority Order**:+1. Local development paths (for contributors)+   - `../synapse/dist-newstyle/build/.../synapse`+   - `../hub-codegen/target/release/hub-codegen`+2. System PATH (for installed versions)+   - `synapse` and `hub-codegen` in $PATH+3. Plexus bin directory (for managed installs)+   - `~/.plexus/bin/synapse`+   - `~/.plexus/bin/hub-codegen`+4. Fail with helpful installation instructions++**Future**: Auto-install via cabal/cargo++### 3. Caching Strategy++**Key Insight**: IR already has hash field!++```json+{+  "irVersion": "2.0",+  "irBackend": "substrate",+  "irHash": "abc123...",+  ...+}+```++**Cache Key**: `irHash` + `target` + `codegen_options`++**Benefits**:+- Only regenerate when schema actually changes+- Share cache across invocations+- Fast iteration during development++### 4. Error Handling++**Philosophy**: Fail fast with helpful messages++**Examples**:+```+❌ Error: synapse not found++synapse-cc requires synapse to generate IR.++Try:+  • Build synapse: cd ../synapse && cabal build+  • Install synapse: cabal install synapse+  • Add to PATH: export PATH="$PATH:~/.plexus/bin"++For more info: https://github.com/hypermemetic/synapse+```++## Success Metrics++### Phase 1 Success+- [x] Can run: `synapse-cc typescript substrate ws://localhost:4444`+- [x] Generates valid TypeScript client+- [x] Clear error if tools missing+- [x] Works from any directory++### Phase 2 Success+- [ ] Runs `npm install` automatically+- [ ] Compiles TypeScript to JavaScript+- [ ] Handles missing dependencies gracefully+- [ ] Works with multiple package managers++### Phase 3 Success+- [ ] Second run is near-instant (cache hit)+- [ ] Detects schema changes and regenerates+- [ ] --force flag bypasses cache+- [ ] Cache can be cleared++### Phase 4 Success+- [ ] Watch mode works reliably+- [ ] Beautiful progress indicators+- [ ] Can auto-install missing tools+- [ ] Production-ready UX++## Example Usage++### Basic TypeScript Client+```bash+synapse-cc typescript substrate ws://localhost:4444++# Output:+# 🔍 Discovering tools...+# ✓ Found synapse at ../synapse/dist-newstyle/.../synapse+# ✓ Found hub-codegen at ../hub-codegen/target/release/hub-codegen+#+# 📡 Connecting to substrate at ws://localhost:4444...+# ✓ Connected+#+# 🔨 Generating IR...+# ✓ IR generated (hash: abc123...)+#+# 🏗️  Generating TypeScript client...+# ✓ Generated 12 files+#+# 📦 Installing dependencies...+# ✓ npm install completed+#+# 🔧 Compiling TypeScript...+# ✓ Compilation successful+#+# ✅ Client ready at ./generated/+```++### External Transport Mode+```bash+synapse-cc typescript substrate ws://localhost:4444 \+  --bundle-transport=false \+  --output ./packages/substrate-client+```++### Watch Mode (Future)+```bash+synapse-cc typescript substrate ws://localhost:4444 --watch++# Output:+# 👀 Watching substrate for schema changes...+# Press Ctrl+C to stop.+#+# [14:23:45] Schema changed (hash: def456...)+# [14:23:45] Regenerating...+# [14:23:46] ✓ Done+```++## Dependencies++### Haskell Dependencies+- `base >= 4.14`+- `optparse-applicative` - CLI parsing+- `process` - Subprocess execution+- `aeson` - JSON parsing (for IR)+- `bytestring` - Efficient strings+- `text` - Text handling+- `filepath` - Path manipulation+- `directory` - File system operations+- `async` - Concurrency (for watch mode)+- `ansi-terminal` - Colorized output+- `time` - Timestamps+- `cryptohash-sha256` - Cache key hashing++### External Tools (Runtime)+- `synapse` - Schema discovery and IR generation+- `hub-codegen` - Code generation from IR+- Language tools (npm, tsc, cargo, pip, etc.) - Language-specific builds++## Future Extensions++### Multi-Target Generation+```bash+synapse-cc typescript,python,rust substrate ws://localhost:4444+# Generates clients for all three languages in parallel+```++### Configuration File+```yaml+# .synapse-cc.yaml+targets:+  - typescript:+      output: ./packages/ts-client+      bundle_transport: false+  - python:+      output: ./packages/py-client++backends:+  - substrate: ws://localhost:4444+  - plexus: ws://localhost:5000++cache_dir: ./.synapse-cache+watch: true+```++### CI/CD Integration+```bash+synapse-cc check substrate ws://localhost:4444+# Exit 0 if schema unchanged, 1 if changed (for CI)+```++### Schema Diff+```bash+synapse-cc diff substrate ws://localhost:4444+# Shows schema changes since last generation+```++## Testing Strategy++### Unit Tests+- Tool discovery logic+- Cache key generation+- Path resolution+- Error message formatting++### Integration Tests+- End-to-end pipeline with mock backend+- Cache hit/miss scenarios+- Error handling paths++### Manual Tests+- Real substrate backend+- Multiple target languages+- Watch mode stability++## Release Plan++### v0.1.0 - MVP+- Phase 1 complete+- Basic TypeScript generation+- Tool discovery+- No caching, no language integration++### v0.2.0 - Language Integration+- Phase 2 complete+- Dependency installation+- TypeScript compilation+- Multiple package manager support++### v0.3.0 - Smart Caching+- Phase 3 complete+- Hash-based caching+- Fast regeneration+- Cache management commands++### v1.0.0 - Production Ready+- Phase 4 complete+- Watch mode+- Auto-install tools+- Beautiful UX+- Documentation complete+- Battle-tested++## Open Questions++1. **Auto-install strategy**: Use cabal/cargo directly or download prebuilt binaries?+2. **Configuration file format**: YAML, TOML, or JSON?+3. **Watch mode implementation**: Polling interval? inotify?+4. **Multi-target parallelism**: Sequential or parallel generation?+5. **Cache eviction**: Keep last N? Time-based? Manual only?++## Conclusion++`synapse-cc` will become the primary interface for generating clients from Plexus backends. By orchestrating the full toolchain, it dramatically improves developer experience and reduces the barrier to entry for consuming backend APIs.++The phased approach ensures we can deliver value quickly (Phase 1) while building toward a polished, production-ready tool (Phase 4).
+ README.md view
@@ -0,0 +1,183 @@+# synapse-cc++**Unified compiler toolchain for Plexus backends**++`synapse-cc` (synapse compiler collection) orchestrates the complete pipeline from backend schema discovery to compiled, ready-to-use client libraries.++## Features++- 🔧 **Unified Interface**: Single command to generate clients from any backend+- 🔍 **Smart Tool Discovery**: Finds local development builds or installed versions+- ⚡ **Fast**: Smart caching avoids regeneration when schemas haven't changed (coming in Phase 3)+- 🎯 **Multi-Language**: TypeScript, Python, Rust support (TypeScript in MVP)+- 🛠️ **Complete Pipeline**: From schema → IR → generated code → compiled artifacts++## Installation++### From Source++```bash+cd synapse-cc+cabal build+cabal install+```++### Dependencies++`synapse-cc` requires these tools to be available:+- **synapse** - Schema discovery and IR generation+- **hub-codegen** - Code generation from IR++The tool will automatically find them if they're:+- In your `$PATH`+- In local development directories (`../synapse`, `../hub-codegen`)+- In `~/.plexus/bin/`++## Usage++### Basic TypeScript Client++```bash+synapse-cc typescript substrate ws://localhost:4444+```++This will:+1. Connect to substrate at `ws://localhost:4444`+2. Discover the schema+3. Generate IR+4. Generate TypeScript client+5. Output to `./generated/`++### Options++```bash+synapse-cc <target> <backend> <url> [OPTIONS]++Arguments:+  target    Target language (typescript, python, rust)+  backend   Backend identifier (substrate, plexus, synapse, etc.)+  url       Backend WebSocket URL (e.g., ws://localhost:4444)++Options:+  -o, --output DIR           Output directory (default: ./generated)+  --bundle-transport BOOL    Bundle transport code (default: true)+  --no-install              Skip dependency installation+  --no-build                Skip compilation step+  --cache-dir DIR           Cache directory (default: ~/.plexus/cache)+  --force                   Force regeneration (ignore cache)+  --watch                   Watch backend and regenerate on changes+  --debug                   Enable debug logging+  -h, --help                Show help message+```++### Examples++**External transport mode** (uses `@plexus/rpc-client` package):+```bash+synapse-cc typescript substrate ws://localhost:4444 \+  --bundle-transport=false \+  --output ./packages/substrate-client+```++**Skip build steps** (just generate code):+```bash+synapse-cc typescript substrate ws://localhost:4444 \+  --no-install \+  --no-build+```++**Debug mode**:+```bash+synapse-cc typescript substrate ws://localhost:4444 --debug+```++## Architecture++```+synapse-cc (Haskell)+    ↓+    ├─→ synapse (Haskell) → IR (JSON)+    ↓+    ├─→ hub-codegen (Rust) → Generated Code+    ↓+    └─→ Language Tools (npm, tsc, etc.) → Compiled Artifact+```++## Development++### Project Structure++```+synapse-cc/+├── synapse-cc.cabal         # Cabal project file+├── PLAN.md                  # Detailed implementation plan+├── app/+│   └── Main.hs              # Entry point+└── src/+    └── SynapseCC/+        ├── Types.hs         # Core types+        ├── CLI.hs           # Command-line parsing+        ├── Discover.hs      # Tool discovery+        ├── Pipeline.hs      # Pipeline orchestration+        ├── Process.hs       # Subprocess helpers+        ├── Cache.hs         # Caching (Phase 3)+        ├── Language.hs      # Language integration (Phase 2)+        └── Logging.hs       # Pretty output+```++### Building++```bash+cabal build+```++### Running from source++```bash+cabal run synapse-cc -- typescript substrate ws://localhost:4444+```++### Testing++```bash+# Ensure substrate is running+cd ../plexus-substrate+cargo run -- --port 4444++# In another terminal, test synapse-cc+cd ../synapse-cc+cabal run synapse-cc -- typescript substrate ws://localhost:4444 --debug+```++## Roadmap++### Phase 1: MVP (Current)+- ✅ Project scaffolding+- ✅ Tool discovery+- ✅ Pipeline orchestration+- ✅ Basic error handling+- [ ] End-to-end testing++### Phase 2: Language Integration+- [ ] Dependency installation (npm, pip, cargo)+- [ ] Build automation+- [ ] Multi-package manager support++### Phase 3: Smart Caching+- [ ] Hash-based caching+- [ ] Cache management+- [ ] Fast iteration++### Phase 4: Production Features+- [ ] Watch mode+- [ ] Progress indicators+- [ ] Auto-install tools+- [ ] Configuration files++## Contributing++See [PLAN.md](./PLAN.md) for detailed implementation plan and design decisions.++## License++MIT
+ app/Main.hs view
@@ -0,0 +1,65 @@+module Main where++import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Exit (exitFailure, exitSuccess)+import System.IO (stdout, stderr, hSetEncoding, utf8)++import SynapseCC.CLI+import SynapseCC.Discover+import SynapseCC.Logging+import SynapseCC.Pipeline+import SynapseCC.Types++-- ============================================================================+-- Main Entry Point+-- ============================================================================++main :: IO ()+main = do+  -- Set UTF-8 encoding for stdout/stderr to handle Unicode characters+  hSetEncoding stdout utf8+  hSetEncoding stderr utf8++  -- Parse command-line arguments+  config <- parseArgs++  let debug = optDebug (cfgOptions config)++  -- Print version+  when debug $ TIO.putStrLn versionInfo++  -- Discover tools+  logStep "Discovering tools..."+  toolsResult <- discoverTools debug+  case toolsResult of+    Left err -> do+      TIO.putStrLn $ formatError err+      exitFailure++    Right tools -> do+      logSuccess "Found all required tools"++      -- Connect to backend+      logStep $ "Connecting to " <> backendName (cfgBackend config) <> " via registry at " <> cfgHost config <> ":" <> cfgPort config <> "..."++      -- Run the pipeline+      pipelineResult <- runPipeline config tools+      case pipelineResult of+        Left err -> do+          TIO.putStrLn $ formatError err+          exitFailure++        Right (CompiledPath outputPath) -> do+          TIO.putStrLn ""+          logSuccess $ "Client ready at " <> T.pack outputPath+          exitSuccess++-- ============================================================================+-- Helpers+-- ============================================================================++when :: Applicative f => Bool -> f () -> f ()+when True action = action+when False _ = pure ()
+ src/SynapseCC/CLI.hs view
@@ -0,0 +1,138 @@+-- | Command-line interface parsing+module SynapseCC.CLI+  ( parseArgs+  , versionInfo+  ) where++import Data.Text (Text)+import qualified Data.Text as T+import Options.Applicative+import System.FilePath ((</>))++import SynapseCC.Types++-- ============================================================================+-- CLI Parser+-- ============================================================================++-- | Parse command-line arguments into Config+parseArgs :: IO Config+parseArgs = execParser opts+  where+    opts = info (configParser <**> helper)+      ( fullDesc+     <> progDesc "Unified compiler toolchain for Plexus backends"+     <> header "synapse-cc - from schema to compiled client in one command"+      )++-- | Main config parser+configParser :: Parser Config+configParser = Config+  <$> targetParser+  <*> backendParser+  <*> hostParser+  <*> portParser+  <*> optionsParser++-- | Parse target language+targetParser :: Parser Target+targetParser = argument readTarget+  ( metavar "TARGET"+ <> help "Target language (typescript, python, rust)"+  )+  where+    readTarget = maybeReader $ \case+      "typescript" -> Just TypeScript+      "ts"         -> Just TypeScript+      "python"     -> Just Python+      "py"         -> Just Python+      "rust"       -> Just Rust+      "rs"         -> Just Rust+      _            -> Nothing++-- | Parse backend name+backendParser :: Parser Backend+backendParser = Backend . T.pack <$> argument str+  ( metavar "BACKEND"+ <> help "Backend identifier (substrate, plexus, synapse, etc.)"+  )++-- | Parse registry host+hostParser :: Parser Text+hostParser = T.pack <$> option str+  ( long "host"+ <> short 'H'+ <> metavar "HOST"+ <> value "127.0.0.1"+ <> showDefault+ <> help "Registry/discovery host"+  )++-- | Parse registry port+portParser :: Parser Text+portParser = T.pack <$> option str+  ( long "port"+ <> short 'P'+ <> metavar "PORT"+ <> value "4444"+ <> showDefault+ <> help "Registry/discovery port"+  )++-- | Parse options+optionsParser :: Parser Options+optionsParser = Options+  <$> option str+      ( long "output"+     <> short 'o'+     <> metavar "DIR"+     <> value (optOutput defaultOptions)+     <> showDefault+     <> help "Output directory"+      )+  <*> option auto+      ( long "bundle-transport"+     <> metavar "BOOL"+     <> value (optBundleTransport defaultOptions)+     <> showDefault+     <> help "Bundle transport code (true/false)"+      )+  <*> flag True False+      ( long "no-install"+     <> help "Skip dependency installation"+      )+  <*> flag True False+      ( long "no-build"+     <> help "Skip compilation step"+      )+  <*> flag True False+      ( long "no-tests"+     <> help "Skip running smoke tests"+      )+  <*> option str+      ( long "cache-dir"+     <> metavar "DIR"+     <> value (optCacheDir defaultOptions)+     <> showDefault+     <> help "Cache directory"+      )+  <*> switch+      ( long "force"+     <> help "Force regeneration (ignore cache)"+      )+  <*> switch+      ( long "watch"+     <> help "Watch backend and regenerate on changes"+      )+  <*> switch+      ( long "debug"+     <> help "Enable debug logging"+      )++-- ============================================================================+-- Version Info+-- ============================================================================++-- | Version information+versionInfo :: Text+versionInfo = "synapse-cc v0.1.0"
+ src/SynapseCC/Cache.hs view
@@ -0,0 +1,383 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Caching support with version-aware invalidation+module SynapseCC.Cache+  ( -- * Cache operations+    validateCache+  , readIRCacheManifest+  , readCodeCacheManifest+  , writeIRCacheManifest+  , writeCodeCacheManifest+  , getCacheDir+  , clearCache++    -- * V2 Validation (internal, exposed for testing)+  , validatePluginCaches+  , validatePluginCache+  ) where++import Control.Exception (catch, SomeException)+import Control.Monad (when, forM_)+import Data.Aeson (encode, eitherDecodeFileStrict)+import qualified Data.ByteString.Lazy as BL+import Data.List (isPrefixOf)+import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Maybe (isJust)+import qualified Data.Set as Set+import Data.Text (Text)+import qualified Data.Text as T+import Data.Time.Clock (getCurrentTime)+import Data.Time.Format (formatTime, defaultTimeLocale)+import System.Directory (createDirectoryIfMissing, doesFileExist, removeDirectoryRecursive, getHomeDirectory)+import System.FilePath ((</>))++import SynapseCC.Types+import qualified SynapseCC.Dependency as Dep++-- ============================================================================+-- Cache Directory Structure+-- ============================================================================++-- Cache directory structure:+-- ~/.cache/plexus-codegen/+-- ├── synapse/+-- │   ├── schemas/          # Level 1: Schema cache (future)+-- │   └── ir/               # Level 2: IR cache+-- │       ├── manifest.json # IR cache manifest+-- │       ├── cone.json     # Generated IR fragments+-- │       └── arbor.json+-- └── hub-codegen/+--     └── typescript/       # Level 3: Code cache+--         ├── manifest.json # Code cache manifest+--         ├── cone/         # Generated code per plugin+--         └── arbor/++-- | Get the cache base directory, expanding ~ to home directory+getCacheDir :: Options -> IO FilePath+getCacheDir opts = expandTilde (optCacheDir opts)++-- | Expand ~ to home directory if path starts with ~+expandTilde :: FilePath -> IO FilePath+expandTilde path+  | "~/" `isPrefixOf` path = do+      home <- getHomeDirectory+      pure $ home ++ drop 1 path  -- Replace ~ with home, keeping the /+  | path == "~" = getHomeDirectory+  | otherwise = pure path++-- | Get IR cache directory+getIRCacheDir :: Options -> Backend -> IO FilePath+getIRCacheDir opts backend = do+  baseDir <- getCacheDir opts+  pure $ baseDir </> "synapse" </> "ir" </> T.unpack (backendName backend)++-- | Get code cache directory+getCodeCacheDir :: Options -> Backend -> Target -> IO FilePath+getCodeCacheDir opts backend target = do+  baseDir <- getCacheDir opts+  pure $ baseDir </> "hub-codegen" </> targetName target </> T.unpack (backendName backend)+  where+    targetName TypeScript = "typescript"+    targetName Python = "python"+    targetName Rust = "rust"++-- ============================================================================+-- Cache Reading+-- ============================================================================++-- | Read IR cache manifest+readIRCacheManifest :: Options -> Backend -> IO (Either SynapseCCError IRCacheManifest)+readIRCacheManifest opts backend = do+  cacheDir <- getIRCacheDir opts backend+  let manifestPath = cacheDir </> "manifest.json"+  exists <- doesFileExist manifestPath+  if not exists+    then pure $ Left $ CacheError "IR cache manifest not found"+    else do+      result <- eitherDecodeFileStrict manifestPath+      case result of+        Left err -> pure $ Left $ CacheError $ "Failed to parse IR cache manifest: " <> T.pack err+        Right manifest -> pure $ Right manifest++-- | Read code cache manifest+readCodeCacheManifest :: Options -> Backend -> Target -> IO (Either SynapseCCError CodeCacheManifest)+readCodeCacheManifest opts backend target = do+  cacheDir <- getCodeCacheDir opts backend target+  let manifestPath = cacheDir </> "manifest.json"+  exists <- doesFileExist manifestPath+  if not exists+    then pure $ Left $ CacheError "Code cache manifest not found"+    else do+      result <- eitherDecodeFileStrict manifestPath+      case result of+        Left err -> pure $ Left $ CacheError $ "Failed to parse code cache manifest: " <> T.pack err+        Right manifest -> pure $ Right manifest++-- ============================================================================+-- Cache Validation+-- ============================================================================++-- | Validate cache with version-aware checking+-- Returns CacheResult indicating what needs regeneration+validateCache :: Config -> IO CacheResult+validateCache config = do+  let opts = cfgOptions config+      backend = cfgBackend config+      target = cfgTarget config+      debug = optDebug opts++  -- If --force flag is set, skip cache entirely+  if optForce opts+    then do+      when debug $ putStrLn "[!] --force flag set, skipping cache"+      pure $ CacheMiss ManifestNotFound+    else do+      -- Try to read IR cache manifest+      irManifestResult <- readIRCacheManifest opts backend+      case irManifestResult of+        Left err -> do+          when debug $ putStrLn $ "[!] IR cache miss: " ++ T.unpack (formatError err)+          pure $ CacheMiss ManifestNotFound++        Right irManifest -> do+          -- Check tool versions against IR cache+          let irToolchain = ircmToolchain irManifest+          if tvSynapseCC irToolchain /= synapseCCVersion ||+             tvSynapse irToolchain /= "0.2.0.0"  -- TODO: Get from synapse somehow+            then do+              when debug $ putStrLn "[!] Tool versions changed, invalidating IR cache"+              pure $ CacheMiss ToolVersionChanged+            else do+              -- IR cache valid, check code cache+              codeManifestResult <- readCodeCacheManifest opts backend target+              case codeManifestResult of+                Left err -> do+                  when debug $ putStrLn $ "[!] Code cache miss: " ++ T.unpack (formatError err)+                  -- IR is cached, but code is not+                  pure $ CacheMiss ManifestNotFound++                Right codeManifest -> do+                  -- Check tool versions against code cache+                  let codeToolchain = ccmToolchain codeManifest+                  if tvSynapseCC codeToolchain /= synapseCCVersion ||+                     tvSynapse codeToolchain /= "0.2.0.0" ||  -- TODO: Get from synapse+                     isJust (tvHubCodegen codeToolchain) && tvHubCodegen codeToolchain /= Just "0.1.0"  -- TODO: Get from hub-codegen+                    then do+                      when debug $ putStrLn "[!] Tool versions changed, invalidating code cache"+                      pure $ CacheMiss ToolVersionChanged+                    else do+                      -- V2: Check plugin hashes for granular invalidation+                      when debug $ putStrLn "[*] Checking plugin hashes for granular invalidation..."+                      validatePluginCaches irManifest codeManifest debug++-- ============================================================================+-- V2 Granular Cache Validation+-- ============================================================================++-- | Validate plugin caches with V2 granular hash checking+validatePluginCaches :: IRCacheManifest -> CodeCacheManifest -> Bool -> IO CacheResult+validatePluginCaches irManifest codeManifest debug = do+  let irPlugins = ircmPlugins irManifest+      codePlugins = ccmPlugins codeManifest+      allPluginNames = Set.toList $ Set.union (Map.keysSet irPlugins) (Map.keysSet codePlugins)++  -- Check each plugin for direct invalidation (schema/IR hash changes)+  results <- mapM (validatePluginCache irManifest codeManifest debug) allPluginNames++  -- Collect directly invalid plugins+  let directlyInvalidPlugins = Set.fromList [name | (name, False) <- results]+      directlyValidPlugins = [name | (name, True) <- results]++  -- Build dependency graph from IR cache+  let depGraph = Map.map ipcDependencies irPlugins++  -- Find transitively invalid plugins using dependency resolution+  let allInvalidPlugins = Dep.findInvalidPlugins directlyInvalidPlugins depGraph+      invalidPluginsList = Set.toList allInvalidPlugins+      validPluginsList = filter (`Set.notMember` allInvalidPlugins) directlyValidPlugins++  -- Report transitive invalidation+  when debug $ do+    let transitivelyInvalid = Set.difference allInvalidPlugins directlyInvalidPlugins+    when (not $ Set.null transitivelyInvalid) $ do+      putStrLn "[!] Transitive invalidation due to dependencies:"+      forM_ (Set.toList transitivelyInvalid) $ \name ->+        putStrLn $ "    - " ++ T.unpack name ++ " (depends on invalid plugin)"++  if null invalidPluginsList+    then do+      when debug $ putStrLn $ "[+] Full cache hit - all " ++ show (length validPluginsList) ++ " plugins valid"+      pure FullCacheHit+    else do+      when debug $ do+        putStrLn $ "[!] Partial cache hit:"+        putStrLn $ "    Valid: " ++ show (length validPluginsList) ++ " plugins"+        putStrLn $ "    Invalid: " ++ show (length invalidPluginsList) ++ " plugins (including transitive)"+        forM_ invalidPluginsList $ \name ->+          putStrLn $ "      - " ++ T.unpack name+      pure $ PartialCacheHit validPluginsList invalidPluginsList++-- | Validate a single plugin's cache+-- Returns (plugin name, is valid)+validatePluginCache :: IRCacheManifest -> CodeCacheManifest -> Bool -> Text -> IO (Text, Bool)+validatePluginCache irManifest codeManifest debug pluginName = do+  let irPlugins = ircmPlugins irManifest+      codePlugins = ccmPlugins codeManifest++  case (Map.lookup pluginName irPlugins, Map.lookup pluginName codePlugins) of+    (Nothing, Nothing) -> do+      -- Plugin not in either cache - invalid+      when debug $ putStrLn $ "[!] Plugin '" ++ T.unpack pluginName ++ "' not found in cache"+      pure (pluginName, False)++    (Just _irCache, Nothing) -> do+      -- IR cached but code not generated - invalid+      when debug $ putStrLn $ "[!] Plugin '" ++ T.unpack pluginName ++ "' has IR cache but no code cache"+      pure (pluginName, False)++    (Nothing, Just _codeCache) -> do+      -- Code exists but no IR cache - invalid (shouldn't happen)+      when debug $ putStrLn $ "[!] Plugin '" ++ T.unpack pluginName ++ "' has code cache but no IR cache"+      pure (pluginName, False)++    (Just irCache, Just codeCache) -> do+      -- Both caches exist - perform V2 granular validation+      validatePluginV2 irCache codeCache pluginName debug++-- | V2 validation logic using self_hash and children_hash+validatePluginV2 :: IRPluginCache -> CodePluginCache -> Text -> Bool -> IO (Text, Bool)+validatePluginV2 irCache codeCache pluginName debug = do+  -- Check if IR hash in code cache matches IR hash in IR cache+  let cachedIRHash = ipcIRHash irCache+      codeIRHash = cpcIRHash codeCache+      cachedSelfHash = ipcSelfHash irCache+      cachedChildrenHash = ipcChildrenHash irCache++  if T.null cachedIRHash || T.null codeIRHash+    then do+      -- Missing hash data - consider invalid for safety+      reportInvalidation pluginName "Missing hash data" debug+      pure (pluginName, False)+    else if cachedIRHash == codeIRHash+      then do+        -- IR hash matches - cache is valid+        reportCacheHit pluginName cachedSelfHash cachedChildrenHash debug+        pure (pluginName, True)+      else do+        -- IR hash changed - analyze granularity with V2 hashes+        analyzeHashChange pluginName irCache codeIRHash debug+        pure (pluginName, False)++-- | Report cache hit with V2 hash details+reportCacheHit :: Text -> Text -> Text -> Bool -> IO ()+reportCacheHit pluginName selfHash childrenHash debug = do+  when debug $ do+    putStrLn $ "[+] Cache hit: '" ++ T.unpack pluginName ++ "'"+    when (not $ T.null selfHash) $+      putStrLn $ "    self_hash:     " ++ T.unpack (T.take 8 selfHash) ++ "..."+    when (not $ T.null childrenHash) $+      putStrLn $ "    children_hash: " ++ T.unpack (T.take 8 childrenHash) ++ "..."++-- | Report cache invalidation with detailed reason+reportInvalidation :: Text -> String -> Bool -> IO ()+reportInvalidation pluginName reason debug = do+  when debug $ do+    putStrLn $ "[!] Cache miss: '" ++ T.unpack pluginName ++ "'"+    putStrLn $ "    Reason: " ++ reason++-- | Analyze what changed using V2 granular hashes+analyzeHashChange :: Text -> IRPluginCache -> Text -> Bool -> IO ()+analyzeHashChange pluginName irCache newCodeHash debug = do+  let cachedIRHash = ipcIRHash irCache+      cachedSelfHash = ipcSelfHash irCache+      cachedChildrenHash = ipcChildrenHash irCache++  when debug $ do+    putStrLn $ "[!] Cache miss: '" ++ T.unpack pluginName ++ "'"+    putStrLn $ "    IR hash changed:"+    putStrLn $ "      cached: " ++ T.unpack (T.take 8 cachedIRHash) ++ "..."+    putStrLn $ "      new:    " ++ T.unpack (T.take 8 newCodeHash) ++ "..."++    -- V2 granular analysis (when fresh hashes are available)+    if T.null cachedSelfHash && T.null cachedChildrenHash+      then do+        putStrLn "    V2 hashes unavailable - using V1 validation"+        putStrLn "    Reason: Schema or IR changed (full regeneration needed)"+      else do+        putStrLn "    V2 granular analysis:"+        when (not $ T.null cachedSelfHash) $+          putStrLn $ "      self_hash:     " ++ T.unpack (T.take 8 cachedSelfHash) ++ "... (methods)"+        when (not $ T.null cachedChildrenHash) $+          putStrLn $ "      children_hash: " ++ T.unpack (T.take 8 cachedChildrenHash) ++ "... (dependencies)"+        putStrLn "    Note: Fresh schema comparison needed for granular invalidation"+        putStrLn "          (requires fetching current schema from backend)"++-- ============================================================================+-- Cache Writing+-- ============================================================================++-- | Write IR cache manifest+writeIRCacheManifest :: Options -> Backend -> Map Text IRPluginCache -> IO ()+writeIRCacheManifest opts backend plugins = do+  cacheDir <- getIRCacheDir opts backend+  createDirectoryIfMissing True cacheDir++  currentTime <- getCurrentTime+  let timestamp = T.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" currentTime++  let manifest = IRCacheManifest+        { ircmVersion = "1.0"+        , ircmIRVersion = "2.0"+        , ircmToolchain = ToolchainVersions+            { tvSynapseCC = synapseCCVersion+            , tvSynapse = "0.2.0.0"  -- TODO: Get from synapse+            , tvHubCodegen = Nothing+            }+        , ircmUpdatedAt = timestamp+        , ircmPlugins = plugins+        }++  let manifestPath = cacheDir </> "manifest.json"+  BL.writeFile manifestPath (encode manifest)++-- | Write code cache manifest+writeCodeCacheManifest :: Options -> Backend -> Target -> Map Text CodePluginCache -> IO ()+writeCodeCacheManifest opts backend target plugins = do+  cacheDir <- getCodeCacheDir opts backend target+  createDirectoryIfMissing True cacheDir++  currentTime <- getCurrentTime+  let timestamp = T.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ" currentTime++  let manifest = CodeCacheManifest+        { ccmVersion = "1.0"+        , ccmTarget = T.pack $ case target of+            TypeScript -> "typescript"+            Python -> "python"+            Rust -> "rust"+        , ccmToolchain = ToolchainVersions+            { tvSynapseCC = synapseCCVersion+            , tvSynapse = "0.2.0.0"  -- TODO: Get from synapse+            , tvHubCodegen = Just "0.1.0"  -- TODO: Get from hub-codegen+            }+        , ccmUpdatedAt = timestamp+        , ccmPlugins = plugins+        }++  let manifestPath = cacheDir </> "manifest.json"+  BL.writeFile manifestPath (encode manifest)++-- ============================================================================+-- Cache Management+-- ============================================================================++-- | Clear the entire cache directory+clearCache :: Options -> IO ()+clearCache opts = do+  cacheDir <- getCacheDir opts+  exists <- doesFileExist cacheDir+  when exists $ do+    catch+      (removeDirectoryRecursive cacheDir)+      (\(e :: SomeException) -> putStrLn $ "Warning: Failed to clear cache: " ++ show e)
+ src/SynapseCC/Dependency.hs view
@@ -0,0 +1,124 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Dependency graph resolution for cache invalidation+module SynapseCC.Dependency+  ( -- * Dependency graph operations+    buildDependencyGraph+  , findInvalidPlugins+  , topologicalSort+  , DependencyGraph+  ) where++import Data.Map.Strict (Map)+import qualified Data.Map.Strict as Map+import Data.Set (Set)+import qualified Data.Set as Set+import Data.Text (Text)++-- ============================================================================+-- Types+-- ============================================================================++-- | Dependency graph: plugin name -> list of dependencies+type DependencyGraph = Map Text [Text]++-- ============================================================================+-- Graph Construction+-- ============================================================================++-- | Build dependency graph from IR plugin cache+-- Maps each plugin to its list of dependencies+buildDependencyGraph :: Map Text [Text] -> DependencyGraph+buildDependencyGraph pluginDeps = pluginDeps++-- ============================================================================+-- Invalidation Resolution+-- ============================================================================++-- | Find all plugins that need regeneration due to dependencies+-- Takes a set of initially invalid plugins and a dependency graph,+-- returns all plugins that are transitively invalid.+--+-- Example:+--   If 'arbor' is invalid and 'cone' depends on 'arbor',+--   then both 'arbor' and 'cone' are invalid.+findInvalidPlugins+  :: Set Text          -- ^ Initially invalid plugins+  -> DependencyGraph   -- ^ Dependency map (plugin -> its dependencies)+  -> Set Text          -- ^ All invalid plugins (with transitive deps)+findInvalidPlugins initialInvalid depGraph =+  let -- Find reverse dependencies: which plugins depend on each plugin+      reverseDeps = buildReverseDependencyMap depGraph++      -- Recursively find all dependents+      allInvalid = fixpoint (expandInvalid reverseDeps) initialInvalid+  in allInvalid++-- | Build reverse dependency map: for each plugin, which other plugins depend on it+buildReverseDependencyMap :: DependencyGraph -> Map Text (Set Text)+buildReverseDependencyMap depGraph =+  Map.foldrWithKey addReverseDeps Map.empty depGraph+  where+    addReverseDeps :: Text -> [Text] -> Map Text (Set Text) -> Map Text (Set Text)+    addReverseDeps plugin deps acc =+      foldr (\dep m -> Map.insertWith Set.union dep (Set.singleton plugin) m) acc deps++-- | Expand the set of invalid plugins by one step (add direct dependents)+expandInvalid :: Map Text (Set Text) -> Set Text -> Set Text+expandInvalid reverseDeps invalid =+  let -- Find all plugins that directly depend on any invalid plugin+      newInvalid = Set.foldr+        (\invalidPlugin acc ->+          case Map.lookup invalidPlugin reverseDeps of+            Nothing -> acc  -- No dependents+            Just dependents -> Set.union acc dependents+        )+        Set.empty+        invalid+  in Set.union invalid newInvalid++-- | Apply a function repeatedly until the result stops changing (fixed point)+fixpoint :: Eq a => (a -> a) -> a -> a+fixpoint f x =+  let x' = f x+  in if x' == x then x else fixpoint f x'++-- ============================================================================+-- Topological Sort+-- ============================================================================++-- | Topological sort for regeneration order+-- Returns plugins in dependency order (dependencies before dependents)+-- If there are cycles, returns an arbitrary but valid partial order+topologicalSort :: DependencyGraph -> [Text]+topologicalSort depGraph =+  let allPlugins = Set.union+        (Map.keysSet depGraph)+        (Set.unions $ map Set.fromList $ Map.elems depGraph)+      sorted = topSort depGraph allPlugins []+  in sorted++-- | Kahn's algorithm for topological sorting+topSort :: DependencyGraph -> Set Text -> [Text] -> [Text]+topSort depGraph remaining acc+  | Set.null remaining = reverse acc+  | otherwise =+      -- Find nodes with no dependencies (or all dependencies already processed)+      let nodesWithNoDeps = Set.filter (hasNoDependencies depGraph (Set.fromList acc)) remaining+      in if Set.null nodesWithNoDeps+           then+             -- Cycle detected or no more nodes without deps+             -- Just take remaining nodes in arbitrary order+             reverse acc ++ Set.toList remaining+           else+             -- Process one node with no dependencies+             let node = Set.findMin nodesWithNoDeps+                 remaining' = Set.delete node remaining+             in topSort depGraph remaining' (node : acc)++-- | Check if a plugin has no unprocessed dependencies+hasNoDependencies :: DependencyGraph -> Set Text -> Text -> Bool+hasNoDependencies depGraph processed plugin =+  case Map.lookup plugin depGraph of+    Nothing -> True  -- No dependencies+    Just deps -> all (`Set.member` processed) deps
+ src/SynapseCC/Discover.hs view
@@ -0,0 +1,148 @@+-- | Tool discovery - finding synapse and hub-codegen executables+module SynapseCC.Discover+  ( discoverTools+  , findTool+  , toolPathToFilePath+  ) where++import Control.Monad (filterM)+import Data.Text (Text)+import qualified Data.Text as T+import System.Directory (doesFileExist, findExecutable, getHomeDirectory)+import System.FilePath ((</>))++import SynapseCC.Types++-- ============================================================================+-- Tool Discovery+-- ============================================================================++-- | Discover all required tools+discoverTools :: Bool -> IO (Either SynapseCCError ToolLocations)+discoverTools debug = do+  when debug $ putStrLn "[*] Discovering tools..."++  synapsePath <- findTool debug "synapse" synapsePaths+  case synapsePath of+    Nothing -> pure $ Left $ ToolNotFound "synapse" synapseSuggestions+    Just synapseToolPath -> do+      when debug $ putStrLn $ "  [+] Found synapse at " ++ toolPathToFilePath synapseToolPath++      hubCodegenPath <- findTool debug "hub-codegen" hubCodegenPaths+      case hubCodegenPath of+        Nothing -> pure $ Left $ ToolNotFound "hub-codegen" hubCodegenSuggestions+        Just hubCodegenToolPath -> do+          when debug $ putStrLn $ "  [+] Found hub-codegen at " ++ toolPathToFilePath hubCodegenToolPath++          pure $ Right $ ToolLocations+            { toolSynapse = synapseToolPath+            , toolHubCodegen = hubCodegenToolPath+            }++-- | Find a tool by checking multiple locations+findTool :: Bool -> String -> (FilePath -> [FilePath]) -> IO (Maybe ToolPath)+findTool debug name pathsF = do+  home <- getHomeDirectory+  let paths = pathsF home++  when debug $ putStrLn $ "  Searching for " ++ name ++ "..."++  -- Try to find the tool in priority order+  tryPaths paths+  where+    tryPaths [] = do+      -- Last resort: check system PATH+      mbSystemPath <- findExecutable name+      case mbSystemPath of+        Just path -> do+          when debug $ putStrLn $ "    Found in PATH: " ++ path+          pure $ Just $ SystemPath path+        Nothing -> pure Nothing++    tryPaths (path:rest) = do+      exists <- doesFileExist path+      if exists+        then do+          when debug $ putStrLn $ "    Found: " ++ path+          pure $ Just $ classifyPath path+        else tryPaths rest++-- | Classify a path based on where it was found+classifyPath :: FilePath -> ToolPath+classifyPath path+  | "dist-newstyle" `elem` splitPath path = LocalDev path+  | "target" `elem` splitPath path = LocalDev path+  | ".plexus/bin" `elem` splitPath path = PlexusBin path+  | otherwise = SystemPath path+  where+    splitPath = filter (not . null) . wordsBy (== '/')+    wordsBy p s = case dropWhile p s of+      [] -> []+      s' -> let (w, s'') = break p s' in w : wordsBy p s''++-- | Convert ToolPath to FilePath+toolPathToFilePath :: ToolPath -> FilePath+toolPathToFilePath = \case+  LocalDev path   -> path+  SystemPath path -> path+  PlexusBin path  -> path++-- ============================================================================+-- Search Paths+-- ============================================================================++-- | Search paths for synapse (in priority order)+synapsePaths :: FilePath -> [FilePath]+synapsePaths home =+  [ -- Local development builds+    "../synapse/dist-newstyle/build/aarch64-linux/ghc-9.4.8/plexus-synapse-0.2.0.0/x/synapse/build/synapse/synapse"+  , "../synapse/dist-newstyle/build/x86_64-linux/ghc-9.4.8/plexus-synapse-0.2.0.0/x/synapse/build/synapse/synapse"+  , "../../synapse/dist-newstyle/build/aarch64-linux/ghc-9.4.8/plexus-synapse-0.2.0.0/x/synapse/build/synapse/synapse"+  , "../../synapse/dist-newstyle/build/x86_64-linux/ghc-9.4.8/plexus-synapse-0.2.0.0/x/synapse/build/synapse/synapse"+    -- Plexus bin directory+  , home </> ".plexus/bin/synapse"+    -- Cabal install location+  , home </> ".cabal/bin/synapse"+  ]++-- | Search paths for hub-codegen (in priority order)+hubCodegenPaths :: FilePath -> [FilePath]+hubCodegenPaths home =+  [ -- Local development builds+    "../hub-codegen/target/release/hub-codegen"+  , "../hub-codegen/target/debug/hub-codegen"+  , "../../hub-codegen/target/release/hub-codegen"+  , "../../hub-codegen/target/debug/hub-codegen"+    -- Plexus bin directory+  , home </> ".plexus/bin/hub-codegen"+    -- Cargo install location+  , home </> ".cargo/bin/hub-codegen"+  ]++-- ============================================================================+-- Suggestions+-- ============================================================================++-- | Suggestions for installing synapse+synapseSuggestions :: [Text]+synapseSuggestions =+  [ "Build synapse: cd ../synapse && cabal build"+  , "Install synapse: cabal install synapse"+  , "Add to PATH: export PATH=\"$PATH:~/.plexus/bin\""+  ]++-- | Suggestions for installing hub-codegen+hubCodegenSuggestions :: [Text]+hubCodegenSuggestions =+  [ "Build hub-codegen: cd ../hub-codegen && cargo build --release"+  , "Install hub-codegen: cargo install --path ../hub-codegen"+  , "Add to PATH: export PATH=\"$PATH:~/.plexus/bin\""+  ]++-- ============================================================================+-- Helpers+-- ============================================================================++when :: Applicative f => Bool -> f () -> f ()+when True action = action+when False _ = pure ()
+ src/SynapseCC/Language.hs view
@@ -0,0 +1,214 @@+-- | Language-specific integrations (dependency install, build, etc.)+module SynapseCC.Language+  ( installDependencies+  , buildProject+  , runTests+  ) where++import Control.Monad (when)+import Data.Maybe (isJust)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as TIO+import System.Directory (doesFileExist, findExecutable)+import System.Exit (ExitCode(..))+import System.FilePath ((</>))++import SynapseCC.Types+import SynapseCC.Process++-- ============================================================================+-- Language Integration (Phase 2)+-- ============================================================================++-- | Supported package managers for TypeScript+data PackageManager+  = Npm+  | Pnpm+  | Yarn+  | Bun+  deriving (Show, Eq)++-- | Get command name for package manager+packageManagerCommand :: PackageManager -> String+packageManagerCommand = \case+  Npm  -> "npm"+  Pnpm -> "pnpm"+  Yarn -> "yarn"+  Bun  -> "bun"++-- | Detect which package manager to use for TypeScript project+detectPackageManager :: GeneratedPath -> Bool -> IO PackageManager+detectPackageManager (GeneratedPath path) debug = do+  when debug $ putStrLn "[*] Detecting package manager..."++  -- Priority 1: Check for lockfiles+  hasPnpmLock <- doesFileExist (path </> "pnpm-lock.yaml")+  if hasPnpmLock+    then do+      when debug $ putStrLn "  [+] Found pnpm-lock.yaml, using pnpm"+      pure Pnpm+    else do+      hasYarnLock <- doesFileExist (path </> "yarn.lock")+      if hasYarnLock+        then do+          when debug $ putStrLn "  [+] Found yarn.lock, using yarn"+          pure Yarn+        else do+          -- Priority 2: Check available commands (prefer pnpm > yarn > bun > npm)+          hasPnpm <- isJust <$> findExecutable "pnpm"+          if hasPnpm+            then do+              when debug $ putStrLn "  [+] pnpm available, using pnpm"+              pure Pnpm+            else do+              hasYarn <- isJust <$> findExecutable "yarn"+              if hasYarn+                then do+                  when debug $ putStrLn "  [+] yarn available, using yarn"+                  pure Yarn+                else do+                  hasBun <- isJust <$> findExecutable "bun"+                  if hasBun+                    then do+                      when debug $ putStrLn "  [+] bun available, using bun"+                      pure Bun+                    else do+                      when debug $ putStrLn "  [+] Defaulting to npm"+                      pure Npm++-- | Check if package.json contains workspace protocol+checkForWorkspaceProtocol :: GeneratedPath -> IO Bool+checkForWorkspaceProtocol (GeneratedPath path) = do+  let packageJson = path </> "package.json"+  exists <- doesFileExist packageJson+  if exists+    then do+      content <- TIO.readFile packageJson+      pure $ "workspace:" `T.isInfixOf` content+    else pure False++-- | Provide helpful suggestions based on error patterns+checkInstallError :: Text -> [Text]+checkInstallError stderr+  | "EACCES" `T.isInfixOf` stderr =+      ["Permission denied - try: sudo chown -R $USER ~/.npm"]+  | "ENOTFOUND" `T.isInfixOf` stderr =+      ["Network error - check your internet connection"]+  | "ENETUNREACH" `T.isInfixOf` stderr =+      ["Network unreachable - check firewall/proxy settings"]+  | "workspace:" `T.isInfixOf` stderr =+      ["npm doesn't support workspace: protocol"+      ,"Use pnpm, yarn, or bun: npm install -g pnpm && pnpm install"+      ,"Or enable bundle-transport: --bundle-transport=true"]+  | otherwise = []++-- ============================================================================+-- Dependency Installation+-- ============================================================================++-- | Install dependencies for generated code+installDependencies :: Target -> GeneratedPath -> Bool -> IO (Either SynapseCCError ())+installDependencies TypeScript genPath debug = do+  when debug $ putStrLn $ "[*] Installing dependencies in " ++ unGeneratedPath genPath++  -- Detect package manager+  pm <- detectPackageManager genPath debug+  let pmCmd = packageManagerCommand pm++  -- Check for workspace protocol with npm (warn user)+  when (pm == Npm) $ do+    hasWorkspace <- checkForWorkspaceProtocol genPath+    when hasWorkspace $ do+      putStrLn "\nWarning: This project uses the 'workspace:*' protocol"+      putStrLn "npm does not support workspace protocol."+      putStrLn "\nOptions:"+      putStrLn "  1. Use pnpm: npm install -g pnpm && pnpm install"+      putStrLn "  2. Use yarn: npm install -g yarn && yarn install"+      putStrLn "  3. Set --bundle-transport=true to avoid workspace dependency"++  -- Verify package manager is actually available+  mbPmPath <- findExecutable pmCmd+  case mbPmPath of+    Nothing -> pure $ Left $ ToolNotFound (T.pack pmCmd)+      [ "Install Node.js from https://nodejs.org"+      , "Or install " <> T.pack pmCmd <> " separately: npm install -g " <> T.pack pmCmd+      ]+    Just _ -> do+      -- Run install command+      result <- runProcess pmCmd ["install"] (Just $ unGeneratedPath genPath) debug++      case prExitCode result of+        ExitSuccess -> do+          when debug $ putStrLn "  [+] Dependencies installed"+          pure $ Right ()+        ExitFailure code -> do+          let suggestions = checkInstallError (prStderr result)+          pure $ Left $ LanguageToolError+            (T.pack $ pmCmd ++ " install")+            (if null suggestions+              then prStderr result+              else prStderr result <> "\n\nSuggestions:\n" <> T.unlines suggestions)+            code++installDependencies Python _ debug = do+  when debug $ putStrLn "[*] Python dependency installation not yet implemented (Phase 2 TODO)"+  pure $ Right ()++installDependencies Rust _ debug = do+  when debug $ putStrLn "[*] Rust dependency installation not yet implemented (Phase 2 TODO)"+  pure $ Right ()++-- ============================================================================+-- Building/Type-checking+-- ============================================================================++-- | Build/compile generated code+buildProject :: Target -> GeneratedPath -> Bool -> IO (Either SynapseCCError CompiledPath)+buildProject TypeScript genPath debug = do+  when debug $ putStrLn $ "[*] Type-checking TypeScript in " ++ unGeneratedPath genPath++  -- Run tsc --noEmit for type-checking+  result <- runProcess "npx" ["tsc", "--noEmit"] (Just $ unGeneratedPath genPath) debug++  case prExitCode result of+    ExitSuccess -> do+      when debug $ putStrLn "  [+] TypeScript type-check passed"+      pure $ Right $ CompiledPath $ unGeneratedPath genPath+    ExitFailure code -> do+      pure $ Left $ LanguageToolError "tsc" (prStderr result) code++buildProject Python genPath debug = do+  when debug $ putStrLn "[*] Python build not yet implemented (Phase 2 TODO)"+  pure $ Right $ CompiledPath $ unGeneratedPath genPath++buildProject Rust genPath debug = do+  when debug $ putStrLn "[*] Rust build not yet implemented (Phase 2 TODO)"+  pure $ Right $ CompiledPath $ unGeneratedPath genPath++-- ============================================================================+-- Testing+-- ============================================================================++-- | Run smoke tests for generated code+runTests :: Target -> GeneratedPath -> Bool -> IO (Either SynapseCCError ())+runTests target genPath debug = case target of+  TypeScript -> runTypeScriptTests genPath debug+  Python -> pure $ Right ()  -- TODO: Implement Python tests+  Rust -> pure $ Right ()  -- TODO: Implement Rust tests++-- | Run TypeScript smoke tests using npm+runTypeScriptTests :: GeneratedPath -> Bool -> IO (Either SynapseCCError ())+runTypeScriptTests (GeneratedPath path) debug = do+  when debug $ putStrLn $ "[*] Running smoke tests in " ++ path++  -- Check if npm is available+  result <- runProcess "npm" ["test"] (Just path) debug++  case prExitCode result of+    ExitSuccess -> do+      when debug $ putStrLn "  [+] Tests passed"+      pure $ Right ()+    ExitFailure code -> do+      let stderr = prStderr result+      pure $ Left $ LanguageToolError "npm test" stderr code
+ src/SynapseCC/Logging.hs view
@@ -0,0 +1,58 @@+-- | Pretty logging and progress indicators+module SynapseCC.Logging+  ( logInfo+  , logSuccess+  , logError+  , logDebug+  , logStep+  ) where++import Data.Text (Text)+import qualified Data.Text.IO as TIO+import System.Console.ANSI++-- ============================================================================+-- Logging+-- ============================================================================++-- | Log an info message+logInfo :: Text -> IO ()+logInfo msg = do+  setSGR [SetColor Foreground Vivid Blue]+  TIO.putStr "[i] "+  setSGR [Reset]+  TIO.putStrLn msg++-- | Log a success message+logSuccess :: Text -> IO ()+logSuccess msg = do+  setSGR [SetColor Foreground Vivid Green]+  TIO.putStr "[+] "+  setSGR [Reset]+  TIO.putStrLn msg++-- | Log an error message+logError :: Text -> IO ()+logError msg = do+  setSGR [SetColor Foreground Vivid Red]+  TIO.putStr "[!] "+  setSGR [Reset]+  TIO.putStrLn msg++-- | Log a debug message (only if debug mode enabled)+logDebug :: Bool -> Text -> IO ()+logDebug True msg = do+  setSGR [SetColor Foreground Vivid Cyan, SetConsoleIntensity FaintIntensity]+  TIO.putStr "[debug] "+  TIO.putStrLn msg+  setSGR [Reset]+logDebug False _ = pure ()++-- | Log a pipeline step+logStep :: Text -> IO ()+logStep msg = do+  TIO.putStrLn ""+  setSGR [SetColor Foreground Vivid Yellow, SetConsoleIntensity BoldIntensity]+  TIO.putStr "==> "+  TIO.putStrLn msg+  setSGR [Reset]
+ src/SynapseCC/Pipeline.hs view
@@ -0,0 +1,321 @@+-- | Pipeline orchestration - running the full toolchain+module SynapseCC.Pipeline+  ( runPipeline+  , generateIR+  , generateCode+  ) where++import Control.Monad (unless, when)+import Data.Aeson (FromJSON, eitherDecodeStrict, eitherDecodeFileStrict)+import qualified Data.Aeson as Aeson+import qualified Data.ByteString as BS+import qualified Data.Map.Strict as Map+import Data.Maybe (fromMaybe)+import Data.Monoid (mempty)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import GHC.Generics (Generic)+import System.Directory (createDirectoryIfMissing, doesFileExist)+import System.Exit (ExitCode(..))+import System.FilePath ((</>), takeDirectory)++import SynapseCC.Types+import SynapseCC.Process+import SynapseCC.Discover+import qualified SynapseCC.Language as Language+import qualified SynapseCC.Cache as Cache++-- ============================================================================+-- Pipeline Orchestration+-- ============================================================================++-- | Run the complete pipeline+runPipeline :: Config -> ToolLocations -> IO (Either SynapseCCError CompiledPath)+runPipeline config tools = do+  let debug = optDebug (cfgOptions config)++  -- Step 0: Check cache (unless --force is set)+  cacheResult <- Cache.validateCache config+  case cacheResult of+    FullCacheHit -> do+      when debug $ putStrLn "\n[+] Full cache hit (versions match)"+      when debug $ putStrLn "  [*] Using cached output"+      -- TODO: Copy cached code to output directory if needed+      let outputPath = optOutput (cfgOptions config)+      pure $ Right $ CompiledPath outputPath++    CacheMiss reason -> do+      when debug $ putStrLn $ "\n[!] Cache miss: " ++ show reason+      when debug $ putStrLn "  [*] Regenerating..."+      runFullPipeline config tools++    PartialCacheHit valid invalid -> do+      when debug $ putStrLn "\n[*] Partial cache hit"+      when debug $ putStrLn $ "  [+] Valid plugins: " ++ show valid+      when debug $ putStrLn $ "  [!] Invalid plugins: " ++ show invalid+      when debug $ putStrLn "  [*] Regenerating invalid plugins..."+      -- TODO: Implement partial regeneration+      -- For now, do full regeneration+      runFullPipeline config tools++-- | Run the full pipeline without cache+runFullPipeline :: Config -> ToolLocations -> IO (Either SynapseCCError CompiledPath)+runFullPipeline config tools = do+  let debug = optDebug (cfgOptions config)++  -- Step 1: Generate IR+  when debug $ putStrLn "\n[*] Generating IR..."+  irResult <- generateIR config tools+  case irResult of+    Left err -> pure $ Left err+    Right irPath -> do+      when debug $ putStrLn $ "  [+] IR generated at " ++ unIRPath irPath++      -- Step 2: Generate code+      when debug $ putStrLn "\n[*] Generating code..."+      codeResult <- generateCode config tools irPath+      case codeResult of+        Left err -> pure $ Left err+        Right genPath -> do+          when debug $ putStrLn $ "  [+] Code generated at " ++ unGeneratedPath genPath++          -- Step 3: Install dependencies (if enabled)+          installResult <- if optInstallDeps (cfgOptions config)+            then do+              when debug $ putStrLn "\n[*] Installing dependencies..."+              Language.installDependencies (cfgTarget config) genPath debug+            else do+              when debug $ putStrLn "\n[*] Skipping dependency installation (--no-install)"+              pure $ Right ()++          case installResult of+            Left err -> pure $ Left err+            Right () -> do++              -- Step 4: Build project (if enabled)+              buildResult <- if optBuild (cfgOptions config)+                then do+                  when debug $ putStrLn "\n[*] Building project..."+                  Language.buildProject (cfgTarget config) genPath debug+                else do+                  when debug $ putStrLn "\n[*] Skipping build (--no-build)"+                  pure $ Right $ CompiledPath $ unGeneratedPath genPath++              case buildResult of+                Left err -> pure $ Left err+                Right compiledPath -> do++                  -- Step 5: Run tests (if enabled)+                  if optRunTests (cfgOptions config)+                    then do+                      when debug $ putStrLn "\n[*] Running smoke tests..."+                      testResult <- Language.runTests (cfgTarget config) genPath debug+                      case testResult of+                        Left err -> pure $ Left err+                        Right () -> do+                          when debug $ putStrLn "  [+] All tests passed"+                          writeCache config compiledPath+                          pure $ Right compiledPath+                    else do+                      writeCache config compiledPath+                      pure $ Right compiledPath++-- | Write cache manifests after successful generation+writeCache :: Config -> CompiledPath -> IO ()+writeCache config _compiledPath = do+  let debug = optDebug (cfgOptions config)+      outputDir = optOutput (cfgOptions config)+  when debug $ putStrLn "\n[*] Writing cache manifests..."++  -- Read file hashes from .codegen-metadata.json+  fileHashesMap <- readFileHashes outputDir debug++  -- Read IR to extract plugin hashes+  irPluginCaches <- readIRPluginHashes outputDir debug++  -- Write IR cache manifest with plugin hashes+  Cache.writeIRCacheManifest (cfgOptions config) (cfgBackend config) irPluginCaches++  -- Write code cache with file hashes+  -- Convert flat file hash map to per-plugin structure+  let pluginCaches = buildPluginCaches fileHashesMap+  Cache.writeCodeCacheManifest (cfgOptions config) (cfgBackend config) (cfgTarget config) pluginCaches++  when debug $ putStrLn "  [+] Cache manifests written"++-- | Read file hashes from .codegen-metadata.json+readFileHashes :: FilePath -> Bool -> IO (Map.Map Text Text)+readFileHashes outputDir debug = do+  let metadataPath = outputDir </> ".codegen-metadata.json"+  exists <- doesFileExist metadataPath+  if not exists+    then do+      when debug $ putStrLn "  [!] .codegen-metadata.json not found, skipping file hashes"+      pure Map.empty+    else do+      result <- eitherDecodeFileStrict metadataPath+      case result of+        Left err -> do+          when debug $ putStrLn $ "  [!] Failed to parse metadata: " ++ err+          pure Map.empty+        Right (metadata :: CodegenMetadata) -> do+          when debug $ putStrLn $ "  [+] Read " ++ show (Map.size (ciFileHashes (cmCache metadata))) ++ " file hashes"+          pure $ ciFileHashes (cmCache metadata)++-- | Read IR plugin hashes from ir.json+readIRPluginHashes :: FilePath -> Bool -> IO (Map.Map Text IRPluginCache)+readIRPluginHashes outputDir debug = do+  let irPath = outputDir </> "ir.json"+  exists <- doesFileExist irPath+  if not exists+    then do+      when debug $ putStrLn "  [!] ir.json not found, skipping IR plugin hashes"+      pure Map.empty+    else do+      result <- eitherDecodeFileStrict irPath+      case result of+        Left err -> do+          when debug $ putStrLn $ "  [!] Failed to parse IR: " ++ err+          pure Map.empty+        Right (irData :: IRData) -> do+          let pluginHashes = fromMaybe Map.empty (irdPluginHashes irData)+              pluginCaches = Map.mapWithKey (buildIRPluginCache pluginHashes) (irdPlugins irData)+          when debug $ putStrLn $ "  [+] Extracted hashes for " ++ show (Map.size pluginCaches) ++ " plugins"+          pure pluginCaches++-- | Build an IRPluginCache entry from plugin info and hashes+buildIRPluginCache :: Map.Map Text PluginHashInfo -> Text -> [Text] -> IRPluginCache+buildIRPluginCache hashMap pluginName _methods =+  case Map.lookup pluginName hashMap of+    Just hashes -> IRPluginCache+      { ipcIRHash = ""  -- TODO: Compute IR hash (WS2)+      , ipcSchemaHash = phiHash hashes+      , ipcSelfHash = phiSelfHash hashes+      , ipcChildrenHash = phiChildrenHash hashes+      , ipcDependencies = []  -- TODO: Extract dependencies from IR+      , ipcCachedAt = ""  -- Will be set by writeIRCacheManifest+      }+    Nothing -> IRPluginCache+      { ipcIRHash = ""+      , ipcSchemaHash = ""  -- No hash info available+      , ipcSelfHash = ""+      , ipcChildrenHash = ""+      , ipcDependencies = []+      , ipcCachedAt = ""+      }++-- | Build per-plugin cache entries from flat file hash map+-- For now, group all files into a single "default" plugin entry+-- This will be improved when we implement per-plugin hash tracking+buildPluginCaches :: Map.Map Text Text -> Map.Map Text CodePluginCache+buildPluginCaches fileHashes =+  if Map.null fileHashes+    then Map.empty+    else Map.singleton "default" CodePluginCache+      { cpcIRHash = ""  -- Will be populated when WS1 is complete+      , cpcFileHashes = fileHashes+      , cpcCachedAt = ""  -- Will be set by writeCodeCacheManifest+      }++-- ============================================================================+-- IR Generation+-- ============================================================================++-- | Generate IR using synapse+generateIR :: Config -> ToolLocations -> IO (Either SynapseCCError IRPath)+generateIR config tools = do+  let debug = optDebug (cfgOptions config)+      synapsePath = toolPathToFilePath (toolSynapse tools)+      Backend backendName = cfgBackend config+      outputDir = optOutput (cfgOptions config)+      irFile = outputDir </> "ir.json"++  -- Ensure output directory exists+  createDirectoryIfMissing True outputDir++  -- Build synapse command: synapse -H <host> -P <port> -i <backend> --generator-info synapse-cc:version+  let host = cfgHost config+      port = cfgPort config+      generatorInfo = "synapse-cc:" <> synapseCCVersion+      args = [ "-H", T.unpack host+             , "-P", T.unpack port+             , "--generator-info", T.unpack generatorInfo+             , "-i"+             , T.unpack backendName+             ]++  -- Run synapse+  result <- runProcess synapsePath args Nothing debug++  case prExitCode result of+    ExitSuccess -> do+      -- Write IR to file+      BS.writeFile irFile (TE.encodeUtf8 $ prStdout result)++      -- Validate IR by trying to parse it+      irBytes <- BS.readFile irFile+      case eitherDecodeStrict irBytes of+        Left parseErr -> pure $ Left $ InvalidIR $ T.pack parseErr+        Right (_ :: IRData) -> pure $ Right $ IRPath irFile++    ExitFailure code -> do+      pure $ Left $ SynapseError (prStderr result) code++-- | IR structure for reading plugin hashes+-- We only need the fields relevant for caching+data IRData = IRData+  { irdVersion      :: !Text+  , irdPlugins      :: !(Map.Map Text [Text])+  , irdPluginHashes :: !(Maybe (Map.Map Text PluginHashInfo))+  } deriving stock (Show, Generic)++instance FromJSON IRData where+  parseJSON = Aeson.withObject "IRData" $ \o -> IRData+    <$> o Aeson..: "irVersion"+    <*> o Aeson..: "irPlugins"+    <*> o Aeson..:? "irPluginHashes"++-- | Plugin hash information from IR+data PluginHashInfo = PluginHashInfo+  { phiHash         :: !Text+  , phiSelfHash     :: !Text+  , phiChildrenHash :: !Text+  } deriving stock (Show, Generic)+    deriving anyclass (FromJSON)++-- ============================================================================+-- Code Generation+-- ============================================================================++-- | Generate code using hub-codegen+generateCode :: Config -> ToolLocations -> IRPath -> IO (Either SynapseCCError GeneratedPath)+generateCode config tools irPath = do+  let debug = optDebug (cfgOptions config)+      hubCodegenPath = toolPathToFilePath (toolHubCodegen tools)+      outputDir = optOutput (cfgOptions config)+      target = cfgTarget config+      opts = cfgOptions config++  -- Build hub-codegen command+  let targetArg = case target of+        TypeScript -> "typescript"+        Python -> "python"+        Rust -> "rust"++      args =+        [ "--target", targetArg+        , "--output", outputDir+        , unIRPath irPath+        ]++  -- Run hub-codegen+  result <- runProcess hubCodegenPath args Nothing debug++  case prExitCode result of+    ExitSuccess -> do+      pure $ Right $ GeneratedPath outputDir++    ExitFailure code -> do+      pure $ Left $ HubCodegenError (prStderr result) code+
+ src/SynapseCC/Process.hs view
@@ -0,0 +1,134 @@+-- | Subprocess execution helpers+module SynapseCC.Process+  ( runProcess+  , runProcessWithInput+  , ProcessResult(..)+  ) where++import qualified Data.ByteString as BS+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE+import qualified Data.Text.Encoding.Error as TE+import qualified Data.Text.IO as TIO+import System.Exit (ExitCode(..))+import System.IO (Handle, hClose, hGetContents, hPutStr)+import System.Process hiding (runProcess)++-- ============================================================================+-- Process Execution+-- ============================================================================++-- | Result of running a process+data ProcessResult = ProcessResult+  { prExitCode :: !ExitCode+  , prStdout   :: !Text+  , prStderr   :: !Text+  } deriving (Show, Eq)++-- | Run a process and capture output+runProcess+  :: FilePath      -- ^ Executable path+  -> [String]      -- ^ Arguments+  -> Maybe FilePath -- ^ Working directory (Nothing = current)+  -> Bool          -- ^ Debug mode+  -> IO ProcessResult+runProcess exe args mbCwd debug = do+  when debug $ do+    putStrLn $ "Running: " ++ exe ++ " " ++ unwords args+    case mbCwd of+      Just cwd -> putStrLn $ "  Working directory: " ++ cwd+      Nothing -> pure ()++  (exitCode, stdout, stderr) <- readProcessWithExitCode' exe args mbCwd ""++  when debug $ do+    unless (T.null stdout) $ do+      let len = T.length stdout+      if len > 1000+        then putStrLn $ "  Stdout: <large output, " ++ show len ++ " chars, truncated>"+        else do+          putStrLn "  Stdout:"+          putStrLn $ "    " ++ T.unpack stdout+    unless (T.null stderr) $ do+      putStrLn "  Stderr:"+      putStrLn $ "    " ++ T.unpack stderr++  pure $ ProcessResult exitCode stdout stderr++-- | Run a process with stdin input+runProcessWithInput+  :: FilePath      -- ^ Executable path+  -> [String]      -- ^ Arguments+  -> Maybe FilePath -- ^ Working directory (Nothing = current)+  -> Text          -- ^ Stdin input+  -> Bool          -- ^ Debug mode+  -> IO ProcessResult+runProcessWithInput exe args mbCwd input debug = do+  when debug $ do+    putStrLn $ "Running: " ++ exe ++ " " ++ unwords args+    case mbCwd of+      Just cwd -> putStrLn $ "  Working directory: " ++ cwd+      Nothing -> pure ()+    putStrLn $ "  With input: " ++ T.unpack (T.take 100 input) ++ "..."++  (exitCode, stdout, stderr) <- readProcessWithExitCode' exe args mbCwd (T.unpack input)++  when debug $ do+    unless (T.null stdout) $ do+      let len = T.length stdout+      if len > 1000+        then putStrLn $ "  Stdout: <large output, " ++ show len ++ " chars, truncated>"+        else do+          putStrLn "  Stdout:"+          putStrLn $ "    " ++ T.unpack stdout+    unless (T.null stderr) $ do+      putStrLn "  Stderr:"+      putStrLn $ "    " ++ T.unpack stderr++  pure $ ProcessResult exitCode stdout stderr++-- ============================================================================+-- Helpers+-- ============================================================================++-- | Like readProcessWithExitCode but with working directory support+readProcessWithExitCode'+  :: FilePath+  -> [String]+  -> Maybe FilePath+  -> String+  -> IO (ExitCode, Text, Text)+readProcessWithExitCode' exe args mbCwd input = do+  let process = (proc exe args)+        { cwd = mbCwd+        , std_in = CreatePipe+        , std_out = CreatePipe+        , std_err = CreatePipe+        }++  (Just stdin, Just stdout, Just stderr, ph) <- createProcess process++  -- Write input and close stdin+  hPutStr stdin input+  hClose stdin++  -- Read output strictly as ByteString first, then decode with lenient error handling+  stdoutBytes <- BS.hGetContents stdout+  stderrBytes <- BS.hGetContents stderr++  let stdoutText = TE.decodeUtf8With TE.lenientDecode stdoutBytes+      stderrText = TE.decodeUtf8With TE.lenientDecode stderrBytes++  -- Wait for process to complete+  exitCode <- waitForProcess ph++  pure (exitCode, stdoutText, stderrText)++when :: Applicative f => Bool -> f () -> f ()+when True action = action+when False _ = pure ()++unless :: Applicative f => Bool -> f () -> f ()+unless False action = action+unless True _ = pure ()
+ src/SynapseCC/Types.hs view
@@ -0,0 +1,346 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}++-- | Core types for synapse-cc+module SynapseCC.Types+  ( -- * Version Information+    synapseCCVersion++    -- * Configuration+  , Config(..)+  , Target(..)+  , Backend(..)+  , Options(..)+  , defaultOptions++    -- * Tool Locations+  , ToolLocations(..)+  , ToolPath(..)++    -- * Pipeline Results+  , IRPath(..)+  , GeneratedPath(..)+  , CompiledPath(..)++    -- * Cache Types+  , ToolchainVersions(..)+  , IRPluginCache(..)+  , CodePluginCache(..)+  , IRCacheManifest(..)+  , CodeCacheManifest(..)+  , CacheResult(..)+  , CacheMissReason(..)++    -- * Metadata Types+  , CodegenMetadata(..)+  , CacheInfo(..)++    -- * Errors+  , SynapseCCError(..)+  , formatError+  ) where++import Data.Aeson (FromJSON, ToJSON, fieldLabelModifier)+import qualified Data.Aeson as Aeson+import Data.Map.Strict (Map)+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics (Generic)+import System.FilePath (FilePath)++-- ============================================================================+-- Version Information+-- ============================================================================++-- | synapse-cc version (from cabal file: synapse-cc.cabal)+synapseCCVersion :: Text+synapseCCVersion = "0.1.0.0"++-- ============================================================================+-- Configuration+-- ============================================================================++-- | Main configuration for synapse-cc+data Config = Config+  { cfgTarget         :: !Target+  , cfgBackend        :: !Backend+  , cfgHost           :: !Text+  , cfgPort           :: !Text+  , cfgOptions        :: !Options+  } deriving stock (Show, Eq, Generic)++-- | Target language for code generation+data Target+  = TypeScript+  | Python+  | Rust+  deriving stock (Show, Eq, Ord, Generic)+  deriving anyclass (FromJSON, ToJSON)++-- | Backend identifier+data Backend = Backend+  { backendName :: !Text  -- ^ Backend name (e.g., "substrate", "plexus")+  } deriving stock (Show, Eq, Generic)+  deriving anyclass (FromJSON, ToJSON)++-- | Options for code generation and compilation+data Options = Options+  { optOutput          :: !FilePath+  , optBundleTransport :: !Bool+  , optInstallDeps     :: !Bool+  , optBuild           :: !Bool+  , optRunTests        :: !Bool+  , optCacheDir        :: !FilePath+  , optForce           :: !Bool+  , optWatch           :: !Bool+  , optDebug           :: !Bool+  } deriving stock (Show, Eq, Generic)++-- | Default options+defaultOptions :: Options+defaultOptions = Options+  { optOutput          = "./generated"+  , optBundleTransport = True+  , optInstallDeps     = True+  , optBuild           = True+  , optRunTests        = False+  , optCacheDir        = "~/.cache/plexus-codegen"+  , optForce           = False+  , optWatch           = False+  , optDebug           = False+  }++-- ============================================================================+-- Tool Locations+-- ============================================================================++-- | Discovered locations of required tools+data ToolLocations = ToolLocations+  { toolSynapse    :: !ToolPath+  , toolHubCodegen :: !ToolPath+  } deriving stock (Show, Eq)++-- | Path to a discovered tool+data ToolPath+  = LocalDev !FilePath    -- ^ Local development build+  | SystemPath !FilePath  -- ^ Found in $PATH+  | PlexusBin !FilePath   -- ^ Installed in ~/.plexus/bin+  deriving stock (Show, Eq)++-- ============================================================================+-- Pipeline Results+-- ============================================================================++-- | Path to generated IR JSON file+newtype IRPath = IRPath { unIRPath :: FilePath }+  deriving stock (Show, Eq)++-- | Path to generated code directory+newtype GeneratedPath = GeneratedPath { unGeneratedPath :: FilePath }+  deriving stock (Show, Eq)++-- | Path to compiled output+newtype CompiledPath = CompiledPath { unCompiledPath :: FilePath }+  deriving stock (Show, Eq)++-- ============================================================================+-- Cache Types+-- ============================================================================++-- | Toolchain version information for cache invalidation+data ToolchainVersions = ToolchainVersions+  { tvSynapseCC   :: !Text+  , tvSynapse     :: !Text+  , tvHubCodegen  :: !(Maybe Text)  -- Only known after codegen+  } deriving stock (Show, Eq, Generic)+    deriving anyclass (FromJSON, ToJSON)++-- | Cache entry for a single plugin's IR+data IRPluginCache = IRPluginCache+  { ipcIRHash       :: !Text         -- Hash of the generated IR for this plugin+  , ipcSchemaHash   :: !Text         -- Hash of the source schema (composite, backward compatible)+  , ipcSelfHash     :: !Text         -- V2: Methods-only hash (for granular invalidation)+  , ipcChildrenHash :: !Text         -- V2: Children-only hash (for granular invalidation)+  , ipcDependencies :: ![Text]       -- List of plugin dependencies+  , ipcCachedAt     :: !Text         -- ISO 8601 timestamp+  } deriving stock (Show, Eq, Generic)+    deriving anyclass (FromJSON, ToJSON)++-- | Cache entry for a single plugin's generated code+data CodePluginCache = CodePluginCache+  { cpcIRHash      :: !Text            -- Hash of the IR that generated this code+  , cpcFileHashes  :: !(Map Text Text) -- Per-file content hashes (path -> hash)+  , cpcCachedAt    :: !Text            -- ISO 8601 timestamp+  } deriving stock (Show, Eq, Generic)+    deriving anyclass (FromJSON, ToJSON)++-- | Cache manifest for IR (synapse/ir/manifest.json)+data IRCacheManifest = IRCacheManifest+  { ircmVersion    :: !Text+  , ircmIRVersion  :: !Text+  , ircmToolchain  :: !ToolchainVersions+  , ircmUpdatedAt  :: !Text+  , ircmPlugins    :: !(Map Text IRPluginCache)+  } deriving stock (Show, Eq, Generic)+    deriving anyclass (FromJSON, ToJSON)++-- | Cache manifest for generated code (hub-codegen/typescript/manifest.json)+data CodeCacheManifest = CodeCacheManifest+  { ccmVersion    :: !Text+  , ccmTarget     :: !Text+  , ccmToolchain  :: !ToolchainVersions+  , ccmUpdatedAt  :: !Text+  , ccmPlugins    :: !(Map Text CodePluginCache)+  } deriving stock (Show, Eq, Generic)+    deriving anyclass (FromJSON, ToJSON)++-- | Reason for cache miss+data CacheMissReason+  = ToolVersionChanged      -- Tool versions don't match+  | SchemaHashChanged       -- Source schema changed+  | IRHashChanged           -- IR changed+  | DependencyInvalidated   -- Transitive dependency changed+  | ManifestNotFound        -- No cache manifest exists+  | ManifestCorrupted       -- Manifest exists but is invalid+  deriving stock (Show, Eq)++-- | Result of cache validation+data CacheResult+  = FullCacheHit+    -- ^ All plugins are cached and valid+  | PartialCacheHit ![Text] ![Text]+    -- ^ Some plugins valid (first list), some invalid (second list)+  | CacheMiss !CacheMissReason+    -- ^ Cache invalid, must regenerate everything+  deriving stock (Show, Eq)++-- ============================================================================+-- Metadata Types (for parsing .codegen-metadata.json)+-- ============================================================================++-- | Cache information from .codegen-metadata.json+data CacheInfo = CacheInfo+  { ciFileHashes :: !(Map Text Text)  -- file path -> hash+  } deriving stock (Show, Eq, Generic)++instance FromJSON CacheInfo where+  parseJSON = Aeson.genericParseJSON Aeson.defaultOptions+    { fieldLabelModifier = \case+        "ciFileHashes" -> "file_hashes"+        other -> other+    }++instance ToJSON CacheInfo where+  toJSON = Aeson.genericToJSON Aeson.defaultOptions+    { fieldLabelModifier = \case+        "ciFileHashes" -> "file_hashes"+        other -> other+    }++-- | Metadata file generated by hub-codegen+data CodegenMetadata = CodegenMetadata+  { cmFormatVersion :: !Text+  , cmCache         :: !CacheInfo+  } deriving stock (Show, Eq, Generic)++instance FromJSON CodegenMetadata where+  parseJSON = Aeson.genericParseJSON Aeson.defaultOptions+    { fieldLabelModifier = \case+        "cmFormatVersion" -> "format_version"+        "cmCache" -> "cache"+        other -> other+    }++instance ToJSON CodegenMetadata where+  toJSON = Aeson.genericToJSON Aeson.defaultOptions+    { fieldLabelModifier = \case+        "cmFormatVersion" -> "format_version"+        "cmCache" -> "cache"+        other -> other+    }++-- ============================================================================+-- Errors+-- ============================================================================++-- | All possible errors in synapse-cc+data SynapseCCError+  = ToolNotFound !Text ![Text]+    -- ^ Tool not found with suggestions+  | SynapseError !Text !Int+    -- ^ Synapse execution failed with stderr and exit code+  | HubCodegenError !Text !Int+    -- ^ hub-codegen execution failed with stderr and exit code+  | LanguageToolError !Text !Text !Int+    -- ^ Language tool (npm, tsc, etc.) failed+  | CacheError !Text+    -- ^ Cache operation failed+  | InvalidIR !Text+    -- ^ IR parsing failed+  | BackendUnreachable !Text !Text+    -- ^ Cannot connect to backend+  | ConfigError !Text+    -- ^ Invalid configuration+  deriving stock (Show, Eq)++-- | Format error for display to user+formatError :: SynapseCCError -> Text+formatError = \case+  ToolNotFound tool suggestions ->+    T.unlines $+      [ "[!] Error: " <> tool <> " not found"+      , ""+      , tool <> " is required by synapse-cc."+      , ""+      , "Try:"+      ] ++ map ("  - " <>) suggestions +++      [ ""+      , "For more info: https://github.com/hypermemetic/synapse-cc"+      ]++  SynapseError stderr exitCode ->+    T.unlines+      [ "[!] Error: synapse failed (exit code " <> T.pack (show exitCode) <> ")"+      , ""+      , "Output:"+      , stderr+      ]++  HubCodegenError stderr exitCode ->+    T.unlines+      [ "[!] Error: hub-codegen failed (exit code " <> T.pack (show exitCode) <> ")"+      , ""+      , "Output:"+      , stderr+      ]++  LanguageToolError tool stderr exitCode ->+    T.unlines+      [ "[!] Error: " <> tool <> " failed (exit code " <> T.pack (show exitCode) <> ")"+      , ""+      , "Output:"+      , stderr+      ]++  CacheError msg ->+    "[!] Cache error: " <> msg++  InvalidIR msg ->+    T.unlines+      [ "[!] Error: Invalid IR"+      , ""+      , msg+      ]++  BackendUnreachable url msg ->+    T.unlines+      [ "[!] Error: Cannot connect to backend"+      , ""+      , "URL: " <> url+      , "Reason: " <> msg+      , ""+      , "Is the backend running?"+      ]++  ConfigError msg ->+    "[!] Configuration error: " <> msg
+ synapse-cc.cabal view
@@ -0,0 +1,73 @@+cabal-version:      3.0+name:               synapse-cc+version:            0.1.0.0+synopsis:           Unified compiler toolchain for Plexus backends+description:+  synapse-cc orchestrates the complete pipeline from backend schema+  discovery to compiled, ready-to-use client libraries.+license:            MIT+author:             Hypermemetic+maintainer:         dev@hypermemetic.com+category:           Development+build-type:         Simple+extra-doc-files:    PLAN.md, README.md++common warnings+    ghc-options: -Wall++library+    import:           warnings+    exposed-modules:  SynapseCC.Types+                    , SynapseCC.CLI+                    , SynapseCC.Discover+                    , SynapseCC.Pipeline+                    , SynapseCC.Process+                    , SynapseCC.Cache+                    , SynapseCC.Dependency+                    , SynapseCC.Language+                    , SynapseCC.Logging+    build-depends:    base >= 4.14 && < 5+                    , optparse-applicative >= 0.18+                    , process >= 1.6+                    , aeson >= 2.0+                    , bytestring >= 0.11+                    , text >= 1.2+                    , filepath >= 1.4+                    , directory >= 1.3+                    , async >= 2.2+                    , ansi-terminal >= 0.11+                    , time >= 1.9+                    , cryptohash-sha256 >= 0.11+                    , containers >= 0.6+    hs-source-dirs:   src+    default-language: Haskell2010+    default-extensions: OverloadedStrings+                      , RecordWildCards+                      , LambdaCase+                      , TypeApplications+                      , DeriveAnyClass+                      , DeriveGeneric+                      , ScopedTypeVariables+                      , DerivingStrategies++executable synapse-cc+    import:           warnings+    main-is:          Main.hs+    build-depends:    base >= 4.14 && < 5+                    , synapse-cc+                    , text+    hs-source-dirs:   app+    default-language: Haskell2010+    ghc-options:      -threaded -rtsopts -with-rtsopts=-N+    default-extensions: OverloadedStrings++test-suite synapse-cc-test+    import:           warnings+    default-language: Haskell2010+    type:             exitcode-stdio-1.0+    hs-source-dirs:   test+    main-is:          Main.hs+    build-depends:    base >= 4.14 && < 5+                    , synapse-cc+                    , hspec >= 2.0+                    , QuickCheck >= 2.14
+ test/Main.hs view
@@ -0,0 +1,9 @@+module Main (main) where++import Test.Hspec++main :: IO ()+main = hspec $ do+  describe "SynapseCC" $ do+    it "placeholder" $ do+      True `shouldBe` True