prvng_platform/crates/ncl-sync/README.md

263 lines
9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ncl-sync
Nickel configuration sync daemon for the provisioning platform.
Watches workspace NCL source files, compiles them to JSON via `nickel export`, and maintains a shared file-based cache used by all Nu processes and the `nu_plugin_nickel` plugin.
---
## What Problem It Solves
Every `prvng` command that reads configuration calls `nickel export --format json` — 124 call sites across the Nu codebase, each taking 25 seconds. With ncl-sync, those exports happen once (proactively, in the background) and subsequent calls read from a JSON file in ~3 ms.
| Command | Without ncl-sync | With warm cache |
|---------|-----------------|-----------------|
| `prvng component list` | 37 s | ~1.5 s |
| `prvng workflow list` | 35 s | ~1.5 s |
| `prvng deploy` | 1530 s | ~35 s |
| `prvng component show` (multi-export) | 1230 s | ~1.5 s |
The 1.5 s floor is Nu module parse startup — a separate problem.
---
## Architecture
### Workspace-local cache (single writer)
```
ncl-sync daemon (--workspace /ws/libre-daoshi)
├── notify watcher → NCL file changed
│ ↓
├── nickel export --format json $path --import-path ...
│ ↓
└── write /ws/libre-daoshi/.ncl-cache/<key>.json ← atomic (tmp → rename)
nu_plugin_nickel (nickel-eval command)
├── compute_cache_key(file_content, sorted_imports, "json")
│ ↓
├── resolve cache dir:
│ 1. $NCL_CACHE_DIR (explicit override)
│ 2. walk up from CWD → <workspace>/.ncl-cache/ (workspace-local)
│ 3. ~/.cache/provisioning/config-cache/ (global fallback)
│ ↓
├── does <cache_dir>/<key>.json exist?
│ ├── yes → open file, return Nu record (~3 ms)
│ └── no → nickel export, write to cache, return Nu record (~100 ms)
└── Nu script receives typed record — no | from json needed
Nu processes (after C1/C2/C3 migration)
└── ncl-eval $path [$ws $prov] → nickel-eval plugin → cache lookup
```
### Cache location policy
The cache lives **inside the workspace** by default. Three resolution levels, in order:
| Priority | Source | When |
|----------|--------|------|
| 1 | `$NCL_CACHE_DIR` env var | Explicit override (CI, tests, custom setups) |
| 2 | `<workspace>/.ncl-cache/` | Workspace is active (primary case) |
| 3 | `~/.cache/provisioning/config-cache/` | No workspace — shouldn't happen in normal use |
**Why workspace-local:**
- Cache is a derived artifact of the workspace — deleting the workspace deletes the cache
- No cross-workspace pollution when the same NCL filename exists in two workspaces with different import paths
- Per-workspace disk usage is easy to measure
- `.ncl-cache/` is gitignored (derived, regenerable)
**Three components stay aligned on the resolution order:**
- `ncl-sync/src/manifest.rs::resolve_cache_dir()`
- `nu_plugin_nickel/src/helpers.rs::get_cache_dir()`
- `lib_provisioning/config/cache/core.nu::get-cache-base-dir`
### Key derivation (shared between daemon and plugin)
Both ncl-sync and `nu_plugin_nickel` compute the cache key identically:
```
key = SHA256(file_content + sorted_import_paths.join(":") + format)
```
- **Content-based**: same file at a different path → same key. Move/rename don't invalidate the cache.
- **Import-path-aware**: same file with different import paths → different key. Correct when schemas resolve differently.
- **Sort-stable**: `--import-path [$a $b]` and `--import-path [$b $a]` produce the same key.
Verify parity between the two implementations:
```bash
nu tests/cache/test_key_parity.nu
```
### Manifest
The daemon maintains `<cache_dir>/manifest.json` — a map of `canonical_path → { cache_key, source_mtime, import_paths, cached_at }`. Used to detect stale entries on warm-up and after file changes. Each workspace has its own manifest at `<workspace>/.ncl-cache/manifest.json`.
### Sync requests
After a mutating operation, Nu writes `.sync-<pid>.json` (atomic rename from `.sync-<pid>.tmp`) to the cache dir. The daemon drains these sidecars every 500 ms:
```nushell
# Nu side (lib_provisioning/config/cache/core.nu)
write-sync-request [{ path: $file, import_paths: [$ws $prov] }]
```
---
## CLI
```
ncl-sync <SUBCOMMAND>
Subcommands:
daemon Start background daemon (watch + warm + serve sync requests)
warm One-shot: export all NCL in workspace, then exit
invalidate Remove a single file from cache
key Print cache key for (file, import_paths, format)
stats Print cache statistics
```
### daemon
```bash
ncl-sync daemon [--workspace PATH] [--idle-timeout SECS]
```
- Reads config from `PROVISIONING_CONFIG_DIR/ncl-sync.ncl` (via `platform-config`)
- Cache dir: `<workspace>/.ncl-cache/` (or `$NCL_CACHE_DIR` if set)
- Writes PID to `<cache_dir>/ncl-sync.pid`
- Auto-shuts down after `idle_timeout_secs` (default 600) with no file events or sync requests
- Env override: `NCL_SYNC_LOG=debug ncl-sync daemon` for verbose output
### warm
```bash
ncl-sync warm /path/to/workspace
```
Exports all `.ncl` files under the workspace path, up to `warm_concurrency` parallel processes. Updates manifest. Exits when done.
```bash
# Warm libre-daoshi before a work session
ncl-sync warm ~/workspaces/libre-daoshi
```
### invalidate
```bash
ncl-sync invalidate /path/to/file.ncl [--workspace PATH]
```
Removes the manifest entry and the corresponding `.json` cache file. `--workspace` resolves the cache location; if omitted, walks up from CWD looking for a workspace root.
### key
```bash
ncl-sync key /path/to/file.ncl [--import-path PATH]...
```
Prints the SHA256 key. Use to verify daemonplugin parity:
```bash
sync_key=$(ncl-sync key settings.ncl --import-path $ws --import-path $prov)
nu_key=$(nu -c "use nu_plugin_nickel; nickel-cache-key settings.ncl --import-path [$ws $prov]")
[ "$sync_key" = "$nu_key" ] && echo "parity OK" || echo "PARITY MISMATCH"
```
### stats
```bash
ncl-sync stats [--workspace PATH]
# entries: 47
# stale: 0
# cache_dir: /Users/me/workspaces/libre-daoshi/.ncl-cache/
# size: 2.3 MB
```
`--workspace` selects which workspace's cache to inspect; if omitted, walks up from CWD.
---
## Configuration
Config file: `provisioning/platform/config/ncl-sync.ncl`
Deploy to `PROVISIONING_CONFIG_DIR` (macOS: `~/Library/Application Support/provisioning/platform/`):
```bash
cp provisioning/platform/config/ncl-sync.ncl \
~/Library/Application\ Support/provisioning/platform/ncl-sync.ncl
```
| Field | Default | Description |
|-------|---------|-------------|
| `cache_dir` | `<workspace>/.ncl-cache` | Override cache location (absolute path; bypasses workspace resolution) |
| `idle_timeout_secs` | `600` | Daemon auto-shutdown after N seconds idle |
| `sync_poll_interval_ms` | `500` | How often to check for `.sync-*.json` sidecars |
| `warm_concurrency` | `4` | Max parallel `nickel export` during warm-up |
| `extra_import_paths` | `[]` | Additional import paths beyond workspace + `$PROVISIONING` |
Env overrides: `NCL_CACHE_DIR` (path), `NCL_SYNC_IDLE_TIMEOUT`, `NCL_SYNC_CONCURRENCY`.
---
## Integration with nu_plugin_nickel
The daemon and plugin are complementary:
| Concern | ncl-sync | nu_plugin_nickel |
|---------|----------|-----------------|
| Cache writer | ✅ primary | ✅ on miss |
| Cache reader | — | ✅ always |
| File watching | ✅ | — |
| Proactive warm-up | ✅ | — |
| Nu value conversion | — | ✅ |
| Fallback on no daemon | — | ✅ (runs nickel directly) |
**Without ncl-sync running**: `nickel-eval` still works — it runs `nickel export` on miss and caches the result itself. Performance degrades to ~100 ms per call (first invocation per file per session).
**With ncl-sync running**: `nickel-eval` hits the daemon-written cache. First call of the session is already ~3 ms because the daemon warmed the cache on `prvng platform start`.
---
## Platform Lifecycle
ncl-sync is managed by the platform service layer:
```nushell
# service-manager.nu
prvng platform start # → ncl-sync-start → nohup ncl-sync daemon &
prvng platform stop # → ncl-sync-stop → kill $(cat ncl-sync.pid)
prvng platform status # → ncl-sync-status → { running, pid }
```
To start manually:
```bash
ncl-sync daemon --workspace ~/workspaces/libre-daoshi &
```
---
## Build & Install
```bash
cd provisioning/platform
cargo build --release --package ncl-sync
install -m 0755 target/release/provisioning-ncl-sync ~/.local/bin/provisioning-ncl-sync
```
---
## Design Constraints
- **No platform service dependencies**: no NATS, no SurrealDB, no orchestrator. The daemon configures those services — it cannot depend on them.
- **Single writer**: only ncl-sync writes `.json` cache files. Nu processes write only `.sync-*.json` sidecar signals.
- **Cache miss is safe**: if the daemon is down, `nickel-eval` falls back to direct `nickel export`. No hard dependency.
- **No HTTP/socket**: reads happen via the filesystem. Zero latency overhead, zero coupling.
See [ADR-022](../../adrs/adr-022-ncl-sync-daemon.ncl) for full design rationale.