Key the profile shape check off flakeExposed and validate at the consumed surface #3

Closed
opened 2026-07-20 15:51:00 +01:00 by vnprc-agent · 0 comments

Remove the hardcoded architecture list from the profile shape check and validate the contract on the surface the framework actually consumes, without adding an inventory input. The list this check carries was never machine-platform policy, so it should not be derived from inventory; it should stop looking like policy and the validation it guards should fire where consumers read it.

Primary goals:

  • De-literalize the check keying — replace the local checkSystems = [ "x86_64-linux" "aarch64-linux" ] with nixpkgs.lib.systems.flakeExposed, so no literal in this repo reads as a machine-platform-policy list.
  • Validate at the consumed surface — hoist the shape asserts onto lib.profileDefinitions so any consumer (the archetypes framework at composition time) trips them, not only a standalone nix flake check in this repo.
  • Keep the charter — no inventory or secrets input; the check is platform-independent and needs only nixpkgs.
  • Preserve the standalone signal — retain a buildable profile-contract-shape check, forcing the shared contract via builtins.seq so nix flake check here still exercises it.

Current state

flake.nix:21 hardcodes checkSystems = [ "x86_64-linux" "aarch64-linux" ], consumed only at flake.nix:88 to key the checks output. The check body (flake.nix:91-99) is a pair of eval-time asserts (profileDefinitions is an attrset; shapeErrors == []) plus a no-op runCommand that only touches its output. It never reads the architecture, so the list is scaffolding to satisfy the system-keyed checks schema, not machine policy — and it cannot drift from inventory because it encodes nothing about machines.

Nothing downstream consumes checks: the archetypes framework reads profiles.lib.profileDefinitions (allod/archetypes flake.nix:121,124,343) and profiles.homeModules.preferences (flake.nix:286), but not profiles.checks. Since a downstream nix flake check does not run an input's checks, this shape check fires only when run standalone in this repo. The scenario the repo header exists to serve — an operator redirecting the framework's profiles input at their own definitions repo — therefore gets no protection from it; a malformed definition surfaces later as a framework-internal error at composition time.

Proposed change

Bind the validated definitions once and expose that through lib, and key the retained check off flakeExposed:

# in let ...
checkedProfileDefinitions =
  assert lib.assertMsg (builtins.isAttrs profileDefinitions)
    "profileDefinitions must be an attrset";
  assert lib.assertMsg (shapeErrors == [])
    "profile contract shape errors: ${lib.concatStringsSep "; " shapeErrors}";
  profileDefinitions;
# in outputs
lib = { profileDefinitions = checkedProfileDefinitions; inherit profileData; };
checks = lib.genAttrs lib.systems.flakeExposed (system:
  let pkgs = nixpkgs.legacyPackages.${system};
  in {
    profile-contract-shape = builtins.seq checkedProfileDefinitions
      (pkgs.runCommand "profile-contract-shape-check" { } ''
        echo "profile contract shape validation passed"
        touch $out
      '');
  });

Footguns to encode as code comments

  • builtins.seq forces only weak-head-normal-form. That is enough to fire the current asserts, but if deeper validation is added later, forcing must upgrade to builtins.deepSeq. Note this at the seq call.
  • flakeExposed is ~30 systems: nix flake check still builds only the host entry, but nix flake show / --all-systems now evaluates all of them (slower) and the check nominally exists on darwin/riscv. Harmless, since the check is platform-independent — note that the breadth is deliberate and non-policy.
  • The consumption-time guarantee holds because lib.profileDefinitions is a single guarded binding: any access forces the asserts. Keep it one checked value — do not later split it into lazily-exposed per-archetype sub-attrs, or the guarantee erodes. Note this at the lib binding.

Scope

In: allod/profiles flake.nix only. No inventory or secrets input; no change to composition behavior. Out: the single-owner rule for real machine-platform lists is unchanged and correct — this issue deliberately does not add an inventory input. It corrects the mechanism proposed in #2 (which would break the definitions-repo charter to fix a non-problem); #2 is closed in favor of this issue.

Remove the hardcoded architecture list from the profile shape check and validate the contract on the surface the framework actually consumes, without adding an `inventory` input. The list this check carries was never machine-platform policy, so it should not be derived from inventory; it should stop looking like policy and the validation it guards should fire where consumers read it. Primary goals: - **De-literalize the check keying** — replace the local `checkSystems = [ "x86_64-linux" "aarch64-linux" ]` with `nixpkgs.lib.systems.flakeExposed`, so no literal in this repo reads as a machine-platform-policy list. - **Validate at the consumed surface** — hoist the shape asserts onto `lib.profileDefinitions` so any consumer (the archetypes framework at composition time) trips them, not only a standalone `nix flake check` in this repo. - **Keep the charter** — no `inventory` or `secrets` input; the check is platform-independent and needs only `nixpkgs`. - **Preserve the standalone signal** — retain a buildable `profile-contract-shape` check, forcing the shared contract via `builtins.seq` so `nix flake check` here still exercises it. ### Current state `flake.nix:21` hardcodes `checkSystems = [ "x86_64-linux" "aarch64-linux" ]`, consumed only at `flake.nix:88` to key the `checks` output. The check body (`flake.nix:91-99`) is a pair of eval-time asserts (`profileDefinitions` is an attrset; `shapeErrors == []`) plus a no-op `runCommand` that only `touch`es its output. It never reads the architecture, so the list is scaffolding to satisfy the system-keyed `checks` schema, not machine policy — and it cannot drift from inventory because it encodes nothing about machines. Nothing downstream consumes `checks`: the archetypes framework reads `profiles.lib.profileDefinitions` (`allod/archetypes` `flake.nix:121,124,343`) and `profiles.homeModules.preferences` (`flake.nix:286`), but not `profiles.checks`. Since a downstream `nix flake check` does not run an input's checks, this shape check fires only when run standalone in this repo. The scenario the repo header exists to serve — an operator redirecting the framework's `profiles` input at their own definitions repo — therefore gets no protection from it; a malformed definition surfaces later as a framework-internal error at composition time. ### Proposed change Bind the validated definitions once and expose that through `lib`, and key the retained check off `flakeExposed`: ```nix # in let ... checkedProfileDefinitions = assert lib.assertMsg (builtins.isAttrs profileDefinitions) "profileDefinitions must be an attrset"; assert lib.assertMsg (shapeErrors == []) "profile contract shape errors: ${lib.concatStringsSep "; " shapeErrors}"; profileDefinitions; # in outputs lib = { profileDefinitions = checkedProfileDefinitions; inherit profileData; }; checks = lib.genAttrs lib.systems.flakeExposed (system: let pkgs = nixpkgs.legacyPackages.${system}; in { profile-contract-shape = builtins.seq checkedProfileDefinitions (pkgs.runCommand "profile-contract-shape-check" { } '' echo "profile contract shape validation passed" touch $out ''); }); ``` ### Footguns to encode as code comments - `builtins.seq` forces only weak-head-normal-form. That is enough to fire the current asserts, but if deeper validation is added later, forcing must upgrade to `builtins.deepSeq`. Note this at the `seq` call. - `flakeExposed` is ~30 systems: `nix flake check` still builds only the host entry, but `nix flake show` / `--all-systems` now evaluates all of them (slower) and the check nominally exists on darwin/riscv. Harmless, since the check is platform-independent — note that the breadth is deliberate and non-policy. - The consumption-time guarantee holds because `lib.profileDefinitions` is a single guarded binding: any access forces the asserts. Keep it one checked value — do not later split it into lazily-exposed per-archetype sub-attrs, or the guarantee erodes. Note this at the `lib` binding. ### Scope In: `allod/profiles` `flake.nix` only. No `inventory` or `secrets` input; no change to composition behavior. Out: the single-owner rule for real machine-platform lists is unchanged and correct — this issue deliberately does not add an inventory input. It corrects the mechanism proposed in #2 (which would break the definitions-repo charter to fix a non-problem); #2 is closed in favor of this issue.
vnprc closed this issue 2026-07-20 17:13:28 +01:00
Sign in to join this conversation.
No description provided.