nix-module-form Constitution

Core Principles

I. Consumer-Agnostic Widget

nix-module-form renders a JSON Schema that describes the options of a Nixpkgs module as an editable web form. It MUST NOT know the domain of any project that embeds it. Names, public API, comments, documentation, outputs and test data MUST NOT refer to the business concepts of an embedder: for example fediversity, its panel, deployments, tenants, authentication, hosting providers or operators, and their internal files, issues or services. The widget edits a configuration value and compares it with a baseline value. What the embedder does with the value (save it, deploy it, bill it) is not a concern of the widget. Public Nixpkgs modules (for example services.mastodon) MAY appear as examples and test data, because every user of Nixpkgs can read them. The documentation MAY name a known embedder as an example of use, but MUST NOT describe its internals.

Rationale: the widget serves any interface that edits module configuration, for example a second front-end of the same API or a tool of an unrelated project. When the widget knows the needs of one embedder, it breaks or behaves strangely when that embedder changes, and it is less useful to others. A need of one embedder goes into the adapter of that embedder (the entry of the package), or into a general option of the widget that other embedders can also use.

II. Only the Public Output of module-schema

The widget reads JSON Schema documents, for example the output of the optionsToSchema function of module-schema. It MUST rely only on that public output: the JSON Schema shapes that module-schema documents (for example the spellings of nullOr, the tagged union of attrTag, attrsOf maps with propertyNames, and default). It MUST NOT read the files, internal functions or internal data of module-schema. Build-time code that calls module-schema (for example the demo) MUST use the public entrypoint of module-schema. The widget evaluates no Nix code at run time.

Rationale: module-schema and the widget have different release cycles. A dependency on internals makes a change inside module-schema break the widget. A dependency on the documented output lets each project change freely behind its contract, and lets other schema generators feed the widget.

III. The Host Owns the Page

The library bundle MUST NOT mount itself, read or write window globals, make network requests, store data, submit forms or add buttons that act on the value. The embedder mounts the form with the public API, gives it the schema and the values, and receives the edited value through callbacks and the returned handle. Code that connects the widget to one host page (for example a hidden input or a submit button) lives in the entry adapter of that host, not in the library.

Rationale: hosts differ in their frameworks, their storage and their rules for access. A widget that stays inside its element can be embedded in any page and tested without a server.

IV. Defaults Are Not Values

The widget MUST NOT write a schema default into the value. An absent key means "use the default of the module". The form shows the default as a placeholder. The only exception is a mandatory tagged union, whose default branch the widget writes into the value, because a union without a branch is not valid.

Rationale: the module system, not the stored value, owns the defaults. When a module changes a default, a value that did not set the option follows the new default. A copied default would hide that change and would show as a change that the user did not make.

V. Keep the Data of the User

The widget MUST NOT drop a value that the user entered without telling the user. When options are renamed, the widget moves the value to the new name before it renders. When an option is removed or its type changes, the widget shows the old value as a read-only field. When the user switches the branch of a union or turns off a nullable option, the widget keeps the other values for the time the form is open. The value that the widget gives to the embedder contains only fields of the current schema, never the synthetic fields that the widget adds for display.

Rationale: a configuration form edits a live system. A value that disappears silently changes that system without a decision of the user.

VI. Standard JSON Schema, Stock Renderers

The widget accepts JSON Schema draft 2020-12 and validates it with a standard validator (AJV) in the mode that the specification defines (for example pattern without the u flag). It uses the stock JSONForms Vuetify renderers. It adds a custom renderer only for a schema shape that the stock renderers do not show correctly (for example nullOr, attrTag or attrsOf), and it patches an upstream package only to correct a bug or a performance defect. It MUST NOT require extensions of JSON Schema from its input. Markers that the widget adds for itself (for example x-nmf-old) stay inside the widget.

Rationale: the schema is the contract between a generator and the form. "We cannot adapt schema generation to the whims of front-end implementations." Standard behavior lets other validators (for example a server) accept the same values as the form.

VII. Stable Public Surface

The public surface is the set of exports of src/index.ts: the mount function, its options and handle, validateConfig, synthesizeNullableLeafErrors, resolveTheme, setTheme, migrateValue, computeSchemaChanges, restrictRenamesToApplied and their types, plus the names of the built files and the entry argument of the package. Other modules under src/ are internal. A change to the public surface is a breaking change.

Rationale: embedders pin the widget and upgrade it on their own schedule. A small, named surface lets them upgrade without a study of the internals.

VIII. One Entrypoint, Pinned Inputs

default.nix is the single entrypoint. It returns the packages, apps, development shells, formatter and checks. flake.nix is a thin wrapper around it and shell.nix reads its development shell. All inputs are pinned with npins. Tests run without a browser where possible (node:test and a headless DOM); only the documentation images need a headless browser.

Rationale: embedders use flakes, npins or plain imports. One entrypoint keeps those routes equal, and pinned inputs make the bundle reproducible. Tests without a browser are fast and stable in CI.

Constraints

  • The bundle is built with buildNpmPackage and vite from the npm lock file. Dependencies are Vue 3, JSONForms with @jsonforms/vue-vuetify, Vuetify, AJV and @mdi/js. Icon fonts are not used.
  • The styles of the widget MUST NOT depend on the CSS of the host page, and MUST NOT change elements outside the element that the widget mounts in. Class names that the widget adds use the nmf- prefix.
  • The widget MUST work in light and dark mode, and follow the theme of the host page.
  • Large schemas (hundreds of options) MUST stay usable: the user can find changed and invalid fields without a scroll through all options.

Development Workflow

  • Changes go through pull requests on git.fediversity.eu. The checks and treefmt workflows MUST pass.
  • A change to a renderer comes with a unit test (node:test) or a headless-DOM test. A change that is visible in the demo updates the documentation images through the screenshots package.
  • The demo builds its schemas at build time from demo/module.nix through module-schema, not from a stored copy, so that the demo follows the real output.
  • A workaround for an upstream bug names the upstream component in a comment, and is removed when upstream has a fix.
  • A pull request that changes behavior updates the matching specification under specs/ in the same pull request.
  • Prose in comments, commit messages and pull requests uses ASD-STE100, US English and ASCII punctuation, with backticks around code.
  • A pull request that adds a feature for one embedder MUST show why the feature is general. If it is not general, it stays in the adapter of that embedder.

Governance

This constitution has priority over other practices in this repository. An amendment is a pull request that changes this file, updates the version and the Sync Impact Report, and updates any specification that the amendment affects. Versions follow semantic versioning: MAJOR for a removed or redefined principle, MINOR for a new principle or section, PATCH for clarifications.

Version: 1.0.0 | Ratified: 2026-09-22 | Last Amended: 2026-09-22