CrowdVision · source-available, not open source · © 2026 Nicolò Ghignatti
CrowdVision is a polyglot monorepo: TypeScript services, a Python AI agent, a Rust contracts service, and shared JavaScript tooling. To keep that manageable on any operating system, the developer experience rests on three cooperating layers, each with a single, well-defined responsibility.
| Layer | Responsibility | Configuration |
|---|---|---|
just | The human-facing command entrypoint. Every workflow is a just <recipe>. It also drives the tasks the layers below do not — Docker, Kubernetes, and documentation. | Justfile |
mise | Tool versions. The single source of truth for which Node, Python, Rust, uv, and moon you run, on every platform. | .mise.toml |
moon | Per-project task orchestration — build, test, lint, audit, deps — with result caching and affected-detection. | .moon/ |
Dependency installation is a moon task like any other (:deps, :relock). There is no separate script layer: everything is mise plus moon, fronted by just.
flowchart TD dev([Developer]) --> just["just (recipe)"] just --> mise["mise exec — injects pinned tools onto PATH"] mise --> moon["moon — task graph, caching, affected"] moon --> tasks["build, test, lint, audit, deps, deps-check, relock"] cfgA["mise.toml"] -->|versions| mise cfgB["moon/workspace.yml"] -->|package list| moon
Before this setup, tools were resolved from whatever happened to be on the developer’s PATH. That is fragile: a globally installed Node (from nvm) or Rust (from rustup) silently shadows the versions the project expects, and a tool with no global fallback — notably uv — simply fails:
'uv' is not recognized as an internal or external commandThe governing principle of this page follows from that failure: never trust the ambient PATH. Resolve every tool through mise, and let one configuration file be the source of truth for each concern.
just is a command runner: a Justfile of named recipes that wrap the longer commands a developer would otherwise memorise. It is the only layer a contributor interacts with directly; mise and moon sit behind it.
Two design points matter:
Justfile selects bash on Unix and PowerShell on Windows, so the same recipe name works on every machine.mise exec -- (see below). Tools that mise does not manage — docker, k3d, kubectl — are invoked directly.Run just --list to see every recipe. The recipe catalogue is documented in Running the Application.
mise pins every language runtime and CLI to an exact version, per project, across platforms. The pins live in .mise.toml at the repository root:
[tools]
node = "24"
python = "3.14.7"
rust = "1.98.1"
go = "1.27.1"
"ubi:moonrepo/moon" = "2.5.4"
uv = "0.12.10"
istioctl = "1.31.0"Running mise install once after cloning fetches every tool at the pinned version into mise’s own store, independent of anything else on the machine.
mise can place those tools on the PATH in two ways. The project is designed to work with or without the first.
| Mechanism | When it is used | How |
|---|---|---|
| Shell activation | Interactive work in your own terminal. | mise activate pwsh | Out-String | Invoke-Expression (PowerShell), or mise activate bash/zsh. Added to a shell profile, the pinned tools are then on PATH directly. |
mise exec -- | Scripts, CI, and the Justfile — anywhere that must work without assuming activation. | mise exec -- <command> resolves the tool from .mise.toml and runs it, injecting the pinned tools onto PATH for that command and every child process it spawns. |
You do not need to activate mise for just to work. Every recipe that calls a managed tool already wraps it in mise exec --, so a fresh clone with only just and mise installed runs immediately. Activation is purely a convenience for invoking tools by hand.
just launches moon as mise exec -- moon …, and mise exec exports the pinned tools onto PATH for the command and every child process it spawns. Every task moon runs — npm ci, cargo fetch, uv sync — therefore inherits the same resolved toolchain. moon’s own toolchain management stays off: mise remains the single source of truth for versions.
If any tool reports “command not found” (classically uv), something bypassed mise. Run it through mise exec -- <tool>, or activate mise in your shell. The tool is installed; it is simply not on the un-activated PATH.
moon runs the per-project tasks — build, test, lint, lint-fix, audit, deps, deps-check, relock — across the whole monorepo, with two capabilities a hand-rolled script set never had:
--affected runs a task only for the projects touched on the current branch.Every package is registered once in .moon/workspace.yml. This is the single list of packages in the repository; nothing else maintains a parallel copy.
projects:
frontend: "frontend"
digital-twin: "backend/digital-twin"
# … the other services …
dashboard: "backend/dashboard"
sensor-simulator: "simulators/sensor-simulator"
# Support packages: deps-only, no build/test/lint
eslint-config: "tooling/eslint-config"
acceptance: "backend/acceptance"Rather than repeat the same build/test/lint configuration in a dozen moon.yml files, the pipeline is defined once per language in .moon/tasks/*.yml, each scoped with inheritedBy so it applies only to projects of that language.
| File | Applies to | Tasks defined |
|---|---|---|
.moon/tasks/typescript.yml | language: typescript | build, test, lint, lint-fix, deps, relock, audit |
.moon/tasks/python.yml | language: python | test, lint, lint-fix, deps, relock, audit |
.moon/tasks/rust.yml | language: rust | build, test, lint, deps (cargo fetch), deps-check (cargo check), audit |
.moon/tasks/go.yml | language: go | build (go build ./...), test (go test ./...), lint (a gofmt -l check then go vet ./... — no golangci-lint in this environment yet), deps (go mod download). registry and tenancy add their own test-integration (go test -tags=integration, testcontainers). No audit task yet. |
.moon/tasks/javascript.yml | language: javascript | deps, relock only — for support packages |
A language file is scoped by its inheritedBy block:
# .moon/tasks/typescript.yml
inheritedBy:
language: 'typescript'
tasks:
test:
command: 'npm test'
inputs: ['src/**/*', '__tests__/**/*', 'package.json', '/schemas/fixtures/**/*', '/schemas/json/**/*']
audit:
command: 'npm audit --audit-level=high --omit=dev'
options: { cache: false }
# … build, lint, lint-fix, deps …Every language’s test inputs include /schemas/fixtures/**/* and /schemas/json/**/* (/ is workspace-relative). Contract tests read those files, and moon hashes a task by its inputs: without them a fixture edit replays a cached pass and --affected selects nothing. Rust’s test also lists tests/**/*, where every crate keeps its conformance tests.
The agent package.json proxies npm test to uv run pytest and npm run lint to uv run ruff … && pyright. This gives moon one uniform task interface across languages while the real work still runs through uv.
Because the pipeline is inherited, a standard service’s entire moon configuration is a single line declaring its language; the tasks come from the language file. Projects declare only what genuinely differs:
Cannot call function tablebyrows(...) with arguments (...):
Unresolved reference: gitignore
.tablebyrows {.headers}
- - `frontend`
- Uses Vite/Vitest: overrides `build` inputs and `test` (to `npm run test:unit`), with `mergeArgs: replace` / `mergeInputs: replace` so the overrides replace the inherited values rather than appending — which is why its `test` re-lists the fixture and schema inputs itself.
- - `sensor-simulator`, `aq-simulator`
- Each asserts `schemas/fixtures/ingest-batch.json`: they hand-build the batch telemetry hand-parses, so the fixture is the only thing holding the three languages to one shape. The Python one overrides `test`/`lint` to call `uv` directly, because the inherited tasks shell out to `npm` and the root `.gitignore` ignores every `package.json`.
- - `ap-simulator`
- Serves a fake ubus endpoint rather than producing telemetry, so it has no fixture to assert and excludes the inherited `test` task; its CI leg runs `smoke.py` instead.
- - `tooling`, `eslint-config`
- Tagged `language: 'javascript'` so they inherit only `deps`. (`acceptance` is `language: 'python'` and excludes the inherited `test`/`lint` tasks: its pytest suite runs via `just test integration` against a composed stack, never as a moon `:test`.)
- - `claims-gateway`, `tenancy`, `registry`, `provisioner`
... (1 more lines)
These read-only commands print resolved configuration without running anything:
mise exec -- moon task digital-twin:test # one task: command, inputs, inheritance
mise exec -- moon query projects # every project moon knows (id + language)
mise exec -- moon query tasks # which projects expose each taskThe payoff of the design is that each fact lives in exactly one place.
| Question | Answered by |
|---|---|
| Which version of Node, uv, or Rust? | .mise.toml |
| Which packages exist in the repository? | .moon/workspace.yml |
| How is a TypeScript project tested, linted, or audited? | .moon/tasks/typescript.yml |
| How are a project’s dependencies installed or relocked? | The same per-language file — its deps / relock tasks |
Crucially, install and clean-install are moon runs, not a parallel hand-written package list:
just setup install # mise exec -- moon run :deps
just setup clean-install # mise exec -- moon run :relock, then :deps
just setup deps-check # mise exec -- moon run :deps :deps-checkSo just setup install installs exactly the packages moon knows about; no drift is possible, and unchanged projects are skipped by moon’s affected-detection.
Both carry options.cache: false: npm ci wipes and reinstalls node_modules, and relock rewrites a lockfile — neither is a cacheable artifact. They still get moon’s fan-out, output grouping, and project graph. relock uses script: rather than command: because it chains rm -rf with npm install.
Thanks to the single registry, onboarding a package is a one-place change:
.moon/workspace.yml under projects:.moon.yml in the package with its language: (and any genuine task overrides).:build / :test / :lint / :deps, and :audit for every language except go), and into just setup install / just setup clean-install via :deps / :relock. Nothing else to register.typescript / python / rust / go each inherit their own full build/test/lint pipeline — use the one matching the package’s actual scripts. Use javascript for support packages that need dependency installs but have no build/test/lint (such as tooling).
| Symptom | Cause and fix |
|---|---|
'uv' is not recognized / command not found | A tool was called outside mise. Use mise exec -- <tool>, or activate mise. The tool is installed; it is not on the un-activated PATH. |
link.exe not found during cargo (Windows) | Rust needs the MSVC linker. Install Visual Studio Build Tools with the “Desktop development with C++” workload. (macOS needs xcode-select --install; Linux needs build-essential.) |
just test all / just lint all re-runs everything | Expected on the first run. The second run replays from moon’s cache. Use --affected to scope to changed projects. |
| A new service is not tested or installed | It is not registered in .moon/workspace.yml, or its moon.yml is missing or has the wrong language. Verify with mise exec -- moon query projects. |