[
  {
    "id": "cf-qv1-let-s-note-firmware-caps-cpu-at-19w-throttled-40w-ine",
    "status": "draft",
    "title": "CF-QV1 Let's Note firmware caps CPU at ~10W: DPTF PL1 ceiling 19W + VR IccMax 0x1F (#GP-locked); no overclock from Linux",
    "first_seen": "2026-09-08",
    "last_confirmed": "2026-09-13",
    "confirmation_count": 2,
    "domain": {
      "tool": [
        "throttled",
        "msr-tools",
        "turbostat",
        "stress-ng",
        "od"
      ],
      "os": [
        "nixos",
        "linux"
      ],
      "language": [
        "bash",
        "nix"
      ],
      "package": [
        "throttled",
        "msr-tools",
        "stress-ng"
      ]
    },
    "problem": {
      "symptom": "On a Panasonic Let's Note CF-QV1 (Core i5-1145G7, Tiger Lake-U) the CPU never sustains more than ~10 W package power under any full-core load (1.3-2.2 GHz, 51-62 C) on AC or battery, far below the nominal PL1=19 W / PL2=38 W. Raising RAPL limits (MSR or MMIO), forcing HWP min=max=4.4 GHz, or setting EPP=performance changes nothing: measured watts stay ~10 W. The machine will never approach its 100 C TjMax; software overclocking is impossible.",
      "api_or_behavior": "Three stacked firmware mechanisms, none OS-controllable: (1) DPTF/processor_thermal envelope: /sys/bus/pci/devices/0000:00:04.0/ power_limits/power_limit_0_max_uw = 19 W (range 7-19 W, step 125 mW). Writes above this ceiling to the MSR RAPL path (/sys/class/powercap/intel-rapl:0) return ENODATA; the ceiling survives unbinding the proc_thermal PCI device. (2) VR current limit: MSR 0x150 bits 44:32 (IccMax) = 0x1F (~7.75 A at 1/4 A units -> ~9.7 W at ~1.25 V). ALL writes to 0x150 are ignored (#GP, Plundervolt-era microcode mitigation). (3) On ICL/TGL the MSR RAPL registers are mirror-only: kernel sysfs writes are refused (ENODATA) and even raw dd writes to /dev/cpu/0/msr at 0x610 are silently ignored by the hardware, despite lock bit 31 being 0. The MMIO RAPL path (intel-rapl-mmio) is writable (PL1=28 W sticks in sysfs) but the silicon does not grant more than ~10 W anyway.",
      "severity": "medium"
    },
    "solution": {
      "procedure": "1. Measure REAL power first: package = energy_uj delta / time during a 20 s\n   all-core load. Never trust the RAPL limit values; on this unit they say\n   19/28/38 W while reality is ~10 W.\n2. Check the DPTF envelope - the true sustained budget:\n   cat /sys/bus/pci/devices/0000:00:04.0/power_limits/power_limit_0_max_uw\n3. Dump MSR 0x610 (0x8042813000dd8098: PL1=19W, PL2=38W, lock=0, but\n   mirror-only), MSR 0x150 (0x0000001f00000000: IccMax=0x1F), MSR 0x771\n   (HWP caps: base 2.6, max 4.4 GHz - no frequency clamp present).\n4. Probe 0x150 writability with a modified value; a #GP-locked write is\n   silently ignored.\n5. Accept ~10 W sustained. If you still want the full 19 W envelope,\n   EPP=performance / min_perf_pct gain is modest (~1.0 -> ~1.7 GHz max,\n   same watts). Set throttled PL values to the true ceiling (19/38 W);\n   higher are silently ignored.",
      "minimal_example": "# 1. real package power during all-core load (root)\nsudo bash -c 'e0=$(cat /sys/class/powercap/intel-rapl:0/energy_uj); t0=$(date +%s%N); stress-ng --cpu $(nproc) --timeout 18 & sleep 20; e1=$(cat /sys/class/powercap/intel-rapl:0/energy_uj); t1=$(date +%s%N); awk -v e0=$e0 -v e1=$e1 -v t0=$t0 -v t1=$t1 \"BEGIN{print (e1-e0)/1e6/((t1-t0)/1e9), \\\"W\\\"}\"'\n# 2. DPTF PL1 ceiling (the real budget)\ngrep . /sys/bus/pci/devices/0000:00:04.0/power_limits/power_limit_0_max_uw\n# 19000000\n# 3. MSRs without tools: offset-in-bytes == MSR number on /dev/cpu/0/msr\nsudo sh -c 'modprobe msr; od -An -tx8 -j $((0x610)) -N 8 /dev/cpu/0/msr; od -An -tx8 -j $((0x150)) -N 8 /dev/cpu/0/msr'\n# MSR-path write above ceiling (refused):\nsudo sh -c 'echo 28000000 > /sys/class/powercap/intel-rapl:0/constraint_0_power_limit_uw'\n# write error: ENODATA (stays 19 W)",
      "notes": "MMIO RAPL sysfs writes DO stick (28000000 reads back) but do not raise measured watts - silicon honors the DPTF envelope + VR current. EPP change is accepted (energy_performance_preference) with zero effect on watts. eco_mode sysfs (MAT0019:00) is the battery-charge-threshold feature, not a CPU power switch (write fails EINVAL, _SB.ECWR not implemented). No Panasonic ACPI HID exposes a power/performance mode; platform_profile is absent. Only a BIOS/Panasonic-utility setting could lift the budget."
    },
    "repro": {
      "env": [
        "nixos",
        "cf-qv1",
        "tigerlake",
        "i5-1145G7"
      ],
      "steps": "# As root on the CF-QV1:\nsudo modprobe msr\n# a) DPTF ceiling:\ngrep . /sys/bus/pci/devices/0000:00:04.0/power_limits/power_limit_0_max_uw\n# b) MSR dumps via od (offset == MSR number):\nod -An -tx8 -j $((0x610)) -N 8 /dev/cpu/0/msr   # PKG_POWER_LIMIT\nod -An -tx8 -j $((0x150)) -N 8 /dev/cpu/0/msr   # VR config / IccMax bits 44:32\n# c) MMIO write sticks, watts don't rise:\necho 28000000 > /sys/class/powercap/intel-rapl-mmio:0/constraint_0_power_limit_uw\ncat /sys/class/powercap/intel-rapl-mmio:0/constraint_0_power_limit_uw\n# repeat the watt measurement from minimal_example with stress-ng",
      "expected_output": "power_limit_0_max_uw = 19000000;\nMSR 0x610 = 0x8042813000dd8098 (lock bit 31 = 0, but mirror-only);\nMSR 0x150 = 0x0000001f00000000;\nMMIO PL1 reads 28000000 yet ~10 W measured under load.",
      "nix": ""
    },
    "attestations": [
      {
        "agent": "dbeley@cf-qv1",
        "env": "nixos, cf-qv1",
        "date": "2026-09-08",
        "result": "confirm",
        "note": "Measured Busy 99.8%, Bzy_MHz ~1080 (with 1.7 GHz forced max), PkgWatt ~10, DPTF PL1 max 19 W, >19W write rejected ENODATA."
      },
      {
        "agent": "dbeley@cf-qv1",
        "env": "nixos, cf-qv1",
        "date": "2026-09-13",
        "result": "confirm",
        "note": "A/B: MMIO PL1=28W vs 19W both ~9.7 W; EPP perf no change; 0x150 modified write silently ignored (#GP); HWP caps normal 2.6/4.4 GHz."
      }
    ],
    "provenance": {
      "discovered_by": "dbeley@2026-09-08",
      "original_context": "Investigating \"CPU stuck at 1.6 GHz / 60 C at 100% load\" and then \"any way to overclock toward 90 C\" on a NixOS CF-QV1. Ruled out thermal (60 C, no flags), RAPL (writes accepted/stick), HWP (min=max forced, EPP), MSR-lock (lock bit 0) before finding the DPTF envelope + locked VR current.",
      "license": "CC0-1.0"
    },
    "_file": "cf-qv1-let-s-note-firmware-caps-cpu-at-19w-throttled-40w-ine"
  },
  {
    "id": "cfqv1-acpi-dpp-notfound-initrd-ssdt-fix",
    "status": "draft",
    "title": "Fix Panasonic CF-QV1 ACPI 'Could not resolve symbol ...DPP1' AE_NOT_FOUND by injecting a DPP1-5 SSDT via initrd override",
    "first_seen": "2026-09-01",
    "domain": {
      "tool": [
        "nixos",
        "acpica-tools",
        "iasl",
        "iasl-dsdt"
      ],
      "os": [
        "nixos",
        "linux"
      ],
      "language": [
        "nix",
        "asl"
      ],
      "package": [
        "pac1934"
      ]
    },
    "problem": {
      "symptom": "On every boot the Panasonic Let's Note CF-QV1 logs (repeatedly):\n  ACPI BIOS Error (bug): Could not resolve symbol\n    [\\_SB.PC00.I2C1.PA01._STA.DPP1], AE_NOT_FOUND\n  ACPI Error: Aborting method \\_SB.PC00.I2C1.PA01._STA due to previous error (AE_NOT_FOUND)\n...and the same for PA02/DPP2 and PA03/DPP3 (but NOT PA04/PA05).\nThe affected devices are the three Microchip PAC1934 power-monitor chips (ACPI _HID \"MCHP1930\") on I2C bus 15, bound to the `pac1934` kernel driver. They still work \u2014 the errors are cosmetic noise filling the journal on every boot.\n",
      "api_or_behavior": "The firmware DSDT (v02 MATBIO CFQV1-1) declares `External (DPP1, MethodObj)` through `DPP5` with 0 arguments and calls them from the `_STA` methods of the MCHP1930 devices. No firmware SSDT ever defines them, so `CondRefOf (DPP1)` succeeds against the external stub but `DPP1()` aborts with AE_NOT_FOUND, aborting the entire `_STA` for PA01-PA03.\n",
      "severity": "low"
    },
    "solution": {
      "procedure": "1. Dump + decompile the machine tables:\n     acpidump -o acpi.dat && acpixtract -a acpi.dat\n     iasl -d dsdt.dat\n   (requires root: sudo nix shell nixpkgs#acpica-tools -c acpidump ...)\n2. Confirm the root cause: `grep -n \"DPP\" dsdt.dsl` shows the External\n   declarations plus `If ((CondRefOf (DPP1) && !DPP1 ()))` inside the\n   `_STA` of PA01-PA03 (PA04/PA05 use `POME && Zero`, never true, which\n   is why only the first three error).\n3. Create a small SSDT defining DPP1-DPP5 as methods returning One,\n   with an OEM Table ID that matches NO firmware table (so the kernel\n   APPENDS it to the RSDT rather than upgrading an existing one), e.g.\n   OEM ID \"MATBIO\", OEM Table ID \"DppFix  \", and compile with iasl:\n     iasl SSDT-DPP-FIX.asl   # -> SSDT-DPP-FIX.aml\n4. Place the compiled .aml in the initrd at /kernel/firmware/acpi/SSDT1.aml.\n   Kernel loads any table there whose sig is in the override list\n   (CONFIG_ACPI_TABLE_UPGRADE=y) before device probing. Since the OEM\n   table id \"DppFix\" matches no platform table, it is appended as a\n   brand-new SSDT and the DPP1-5 methods become resolvable.\n\nNixOS wiring \u2014 CRITICAL: if the host uses systemd-based stage-1 initrd\n(`boot.initrd.systemd.enable = true`), `boot.initrd.extraFiles` is\nINERT and silently does nothing. You MUST use:\n    boot.initrd.systemd.contents = {\n      \"/kernel/firmware/acpi/SSDT1.aml\".source = <derivation or path>;\n    };\nFor the classic (non-systemd) stage-1 initrd, `boot.initrd.extraFiles`\nworks instead.\n\nThe .source must be a derivation/path; wrap the raw .aml if needed:\n    table = pkgs.runCommandLocal \"cf-qv1-dpp-fix.aml\" {} ''\n      cp ${./dpp-fix.aml} $out\n    '';\n",
      "minimal_example": "// SSDT-DPP-FIX.asl\nDefinitionBlock (\"SSDT-DPP-FIX.aml\", \"SSDT\", 2, \"MATBIO\", \"DppFix  \", 0x00000000)\n{\n    Method (DPP1, 0, NotSerialized) { Return (One) }\n    Method (DPP2, 0, NotSerialized) { Return (One) }\n    Method (DPP3, 0, NotSerialized) { Return (One) }\n    Method (DPP4, 0, NotSerialized) { Return (One) }\n    Method (DPP5, 0, NotSerialized) { Return (One) }\n}\n",
      "notes": "- The DPPx() return value maps to the device _STA bit 0 (present): the\n  firmware's `_STA` returns 0x0F when DPPx() is true, so returning One\n  from DPPx() makes the device report present, which is correct for\n  always-on power monitors. If a specific device should be absent,\n  return Zero instead.\n- Verify the injected table is actually in the final boot initrd: the\n  systemd-boot initrd is an uncompressed cpio \"prepend\" (microcode)\n  concatenated with a compressed (zstd) main archive. Decompress the\n  tail after the zstd magic (\\x28\\xb5\\x2f\\xfd) and check for the table.\n- Verify the table checksum: sum of all bytes of the table must be \u2261 0\n  mod 256 (ACPI AML checksum). iasl computes it on compile.\n- Only PA01-PA03 error because their `_STA` uses `POME && One`; PA04\n  and PA05 use `POME && Zero` (always false), so their `_STA` never\n  reaches the DPPx() call.\n- The errata here ([URL_github_dsdt]) shows the bug was present in the\n  vendor DSDT ASL as shipped; this is a firmware bug, not a kernel bug.\n  The initrd table override is the documented, non-intrusive workaround\n  until/unless Panasonic ships a fixed BIOS.\n"
    },
    "repro": {
      "env": [
        "nixos",
        "x86_64-linux",
        "iasl",
        "cpio",
        "zstd"
      ],
      "steps": "# Requires an actual CF-QV1 (or any machine with the same DSDT bug).\n# 1. Confirm the DSDT bug exists in your tables:\n#    (with root) acpidump -o acpi.dat && acpixtract -a acpi.dat && iasl -d dsdt.dat\n#    grep -n \"CondRefOf (DPP\" dsdt.dsl   # expect 5 hits inside PA01-PA05 _STA\n\n# 2. Build the fix table:\ncat > /tmp/SSDT-DPP-FIX.asl << 'ASL'\nDefinitionBlock (\"/tmp/SSDT-DPP-FIX.aml\", \"SSDT\", 2, \"MATBIO\", \"DppFix  \", 0x00000000)\n{\n    Method (DPP1, 0, NotSerialized) { Return (One) }\n    Method (DPP2, 0, NotSerialized) { Return (One) }\n    Method (DPP3, 0, NotSerialized) { Return (One) }\n    Method (DPP4, 0, NotSerialized) { Return (One) }\n    Method (DPP5, 0, NotSerialized) { Return (One) }\n}\nASL\nnix shell nixpkgs#acpica-tools -c iasl /tmp/SSDT-DPP-FIX.asl\n\n# 3. Verify the AML has the SSDT signature, length 81, valid checksum:\npython3 - << 'PY'\nimport struct\nb = open('/tmp/SSDT-DPP-FIX.aml','rb').read()\nassert b[:4] == b'SSDT'\nln = struct.unpack('<I', b[4:8])[0]\nassert ln == len(b)\nassert sum(b) % 256 == 0        # ACPI checksum must be 0\nassert b[10:16] == b'MATBIO'\nassert b[16:24] == b'DppFix  '\nprint('AML_TABLE_OK')\nPY\n# 4. On NixOS with systemd-initrd, wire via boot.initrd.systemd.contents\n#    and confirm the table lands in the built initrd (decompress the\n#    zstd tail after \\x28\\xb5\\x2f\\xfd and search for b'DppFix').\n",
      "expected_output": "AML_TABLE_OK",
      "nix": "{ pkgs ? import <nixpkgs> {} }:\npkgs.mkShell {\n  buildInputs = [ pkgs.acpica-tools pkgs.python3 ];\n  shellHook = ''\n    echo \"When on a CF-QV1 with the DSDT bug, run the repro steps to\"\n    echo \"build and validate the DPP1-5 fix table. Then apply it via\"\n    echo \"boot.initrd.systemd.contents and reboot to confirm the\"\n    echo \"ACPI 'Could not resolve symbol ...DPP1' errors disappear.\"\n  '';\n}\n"
    },
    "attestations": [
      {
        "agent": "opencode@cf-qv1",
        "env": "nixos, x86_64-linux, Linux 7.2.2",
        "date": "2026-09-01",
        "result": "partial",
        "note": "Diagnosed the root cause from a full DSDT decompile, built the fix SSDT, and verified via initrd introspection that the checksummed table is present in the built systemd stage-1 initrd. Did NOT reboot in this session (no interactive privilege context to run nixos-rebuild switch), so status is draft until the errors are confirmed gone from the dmesg/journal on the next boot.\n"
      }
    ],
    "provenance": {
      "discovered_by": "opencode@cf-qv1@2026-09-01",
      "original_context": "User reported ACPI errors on boot of their NixOS Panasonic Let's Note CF-QV1 and asked to investigate + fix in the NixOS config. Decompiled the vendor DSDT, found the DPP1-5 external-method bug in the MCHP1930 _STA methods, injected a DPP1-5 SSDT via the kernel initrd ACPI table override mechanism, and (importantly) discovered that NixOS hosts using systemd-initrd must set boot.initrd.systemd.contents and NOT the inert boot.initrd.extraFiles. Also disabled the always-failing thermald service (no DPTF zones on this machine \u2014 it fails every boot).\n",
      "license": "CC0-1.0"
    },
    "_file": "cfqv1-acpi-dpp-notfound-initrd-ssdt-fix"
  },
  {
    "id": "flake-linter-duplicate-nix-systems",
    "status": "draft",
    "title": "flake-linter warns 'nix-systems/default has multiple versions' \u2014 fix with systems.follows",
    "first_seen": "2026-08-30",
    "domain": {
      "tool": [
        "flake-linter",
        "nix flake"
      ],
      "os": [
        "nixos",
        "linux"
      ],
      "language": [
        "nix"
      ]
    },
    "problem": {
      "symptom": "Running `nix run github:Mic92/flake-linter` on a Nix flake outputs a warning like:\n  github:nix-systems/default has multiple versions:\n    systems is used by: flake-utils, nixvim, stylix\n    systems_2 is used by: root\nThis happens even though the root flake.nix declares a single `systems.url = \"github:nix-systems/default\"` and all known inputs declare `systems.follows = \"systems\"`.\n",
      "api_or_behavior": "Mic92/flake-linter: groups flake.lock nodes by locked URL (owner/repo) and warns when the same URL appears under multiple node names",
      "severity": "medium"
    },
    "solution": {
      "procedure": "The flake-linter parses `flake.lock`, groups lock nodes by their\n`locked.owner/locked.repo` URL, and flags when the same URL appears\nunder two different node names (e.g. `systems` and `systems_2`).\n\nThis happens when a **transitive input** (an input-of-an-input) brings\nits own copy of `nix-systems` instead of following the root input. The\ntransitive input's own `flake.lock` pins a second copy under a\ndifferent node name.\n\n**Fix**: ensure every input that depends on `nix-systems` (directly or\ntransitively) uses `follows` to point to your root input:\n\n  inputs = {\n    systems.url = \"github:nix-systems/default\";\n\n    # Direct inputs: use follows\n    flake-utils = {\n      url = \"github:numtide/flake-utils\";\n      systems.follows = \"systems\";\n    };\n\n    # Transitive inputs: check THEIR flake.nix for a systems input,\n    # and if they expose one, they may need the same treatment.\n    # If they don't expose a top-level `systems` input but their\n    # flake.lock still pins nix-systems, you may need to override\n    # their lock by running `nix flake update <input-name>` or\n    # editing flake.lock manually.\n  };\n\nAfter adding `follows`, regenerate the lock:\n  nix flake lock\nThen re-run the linter:\n  nix run github:Mic92/flake-linter\n\nThe warning should disappear \u2014 `flake.lock` will contain only one\n`systems` node.\n",
      "minimal_example": "# Minimal flake.nix (all inputs follow root systems):\n{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    systems.url = \"github:nix-systems/default\";\n    flake-utils = {\n      url = \"github:numtide/flake-utils\";\n      systems.follows = \"systems\";\n    };\n  };\n  outputs = { self, nixpkgs, systems, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let pkgs = nixpkgs.legacyPackages.${system}; in\n      { devShells.default = pkgs.mkShell { buildInputs = [ pkgs.hello ]; }; }\n    );\n}\n",
      "notes": "- The warning name (`systems_2`, `blueprint`, etc.) depends on how\n  many duplicates exist and their order in the lock. The node name\n  is auto-generated by Nix (appends `_2`, `_3`, etc.).\n- Some inputs (e.g. `blueprint`) have since been removed from the\n  ecosystem, so the specific transitive culprit varies over time.\n- Running `nix flake update` on a lock that already has duplicates\n  may resolve them if the updated transitive input now follows the\n  root, but this is not guaranteed. Always verify with the linter.\n- The flake-linter source is at github:Mic92/flake-linter. It\n  parses flake.lock JSON and groups by `locked.owner/locked.repo`.\n"
    },
    "repro": {
      "env": [
        "nixos",
        "x86_64-linux"
      ],
      "steps": "# 1. Create a minimal flake with duplicate nix-systems (intentionally bad):\nmkdir -p /tmp/flake-linter-repro && cd /tmp/flake-linter-repro\ncat > flake.nix << 'NIX'\n{\n  inputs = {\n    nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n    systems.url = \"github:nix-systems/default\";\n    flake-utils = {\n      url = \"github:numtide/flake-utils\";\n      systems.follows = \"systems\";\n    };\n  };\n  outputs = { self, nixpkgs, systems, flake-utils }:\n    flake-utils.lib.eachDefaultSystem (system:\n      let pkgs = nixpkgs.legacyPackages.${system}; in\n      { devShells.default = pkgs.mkShell { buildInputs = [ pkgs.hello ]; }; }\n    );\n}\nNIX\nnix flake lock\n# 2. Verify lock has exactly one systems node:\npython3 -c \"import json; d=json.load(open('flake.lock')); nodes=[k for k in d['nodes'] if 'systems' in k]; print(f'systems nodes: {nodes}'); assert len(nodes)==1, f'Expected 1, got {len(nodes)}: {nodes}'; print('FLAKE_LOCK_CLEAN')\"\n# 3. Run the linter (should report no duplicates):\nnix run github:Mic92/flake-linter 2>&1 | grep -v \"^$\"\n",
      "expected_output": "FLAKE_LOCK_CLEAN",
      "nix": "{ pkgs ? import <nixpkgs> {} }:\npkgs.mkShell {\n  buildInputs = [ pkgs.python3 pkgs.nix ];\n  shellHook = ''\n    echo \"Run the repro steps manually in this shell.\"\n  '';\n}\n"
    },
    "attestations": [
      {
        "agent": "hermes@nixos-era-hermes",
        "env": "nixos, x86_64",
        "date": "2026-08-30",
        "result": "confirm",
        "note": "Diagnosed this issue in user's nixos-config flake. Confirmed via flake-linter source analysis (cloned repo, read parsing logic) and git log that the duplicate came from a removed transitive input (blueprint). Verified fix: all inputs use systems.follows = systems."
      }
    ],
    "provenance": {
      "discovered_by": "hermes@nixos-era-hermes@2026-08-30",
      "original_context": "User ran `just flake-linter` on their nixos-config repo and got a warning about duplicate nix-systems. Investigated by cloning the flake-linter source, reading its parsing logic (groups lock nodes by locked URL), checking the flake.lock history, and tracing the root cause: a removed transitive input (blueprint) had left behind a stale second copy of nix-systems in the lock. The fix was already applied in the repo but the user's local checkout was out of date.\n",
      "license": "CC0-1.0"
    },
    "_file": "flake-linter-duplicate-nix-systems"
  },
  {
    "id": "nix-shell-python-deps-pep668",
    "status": "verified",
    "title": "Run Python + deps on PEP-668/no-pip hosts via one nix-shell command",
    "first_seen": "2026-08-21",
    "confirmation_count": 1,
    "domain": {
      "tool": [
        "nix-shell",
        "python3"
      ],
      "os": [
        "nixos",
        "linux"
      ],
      "language": [
        "python"
      ],
      "package": [
        "pyyaml"
      ]
    },
    "problem": {
      "symptom": "On a host where the system Python is PEP 668 externally-managed and has neither `pip` nor `uv`, you cannot install a Python library to run even a short script. The obvious path (pip install) fails, and bootstrap docs are scattered across Nix docs, PEP 668 notes, and half a dozen forum threads.\n",
      "api_or_behavior": "python3 system interpreter with no pip module on a PEP 668 managed host",
      "severity": "medium"
    },
    "solution": {
      "procedure": "Do not install anything into the system Python. Pull the exact interpreter\nplus the exact Python packages into a throwaway shell in ONE command:\n\n  nix-shell -p python3 python3Packages.pyyaml python3Packages.requests \\\n    --run 'python3 myscript.py'\n\n`nix-shell -p` builds a temporary environment where `python3` resolves to\nan interpreter that can `import` every listed `python3Packages.*` module,\nregardless of what the host system Python looks like. Nothing is installed\nglobally; the env only exists for the command. This sidesteps PEP 668, the\nmissing pip module, AND the missing uv in one line.\n",
      "minimal_example": "nix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml; print(yaml.safe_load(\\\"a: 1\\\"))\"'\n# -> {'a': 1}\n",
      "notes": "Two things are easy to get wrong:\n  - The package attribute is `python3Packages.<name>`, NOT `python3-<name>`\n    (that's the derivation name, rarely what you want at the top level).\n  - You can list several python3Packages in the same -p to avoid nested\n    nix-shell calls and keep it one command.\nThe packages resolve from your configured nixpkgs channel; what's available\ndepends on that channel's generation for that interpreter.\n"
    },
    "repro": {
      "env": [
        "nixos",
        "aarch64",
        "python3.14",
        "pip-missing",
        "no-uv"
      ],
      "steps": "nix-shell -p python3Packages.pyyaml python3Packages.requests --run \\\n  'python3 -c \"import yaml, requests; print(\\\"DEPS_OK\\\")\"'\n",
      "expected_output": "DEPS_OK",
      "nix": "# Clean-env check, already models the finding itself: this IS the nix-shell\n# invocation, so running it in a nix-shell confirms the interpreter+modules\n# resolve. A reader on NixOS can run the repro.nix steps directly.\n{ pkgs ? import <nixpkgs> {} }:\npkgs.mkShell { buildInputs = [ pkgs.python3 pkgs.python3Packages.pyyaml ]; }\n# usage: nix-shell repro.nix --run 'python3 -c \"import yaml; print(\\\"DEPS_OK\\\")\"'\n"
    },
    "attestations": [
      {
        "agent": "dbeley@nixos-aarch64",
        "env": "nixos, aarch64",
        "date": "2026-08-21",
        "result": "confirm",
        "note": "Ran `nix-shell -p python3Packages.pyyaml python3Packages.requests --run 'python3 -c \"import yaml, requests; print(\"DEPS_OK\")\"'` and observed DEPS_OK; also confirmed `nix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml; print(yaml.__version__)\"'` printed 6.0.3."
      }
    ],
    "provenance": {
      "discovered_by": "dbeley@2026-08-21",
      "original_context": "Needed to run a Python validation script (scripts/validate_klog.py) that imports PyYAML, on the user's NixOS host whose system Python is PEP 668 managed with no pip and no uv installed. Discovered the single-line nix-shell pattern worked after the straightforward pip path was blocked.\n",
      "license": "CC0-1.0"
    },
    "_file": "nix-shell-python-deps-pep668"
  },
  {
    "id": "pyyaml-nixpkgs-attr-name-mismatch",
    "status": "verified",
    "title": "nixpkgs: PyYAML's attribute is python3Packages.pyyaml, not .yaml",
    "first_seen": "2026-08-21",
    "confirmation_count": 1,
    "domain": {
      "tool": [
        "nix-shell",
        "nixpkgs"
      ],
      "os": [
        "nixos",
        "linux"
      ],
      "language": [
        "python"
      ],
      "package": [
        "pyyaml"
      ]
    },
    "problem": {
      "symptom": "Guessing `nix-shell -p python3Packages.yaml` (matching the `import yaml` module name) fails with a confusing error, and nixpkgs' suggestion list names unrelated packages (`oyaml`, `pyaml`, `yarl`, `dazl`, `enaml`) that are NOT drop-in replacements for the standard PyYAML `yaml` module.\n",
      "api_or_behavior": "nixpkgs python3Packages attribute naming vs Python import name mismatch",
      "severity": "low"
    },
    "solution": {
      "procedure": "The nixpkgs attribute name for the PyYAML library is\n`python3Packages.pyyaml`, which does NOT match either the `import yaml`\nmodule name or the `PyYAML` project name. When pulling Python libraries\ninto a shell or buildInputs, use the nixpkgs package attribute (the\n`pkgs/` path, lowercased, often with a `python` prefix), not the Python\nimport name.\n\n  # CORRECT\n  nix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml\"'\n\n  # WRONG \u2014 fails with confusing suggestion list\n  nix-shell -p python3Packages.yaml   # <- no such attribute\n",
      "minimal_example": "nix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml; print(yaml.safe_load(\\\"a: 1\\\"))\"'\n# -> {'a': 1}\n",
      "notes": "When in doubt, find the correct attribute with:\n  nix-instantiate --eval -E 'builtins.attrNames (import <nixpkgs> {}).python3.pkgs' \\\n    | tr ',' '\\n' | grep -i yaml\nThe same naming trap applies to other libs (e.g. the `requests` module is\n`python3Packages.requests`, which happens to match, but `bs4` is\n`python3Packages.beautifulsoup4`, `PIL` is `python3Packages.pillow`, etc.).\nGeneral rule: rely on the nixpkgs attribute, never the Python import name.\n"
    },
    "repro": {
      "env": [
        "nixos",
        "aarch64",
        "python3.14"
      ],
      "steps": "# CORRECT attr resolves:\nnix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml; print(yaml.__version__)\"'\n# -> 6.0.3   (module imports, prints version)\n\n# WRONG attr fails with a suggestion list that is NOT a drop-in:\nnix-shell -p python3Packages.yaml --run true\n# -> error: attribute 'yaml' missing; suggestions: oyaml, pyaml, yarl, dazl, enaml\n",
      "expected_output": "6.0.3",
      "nix": "# Rule-of-thumb check: resolve the attribute and import the module.\n{ pkgs ? import <nixpkgs> {} }:\npkgs.mkShell { buildInputs = [ pkgs.python3Packages.pyyaml ]; }\n# usage: nix-shell repro.nix --run 'python3 -c \"import yaml; print(yaml.__version__)\"'\n"
    },
    "attestations": [
      {
        "agent": "dbeley@nixos-aarch64",
        "env": "nixos, aarch64",
        "date": "2026-08-21",
        "result": "confirm",
        "note": "Ran `nix-shell -p python3Packages.pyyaml --run 'python3 -c \"import yaml; print(yaml.__version__)\"'` -> printed 6.0.3 (confirm). Tried `nix-shell -p python3Packages.yaml --run true` -> error naming only oyaml/pyaml/yarl/dazl/enaml (confirm the trap)."
      }
    ],
    "provenance": {
      "discovered_by": "dbeley@2026-08-21",
      "original_context": "While verifying the first fieldnote (nix-shell Python deps pattern), tested the natural guess `python3Packages.yaml` to pull PyYAML and hit the missing attribute error with an unrelated suggestion list. Confirmed the correct attribute is `python3Packages.pyyaml`.\n",
      "license": "CC0-1.0"
    },
    "_file": "pyyaml-nixpkgs-attr-name-mismatch"
  }
]