Monorepo validation recipes
Doc type: How-to · Applies to: POSE ≥ 0.9.0
POSE does not implement a monorepo orchestrator. It composes with whatever
build graph a repository already has — real Bazel, Nx, npm/yarn workspaces,
or nothing at all — through two versioned inputs: .pose/indexes/module-
metadata.json (dependency edges and criticality) and .pose/indexes/
validation-matrix.json (per-stack checks). pose validate --changed-from
turns those into a minimum safe selection (see
technical architecture); every recipe below is a docs-as-
test — the exact fixture layout and commands are executed in CI
(internal/cli/monorepo_recipes_test.go), so this page cannot drift from
what the engine actually does.
Non-guarantee: path- and metadata-based selection cannot see semantic
coupling POSE was never told about. Declare dependsOn edges for anything
that must widen together; an undeclared edge is a gap in your metadata, not
a POSE defect.
Recipe 1 — JavaScript/npm workspace
package.json # "workspaces": ["packages/*"]
packages/core/package.json
packages/app/package.json # depends on core
.pose/indexes/module-metadata.json
{
"schemaVersion": 1,
"modules": {
"packages/app": { "dependsOn": ["packages/core"] }
}
}
A change inside packages/core selects core directly and widens to app
through the declared edge:
pose validate --changed-from HEAD --explain
[changed-scope] HEAD..worktree: 1 changed file(s), 2/2 module(s) selected
+ packages/app: depends on selected module packages/core (contains changed file)
+ packages/core: contains changed file: packages/core/index.js
A change to the root package.json (the workspace manifest itself, outside
every module) runs the whole workspace — root-level changes always prefer
safe execution:
+ packages/app: root-level change outside any module: package.json
+ packages/core: root-level change outside any module: package.json
Recipe 2 — declared dependency graph (Bazel-style fine-grained modules)
POSE does not read BUILD files. When a repository already runs Bazel (or
any other build-graph tool), declare the same edges in module-metadata.json
so pose validate can select without invoking that tool, or front the real
bazel test //affected/... command as a single structured check when you
want POSE to delegate execution entirely.
base/go.mod
mid/go.mod # depends on base
leaf/go.mod # depends on mid
.pose/indexes/module-metadata.json
{
"schemaVersion": 1,
"modules": {
"mid": { "dependsOn": ["base"] },
"leaf": { "dependsOn": ["mid"] }
}
}
A change in base widens transitively through the whole chain — dependency
resolution is a fixed point, not a single hop:
pose validate --changed-from HEAD --explain
+ base: contains changed file: base/changed.go
+ mid: depends on selected module base (contains changed file)
+ leaf: depends on selected module mid (depends on selected module base)
Recipe 3 — mixed-language monorepo with a shared dependency
services/api/go.mod
services/web/package.json
services/worker/requirements.txt
shared/go.mod # criticality: high
.pose/indexes/module-metadata.json
{
"schemaVersion": 1,
"modules": {
"shared": { "criticality": "high" }
}
}
pose stacks --path <module> identifies each module's stack independently —
go, node and python coexist in one repository with no cross-contamination:
pose stacks --path services/api # -> # go
pose stacks --path services/web # -> # node
pose stacks --path services/worker # -> # python
A change to services/web selects only web — plus shared, which always
runs because it is declared criticality: high (policy widening, independent
of any dependsOn edge — this is how a repository marks a module every
change must validate, such as a shared schema or proto directory):
pose validate --changed-from HEAD --explain --json result.json
+ services/web: contains changed file: services/web/index.js
+ shared: policy: criticality high always runs
- services/api: not affected by HEAD..worktree
- services/worker: not affected by HEAD..worktree
Severity composes across stacks in the one structured result: shared's Go
check is required, web's Node check is optional — a required failure
in a Python or Go module blocks the run the same way it would in a
single-language repository, and an optional failure elsewhere never does.
services/api and services/worker checks are recorded skipped in
result.json with the changed-scope reason, never silently dropped.
What these recipes prove
- Metadata (
dependsOn,criticality) is the only monorepo-specific input POSE needs — no new configuration surface. - Changed scope composes with metadata: direct match → transitive widening → policy widening → safe-execution fallback, in that order.
- Severity is per-check, per-stack, and unaffected by module count or language mix.
- Shared dependencies are declared, not inferred —
criticality: highis the explicit "always validate this" signal.
Non-goals
- POSE does not replace Bazel, Nx, Turborepo or any native build graph — it reads declared metadata and delegates execution to structured checks.
- Perfect semantic impact analysis from paths alone is not promised; an
undeclared dependency is a metadata gap the safe-execution fallback and
--explainoutput make visible, not a silent risk.