Skip to main content

Workspace Rules

Every workspace in a Nova monorepo is classified by a role and a policy.

These two properties, defined in nova.config.json, control how recipes read, create, modify, and remove fields in each workspace's package.json.

Roles

A role describes what the workspace is. It determines which package.json fields are allowed, which are removed, and which are created when missing.

RolePurpose
projectThe ESM monorepo root. Retains "type": "module" and owns the workspaces array, packageManager, and dependency install-script policy.
docsA documentation site (e.g., Docusaurus). Does not publish to a package registry.
configA shared configuration package (e.g., ESLint presets, TSConfig presets). Retains the types and module bundler convention fields.
appAn application workspace (e.g., a web app, an API server). Not intended for registry publishing.
packageA publishable library. Retains artifact fields (files, bin, man, directories) and bundler convention fields (types, module, sideEffects, esnext).
toolAn internal CLI tool. Similar to package but does not retain sideEffects or esnext.
templateA template workspace used by scaffolding commands. Not intended for registry publishing.

Role-Gated Fields

The following table shows which package.json fields are retained or removed based on role. Fields not listed here are handled independently of role.

FieldRetained byRemoved from all others
workspacesprojectYes
typeproject, config, app, package, toolYes
filespackage, toolYes
binpackage, toolYes
manpackage, toolYes
directoriespackage, toolYes
typesconfig, package, toolYes
moduleconfig, package, toolYes
sideEffectspackageYes
esnextpackageYes

Root Package Manager

The project workspace is the only workspace allowed to own packageManager. Use one exact Corepack descriptor:

ClientRoot package.json shape
npm"packageManager": "[email protected]"
pnpm"packageManager": "[email protected]"
Yarn"packageManager": "[email protected]"

Nova uses this field as executable configuration, not just metadata. Publish workflow generation selects the matching client for dependency installation, local package executables, and project scripts. A missing, partial, ranged, or unsupported descriptor is invalid for publish generation, which stops before changing workflow files. The sync-environment recipe can repair the root field and removes duplicate copies from child workspaces.

The package manager is separate from a publish destination. For example, a Yarn-managed project can still deploy a package to the npm target; Yarn manages the project, while the target identifies the registry operation.

Root Install-Script Policy

An npm-managed project keeps its dependency lifecycle-script decisions in the root allowScripts object. Each entry answers one security question: may this installed dependency run preinstall, install, postinstall, or a source-package prepare script?

json
{
  "allowScripts": {
    "core-js": true,
    "es5-ext": false,
    "[email protected]": true
  }
}

Use an exact name@version key when approval belongs to one reviewed release, or a name-only key when the repository intentionally carries that decision through dependency updates. Use a name-only false entry for an explicit denial. A missing entry is unreviewed: npm 11 reports it while still running the script, whereas npm 12 blocks it by default.

Nova recipes recognize and preserve non-empty allowScripts objects but never infer that arbitrary dependency code is trustworthy. A scaffold is narrower: Nova knows the dependency capabilities it is generating, so it composes only their reviewed lifecycle-script profiles. Adding a scaffold to an existing monorepo fills missing decisions without overriding choices already recorded by the consumer.

Review the pending set with the command that belongs to the npm version pinned by packageManager:

Both commands above are read-only. Approve or deny a package only after reviewing its lifecycle commands and verifying the project without them.

Policies

A policy describes how the workspace is versioned and package-published. It determines whether the workspace is locked, tracked privately, or eligible for release to a package registry.

PolicyVersion behaviorPackage publishingprivate value
freezableLocked at the reserved 0.0.0 sentinel. Never incremented.Never package-published.true
trackableTracked with the project's configured SemVer or CalVer strategy.Never package-published.true
distributableTracked with the project's configured SemVer or CalVer strategy.Published to a registry.false

Deployment is independent of workspace policy. A freezable documentation site can deploy to a hosting platform while its package version remains locked at 0.0.0; a trackable application can deploy without becoming registry-publishable.

0.0.0 belongs exclusively to freezable workspaces. A new non-freezable workspace starts at 0.0.1 under SemVer or the current UTC YYYY.M.0 period under CalVer. When lock-step versioning is enabled in an existing project, a newly scaffolded non-freezable workspace inherits the one shared project version instead.

Policy-Gated Fields

The following table shows which package.json fields are retained or removed based on policy. Fields not listed here are handled independently of policy.

FieldRetained byRemoved from all others
descriptiondistributableYes
keywordsdistributableYes
homepagedistributableYes
bugsdistributableYes
authordistributableYes
contributorsdistributableYes
fundingdistributableYes
repositorydistributableYes
publishConfigdistributableYes

Allowed Policies by Role

Not every policy is available to every role. The following table shows which policies each role supports:

Rolefreezabletrackabledistributable
projectYes
docsYesYes
configYesYes
appYes
packageYesYes
toolYesYesYes
templateYesYes

Workspace Naming

Each workspace's name (the value written to its package.json) must follow a role-specific pattern. The parser silently drops workspaces whose names do not match, so a typo here is a quiet foot-gun.

Patterns reference the project slug from project.name.slug. When the slug is not set, the prefix falls back to the literal role name.

RoleRequired pattern
projectExactly "{slug}-project", or "project" when no slug is set.
docsExactly "{slug}-docs", or "docs" when no slug is set.
config"{slug}-config-{descriptor}", or "config-{descriptor}" when no slug is set. The descriptor must match the simple slug pattern.
app"{slug}-app-{descriptor}", or "app-{descriptor}" when no slug is set. The descriptor must match the simple slug pattern.
tool"{slug}-tool-{descriptor}", or "tool-{descriptor}" when no slug is set. The descriptor must match the simple slug pattern.
packageAny name matching the simple slug pattern (e.g., "my-lib") or the scoped npm pattern (e.g., "@scope/my-lib").
templateSame as package: simple or scoped slug.

The patterns are:

  • Simple slug: ^[a-z0-9]+(?:[a-z0-9-_]*[a-z0-9])?$. Lowercase letters, digits, hyphens, and underscores; must start and end with a letter or digit.
  • Scoped slug: ^@[scope]/[name]$, where both scope and name match the simple slug pattern.

Required vs. Conditional

Each field that a recipe handles is classified as either required or conditional. This classification describes when the recipe will act on the field.

ClassificationMeaning
RequiredThe recipe ensures the field exists. If missing, it is created with a computed or default value. If invalid, it is corrected.
ConditionalThe recipe only acts when the field is already present, or when a specific recipe setting, workspace role, or workspace policy triggers it. A conditional field is never created from nothing without an explicit trigger.

How to Read Recipe Documentation

In each recipe's Behavior section, every field is tagged with (Required) or (Conditional):

  • name (Required) means the recipe guarantees this field will exist after it runs. If the field is missing, the recipe creates it.
  • description (Conditional) means the recipe will only touch this field if it is already present or if the workspace meets certain criteria (e.g., a specific policy and an enabled setting).

This distinction matters because required fields are safe to omit from a new package.json — the recipe will create them automatically.

Conditional fields, on the other hand, are only managed if they already exist or if you explicitly opt in through configuration.