From 02ef0085ccf614a079ad98867e5b2cbb4691ef1d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 02:54:35 +0000 Subject: [PATCH 01/28] Model firmware targets, boot diagnostics, and memory part selection Modeling work from a live srv3/srv4 incident: both hosts failed to boot after a DIMM upgrade. Root cause of the first failure was host firmware three years stale; the residual failure is a DDR4 LRDIMM data-buffer training step. Nothing here is a blocklist -- each avoidance is derived from a mechanism, so a future ordering workflow can shop the whole market and explain every refusal by naming the failing step. extdeps.firmware.types (new) FirmwareTarget = BmcController | HostBios. The fleet could express which BMC image a host should run and had no way to express which BIOS, though BIOS owns DDR training. FirmwareImageKind distinguishes the OpenBMC tar (pushable to a dead host) from the Instant Flash ROM (needs a booting host). FirmwareReleaseRow carries a prerequisite, so an unappliable push is refused from the catalog rather than discovered from a vendor shell script after a 33MB upload. Consolidates BmcFirmwareSemanticVersion -> FirmwareSemanticVersion across 8 files: a version triple is one concept, and minting a second name for host firmware would be the CpuArchitecture/TargetArchitecture class. extdeps.ampere.scp_diagnostic (new) SCP console tokens as class+number with meaning held separately. Deliberately no token-to-meaning function: no codebook was obtained, so every implementation of one would be invention. The E prefix classifies a namespace, not a consequence -- E559/E571 appear on a same-family machine that boots normally. DiagnosticReadEffect records that several of these interfaces mutate what they report. extdeps.memory.training (new) Training steps, with the required set DERIVED from buffering. A registered module has no data buffer and cannot fail buffer calibration -- a difference in kind, which is what makes it sound as a purchase filter. capacity_reachable_as_dual_rank_registered derives from JEDEC die density why cheap high-capacity DDR4 is disproportionately load-reduced. gunbc.host_memory_qualification (new) Four independent standings: board class support, specific part qualification, DDR training outcome, host boot outcome. None derives another. Their collapse into one "does the RAM work" question cost three refuted hypotheses during the incident. Causal experiments land as receipts, including HypothesisWeakenedByIntervention for an intervention weaker than the one that would settle the question. gunbc.memory_part_selection (new) A candidate is refused iff it must execute a step this board has been observed to fail -- independent of vendor, part number, capacity, speed grade, or AVL membership. Implication is not a Pareto objective: implicated parts are refused before the frontier is computed. gunbc.host_boot_supervision, gunbc.host_diagnostic_channel (new) Two independent reset supervisors, kept separate because only one is configurable. Interval typed by how it is KNOWN: powermon's 306s is measured from eleven journal timestamps, not read from configuration. Five diagnostic authorities with their transports, and the standing that a missing projection does not establish a missing source. Witnesses cover the eight properties an external review required. Every digest was measured by acquiring the artifact; nothing is transcribed. NOT COMPILED. The local `gunbc compile --target dag` path rejects `//` at 1536 files corpus-wide, nearly all untouched, so it does not implement the section 4c annotation channel and cannot check this. CI is the first real compile. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/ampere/scp_diagnostic.dag | 145 +++++++++++++++ dag/extdeps/bmc/capability.dag | 25 +-- dag/extdeps/boards/asrock_rack.dag | 137 +++++++++++++- dag/extdeps/boards/types.dag | 36 +++- dag/extdeps/cpu/ampere.dag | 14 ++ dag/extdeps/firmware/types.dag | 145 +++++++++++++++ dag/extdeps/memory/training.dag | 118 ++++++++++++ dag/gunbc/fleet_bmc_firmware_evidence.dag | 6 +- dag/gunbc/fleet_bmc_observation.dag | 6 +- dag/gunbc/host_boot_supervision.dag | 115 ++++++++++++ dag/gunbc/host_diagnostic_channel.dag | 150 ++++++++++++++++ dag/gunbc/host_memory_qualification.dag | 135 ++++++++++++++ dag/gunbc/memory_part_selection.dag | 169 ++++++++++++++++++ .../claim/asrock_live_probe_witness_test.dag | 4 +- .../bmc_capability_solve_witness_test.dag | 16 +- .../claim/bmc_fan_converge_witness_test.dag | 4 +- ...vidence_install_mechanism_witness_test.dag | 4 +- ...host_memory_qualification_witness_test.dag | 152 ++++++++++++++++ 18 files changed, 1330 insertions(+), 51 deletions(-) create mode 100644 dag/extdeps/ampere/scp_diagnostic.dag create mode 100644 dag/extdeps/firmware/types.dag create mode 100644 dag/extdeps/memory/training.dag create mode 100644 dag/gunbc/host_boot_supervision.dag create mode 100644 dag/gunbc/host_diagnostic_channel.dag create mode 100644 dag/gunbc/host_memory_qualification.dag create mode 100644 dag/gunbc/memory_part_selection.dag create mode 100644 dag/test/claim/host_memory_qualification_witness_test.dag diff --git a/dag/extdeps/ampere/scp_diagnostic.dag b/dag/extdeps/ampere/scp_diagnostic.dag new file mode 100644 index 00000000000..5e3f8763661 --- /dev/null +++ b/dag/extdeps/ampere/scp_diagnostic.dag @@ -0,0 +1,145 @@ +module extdeps.ampere.scp_diagnostic + +import std.types { EpochMs, List, NonEmptyStr } +import std.nat { Nat } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "amperecomputing.com/customer-connect/products/altra-family-software" + } +} + +// AMPERE owns the SCP / SMpro / PMpro diagnostic interfaces and the token +// namespace. This module models that upstream shape; it does NOT record what +// any particular host did, which is a fleet observation and lives downstream. +// +// The board vendor owns only how these consoles are ROUTED, and an AMI POST +// code table — the ordinary x86-style one printed in board manuals — is a +// different namespace that does not decode these tokens at all. + +// A console token is a CLASS and a NUMBER, and the class carries no verdict. +// An earlier revision of this model treated the E prefix as meaning "error" +// and therefore "fatal". That is refuted: E559 and E571 are observed on a +// same-family machine that continues booting normally. Fatality is a property +// of the TRANSITION observed after a token, never of the lexeme. +type AmpereScpTokenClass + = ScpPCode + | ScpECode + +type AmpereScpConsoleToken { + class: AmpereScpTokenClass + number: Nat +} + +// Whether this repository can say what a token MEANS. Separated from the +// token itself so a future vendor codebook upgrades the meaning without +// rewriting a single historical receipt. +type AmpereDiagnosticMeaning + = MeaningDecoded { + description: NonEmptyStr + authority: ExternalAuthority + } + // No codebook obtained. The token may be compared and counted; its + // semantics may NOT be asserted. Construction of a description from an + // undecoded token is therefore impossible rather than merely discouraged. + | MeaningUndecoded { + required_codebook: ExternalAuthority? + } + +// The structured boot-progress interface, which is a DIFFERENT and richer +// surface than the console token stream: it separates status, stage, and a +// stage-specific payload, where the console emits only an opaque lexeme. +type AmpereBootStage + = Smpro + | Pmpro + | AtfBl1 + | DdrInitialization + | DdrTraining + | AtfBl2 + | AtfBl31 + | AtfBl32 + | Uefi + | OperatingSystem + +type AmpereBootStatus + = NotStarted + | Started + | Completed + | Failed + +// The decoded projection is Optional against the raw bytes, and the raw is +// REQUIRED. A reading whose field layout this repository has not established +// from a cited ABI must be storable in full without a decode: recording +// "stage 4, failed" from an unverified byte split would manufacture a +// semantic claim out of an arithmetic guess. +type AmpereStructuredBootProgress { + raw: NonEmptyStr + decoded_stage: AmpereBootStage? + decoded_status: AmpereBootStatus? + decoded_payload: Nat? + decode_authority: ExternalAuthority? + observed_at: EpochMs +} + +// READING IS NOT ALWAYS OBSERVING. Several of these interfaces mutate the +// state they report: error queues pop their oldest record, event registers +// clear on read, and the boot-progress read participates in progress +// acknowledgement. A convergence that polls them as if they were immutable +// files destroys evidence it was sent to collect — which happened during the +// 2026-08-11 incident, where the error registers were read repeatedly before +// their clear-on-read semantics were modeled. +type DiagnosticReadEffect + = ReadSnapshot + | ReadAcknowledgesBootProgress + | ReadConsumesOldestRecord + | ReadClearsReportedEvent + +type AmpereDiagnosticInterface { + sysfs_attribute: NonEmptyStr + read_effect: DiagnosticReadEffect +} + +// The interfaces observed present on the ALTRAD8UD BMC's smpro drivers. +// read_effect values are declared conservatively: where the effect is not +// established from a cited ABI, the SAFER assumption is that the read is +// destructive, because treating a destructive read as a snapshot loses data +// while treating a snapshot as destructive only costs a re-read. +data ampere_smpro_boot_progress_interface: AmpereDiagnosticInterface = AmpereDiagnosticInterface { + sysfs_attribute: "boot_progress", + read_effect: ReadAcknowledgesBootProgress, +} + +data ampere_smpro_error_queue_interfaces: List = [ + AmpereDiagnosticInterface { sysfs_attribute: "error_smpro", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_pmpro", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "warn_smpro", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "warn_pmpro", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_mem_ce", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_mem_ue", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_core_ce", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_core_ue", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_other_ce", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_other_ue", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_pcie_ce", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "error_pcie_ue", read_effect: ReadConsumesOldestRecord }, + AmpereDiagnosticInterface { sysfs_attribute: "event_dimm_hot", read_effect: ReadClearsReportedEvent }, + AmpereDiagnosticInterface { sysfs_attribute: "event_dimm_2x_refresh", read_effect: ReadClearsReportedEvent }, + AmpereDiagnosticInterface { sysfs_attribute: "event_vrd_hot", read_effect: ReadClearsReportedEvent }, + AmpereDiagnosticInterface { sysfs_attribute: "event_vrd_warn_fault", read_effect: ReadClearsReportedEvent }, + // Overflow counters are the one genuinely non-destructive group: they + // report whether records were LOST, which is what makes an empty queue + // interpretable at all. + AmpereDiagnosticInterface { sysfs_attribute: "overflow_mem_ce", read_effect: ReadSnapshot }, + AmpereDiagnosticInterface { sysfs_attribute: "overflow_mem_ue", read_effect: ReadSnapshot }, +] + +fn scp_token_is_error_class(token: AmpereScpConsoleToken) -> Bool { + token.class == ScpECode +} + +// Deliberately absent: any function from a token to a verdict, a severity, or +// a description. There is no codebook, so no such function can be honest. +data ampere_scp_token_class_is_not_a_verdict_note: String = "The E prefix classifies a token's NAMESPACE, not its consequence. Public boot output from an ALTRAD8UD-1L2T / Altra Max M128-30 machine that boots successfully contains E559 and E571, so an E token is routinely non-fatal. Conversely P012 appears in that same successful progression immediately after 'PMpro output complete', so it is ordinary progress context rather than a stall marker. Any claim that a specific token is fatal must come from an observed transition on an observed host — token, then reset — and belongs in a fleet receipt, never in this upstream module. scp_token_is_error_class answers which NAMESPACE a token belongs to and nothing else; there is intentionally no token-to-meaning function here, because with no codebook obtained every implementation of one would be invention." diff --git a/dag/extdeps/bmc/capability.dag b/dag/extdeps/bmc/capability.dag index 65d0bb7b175..02f31eb5fd7 100644 --- a/dag/extdeps/bmc/capability.dag +++ b/dag/extdeps/bmc/capability.dag @@ -4,6 +4,7 @@ import std.dissolution { DissolutionCondition, unbound_dissolution } import std.bmc { BmcFirmwareFamily, BmcEndpoint } import std.nat { Nat } +import extdeps.firmware.types { FirmwareSemanticVersion, firmware_semantic_version } import std.types { Bool, EpochMs, Int, List, NonEmptyStr, String } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -25,31 +26,13 @@ type BmcCapability type BmcFirmwareVersion { family: BmcFirmwareFamily - version: BmcFirmwareSemanticVersion -} - -type BmcFirmwareSemanticVersion { - major: Nat - minor: Nat - patch: Nat + version: FirmwareSemanticVersion } type BmcFirmwareVersionParse - = BmcFirmwareVersionParsed { version: BmcFirmwareSemanticVersion } + = BmcFirmwareVersionParsed { version: FirmwareSemanticVersion } | BmcFirmwareVersionRefused { wire: String } -fn bmc_firmware_semantic_version( - major: Nat, - minor: Nat, - patch: Nat, -) -> BmcFirmwareSemanticVersion { - BmcFirmwareSemanticVersion { - major: major, - minor: minor, - patch: patch, - } -} - fn bmc_firmware_version_from_wire(wire: String) -> BmcFirmwareVersionParse { let fields = split(s: wire, delimiter: ".") if fields.length() != 3 { @@ -68,7 +51,7 @@ fn bmc_firmware_version_from_wire(wire: String) -> BmcFirmwareVersionParse { BmcFirmwareVersionRefused { wire: wire } } else { BmcFirmwareVersionParsed { - version: bmc_firmware_semantic_version( + version: firmware_semantic_version( major: major, minor: minor, patch: patch, diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index 2c40ba4fc4a..bda08820429 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -1,6 +1,6 @@ module extdeps.boards.asrock_rack -import extdeps.boards.types { BaseboardCatalogRow } +import extdeps.boards.types { BaseboardCatalogRow, DimmPopulation } import extdeps.vendor.asrock_rack { asrock_rack } import extdeps.bmc.webui.nbd_proxy { BmcwebNbdProxyEndpointVariant, VmSlotIndexEndpoint, NbdSlotEndpoint } import extdeps.bmc.types { @@ -11,10 +11,16 @@ import extdeps.bmc.types { FanPidAggregatedEntry, FanPidIndependentEntries, } -import extdeps.bmc.capability { - BmcFirmwareSemanticVersion, - HistoricalEvidenceSnapshot, - bmc_firmware_semantic_version, +import extdeps.bmc.capability { HistoricalEvidenceSnapshot } +import extdeps.firmware.types { + BmcController, + NoFirmwarePrerequisite, + RequiresMinimumTargetFirmware, + FirmwareReleaseRow, + FirmwareSemanticVersion, + HostBios, + InstantFlashRom, + firmware_semantic_version, } import std.measure { celsius, percent, positive_celsius_delta, positive_measure_count } @@ -78,7 +84,7 @@ data altrad8ud_openbmc_2_07_00_serial_console_probe: NonEmptyStr = "Historical p data asrock_altrad8ud_openbmc_2_07_00_capabilities: BmcFirmwareCapabilityRow = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), }, capabilities: altrad8ud_openbmc_2_07_00_capabilities_list, } @@ -86,7 +92,7 @@ data asrock_altrad8ud_openbmc_2_07_00_capabilities: BmcFirmwareCapabilityRow = B data asrock_altrad8ud_openbmc_3_06_00_capabilities: BmcFirmwareCapabilityRow = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 3, minor: 6, patch: 0), + version: firmware_semantic_version(major: 3, minor: 6, patch: 0), }, capabilities: altrad8ud_openbmc_base_capabilities, } @@ -96,7 +102,7 @@ data asrock_altrad8ud_openbmc_3_06_00_catalog_authority: ExternalAuthority = asr data asrock_altrad8ud_openbmc_3_22_00_capabilities: BmcFirmwareCapabilityRow = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 3, minor: 22, patch: 0), + version: firmware_semantic_version(major: 3, minor: 22, patch: 0), }, capabilities: altrad8ud_openbmc_base_capabilities, } @@ -231,7 +237,7 @@ data asrock_srv3_live_readonly_probe_rows: List = [ }, ] -data asrock_srv3_live_probe_firmware_version: BmcFirmwareSemanticVersion = bmc_firmware_semantic_version( +data asrock_srv3_live_probe_firmware_version: FirmwareSemanticVersion = firmware_semantic_version( major: 2, minor: 7, patch: 0, @@ -284,3 +290,116 @@ fn live_probe_confirms_nbd_slot_ws_absent() -> Bool { verdict: ProbeAbsent ) } + +// Vendor firmware releases for this board, cited to the product download page. +// BMC and BIOS are separate FirmwareTarget rows because they are separately +// versioned and separately flashed: srv4 runs BMC 03.22.00 and still fails to +// boot, so "this host is on the current firmware" is not a single fact. +data asrock_altrad8ud_bios_3_10_instant_flash_release: FirmwareReleaseRow = FirmwareReleaseRow { + board: AsrockRackAltrad8ud1l2t, + target: HostBios, + version: firmware_semantic_version(major: 3, minor: 10, patch: 0), + released_on: 1776124800000, + image_kind: InstantFlashRom, + image_uri: Uri { + scheme: Https + locator: "download.asrock.com/BIOS/Server/ALTRAD8UD-1L2T(3.10)ROM.zip" + }, + image_bytes: 5359089, + image_digest: Present { + value: sha256_digest( + hex: "4338f8f23c86ca8fe1a27267153739893b3c27d72f6c09c7af8fbfd22347a223", + ), + }, + release_notes: "1. Updated RC to 0ACRB_021 2. Enhanced system compatibility", + prerequisite: NoFirmwarePrerequisite, + authority: asrock_altrad8ud_download_authority, +} + +data asrock_altrad8ud_bios_3_10_openbmc_release: FirmwareReleaseRow = FirmwareReleaseRow { + board: AsrockRackAltrad8ud1l2t, + target: HostBios, + version: firmware_semantic_version(major: 3, minor: 10, patch: 0), + released_on: 1776124800000, + image_kind: OpenBmcWebUiTar, + image_uri: Uri { + scheme: Https + locator: "download.asrock.com/BIOS/Server/ALTRAD8UD-1L2T(3.10)TAR.zip" + }, + image_bytes: 5359341, + image_digest: Present { + value: sha256_digest( + hex: "7693421cd665dd674f919856b1d3a46ecdbd80df2626665dcf0376f9cb7b7eb4", + ), + }, + release_notes: "1. Updated RC to 0ACRB_021 2. Enhanced system compatibility", + prerequisite: RequiresMinimumTargetFirmware { + target: BmcController, + minimum: firmware_semantic_version(major: 3, minor: 22, patch: 0), + }, + authority: asrock_altrad8ud_download_authority, +} + +// The bytes the BMC UpdateService actually consumes: the OpenBMC-packaged tar +// INSIDE the distribution zip above, carrying its own MANIFEST. Modeled apart +// from the zip because the push artifact and the download artifact are two +// different byte sequences with two different digests, and pushing the zip +// (or the raw .ROM) to the UpdateService is a different act from pushing this. +data asrock_altrad8ud_bios_3_10_obmc_push_artifact_name: NonEmptyStr = "obmc-bios-altrad8ud-1l2t-3.10.all.tar" + +data asrock_altrad8ud_bios_3_10_obmc_push_artifact_bytes: Nat = 33566720 + +data asrock_altrad8ud_bios_3_10_obmc_push_artifact_digest: Digest = sha256_digest( + hex: "e3bb2a26b863f24045e9e3bc950a89cbe3f8a86d7dc1c6c7a243f3afe35416ec", +) + +data asrock_altrad8ud_bios_3_10_obmc_manifest_purpose: NonEmptyStr = "xyz.openbmc_project.Software.Version.VersionPurpose.Host" + +data asrock_altrad8ud_bios_3_10_obmc_manifest_machine_name: NonEmptyStr = "altrad8ud-1l2t" + +data asrock_altrad8ud_bmc_3_22_00_release: FirmwareReleaseRow = FirmwareReleaseRow { + board: AsrockRackAltrad8ud1l2t, + target: BmcController, + version: firmware_semantic_version(major: 3, minor: 22, patch: 0), + released_on: 1776124800000, + image_kind: OpenBmcWebUiTar, + image_uri: Uri { + scheme: Https + locator: "www.asrockrack.com/general/productdetail.asp?Model=ALTRAD8UD-1L2T" + }, + image_bytes: 29056000, + image_digest: Absent, + release_notes: "1. A password change is enforced upon the first login to the WebUI 2. Improved BMC web information 3. Enhanced BMC system compatibility", + prerequisite: NoFirmwarePrerequisite, + authority: asrock_altrad8ud_download_authority, +} + +data asrock_altrad8ud_firmware_release_catalog: List = [ + asrock_altrad8ud_bios_3_10_instant_flash_release, + asrock_altrad8ud_bios_3_10_openbmc_release, + asrock_altrad8ud_bmc_3_22_00_release, +] + +data asrock_altrad8ud_firmware_release_note: String = "Cited from the ALTRAD8UD-1L2T product download table; all rows dated 2026-04-14. The BIOS rows are the load-bearing ones for the 2026-08-11 fleet incident: 'Updated RC to 0ACRB_021' is the MEMORY REFERENCE CODE, and srv3 fails DDR training on Samsung M386A4G40DM0-CPB 32GB 4Rx4 LRDIMMs while running DRAM FW 220414 (2022) under BL1 built 2023-05-17, emitting per-controller WRLVL errors and 'DDR initialization failure. Can't fall back!' on all eight controllers. ONE RELEASE, TWO PACKAGINGS, and they are not interchangeable: the ROM zip carries a bare 33554432-byte ALTRAD8UD-1L2T_3.10.ROM applied from BIOS setup by Instant Flash, which is unreachable on a host that cannot POST; the TAR zip carries obmc-bios-altrad8ud-1l2t-3.10.all.tar, whose MANIFEST declares purpose=...VersionPurpose.Host, version=3.10, MachineName=altrad8ud-1l2t, Type=bios, and which is what the BMC UpdateService consumes out-of-band. Every byte count and digest here was MEASURED on 2026-08-11 by acquiring the artifact and hashing it, never transcribed: the ROM zip at 5359089 bytes, the TAR zip at 5359341, the inner push tar at 33566720. The TAR locator was not published on the product page — it was found by probing the vendor download host and confirmed by HTTP 200 plus successful extraction, so it is weaker provenance than a page-published link and should be re-derived if the vendor reorganizes. The BMC 03.22.00 row remains digest-Absent with an unobserved locator: its image_bytes is the page's stated 27.71MB converted, NOT a Content-Length, so it must not be treated as a fetchable artifact until its own locator is observed." + +data asrock_altrad8ud_bios_openbmc_prerequisite_receipt: String = "The BIOS 3.10 OpenBMC-tar prerequisite (BMC >= 3.22.00) is an EXECUTED observation, not an inference from the vendor page, which states no such dependency. On 2026-08-11 the identical 33566720-byte obmc-bios-altrad8ud-1l2t-3.10.all.tar was pushed to two hosts of this board model. srv3 (BMC 2.07.00) accepted the upload, untarred it, and refused at the vendor flash script: 'Invalid ExtendedVersion: primary-all. Please check MANIFEST file!', with phosphor-image-updater logging 'Bios upgrade failed.' and Redfish reporting TaskState=Exception carrying NO machine-readable cause — the cause was only readable in the BMC journal over authenticated SSH. srv4 (BMC 3.22.00) accepted the same bytes. The minimum is therefore witnessed at 3.22.00 as an upper bound on the true threshold: versions between 2.07.00 and 3.22.00 (3.06.00 exists in the capability catalog) are UNTESTED, so the row may be stricter than the vendor requirement, which is the safe direction. The Instant Flash ROM packaging carries NoFirmwarePrerequisite because it never passes through the BMC at all; that asymmetry is why the prerequisite is a property of the PACKAGING and not of the release." + +// The DOCUMENTED DIMM populations. A population absent from this list is an +// unsupported configuration whose boot failure proves nothing about the +// modules installed in it — note in particular that the four-module +// population is A1+B1+E1+F1 and is NOT one module per channel. +data asrock_altrad8ud_documented_dimm_populations: List = [ + DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1", "E1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { board: AsrockRackAltrad8ud1l2t, slots: ["A1", "B1", "E1", "F1"], authority: extdeps_external_authority_anchor }, + DimmPopulation { + board: AsrockRackAltrad8ud1l2t, + slots: ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + authority: extdeps_external_authority_anchor, + }, +] + +// Board integration facts only: HOW the Ampere consoles are routed. The board +// does not own the meanings of Ampere SCP tokens, and its ordinary AMI POST +// code table is a different namespace that does not decode P012/E588/P029. +data asrock_altrad8ud_scp_console_tcp_port: Nat = 2202 diff --git a/dag/extdeps/boards/types.dag b/dag/extdeps/boards/types.dag index ef15e98cea6..7b8f4685208 100644 --- a/dag/extdeps/boards/types.dag +++ b/dag/extdeps/boards/types.dag @@ -1,6 +1,6 @@ module extdeps.boards.types -import std.types { NonEmptyStr } +import std.types { Bool, List, NonEmptyStr } import std.bmc { BmcFirmwareFamily, BmcEndpoint } import extdeps.vendor { Vendor } import extdeps.hardware { Hardware } @@ -28,3 +28,37 @@ type ServerBaseboard { catalog: BaseboardCatalogRow bmc: BmcEndpoint } + +// The documented DIMM populations for a board. A population that is not on +// this list is not a smaller version of a supported one — it is an +// unsupported configuration whose failure proves nothing about the modules. +type DimmPopulation { + board: BaseboardModel + slots: List + authority: ExternalAuthority +} + +type PopulationAdmission + = PopulationSupported + | PopulationUnsupported { slots: List } + +fn population_slots_match(a: List, b: List) -> Bool { + count(a) == count(b) + && fold(a, init: true, f: (acc, s) => acc && any(b, p => p == s)) +} + +// Fail-closed: a population absent from the documented list is REFUSED rather +// than assumed workable. Testing an unsupported population and reading its +// failure as a verdict on the modules is a fabricated conclusion, and the +// original plan for this incident — "four modules, one per channel" — was +// exactly such a population. +fn admit_population( + candidate: List, + documented: List, +) -> PopulationAdmission { + if any(documented, p => population_slots_match(a: p.slots, b: candidate)) { + PopulationSupported + } else { + PopulationUnsupported { slots: candidate } + } +} diff --git a/dag/extdeps/cpu/ampere.dag b/dag/extdeps/cpu/ampere.dag index 28cfb12149f..d2bd062951a 100644 --- a/dag/extdeps/cpu/ampere.dag +++ b/dag/extdeps/cpu/ampere.dag @@ -162,3 +162,17 @@ data altra_q6430_catalog: CpuModelCatalogRow = CpuModelCatalogRow { l3_cache: ampere_altra_classic_l3_cache_geometry, l3_cache_citations: ampere_altra_classic_l3_cache_citations, } + +// The Approved Vendor List: Ampere's positive qualification results for +// memory on the Altra family. Absence of a part from this list is the absence +// of a positive result, NOT a negative result — see +// gunbc.host_memory_qualification SpecificPartQualification, which keeps those +// two states distinct. +data ampere_altra_avl_authority: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "connect-admin.amperecomputing.com/api/secure-file-download/download-regular/?documentId=ppuyl31kktnze6gbof70xitc" + } +} + +data ampere_altra_avl_current_issue: NonEmptyStr = "1.15 (December 2025)" diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag new file mode 100644 index 00000000000..bf79b5a557e --- /dev/null +++ b/dag/extdeps/firmware/types.dag @@ -0,0 +1,145 @@ +module extdeps.firmware.types + +import std.nat { Nat } +import std.types { EpochMs, NonEmptyStr } +import extdeps.crypto.hash { Digest } +import extdeps.boards.types { BaseboardModel } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "en.wikipedia.org/wiki/Firmware" + } +} + +// A version triple is one concept. It was previously spelled +// BmcFirmwareSemanticVersion inside the BMC module, which made the SAME +// concept unnameable for host firmware without minting a second name for +// it (DESIGN section 3, the CpuArchitecture/TargetArchitecture class). +// The BMC carrier now imports this one. +type FirmwareSemanticVersion { + major: Nat + minor: Nat + patch: Nat +} + +fn firmware_semantic_version( + major: Nat, + minor: Nat, + patch: Nat, +) -> FirmwareSemanticVersion { + FirmwareSemanticVersion { + major: major, + minor: minor, + patch: patch, + } +} + +// The two independently versioned, independently flashed firmware subjects +// on one baseboard. They are NOT one subject at two versions: the BMC runs +// continuously and serves the update surface, while host firmware executes +// only during host boot and owns DRAM training. A single "firmware version" +// field cannot express "BMC 3.22.00 and BIOS 3.10" and that is exactly the +// state srv4 is in. +type FirmwareTarget + = BmcController + | HostBios + +// How a release artifact is applied. The distinction is load-bearing rather +// than cosmetic: the OpenBMC web-UI tar is pushed to the BMC's UpdateService +// over the network, while the Instant Flash ROM is applied from host firmware +// setup and is unreachable while a host fails to POST. +type FirmwareImageKind + = OpenBmcWebUiTar + | InstantFlashRom + +// A vendor release, cited. image_digest is Optional because a digest this +// repository has not COMPUTED must not be authored: an unverified hex would +// be a fabricated plausible value at exactly the boundary where the wrong +// bytes brick a controller (DESIGN section 5). It is filled by measuring the +// acquired file, never transcribed. +type FirmwareReleaseRow { + board: BaseboardModel + target: FirmwareTarget + version: FirmwareSemanticVersion + released_on: EpochMs + image_kind: FirmwareImageKind + image_uri: Uri + image_bytes: Nat + image_digest: Digest? + release_notes: NonEmptyStr + // What must already be running before these bytes can be applied. Carried + // ON the release row so an unappliable push is refused from the catalog, + // not discovered from a vendor shell script after the upload. + prerequisite: FirmwarePrerequisite + authority: ExternalAuthority +} + +// A release is not universally applicable to its own board. Vendor packaging +// carries manifest fields that only a NEWER controller understands, so +// "correct image for this board" and "appliable to this host right now" are +// different facts and fusing them is what let a valid image be pushed at a +// host that could never accept it. +// +// RECEIPT (2026-08-11, srv3 192.168.1.192): the BIOS 3.10 OpenBMC tar was +// accepted for transfer by BMC 2.07.00, untarred, and then refused by the +// vendor flash script with "Invalid ExtendedVersion: primary-all. Please check +// MANIFEST file!" — phosphor-image-updater reported "Bios upgrade failed." +// The refusal was clean (no flash write), but it arrived only after a 33 MB +// upload, a host power-off, and a Redfish task that reports Exception with no +// machine-readable cause. srv4 on BMC 3.22.00 accepted the same bytes. +type FirmwarePrerequisite + = NoFirmwarePrerequisite + | RequiresMinimumTargetFirmware { + target: FirmwareTarget, + minimum: FirmwareSemanticVersion, + } + +type FirmwareApplicabilityVerdict + = FirmwareApplicable + | FirmwarePrerequisiteUnmet { + target: FirmwareTarget, + minimum: FirmwareSemanticVersion, + observed: FirmwareSemanticVersion, + } + // Fail-closed third arm. An unobserved prerequisite is NOT met: srv3 reports + // its BIOS version as null through Redfish, so "we could not read it" must + // refuse rather than collapse into either pole (DESIGN section 5). + | FirmwarePrerequisiteUnobserved { + target: FirmwareTarget, + minimum: FirmwareSemanticVersion, + } + +fn firmware_semantic_version_at_least( + observed: FirmwareSemanticVersion, + minimum: FirmwareSemanticVersion, +) -> Bool { + if observed.major != minimum.major { + observed.major > minimum.major + } else if observed.minor != minimum.minor { + observed.minor > minimum.minor + } else { + observed.patch >= minimum.patch + } +} + +fn firmware_release_applicability( + prerequisite: FirmwarePrerequisite, + observed: FirmwareSemanticVersion?, +) -> FirmwareApplicabilityVerdict { + match prerequisite { + NoFirmwarePrerequisite => FirmwareApplicable + RequiresMinimumTargetFirmware { target: t, minimum: m } => + match observed { + Absent => FirmwarePrerequisiteUnobserved { target: t, minimum: m } + Present { value: v } => + if firmware_semantic_version_at_least(observed: v, minimum: m) { + FirmwareApplicable + } else { + FirmwarePrerequisiteUnmet { target: t, minimum: m, observed: v } + } + } + } +} diff --git a/dag/extdeps/memory/training.dag b/dag/extdeps/memory/training.dag new file mode 100644 index 00000000000..d84ab47e54b --- /dev/null +++ b/dag/extdeps/memory/training.dag @@ -0,0 +1,118 @@ +module extdeps.memory.training + +import std.types { Bool, Int, List, NonEmptyStr } +import std.measure { ByteSize, BitWidth, bit_width, byte_size, byte_size_count, bit_width_count } +import extdeps.external_authority { ExternalAuthority } +import extdeps.uri { Uri, Https } +import extdeps.memory.types { + DimmBuffering, + DramGeneration, + DramModuleCatalogRow, + JedecDdrGenerationCatalogRow, + LoadReduced, + Registered, + Unbuffered, + dram_data_chips_per_rank, + dram_module_is_realizable, +} + +data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { + uri: Uri { + scheme: Https + locator: "www.jedec.org/standards-documents/docs/jesd79-4a" + } +} + +// WHY A TRAINING-STEP MODEL RATHER THAN A BLOCKLIST. +// +// "Avoid 4Rx4 LRDIMM" is a conclusion, and a conclusion authored as a rule +// cannot be re-derived, cannot explain itself to a buyer, and cannot extend to +// a part nobody has tried. What actually happened is mechanical: initialization +// runs a SEQUENCE OF CALIBRATION STEPS, the set of steps depends on the +// module's construction, and a platform that fails one step implicates exactly +// those modules that must execute it. Encoding the steps makes the avoidance +// list a DERIVED consequence, and makes the reason legible at the point of +// purchase. +type MemoryTrainingStep + // Host-to-DRAM PHY calibration. Every module executes these regardless of + // buffering, so a failure here implicates nothing about module class. + = PhyWriteLeveling + | PhyReadGateLeveling + | PhyReadLeveling + | PhyVrefdqCalibration + // Data-buffer calibration. These exist ONLY on load-reduced modules, which + // interpose a buffer between the DRAM and the host. A registered module has + // no data buffer and never executes them — that is why the discriminator + // below is structural rather than statistical. + | LrdimmDataBufferMrep + | LrdimmDataBufferWriteLeveling + | LrdimmDataBufferReadTraining + +data phy_training_steps: List = [ + PhyWriteLeveling, + PhyReadGateLeveling, + PhyReadLeveling, + PhyVrefdqCalibration, +] + +data lrdimm_data_buffer_training_steps: List = [ + LrdimmDataBufferMrep, + LrdimmDataBufferWriteLeveling, + LrdimmDataBufferReadTraining, +] + +// The step set a module must complete, DERIVED from its construction. This is +// the single place the RDIMM/LRDIMM distinction becomes a set difference +// rather than a preference. +fn training_steps_required(module: DramModuleCatalogRow) -> List { + match module.buffering { + LoadReduced => concat(phy_training_steps, lrdimm_data_buffer_training_steps) + Registered => phy_training_steps + Unbuffered => phy_training_steps + } +} + +fn module_executes_step(module: DramModuleCatalogRow, step: MemoryTrainingStep) -> Bool { + any(training_steps_required(module: module), s => s == step) +} + +// CAPACITY FORCES TOPOLOGY, and this is derived from JEDEC die density rather +// than asserted from market observation. A module's required die density is +// capacity / (data chips per rank * ranks). When the density a simple low-rank +// module would need exceeds the largest monolithic die the generation defines, +// that capacity is UNREACHABLE without adding ranks or stacking — which is +// precisely how 64GB DDR4 came to exist as quad-rank LRDIMM before 16Gbit dies +// shipped, and why the CHEAPEST 64GB DDR4 parts are disproportionately the +// load-reduced ones. The buyer's intuition that a lower speed grade is simply +// a discount is therefore wrong at capacities near the density ceiling: the +// older part is not the same module clocked slower, it is a different topology. +fn hypothetical_module_at_rank_count( + module: DramModuleCatalogRow, + rank_count: Int, +) -> DramModuleCatalogRow { + DramModuleCatalogRow { + part_number: module.part_number, + manufacturer: module.manufacturer, + generation: module.generation, + capacity_bytes: module.capacity_bytes, + data_rate_mts: module.data_rate_mts, + ecc: module.ecc, + buffering: Registered, + chip_width: module.chip_width, + rank_count: rank_count, + } +} + +// Can this capacity be built as an ordinary low-rank registered module in this +// generation, or does reaching it REQUIRE extra ranks or die stacking? +fn capacity_reachable_as_dual_rank_registered( + module: DramModuleCatalogRow, + jedec: JedecDdrGenerationCatalogRow, +) -> Bool { + dram_module_is_realizable( + module: hypothetical_module_at_rank_count(module: module, rank_count: 2), + jedec: jedec, + ) +} + +data memory_training_step_note: String = "The step sets here are the structural claim this model rests on: a load-reduced module interposes a data buffer between the DRAM and the host, so it must calibrate that buffer, and a registered module has no such buffer and therefore cannot fail a buffer-calibration step. That is a difference in KIND, not in reliability, which is what makes it usable as a purchase filter rather than as a probability. It is also why the same failure reproduced on two different modules of one family and never on the registered parts the host ran previously. MREP is the DDR4 data-buffer read-phase calibration step; it is named explicitly because it is the step observed failing on the ALTRAD8UD-1L2T platform, both on gunbc's own hosts and in an independent public report carrying byte-identical controller status words. The PHY steps are listed for completeness and to keep the model honest in the other direction: a PHY-step failure would implicate every module class equally and would NOT support a buffering-based purchase rule." diff --git a/dag/gunbc/fleet_bmc_firmware_evidence.dag b/dag/gunbc/fleet_bmc_firmware_evidence.dag index ce69e6ef58d..87875833957 100644 --- a/dag/gunbc/fleet_bmc_firmware_evidence.dag +++ b/dag/gunbc/fleet_bmc_firmware_evidence.dag @@ -11,10 +11,10 @@ import extdeps.bmc.capability { BmcFirmwareObservation, BmcFirmwareVersion, bmc_firmware_capability_row_for, - bmc_firmware_semantic_version, } import gunbc.fleet_intent { srv1_baseboard, srv2_baseboard } import gunbc.fleet_bmc_observation { +import extdeps.firmware.types { firmware_semantic_version } srv3_bmc_firmware_observation, srv4_bmc_firmware_observation, } @@ -97,7 +97,7 @@ data srv1_bmc_firmware_attestation: BmcFirmwareAttestation = BmcFirmwareAttestat endpoint: srv1_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), }, attested_by: "briansrls", attested_at: 1786320000000, @@ -107,7 +107,7 @@ data srv2_bmc_firmware_attestation: BmcFirmwareAttestation = BmcFirmwareAttestat endpoint: srv2_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), }, attested_by: "briansrls", attested_at: 1786320000000, diff --git a/dag/gunbc/fleet_bmc_observation.dag b/dag/gunbc/fleet_bmc_observation.dag index e75cd274413..04660f6417b 100644 --- a/dag/gunbc/fleet_bmc_observation.dag +++ b/dag/gunbc/fleet_bmc_observation.dag @@ -7,7 +7,6 @@ import extdeps.bmc.capability { BmcFirmwareVersion, BmcEvidenceTemporalScope, HistoricalEvidenceSnapshot, - bmc_firmware_semantic_version, } import extdeps.bmc.types { BmcConfigPersistenceObservation, @@ -72,6 +71,7 @@ import std.state_durability { StateRealizationIdentity, } import std.types { Bool, EpochMs, List, NonEmptyStr, String } +import extdeps.firmware.types { firmware_semantic_version } type BmcFanTachReceipt { sensor: NonEmptyStr @@ -129,7 +129,7 @@ data srv3_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservat endpoint: srv3_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), }, evidence_scope: HistoricalEvidenceSnapshot, observed_at: 1785110400000, @@ -139,7 +139,7 @@ data srv4_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservat endpoint: srv4_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 3, minor: 22, patch: 0), + version: firmware_semantic_version(major: 3, minor: 22, patch: 0), }, evidence_scope: HistoricalEvidenceSnapshot, observed_at: 1785110400000, diff --git a/dag/gunbc/host_boot_supervision.dag b/dag/gunbc/host_boot_supervision.dag new file mode 100644 index 00000000000..8141e3e50d3 --- /dev/null +++ b/dag/gunbc/host_boot_supervision.dag @@ -0,0 +1,115 @@ +module gunbc.host_boot_supervision + +import std.types { Bool, EpochMs, List, NonEmptyStr } +import std.nat { Nat } +import product.placement_supply { HostIdentity } + +// TWO INDEPENDENT SUPERVISORS CAN RESET A HOST, and the fleet modeled +// neither. Both were discovered live on 2026-08-11 while diagnosing a boot +// loop, and the diagnosis was wrong until they were separated: with one +// stopped the host still cycled, which is what proved the loop was NOT a +// supervision timeout at all. +// +// Modeling them as one "watchdog" field would rebuild exactly the confusion +// that cost that hour — they have different owners, different mechanisms, +// different observability, and only one of them is configurable. +type HostBootSupervisor + // OpenBMC's standard boot watchdog, a systemd unit with D-Bus properties + // (Enabled, Interval, ExpireAction). Observed INACTIVE on srv4. + = PhosphorWatchdogPowerOn + // The vendor's power monitor (power-monitor.service, /usr/bin/powermon). + // Emits "force reset on state:1" and drives obmc-host-force-warm-reboot. + // Observed ACTIVE on srv4. It is a compiled binary invoked with NO + // arguments and NO configuration file, so its interval is not readable. + | PowerMonForceReset + +type SupervisorRunState + = SupervisorActive + | SupervisorInactive + | SupervisorRunStateUnobserved + +// The interval axis is deliberately NOT a plain Nat. powermon's period is +// not configuration this repository can read — it was MEASURED from eleven +// consecutive reset timestamps in the BMC journal, all 5m06s apart. A +// measured period and a declared setting are different epistemic objects: +// the first can drift with firmware and carries a sample count, the second +// can be changed. Storing 306 in a field named "interval" would present a +// measurement as a setting, and the next reader would try to edit it. +type SupervisorIntervalKnowledge + = IntervalDeclared { seconds: Nat } + | IntervalMeasured { seconds: Nat, samples: Nat, observed_at: EpochMs } + | IntervalUnknown + +type HostBootSupervisorObservation { + host: HostIdentity + supervisor: HostBootSupervisor + run_state: SupervisorRunState + interval: SupervisorIntervalKnowledge + observed_at: EpochMs +} + +// Boot progress as the host itself reports it on the serial console, which +// is a DIFFERENT and strictly richer surface than Redfish BootProgress. +// Redfish reported LastState=None throughout every failure in this incident +// — it distinguished nothing — while the console carried the exact postcode +// and error code. A model that consumes only BootProgress cannot see any of +// this, which is why the console codes are first-class here. +type HostBootCode + = HostBootCheckpoint { code: NonEmptyStr } + | HostBootError { code: NonEmptyStr } + +type HostBootStallSignature { + host: HostIdentity + // The last checkpoint reached before the failure. + last_checkpoint: NonEmptyStr + // The error code emitted at the failure, if the host emitted one. Absent + // means the host went silent WITHOUT an error, which is a different fault + // from an error-and-reset and must not be rendered as the same state. + error_code: NonEmptyStr? + // How many complete cycles of this signature were observed. One occurrence + // is an event; a repeating identical signature is a deterministic fault, + // and the two justify different responses. + observed_cycles: Nat + observed_at: EpochMs +} + +type HostBootLoopCause + // The host emitted an error and restarted itself. No supervisor involved. + = HostSelfResetOnError { code: NonEmptyStr } + // A supervisor's timer expired and forced the reset. + | SupervisorForcedReset { supervisor: HostBootSupervisor } + // The host went silent and a supervisor eventually reset it: the two are + // CONFOUNDED until one supervisor is stopped and the loop is re-observed. + // This arm exists because it was the real state of knowledge for an hour, + // and collapsing it into either neighbour would have been a guess. + | LoopCauseConfounded { candidates: List } + | LoopCauseUnobserved + +// Discriminating a self-reset from a forced reset requires stopping the +// supervisor and observing whether the loop PERSISTS. Nothing about the +// loop's shape alone decides it — a 306-second period looks exactly like a +// timeout whether or not a timeout causes it. +fn host_boot_loop_cause( + signature: HostBootStallSignature, + supervisor_stopped: Bool, + looped_after_stop: Bool, +) -> HostBootLoopCause { + match signature.error_code { + Present { value: code } => + if supervisor_stopped && looped_after_stop { + HostSelfResetOnError { code: code } + } else { + LoopCauseConfounded { + candidates: [PowerMonForceReset, PhosphorWatchdogPowerOn], + } + } + Absent => + if supervisor_stopped && looped_after_stop { + LoopCauseUnobserved + } else { + LoopCauseConfounded { + candidates: [PowerMonForceReset, PhosphorWatchdogPowerOn], + } + } + } +} diff --git a/dag/gunbc/host_diagnostic_channel.dag b/dag/gunbc/host_diagnostic_channel.dag new file mode 100644 index 00000000000..c3d3dcb44ba --- /dev/null +++ b/dag/gunbc/host_diagnostic_channel.dag @@ -0,0 +1,150 @@ +module gunbc.host_diagnostic_channel + +import std.types { Bool, EpochMs, List, NonEmptyStr } +import std.nat { Nat } +import product.placement_supply { HostIdentity } + +// WHOSE ERROR IS THIS? Five distinct authorities emitted diagnostics during +// the 2026-08-11 fleet incident, and they were being read as one +// undifferentiated stream of "errors". They are not one thing: they have +// different owners, different registries, different stability guarantees, +// and — the operationally decisive part — different TRANSPORTS, so a +// consumer wired to one channel is structurally blind to the others. +// +// Modeling the emitter is not taxonomy for its own sake. A DMTF message id +// is a versioned identifier in a published registry and can be decoded +// offline; an Ampere postcode is an opaque integer whose meaning we do not +// possess; a vendor shell script's English sentence has no registry at all +// and can change wording in any firmware release. Rendering all three as +// "error: " throws away exactly the facts that decide whether a +// diagnostic can be matched on, ranged over, or only shown to a human. +type DiagnosticAuthority + // DMTF-standardized Redfish message registries. Identifiers carry their + // own registry name AND version, e.g. "Base.1.16.0.InternalError". + = RedfishMessageRegistry + // OpenBMC's D-Bus error namespace, e.g. + // "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout". Structured, + // hierarchical, enumerable from the running BMC. + | OpenBmcPhosphorNamespace + // Ampere SCP/SMpro/PMpro console tokens (P012, E588, P029), modeled + // upstream in extdeps.ampere.scp_diagnostic. Opaque to this repository: + // no codebook has been obtained, so a token is an IDENTITY we compare and + // count, never a meaning we claim. + | AmpereScpConsoleTokenStream + // The STRUCTURED boot-progress interface: status, stage and payload from + // smpro-misc. A different surface from the token stream above, and a + // strictly richer one — it localized the 2026-08-11 stall to a stage while + // the console emitted only an undecodable lexeme. Fusing the two would + // have hidden that the structured source existed at all. + | AmpereStructuredBootProgressInterface + // Ampere DRAM reference-code training diagnostics, e.g. + // "MC3:R0: WRLVL Err=0 Stat=0x0 NibbleMsk=0xfc0", "SK[0]: 001200ff". + // Structured per memory controller and rank, but privately defined. + | AmpereDramReferenceCode + // Free text printed by vendor shell scripts, e.g. firmware_update.sh's + // "Invalid ExtendedVersion: primary-all. Please check MANIFEST file!". + // No registry, no identifier, no stability contract. Human-readable only. + | VendorScriptFreeText + +// HOW WAS IT READ? This axis is why the incident took as long as it did. +// The convergence surface is Redfish, and Redfish could see almost none of +// the diagnostics that mattered. +type DiagnosticTransport + // Redfish over HTTPS: reachable by any authenticated convergence run. + = RedfishHttps + // The BMC's captured host serial console (/var/log/obmc-console.log), + // reached by authenticated BMC SSH. + | BmcConsoleLogViaSsh + // SMpro/PMpro sysfs on the BMC. Note this is a TRANSPORT, not an + // authority: the same underlying data is reachable by other handlers + // where an image implements them (a correct D-Bus/Redfish projection, + // the dedicated SCP console, or Ampere's OEM IPMI register read). + | SmproSysfsViaSsh + // The BMC's systemd journal. Requires authenticated BMC SSH. + | BmcJournalViaSsh + +// Can a machine act on this diagnostic, or can it only be shown to a person? +// The distinction is not cosmetic: it decides whether convergence may branch +// on the diagnostic at all. +type DiagnosticDecodability + // Identifier resolves in a registry this repository can obtain and cite. + = DecodableFromPublishedRegistry { registry: NonEmptyStr } + // Stable identity, meaning unavailable. Comparable and countable, but its + // SEMANTICS may not be asserted. E588 is here. Note what this does NOT + // claim: the E prefix places the token in a namespace, it does not make it + // an error and it does not make it fatal — E559 and E571 appear on a + // same-family machine that boots normally. That E588 is followed by a reset + // on srv3 and srv4 is an observed transition recorded in a fleet receipt, + // never a property read off the lexeme. + | OpaqueStableIdentifier + // No identifier at all. Matching on the prose is matching on a wording the + // vendor may change without notice. + | UnstructuredProse + +type HostDiagnostic { + host: HostIdentity + authority: DiagnosticAuthority + transport: DiagnosticTransport + decodability: DiagnosticDecodability + // Verbatim, as emitted. Never normalized, because normalizing a code we + // cannot decode can only lose information. + raw: NonEmptyStr + observed_at: EpochMs +} + +// Whether a diagnostic may be READ by a given convergence run, derived from +// what that run can reach. An unreachable channel is not an absence of +// errors — it is an absence of observation, and the two must never collapse +// (DESIGN section 5: empty-observation narrow). +type DiagnosticChannelReachability + = ChannelReachable + | ChannelRequiresBmcSsh + | ChannelReachabilityUnobserved + +fn diagnostic_channel_reachability( + transport: DiagnosticTransport, + bmc_ssh_available: Bool, +) -> DiagnosticChannelReachability { + match transport { + RedfishHttps => ChannelReachable + BmcConsoleLogViaSsh => + if bmc_ssh_available { ChannelReachable } else { ChannelRequiresBmcSsh } + SmproSysfsViaSsh => + if bmc_ssh_available { ChannelReachable } else { ChannelRequiresBmcSsh } + BmcJournalViaSsh => + if bmc_ssh_available { ChannelReachable } else { ChannelRequiresBmcSsh } + } +} + +// A MISSING PROJECTION IS NOT MISSING DATA. Redfish returning None, an empty +// LogService, or a BootProgress of "None" says something about the PROJECTION +// this firmware image implements, and nothing whatever about whether the +// source data exists. Proven live on 2026-08-11: srv3's Redfish HostLogger +// reported zero entries and BootProgress None while the console file on disk +// held every boot cycle and smpro-misc boot_progress returned an advancing +// structured value — and no ampere-boot-progress service was installed on the +// image at all, so the projection was not incomplete, it was absent. +// +// An earlier revision of this module concluded from those same observations +// that these channels are "BMC SSH only". That inferred a property of the +// SOURCE from a property of one vendor image's PROJECTION, which is the +// empty-observation narrow this module exists to prevent. +type DiagnosticProjectionStanding + = ProjectionPresentAndPopulated + | ProjectionUnavailableOrIncomplete { projection: NonEmptyStr } + | SourceConfirmedAbsent { source: NonEmptyStr, evidence: NonEmptyStr } + +fn projection_absence_establishes_source_absence() -> Bool { + false +} + +// A convergence run that reads ONLY Redfish and finds nothing has not +// established that the host is healthy. Both hosts in this incident sat in +// deterministic reset loops while Redfish reported BootProgress=None, an +// empty HostLogger, and TaskState=Exception with no cause. This function +// exists so that "no diagnostics found" carries its own coverage caveat. +fn redfish_only_observation_is_complete() -> Bool { + false +} + +data host_diagnostic_channel_incident_receipt: String = "Executed 2026-08-11 against srv3 (192.168.1.192) and srv4 (192.168.1.195), ALTRAD8UD-1L2T / Ampere Altra Max M128-30. FIVE authorities were read, and the three most diagnostic ones were unreachable from Redfish. (1) RedfishMessageRegistry: 'Base.1.16.0.InternalError' from Bios.ResetBios, the only identifier of the five that names its own registry and version. (2) OpenBmcPhosphorNamespace: 'xyz.openbmc_project.State.Shutdown.Power.Error.Blackout' in the Redfish EventLog. (3) AmpereHostFirmwarePostCode: the repeating signature P012 -> E588 -> P029, read ONLY from /var/log/obmc-console.log over authenticated BMC SSH — the Redfish HostLogger LogService returned Members@odata.count=0 for the same period, so a Redfish-only consumer saw an empty console while the disk file held every cycle. (4) AmpereDramReferenceCode: 'MC3:R0: WRLVL Err=0 Stat=0x0 NibbleMsk=0xfc0', 'SK[0]: 001200ff', 'MC[0..7]: 00f100b4', 'DDR initialization failure. Can't fall back!' — same console-only transport; this is the evidence that identified the BIOS 2.06 memory reference code as the fault and it was invisible to Redfish. (5) VendorScriptFreeText: firmware_update.sh's 'Invalid ExtendedVersion: primary-all. Please check MANIFEST file!' in the BMC journal, which was the ONLY statement of why a firmware push failed — Redfish reported TaskState=Exception with the message 'The task with Id 0 has completed with errors' and no cause whatsoever. THE STANDING CONSEQUENCE: any convergence that consumes only Redfish can observe THAT something failed and never WHY, so BMC SSH is a required diagnostic transport for this fleet rather than a debugging convenience. E588 and P012 remain OpaqueStableIdentifier: no ASRock or Ampere postcode table has been obtained (both published manuals are PDFs whose text this repository could not extract, and the vendor support site is behind bot protection), so their meaning is UNCLAIMED. They may be compared and counted; they may not be interpreted." diff --git a/dag/gunbc/host_memory_qualification.dag b/dag/gunbc/host_memory_qualification.dag new file mode 100644 index 00000000000..93521d3db4a --- /dev/null +++ b/dag/gunbc/host_memory_qualification.dag @@ -0,0 +1,135 @@ +module gunbc.host_memory_qualification + +import std.types { Bool, EpochMs, List, NonEmptyStr } +import std.nat { Nat } +import product.placement_supply { HostIdentity } +import extdeps.boards.types { BaseboardModel, DimmPopulation } +import extdeps.external_authority { ExternalAuthority } + +// FOUR INDEPENDENT STANDINGS. None derives another, and that independence is +// the whole content of this module. +// +// The 2026-08-11 incident ran aground on exactly this collapse: once all eight +// modules reported trained, "the memory works" was treated as established, and +// the remaining failure was attributed to firmware, then to a watchdog, then +// to heat — three wrong theories, each costing an experiment — before the +// memory returned as the leading hypothesis. Training success is evidence +// about training and about nothing downstream of it. + +// Does the BOARD accept this CLASS of module at all (RDIMM / LRDIMM / rank +// count / capacity)? A vendor's generic statement of support. +type DimmClassSupport + = ClassSupportedByBoard + | ClassUnsupportedByBoard + | ClassSupportUnobserved + +// Is the EXACT PART qualified? Generic class support is not part +// qualification, and conflating them is how an unvalidated module reaches a +// production host. A part's absence from a vendor list is NOT a proof of +// incompatibility — it is the absence of a positive result, which is a +// materially weaker and differently-actionable fact. +type SpecificPartQualification + = ListedInVendorAvl { + avl_authority: ExternalAuthority + avl_issue: NonEmptyStr + } + | NotListedInVendorAvl { + avl_authority: ExternalAuthority + avl_issue: NonEmptyStr + } + | PartQualificationUnobserved + +// Did DDR training complete? This is the ONLY axis the 2026-08-11 BIOS +// upgrade moved. +type DdrTrainingOutcome + = TrainingOperational { dimm_mask: NonEmptyStr } + | TrainingFailed { detail: NonEmptyStr } + | TrainingOutcomeUnobserved + +// Did the HOST actually boot? Downstream of training and independent of it. +type HostBootOutcome + = BootReachedOperatingSystem + | RepeatingFirmwareReset { token: NonEmptyStr, cycles: Nat } + | BootStalledWithoutReset + | BootOutcomeUnobserved + +type HostMemoryStanding { + host: HostIdentity + part_number: NonEmptyStr + population: List + class_support: DimmClassSupport + part_qualification: SpecificPartQualification + training: DdrTrainingOutcome + boot: HostBootOutcome + observed_at: EpochMs +} + +// The load-bearing negative. There is deliberately NO function deriving boot +// from training, or qualification from class support: the compiler should make +// the collapse unwritable rather than warn about it. This predicate exists so +// a consumer can ASK whether the downstream question is still open, and it +// answers from the boot axis alone. +fn host_boot_is_established(standing: HostMemoryStanding) -> Bool { + match standing.boot { + BootReachedOperatingSystem => true + RepeatingFirmwareReset { token: _, cycles: _ } => false + BootStalledWithoutReset => false + BootOutcomeUnobserved => false + } +} + +data host_memory_qualification_note: String = "Written after the 2026-08-11 srv3/srv4 incident, where the collapse of these four axes into one 'does the RAM work' question cost three refuted hypotheses. The four axes stood as follows for Samsung M386A4G40DM0-CPB (32GB, 4Rx4, DDR4-2133 LRDIMM) in an eight-module population on ALTRAD8UD-1L2T: class support TRUE (the board states generic RDIMM and LRDIMM support); specific part qualification NOT LISTED in the Ampere Altra Family AVL issue 1.15 of December 2025, which contains neither this part number nor the M386A4G40 family nor any 2133 entry, and whose qualified Samsung 32GB rows are 1Rx4 or 2Rx4 at 2666/2933/3200 while its 4Rx4 rows are 128GB parts tested at 3200; training OPERATIONAL with mask 0x5555 on both hosts after BIOS 3.10; boot REPEATING FIRMWARE RESET at token E588. Absence from an AVL is not a proof of incompatibility and this module does not encode it as one — it is the absence of a positive qualification result, which is why NotListedInVendorAvl is a distinct variant from an unobserved qualification and from a refutation. The documented ALTRAD8UD populations are A1; A1+E1; A1+B1+E1+F1; and A1 through H1: note that the four-module population is NOT one-per-channel, so the diagnostic plan this module's admission check refuses is a real plan that was about to be executed." + +// CAUSAL EXPERIMENTS ARE RECEIPTS, NOT PROSE. Each records what was actually +// done, so a later reader can re-judge the verdict against the intervention +// rather than inheriting the conclusion. +type ExperimentVerdict + = HypothesisSupported + | HypothesisRefuted + // The intervention was WEAKER than the one that would settle the question. + // Distinct from Refuted because the hypothesis remains live and the + // experiment is repeatable in a stronger form. + | HypothesisWeakenedByIntervention { performed: NonEmptyStr, required: NonEmptyStr } + | ExperimentInconclusive { reason: NonEmptyStr } + +type CausalExperimentReceipt { + hypothesis: NonEmptyStr + intervention: NonEmptyStr + observation: NonEmptyStr + verdict: ExperimentVerdict + observed_at: EpochMs +} + +data srv3_incident_experiments: List = [ + CausalExperimentReceipt { + hypothesis: "Old host firmware caused the DDR training failure", + intervention: "BMC 2.07.00 -> 3.22.00, then BIOS 2.06 -> 3.10 (reference code 0ACRB_021)", + observation: "srv3 changed from per-controller WRLVL errors and 'DDR initialization failure. Can't fall back!' to 'DDR: Operational DIMM: 0x00005555' across all eight modules", + verdict: HypothesisSupported, + observed_at: 1786580000000, + }, + CausalExperimentReceipt { + hypothesis: "The powermon 306-second timeout caused the reset loop", + intervention: "systemctl stop power-monitor.service on srv4; loop re-observed", + observation: "No further 'force reset on state:1' entries, and the host continued to emit E588 and restart itself", + verdict: HypothesisRefuted, + observed_at: 1786580000000, + }, + CausalExperimentReceipt { + hypothesis: "DIMM_HOT thermal events caused E588", + intervention: "phosphor-pid-control stopped, all eight PWM channels forced to 255, host powered off to cool, then single cold start", + observation: "E588 recurred on the first cold cycle with DIMM temperatures 62-69C rather than 80C; DIMM_HOT assertions rose only AFTER hours of repeated retraining, while the first E588 predated them", + verdict: HypothesisRefuted, + observed_at: 1786580000000, + }, + CausalExperimentReceipt { + hypothesis: "Stale UEFI NVRAM caused E588", + intervention: "CMOS cable removed for approximately one second and reconnected", + observation: "The P012 -> E588 -> restart sequence survived unchanged across two subsequent cycles", + verdict: HypothesisWeakenedByIntervention { + performed: "CMOS cable interrupted briefly; BMC lost power and its console log rotated", + required: "vendor procedure: remove the battery AND short the CLRMOS1 contact pads with standby power absent", + }, + observed_at: 1786580000000, + }, +] diff --git a/dag/gunbc/memory_part_selection.dag b/dag/gunbc/memory_part_selection.dag new file mode 100644 index 00000000000..7bd2e153874 --- /dev/null +++ b/dag/gunbc/memory_part_selection.dag @@ -0,0 +1,169 @@ +module gunbc.memory_part_selection + +import std.types { Bool, EpochMs, Int, List, NonEmptyStr } +import std.measure { ByteSize, byte_size_count } +import extdeps.boards.types { AsrockRackAltrad8ud1l2t, BaseboardModel } +import extdeps.external_authority { ExternalAuthority } +import extdeps.memory.types { DramModuleCatalogRow, JedecDdrGenerationCatalogRow } +import extdeps.boards.asrock_rack { asrock_altrad8ud_download_authority } +import extdeps.memory.training { + LrdimmDataBufferMrep, + MemoryTrainingStep, + capacity_reachable_as_dual_rank_registered, + module_executes_step, + training_steps_required, +} +import gunbc.host_memory_qualification { SpecificPartQualification } + +// HOW WE KNOW A STEP FAILS. Not all evidence is equal, and a selection model +// that flattens these would let the weakest link carry the strongest verdict. +type TrainingEvidenceStanding + // The platform's own syndrome register named the failing step. This is the + // strongest form and is NOT what gunbc currently holds for MREP. + = DecodedFromPlatformSyndrome { register: NonEmptyStr } + // A vendor or third party reported the step by name alongside status words + // byte-identical to ours. Strong, but an association rather than a decode: + // it establishes that the same failure signature carries that step's name, + // not that our platform's registers were read and interpreted. + | AssociatedByIdenticalSignature { signature: NonEmptyStr, source: ExternalAuthority } + // The step is inferred from position in the boot sequence only. + | InferredFromSequencePosition + | EvidenceUnobserved + +type PlatformTrainingFailure { + board: BaseboardModel + step: MemoryTrainingStep + standing: TrainingEvidenceStanding + // The module population the failure was observed with. A step failure seen + // only in a sparse population is weaker support for rejecting a part in a + // full population, so the observation carries its own conditions. + observed_population: List + observed_at: EpochMs +} + +// A candidate part is IMPLICATED when it must execute a step this platform has +// been observed to fail. This is the whole selection rule, and note what it is +// not: there is no list of forbidden part numbers, no vendor preference, and no +// appeal to a qualification list. Those are separate axes handled below. +type PartImplication + = PartNotImplicated + | PartImplicatedByFailingStep { + step: MemoryTrainingStep + standing: TrainingEvidenceStanding + } + +fn implication_for_step( + candidate: DramModuleCatalogRow, + failure: PlatformTrainingFailure, +) -> PartImplication { + if module_executes_step(module: candidate, step: failure.step) { + PartImplicatedByFailingStep { step: failure.step, standing: failure.standing } + } else { + PartNotImplicated + } +} + +fn part_implication( + candidate: DramModuleCatalogRow, + failures: List, +) -> PartImplication { + fold( + failures, + init: PartNotImplicated, + f: (acc, failure) => + match acc { + PartImplicatedByFailingStep { step: s, standing: st } => + PartImplicatedByFailingStep { step: s, standing: st } + PartNotImplicated => implication_for_step(candidate: candidate, failure: failure) + }, + ) +} + +// THE SECOND RISK AXIS, kept separate because it has a different cause. A part +// whose capacity is unreachable as a plain dual-rank registered module in its +// generation had to be built with extra ranks or stacked die — so at that +// capacity, "cheaper and slower" systematically selects the exotic topology. +// This is derived from JEDEC die density, not from price observation. +type TopologyPressure + = CapacityReachableConventionally + | CapacityForcesExoticTopology { capacity_bytes: ByteSize } + +fn topology_pressure( + candidate: DramModuleCatalogRow, + jedec: JedecDdrGenerationCatalogRow, +) -> TopologyPressure { + if capacity_reachable_as_dual_rank_registered(module: candidate, jedec: jedec) { + CapacityReachableConventionally + } else { + CapacityForcesExoticTopology { capacity_bytes: candidate.capacity_bytes } + } +} + +// A candidate for an ordering decision. Price is carried as an integer minor +// unit and is deliberately NOT compared across currencies here. +type PartCandidate { + module: DramModuleCatalogRow + unit_price_minor: Int + currency: NonEmptyStr + sticks_required: Int + qualification: SpecificPartQualification +} + +fn candidate_total_capacity_bytes(candidate: PartCandidate) -> Int { + byte_size_count(b: candidate.module.capacity_bytes) * candidate.sticks_required +} + +fn candidate_total_price_minor(candidate: PartCandidate) -> Int { + candidate.unit_price_minor * candidate.sticks_required +} + +// PARETO SELECTION. Three objectives, and the implication axis is NOT one of +// them: an implicated part is REFUSED before the frontier is computed, because +// trading "it might not train" against price is how the failing modules were +// bought in the first place. Pareto ranks the parts that could work; it does +// not price the risk of parts we have mechanical reason to reject. +fn candidate_admissible( + candidate: PartCandidate, + failures: List, +) -> Bool { + match part_implication(candidate: candidate.module, failures: failures) { + PartNotImplicated => true + PartImplicatedByFailingStep { step: _, standing: _ } => false + } +} + +// a dominates b when it is at least as good on every objective and strictly +// better on one. More capacity is better; less total price is better; a higher +// data rate is better (it is headroom, and the platform downclocks to the +// slowest module present). +fn candidate_dominates(a: PartCandidate, b: PartCandidate) -> Bool { + let cap_a = candidate_total_capacity_bytes(candidate: a) + let cap_b = candidate_total_capacity_bytes(candidate: b) + let price_a = candidate_total_price_minor(candidate: a) + let price_b = candidate_total_price_minor(candidate: b) + let rate_a = a.module.data_rate_mts + let rate_b = b.module.data_rate_mts + (cap_a >= cap_b && price_a <= price_b && rate_a >= rate_b) + && (cap_a > cap_b || price_a < price_b || rate_a > rate_b) +} + +fn pareto_frontier( + candidates: List, + failures: List, +) -> List { + let admissible = candidates |> filter(c => candidate_admissible(candidate: c, failures: failures)) + admissible |> filter(c => !any(admissible, other => candidate_dominates(a: other, b: c))) +} + +data altrad8ud_mrep_failure_observation: PlatformTrainingFailure = PlatformTrainingFailure { + board: AsrockRackAltrad8ud1l2t, + step: LrdimmDataBufferMrep, + standing: AssociatedByIdenticalSignature { + signature: "SK[0]: 00120001 / MC[0]: 00f100b4", + source: asrock_altrad8ud_download_authority, + }, + observed_population: ["A1"], + observed_at: 1786590000000, +} + +data memory_part_selection_note: String = "The selection rule is mechanical and derived: a candidate is refused when it must execute a training step this platform has been observed to fail. For the ALTRAD8UD-1L2T that step is the DDR4 data-buffer read-phase calibration (MREP), so every load-reduced module is refused and every registered module is admitted, INDEPENDENT of vendor, part number, capacity, speed grade, or presence on any approved-vendor list. That independence is the point: an ordering workflow can shop the whole market instead of a list, and can explain each refusal by naming the step. THE EVIDENCE STANDING IS DELIBERATELY NOT THE STRONGEST ONE AVAILABLE. gunbc has not decoded its own platform syndrome register; what it has is a public report from the same board model naming MREP alongside controller status words byte-identical to its own (SK[0]: 00120001, MC[0]: 00f100b4), plus two different modules of one family failing identically in a documented-supported single-slot population while registered modules ran in that machine for years. That is AssociatedByIdenticalSignature, not DecodedFromPlatformSyndrome, and the difference is live rather than pedantic: Ampere documents SCP register B5 as carrying a per-DIMM training syndrome whose primary-syndrome field distinguishes DRAM VREFDQ failure from LRDIMM data-buffer training failure from LRDIMM data-buffer software-training failure, with B4 selecting the slot. Reading B5 for slot 0 would upgrade this row to a decode and would additionally discriminate WHICH buffer-training failure occurred. It was not read because neither the BMC nor the session host carries an ipmitool binary and no other transport to the SCP register file has been established. The observed_population is recorded as A1 alone because that is where the signature was captured; the eight-module population trained successfully and failed later at a different token, which this model does NOT treat as the same failure. The second axis, topology pressure, is derived rather than observed: when a capacity cannot be reached as a dual-rank registered module within the generation's monolithic die-density ceiling, that capacity forces extra ranks or stacking, which is why 64GB DDR4 at older speed grades is disproportionately load-reduced while 32GB is not. A buyer economizing on speed grade at 64GB is therefore not buying the same module more slowly, and the model derives that from JEDEC density rather than asserting it from market prices." diff --git a/dag/test/claim/asrock_live_probe_witness_test.dag b/dag/test/claim/asrock_live_probe_witness_test.dag index 78fe0f2659b..3f859c8a000 100644 --- a/dag/test/claim/asrock_live_probe_witness_test.dag +++ b/dag/test/claim/asrock_live_probe_witness_test.dag @@ -16,11 +16,11 @@ import extdeps.boards.asrock_rack { } import extdeps.bmc.capability { firmware_has_capability, - bmc_firmware_semantic_version, CapabilityVirtualMedia, CapabilityNbdProxyVirtualMedia, } import extdeps.bmc.webui.nbd_proxy { VmSlotIndexEndpoint } +import extdeps.firmware.types { firmware_semantic_version } data asrock_live_probe_binding_note: String = "gunbc#7398 collateral repair. This file declared NO imports and bound every cross-module name by bare reference, which resolves only when some unrelated module elsewhere in the assembled closure happens to drag extdeps.boards.asrock_rack into the pool — the Class B pool-membership coincidence DESIGN.md records as blocking further import-stripping. Adding one witness root in an unrelated lane re-shaped witness_layer_roots and ProbeAbsent stopped resolving, while every other name in this same file still bound by luck. MEASURED: merged roadmap PRs #7393 and #7395 hit the same whole-tree baseline and passed, so the baseline is not generally broken — a NEW witness root is what perturbs it. The imports above make the binding closure-independent, which is the remedy that thread names; they restore explicit dependency edges rather than removing any." @@ -31,7 +31,7 @@ test fn srv3_live_bmc_host_is_grounded() -> Bool { } test fn live_probe_firmware_matches_shipped_catalog_row() -> Bool { - let expected = bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0) + let expected = firmware_semantic_version(major: 2, minor: 7, patch: 0) live_probe_confirms_shipped_firmware_version() && asrock_srv3_live_probe_firmware_version == expected && asrock_altrad8ud_shipped_bmc_firmware_capabilities.firmware.version == expected diff --git a/dag/test/claim/bmc_capability_solve_witness_test.dag b/dag/test/claim/bmc_capability_solve_witness_test.dag index a5b96664ebb..0966bd86de8 100644 --- a/dag/test/claim/bmc_capability_solve_witness_test.dag +++ b/dag/test/claim/bmc_capability_solve_witness_test.dag @@ -20,7 +20,6 @@ import extdeps.bmc.capability { CapabilityVirtualMedia, catalog_has_vm_capable_firmware, bmc_firmware_observation_intent_verdict, - bmc_firmware_semantic_version, bmc_firmware_version_from_wire, bmc_firmware_version_matches_track, firmware_has_capability, @@ -43,6 +42,7 @@ import gunbc.os_install_mechanism { import std.bmc { BmcEndpoint, OpenBmc, Redfish } import std.types { Bool, list_length } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.firmware.types { firmware_semantic_version } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -75,7 +75,7 @@ test fn firmware_intent_match_crosses_as_typed_disposition() -> Bool { }, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version( + version: firmware_semantic_version( major: 3, minor: 22, patch: 0, @@ -151,7 +151,7 @@ test fn nbd_proxy_vm_capable_row_solves() -> Bool { let nbd_proxy_only_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), }, capabilities: [ CapabilityPowerControl, @@ -171,7 +171,7 @@ test fn redfish_vm_preferred_over_nbd_proxy() -> Bool { let both_vm_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 4, minor: 0, patch: 0), + version: firmware_semantic_version(major: 4, minor: 0, patch: 0), }, capabilities: [ CapabilityPowerControl, @@ -191,7 +191,7 @@ test fn vm_capable_firmware_row_flips_to_update_path() -> Bool { let update_only_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 1, minor: 0, patch: 0), + version: firmware_semantic_version(major: 1, minor: 0, patch: 0), }, capabilities: [ CapabilityPowerControl, @@ -203,7 +203,7 @@ test fn vm_capable_firmware_row_flips_to_update_path() -> Bool { let vm_capable_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 3, minor: 0, patch: 0), + version: firmware_semantic_version(major: 3, minor: 0, patch: 0), }, capabilities: [ CapabilityPowerControl, @@ -226,7 +226,7 @@ test fn update_only_row_without_vm_catalog_falls_to_pxe() -> Bool { let update_only_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 1, minor: 0, patch: 0), + version: firmware_semantic_version(major: 1, minor: 0, patch: 0), }, capabilities: [ CapabilityPowerControl, @@ -245,7 +245,7 @@ test fn node_with_virtual_media_solves_direct() -> Bool { let vm_row = BmcFirmwareCapabilityRow { firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 3, minor: 0, patch: 0), + version: firmware_semantic_version(major: 3, minor: 0, patch: 0), }, capabilities: [ CapabilityPowerControl, diff --git a/dag/test/claim/bmc_fan_converge_witness_test.dag b/dag/test/claim/bmc_fan_converge_witness_test.dag index 710bcf67e6d..9556e5fbe6e 100644 --- a/dag/test/claim/bmc_fan_converge_witness_test.dag +++ b/dag/test/claim/bmc_fan_converge_witness_test.dag @@ -8,7 +8,6 @@ import extdeps.bmc.openbmc_fan_control { import extdeps.crypto.hash { Digest, sha256_digest } import extdeps.bmc.capability { BmcFirmwareVersion, - bmc_firmware_semantic_version, } import extdeps.boards.asrock_rack { asrock_altrad8ud_openbmc_3_22_00_capabilities, @@ -45,6 +44,7 @@ import gunbc.bmc_fan_monitor { import gunbc.fleet_intent { srv3_host, srv4_host } import std.bmc { OpenBmc } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } +import extdeps.firmware.types { firmware_semantic_version } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -188,7 +188,7 @@ test fn backup_manifest_identity_is_derived_from_every_manifest_field() -> Bool stable_bmc_identity: cleanup_debt_test_manifest.stable_bmc_identity, firmware: BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version( + version: firmware_semantic_version( major: cleanup_debt_test_manifest.firmware.version.major + 1, minor: cleanup_debt_test_manifest.firmware.version.minor, patch: cleanup_debt_test_manifest.firmware.version.patch, diff --git a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag index 32656e72278..77afa8a82d9 100644 --- a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag +++ b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag @@ -7,7 +7,6 @@ import extdeps.bmc.capability { BmcFirmwareCapabilityUncatalogued, BmcFirmwareVersion, bmc_firmware_capability_row_for, - bmc_firmware_semantic_version, bmc_firmware_version_eq, firmware_has_capability, CapabilityBootSourceOverride, @@ -81,6 +80,7 @@ import gunbc.os_install_mechanism { import std.bmc { BmcEndpoint, OpenBmc, Redfish } import std.types { Bool, List, list_length } import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.firmware.types { firmware_semantic_version } // Control-only fixtures. Every axis gets its OWN synthetic observation: the earlier version of // this control constructed the established arm from gunbc.os_install srv3_pxe_network_boot_chain, @@ -122,7 +122,7 @@ data unknown_endpoint: BmcEndpoint = BmcEndpoint { host: "192.168.1.254", protoc data uncatalogued_firmware: BmcFirmwareVersion = BmcFirmwareVersion { family: OpenBmc, - version: bmc_firmware_semantic_version(major: 2, minor: 7, patch: 1), + version: firmware_semantic_version(major: 2, minor: 7, patch: 1), } // THE FINDING, made executable, at its honest strength. srv4's observed firmware (OpenBMC diff --git a/dag/test/claim/host_memory_qualification_witness_test.dag b/dag/test/claim/host_memory_qualification_witness_test.dag new file mode 100644 index 00000000000..13abd7aef04 --- /dev/null +++ b/dag/test/claim/host_memory_qualification_witness_test.dag @@ -0,0 +1,152 @@ +module test.claim.host_memory_qualification_witness_test + +import std.types { Bool, List, NonEmptyStr } +import extdeps.boards.types { + AsrockRackAltrad8ud1l2t, + DimmPopulation, + PopulationSupported, + PopulationUnsupported, + admit_population, +} +import extdeps.boards.asrock_rack { asrock_altrad8ud_documented_dimm_populations } +import extdeps.cpu.ampere { ampere_altra_avl_authority } +import extdeps.ampere.scp_diagnostic { + AmpereScpConsoleToken, + ScpECode, + ScpPCode, + ReadSnapshot, + ReadConsumesOldestRecord, + ampere_smpro_boot_progress_interface, + ReadAcknowledgesBootProgress, + scp_token_is_error_class, +} +import gunbc.host_memory_qualification { + BootReachedOperatingSystem, + ClassSupportedByBoard, + HostMemoryStanding, + HostBootOutcome, + NotListedInVendorAvl, + RepeatingFirmwareReset, + TrainingOperational, + host_boot_is_established, +} +import gunbc.host_diagnostic_channel { + projection_absence_establishes_source_absence, + redfish_only_observation_is_complete, +} + +// A four-module ALTRAD8UD population must be exactly A1+B1+E1+F1. The +// one-per-channel population that was about to be tested on 2026-08-11 is +// REFUSED, so an unsupported configuration cannot produce a verdict. +test fn four_dimm_population_must_be_a1_b1_e1_f1() -> Bool { + admit_population( + candidate: ["A1", "B1", "E1", "F1"], + documented: asrock_altrad8ud_documented_dimm_populations, + ) == PopulationSupported +} + +test fn one_per_channel_four_dimm_population_is_refused() -> Bool { + match admit_population( + candidate: ["A1", "C1", "E1", "G1"], + documented: asrock_altrad8ud_documented_dimm_populations, + ) { + PopulationUnsupported { slots: _ } => true + PopulationSupported => false + } +} + +test fn single_and_dual_populations_are_supported() -> Bool { + admit_population(candidate: ["A1"], documented: asrock_altrad8ud_documented_dimm_populations) == PopulationSupported + && admit_population(candidate: ["A1", "E1"], documented: asrock_altrad8ud_documented_dimm_populations) == PopulationSupported +} + +// RED control: a two-module population in the WRONG slots is refused, so the +// check is discriminating on slot identity rather than on module count. +test fn two_dimm_population_in_wrong_slots_is_refused() -> Bool { + match admit_population( + candidate: ["A1", "B1"], + documented: asrock_altrad8ud_documented_dimm_populations, + ) { + PopulationUnsupported { slots: _ } => true + PopulationSupported => false + } +} + +// DDR training success does not construct host boot success. The srv3 +// standing carries trained memory AND a repeating reset simultaneously, and +// the boot question still answers false. +data srv3_post_bios_310_standing: HostMemoryStanding = HostMemoryStanding { + host: "srv3", + part_number: "M386A4G40DM0-CPB", + population: ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + class_support: ClassSupportedByBoard, + part_qualification: NotListedInVendorAvl { + avl_authority: ampere_altra_avl_authority, + avl_issue: "1.15 (December 2025)", + }, + training: TrainingOperational { dimm_mask: "0x00005555" }, + boot: RepeatingFirmwareReset { token: "E588", cycles: 6 }, + observed_at: 1786580000000, +} + +test fn ddr_training_success_does_not_establish_host_boot() -> Bool { + !host_boot_is_established(standing: srv3_post_bios_310_standing) +} + +// Positive control: the ONLY thing that establishes boot is the boot axis. +test fn only_reached_operating_system_establishes_boot() -> Bool { + host_boot_is_established( + standing: HostMemoryStanding { + host: "srv3", + part_number: "M386A4G40DM0-CPB", + population: ["A1"], + class_support: ClassSupportedByBoard, + part_qualification: NotListedInVendorAvl { + avl_authority: ampere_altra_avl_authority, + avl_issue: "1.15 (December 2025)", + }, + training: TrainingOperational { dimm_mask: "0x00000001" }, + boot: BootReachedOperatingSystem, + observed_at: 1786580000000, + }, + ) +} + +// Generic class support does not construct exact-part qualification: the same +// standing is ClassSupportedByBoard AND NotListedInVendorAvl. +test fn class_support_does_not_establish_part_qualification() -> Bool { + srv3_post_bios_310_standing.class_support == ClassSupportedByBoard + && match srv3_post_bios_310_standing.part_qualification { + NotListedInVendorAvl { avl_authority: _, avl_issue: _ } => true + _ => false + } +} + +// A P token is not an error and an E token is not a verdict: the classifier +// answers NAMESPACE only, and no token-to-meaning function exists to call. +test fn p_token_is_not_an_error() -> Bool { + !scp_token_is_error_class(token: AmpereScpConsoleToken { class: ScpPCode, number: 12 }) +} + +test fn e_token_class_is_namespace_not_fatality() -> Bool { + // E588 (observed followed by reset on srv3/srv4) and E559 (observed on a + // same-family machine that continues booting) are the SAME class. Fatality + // therefore cannot be a function of the class. + scp_token_is_error_class(token: AmpereScpConsoleToken { class: ScpECode, number: 588 }) + && scp_token_is_error_class(token: AmpereScpConsoleToken { class: ScpECode, number: 559 }) +} + +// A missing projection does not establish a missing source. +test fn projection_absence_does_not_establish_source_absence() -> Bool { + !projection_absence_establishes_source_absence() +} + +test fn redfish_only_observation_is_not_complete() -> Bool { + !redfish_only_observation_is_complete() +} + +// A destructive or acknowledging read cannot be modeled as a pure snapshot. +test fn boot_progress_read_is_not_a_snapshot() -> Bool { + ampere_smpro_boot_progress_interface.read_effect == ReadAcknowledgesBootProgress + && ampere_smpro_boot_progress_interface.read_effect != ReadSnapshot +} From 4ce9d108e04c73ce29bcd7111c30c43242323da7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:07:16 +0000 Subject: [PATCH 02/28] Fix import placement, ground firmware/price units on std.measure The parse error CI named (expected name, found LBrace) was a misplaced import: the migration insert landed between the opening brace of a multi-line import block and its first name. Only this one file was affected; the other five insert sites were checked and are well-formed. Review 51271, both blockers, verified present and fixed: - FirmwareReleaseRow.image_bytes was a flat Nat beside a sibling capacity_bytes already carrying ByteSize in the same diff. Now ByteSize; the three ASRock call sites construct through byte_size. - PartCandidate carried unit_price_minor: Int and currency: NonEmptyStr, with a comment saying prices are deliberately not compared across currencies. That is exactly the invariant the type should enforce, so it is now MoneyAmountMicro + std.currency.CurrencyCode, and admission returns a typed CandidateAdmission whose CandidateCurrencyMismatch arm refuses a candidate priced in an undeclared currency rather than ranking it. The narration is now a wall. The selection rule had no executing consumer, which the unit fix made conspicuous. Adds memory_part_selection_witness_test with the currency refusal and its positive control, the implication rule and its RED control (a registered module of the same vendor/capacity/generation is NOT implicated, so the rule is discriminating on buffering rather than on part identity), and a no-observed-failure control proving the refusal comes from the observation rather than from a preference in the model. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/boards/asrock_rack.dag | 8 +- dag/extdeps/firmware/types.dag | 3 +- dag/gunbc/fleet_bmc_firmware_evidence.dag | 2 +- dag/gunbc/memory_part_selection.dag | 86 ++++++++-- .../memory_part_selection_witness_test.dag | 159 ++++++++++++++++++ 5 files changed, 236 insertions(+), 22 deletions(-) create mode 100644 dag/test/claim/memory_part_selection_witness_test.dag diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index bda08820429..44443712eb5 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -22,7 +22,7 @@ import extdeps.firmware.types { InstantFlashRom, firmware_semantic_version, } -import std.measure { celsius, percent, positive_celsius_delta, positive_measure_count } +import std.measure { byte_size, celsius, percent, positive_celsius_delta, positive_measure_count } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -305,7 +305,7 @@ data asrock_altrad8ud_bios_3_10_instant_flash_release: FirmwareReleaseRow = Firm scheme: Https locator: "download.asrock.com/BIOS/Server/ALTRAD8UD-1L2T(3.10)ROM.zip" }, - image_bytes: 5359089, + image_bytes: byte_size(count: 5359089), image_digest: Present { value: sha256_digest( hex: "4338f8f23c86ca8fe1a27267153739893b3c27d72f6c09c7af8fbfd22347a223", @@ -326,7 +326,7 @@ data asrock_altrad8ud_bios_3_10_openbmc_release: FirmwareReleaseRow = FirmwareRe scheme: Https locator: "download.asrock.com/BIOS/Server/ALTRAD8UD-1L2T(3.10)TAR.zip" }, - image_bytes: 5359341, + image_bytes: byte_size(count: 5359341), image_digest: Present { value: sha256_digest( hex: "7693421cd665dd674f919856b1d3a46ecdbd80df2626665dcf0376f9cb7b7eb4", @@ -367,7 +367,7 @@ data asrock_altrad8ud_bmc_3_22_00_release: FirmwareReleaseRow = FirmwareReleaseR scheme: Https locator: "www.asrockrack.com/general/productdetail.asp?Model=ALTRAD8UD-1L2T" }, - image_bytes: 29056000, + image_bytes: byte_size(count: 29056000), image_digest: Absent, release_notes: "1. A password change is enforced upon the first login to the WebUI 2. Improved BMC web information 3. Enhanced BMC system compatibility", prerequisite: NoFirmwarePrerequisite, diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index bf79b5a557e..3e68e367034 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -1,6 +1,7 @@ module extdeps.firmware.types import std.nat { Nat } +import std.measure { ByteSize, byte_size } import std.types { EpochMs, NonEmptyStr } import extdeps.crypto.hash { Digest } import extdeps.boards.types { BaseboardModel } @@ -67,7 +68,7 @@ type FirmwareReleaseRow { released_on: EpochMs image_kind: FirmwareImageKind image_uri: Uri - image_bytes: Nat + image_bytes: ByteSize image_digest: Digest? release_notes: NonEmptyStr // What must already be running before these bytes can be applied. Carried diff --git a/dag/gunbc/fleet_bmc_firmware_evidence.dag b/dag/gunbc/fleet_bmc_firmware_evidence.dag index 87875833957..0605be9af33 100644 --- a/dag/gunbc/fleet_bmc_firmware_evidence.dag +++ b/dag/gunbc/fleet_bmc_firmware_evidence.dag @@ -13,8 +13,8 @@ import extdeps.bmc.capability { bmc_firmware_capability_row_for, } import gunbc.fleet_intent { srv1_baseboard, srv2_baseboard } -import gunbc.fleet_bmc_observation { import extdeps.firmware.types { firmware_semantic_version } +import gunbc.fleet_bmc_observation { srv3_bmc_firmware_observation, srv4_bmc_firmware_observation, } diff --git a/dag/gunbc/memory_part_selection.dag b/dag/gunbc/memory_part_selection.dag index 7bd2e153874..e0f2cf92eb1 100644 --- a/dag/gunbc/memory_part_selection.dag +++ b/dag/gunbc/memory_part_selection.dag @@ -1,7 +1,9 @@ module gunbc.memory_part_selection -import std.types { Bool, EpochMs, Int, List, NonEmptyStr } -import std.measure { ByteSize, byte_size_count } +import std.types { Bool, EpochMs, List, NonEmptyStr } +import std.nat { Nat } +import std.currency { CurrencyCode } +import std.measure { ByteSize, MoneyAmountMicro, byte_size_count, money_amount_micro_count } import extdeps.boards.types { AsrockRackAltrad8ud1l2t, BaseboardModel } import extdeps.external_authority { ExternalAuthority } import extdeps.memory.types { DramModuleCatalogRow, JedecDdrGenerationCatalogRow } @@ -99,22 +101,26 @@ fn topology_pressure( } } -// A candidate for an ordering decision. Price is carried as an integer minor -// unit and is deliberately NOT compared across currencies here. +// A candidate for an ordering decision. Price rides the std.measure money +// carrier rather than a bare integer, and the currency is the std.currency +// closed code rather than a free string: a cross-currency comparison is then +// refused at admission below (DESIGN section 5 construction over validation), +// where an earlier revision of this module only narrated the invariant in a +// comment the compiler could not read. type PartCandidate { module: DramModuleCatalogRow - unit_price_minor: Int - currency: NonEmptyStr - sticks_required: Int + unit_price: MoneyAmountMicro + currency: CurrencyCode + sticks_required: Nat qualification: SpecificPartQualification } -fn candidate_total_capacity_bytes(candidate: PartCandidate) -> Int { +fn candidate_total_capacity_bytes(candidate: PartCandidate) -> Nat { byte_size_count(b: candidate.module.capacity_bytes) * candidate.sticks_required } -fn candidate_total_price_minor(candidate: PartCandidate) -> Int { - candidate.unit_price_minor * candidate.sticks_required +fn candidate_total_price_micros(candidate: PartCandidate) -> Nat { + money_amount_micro_count(m: candidate.unit_price) * candidate.sticks_required } // PARETO SELECTION. Three objectives, and the implication axis is NOT one of @@ -122,13 +128,55 @@ fn candidate_total_price_minor(candidate: PartCandidate) -> Int { // trading "it might not train" against price is how the failing modules were // bought in the first place. Pareto ranks the parts that could work; it does // not price the risk of parts we have mechanical reason to reject. +// Admission is a typed verdict rather than a Bool, so a dropped candidate +// carries WHY it was dropped. The currency arm is the load-bearing one: the +// frontier is computed against a DECLARED currency, and a candidate priced in +// another one is refused rather than silently ranked, which is what makes +// "prices are not compared across currencies" a property of the code instead +// of a sentence beside it. +type CandidateAdmission + = CandidateAdmissible + | CandidateImplicated { + step: MemoryTrainingStep + standing: TrainingEvidenceStanding + } + | CandidateCurrencyMismatch { + declared: CurrencyCode + candidate_currency: CurrencyCode + } + +fn candidate_admission( + candidate: PartCandidate, + failures: List, + declared_currency: CurrencyCode, +) -> CandidateAdmission { + if candidate.currency != declared_currency { + CandidateCurrencyMismatch { + declared: declared_currency, + candidate_currency: candidate.currency, + } + } else { + match part_implication(candidate: candidate.module, failures: failures) { + PartNotImplicated => CandidateAdmissible + PartImplicatedByFailingStep { step: s, standing: st } => + CandidateImplicated { step: s, standing: st } + } + } +} + fn candidate_admissible( candidate: PartCandidate, failures: List, + declared_currency: CurrencyCode, ) -> Bool { - match part_implication(candidate: candidate.module, failures: failures) { - PartNotImplicated => true - PartImplicatedByFailingStep { step: _, standing: _ } => false + match candidate_admission( + candidate: candidate, + failures: failures, + declared_currency: declared_currency, + ) { + CandidateAdmissible => true + CandidateImplicated { step: _, standing: _ } => false + CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => false } } @@ -139,8 +187,8 @@ fn candidate_admissible( fn candidate_dominates(a: PartCandidate, b: PartCandidate) -> Bool { let cap_a = candidate_total_capacity_bytes(candidate: a) let cap_b = candidate_total_capacity_bytes(candidate: b) - let price_a = candidate_total_price_minor(candidate: a) - let price_b = candidate_total_price_minor(candidate: b) + let price_a = candidate_total_price_micros(candidate: a) + let price_b = candidate_total_price_micros(candidate: b) let rate_a = a.module.data_rate_mts let rate_b = b.module.data_rate_mts (cap_a >= cap_b && price_a <= price_b && rate_a >= rate_b) @@ -150,8 +198,14 @@ fn candidate_dominates(a: PartCandidate, b: PartCandidate) -> Bool { fn pareto_frontier( candidates: List, failures: List, + declared_currency: CurrencyCode, ) -> List { - let admissible = candidates |> filter(c => candidate_admissible(candidate: c, failures: failures)) + let admissible = candidates |> filter(c => + candidate_admissible( + candidate: c, + failures: failures, + declared_currency: declared_currency, + )) admissible |> filter(c => !any(admissible, other => candidate_dominates(a: other, b: c))) } diff --git a/dag/test/claim/memory_part_selection_witness_test.dag b/dag/test/claim/memory_part_selection_witness_test.dag new file mode 100644 index 00000000000..434f4178f81 --- /dev/null +++ b/dag/test/claim/memory_part_selection_witness_test.dag @@ -0,0 +1,159 @@ +module test.claim.memory_part_selection_witness_test + +import std.types { Bool, List, list_length } +import std.currency { CurrencyCode, Eur, Usd } +import std.measure { ByteSize, byte_size, money_amount_micro } +import std.nat { Nat } +import extdeps.vendor.samsung { samsung } +import extdeps.memory.types { + DramModuleCatalogRow, + LoadReduced, + Registered, + bit_width, + dram_generation_ddr4, +} +import extdeps.memory.training { LrdimmDataBufferMrep, module_executes_step } +import gunbc.host_memory_qualification { PartQualificationUnobserved } +import gunbc.memory_part_selection { + CandidateAdmissible, + CandidateCurrencyMismatch, + CandidateImplicated, + PartCandidate, + PartImplicatedByFailingStep, + PartNotImplicated, + altrad8ud_mrep_failure_observation, + candidate_admission, + pareto_frontier, + part_implication, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +data stick_32g: ByteSize = byte_size(count: 34359738368) + +// The family observed failing MREP on this board: load-reduced, so it must +// execute the data-buffer step. +data lrdimm_32g: DramModuleCatalogRow = DramModuleCatalogRow { + part_number: "M386A4G40DM0-CPB", + manufacturer: samsung, + generation: dram_generation_ddr4, + capacity_bytes: stick_32g, + data_rate_mts: 2133, + ecc: true, + buffering: LoadReduced, + chip_width: bit_width(count: 4), + rank_count: 4, +} + +// Same capacity, same vendor, same generation — registered rather than load +// reduced. The ONLY axis that differs is the one the rule reads. +data rdimm_32g: DramModuleCatalogRow = DramModuleCatalogRow { + part_number: "M393A4K40DB2-CTD", + manufacturer: samsung, + generation: dram_generation_ddr4, + capacity_bytes: stick_32g, + data_rate_mts: 2666, + ecc: true, + buffering: Registered, + chip_width: bit_width(count: 4), + rank_count: 2, +} + +data observed_failures = [altrad8ud_mrep_failure_observation] + +fn candidate_of( + module: DramModuleCatalogRow, + currency: CurrencyCode, +) -> PartCandidate { + PartCandidate { + module: module, + unit_price: money_amount_micro(count: 100000000), + currency: currency, + sticks_required: 8, + qualification: PartQualificationUnobserved, + } +} + +// THE RULE. A load-reduced module must execute MREP, which this board has been +// observed to fail, so it is implicated by the step rather than by its part +// number. +test fn load_reduced_module_is_implicated_by_the_failing_step() -> Bool { + match part_implication(candidate: lrdimm_32g, failures: observed_failures) { + PartImplicatedByFailingStep { step: _, standing: _ } => true + PartNotImplicated => false + } +} + +// RED control on the discriminating axis: an otherwise identical registered +// module has no data buffer, cannot execute MREP, and is NOT implicated. If +// the rule were keyed on vendor, capacity, generation or rank it would answer +// the same for both fixtures. +test fn registered_module_of_same_family_is_not_implicated() -> Bool { + match part_implication(candidate: rdimm_32g, failures: observed_failures) { + PartNotImplicated => true + PartImplicatedByFailingStep { step: _, standing: _ } => false + } +} + +test fn only_the_load_reduced_module_executes_mrep() -> Bool { + module_executes_step(module: lrdimm_32g, step: LrdimmDataBufferMrep) + && !module_executes_step(module: rdimm_32g, step: LrdimmDataBufferMrep) +} + +// A candidate priced in a currency other than the declared one is REFUSED with +// a typed cause, never silently ranked against it. +test fn cross_currency_candidate_is_refused_not_ranked() -> Bool { + match candidate_admission( + candidate: candidate_of(module: rdimm_32g, currency: Eur), + failures: observed_failures, + declared_currency: Usd, + ) { + CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => true + CandidateAdmissible => false + CandidateImplicated { step: _, standing: _ } => false + } +} + +// Positive control: the SAME candidate in the declared currency is admissible, +// so the refusal above is discriminating on currency and not on the candidate. +test fn same_candidate_in_declared_currency_is_admissible() -> Bool { + match candidate_admission( + candidate: candidate_of(module: rdimm_32g, currency: Usd), + failures: observed_failures, + declared_currency: Usd, + ) { + CandidateAdmissible => true + CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => false + CandidateImplicated { step: _, standing: _ } => false + } +} + +// The implicated candidate is refused BEFORE the frontier is computed, so the +// frontier never trades "it might not train" against price. +test fn frontier_excludes_the_implicated_candidate() -> Bool { + let front = pareto_frontier( + candidates: [ + candidate_of(module: lrdimm_32g, currency: Usd), + candidate_of(module: rdimm_32g, currency: Usd), + ], + failures: observed_failures, + declared_currency: Usd, + ) + list_length(front) == 1 +} + +// RED control: with NO observed platform failure, the load-reduced module is +// admitted — the refusal comes from the observation, not from a preference +// baked into the model. +test fn without_an_observed_failure_the_load_reduced_module_is_admitted() -> Bool { + let front = pareto_frontier( + candidates: [ + candidate_of(module: lrdimm_32g, currency: Usd), + candidate_of(module: rdimm_32g, currency: Usd), + ], + failures: [], + declared_currency: Usd, + ) + list_length(front) == 2 +} From 1414f81668564f65be8bc1c0e5fa29246c1f56ff Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:16:18 +0000 Subject: [PATCH 03/28] Annotate observed_failures with its type data observed_failures was written without a type annotation, which the parser refuses (expected Colon, found Eq). Mine, in the witness added by the previous commit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/test/claim/memory_part_selection_witness_test.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/test/claim/memory_part_selection_witness_test.dag b/dag/test/claim/memory_part_selection_witness_test.dag index 434f4178f81..05d810c0239 100644 --- a/dag/test/claim/memory_part_selection_witness_test.dag +++ b/dag/test/claim/memory_part_selection_witness_test.dag @@ -3,7 +3,6 @@ module test.claim.memory_part_selection_witness_test import std.types { Bool, List, list_length } import std.currency { CurrencyCode, Eur, Usd } import std.measure { ByteSize, byte_size, money_amount_micro } -import std.nat { Nat } import extdeps.vendor.samsung { samsung } import extdeps.memory.types { DramModuleCatalogRow, @@ -21,6 +20,7 @@ import gunbc.memory_part_selection { PartCandidate, PartImplicatedByFailingStep, PartNotImplicated, + PlatformTrainingFailure, altrad8ud_mrep_failure_observation, candidate_admission, pareto_frontier, @@ -60,7 +60,7 @@ data rdimm_32g: DramModuleCatalogRow = DramModuleCatalogRow { rank_count: 2, } -data observed_failures = [altrad8ud_mrep_failure_observation] +data observed_failures: List = [altrad8ud_mrep_failure_observation] fn candidate_of( module: DramModuleCatalogRow, From 6b932085750611fb54715b4eb902899ee347e116 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:19:22 +0000 Subject: [PATCH 04/28] Ground the obmc push artifact byte count on ByteSize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 51280, blocking item, verified present at extdeps.boards.asrock_rack asrock_altrad8ud_bios_3_10_obmc_push_artifact_bytes (the review cited line 369; the symbol is at 350 — naming it symbolically per DESIGN section 3). It was the last bare-Nat byte count in the diff and sits beside FirmwareReleaseRow.image_bytes, which the previous commit had already moved to ByteSize, so it was a fork of the carrier in one file. Now byte_size(count: 33566720). It has no consumers, so nothing downstream changes shape. Not taking the non-blocking import cleanup in extdeps.memory.training; see the PR reply for why. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/boards/asrock_rack.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index 44443712eb5..8551a4ae79a 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -22,7 +22,7 @@ import extdeps.firmware.types { InstantFlashRom, firmware_semantic_version, } -import std.measure { byte_size, celsius, percent, positive_celsius_delta, positive_measure_count } +import std.measure { ByteSize, byte_size, celsius, percent, positive_celsius_delta, positive_measure_count } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -347,7 +347,7 @@ data asrock_altrad8ud_bios_3_10_openbmc_release: FirmwareReleaseRow = FirmwareRe // (or the raw .ROM) to the UpdateService is a different act from pushing this. data asrock_altrad8ud_bios_3_10_obmc_push_artifact_name: NonEmptyStr = "obmc-bios-altrad8ud-1l2t-3.10.all.tar" -data asrock_altrad8ud_bios_3_10_obmc_push_artifact_bytes: Nat = 33566720 +data asrock_altrad8ud_bios_3_10_obmc_push_artifact_bytes: ByteSize = byte_size(count: 33566720) data asrock_altrad8ud_bios_3_10_obmc_push_artifact_digest: Digest = sha256_digest( hex: "e3bb2a26b863f24045e9e3bc950a89cbe3f8a86d7dc1c6c7a243f3afe35416ec", From 8c2749ec8d03c4b05abe8d6fe1b54016f20e4fa7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:22:57 +0000 Subject: [PATCH 05/28] Derive the BMC Redfish surface from firmware instead of storing it per host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The srv3 flash exposed a dual representation. Which thermal resources bmcweb serves, how many sensors the chassis collection carries, and whether a fan-control resource exists are decided by the firmware BUILD for a board, but BmcRedfishSurfaceObservation stored those three fields per host, beside the firmware version they are a function of. So when srv3 went 2.07.00 -> 3.22.00 on 2026-08-11 its hand-authored row went on claiming ThermalSubsystemSensorCollection with 51 sensors, and two witnesses ASSERTED those constants as literals — the corpus stayed green while describing a surface the host did not have. There was no second authority that could disagree, so nothing could have caught it. MEASURED 2026-08-12, both hosts at once, which is what makes this a catalog row rather than a per-host note: srv3 (192.168.1.192) and srv4 (192.168.1.195), flashed independently on different days, both returned legacy Chassis Thermal 200, ThermalSubsystem 404, ThermalSubsystem/Fans 404, a 9-member Sensors collection, and a byte-identical Thermal key shape (12974 / 12975 bytes, differing in one reading value). Two independently-arrived-at hosts agreeing on every axis is the evidence that the shape follows the image; one host would only have established what that host does. An earlier probe returned 401 on BOTH hosts, which was a credential fact rather than a surface fact — checking the pair is what distinguished them. - extdeps.bmc.types gains BmcRedfishSurfaceShape, a cited BmcRedfishSurfaceRow catalog keyed by version, and bmc_redfish_surface_for / bmc_redfish_surface_of_host. An uncatalogued image derives SurfaceShapeUncatalogued carrying the version rather than a nearest-neighbour or default shape, so the next flash is loud at derivation instead of silently wrong at use. - BmcRedfishSurfaceObservation is DELETED rather than left beside the derivation; a dead carrier that can still be constructed re-opens the fork it was replaced for (DESIGN section 4b, dissolution on climb). - srv3's firmware observation is re-observed to 3.22.00 (VERSION_ID from /etc/os-release, authenticated BMC SSH). Its surface moves with it in one motion, because there is no longer a second place to update. Witnesses: the two that pinned the fork are replaced by the property that actually holds — hosts on the same image derive the same surface — plus a RED control proving the derivation discriminates on version (the 2.07.00 row still derives the old shape) and a fail-closed control that an uncatalogued image refuses. Per section 4b the evidence stays enrolled rather than retiring with the machinery it replaced. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/bmc/types.dag | 64 ++++++++++++++++- dag/extdeps/boards/asrock_rack.dag | 34 +++++++++ dag/gunbc/fleet_bmc_observation.dag | 38 +++++----- .../bmc_firmware_thermal_witness_test.dag | 69 ++++++++++++++++--- .../claim/bmc_live_receipts_witness_test.dag | 17 +++-- 5 files changed, 186 insertions(+), 36 deletions(-) diff --git a/dag/extdeps/bmc/types.dag b/dag/extdeps/bmc/types.dag index f50f7c4a663..391c905c20a 100644 --- a/dag/extdeps/bmc/types.dag +++ b/dag/extdeps/bmc/types.dag @@ -124,13 +124,73 @@ type RedfishFanControlSurface = RedfishFanControlResourceAbsent | RedfishInitialDutyControlResource -type BmcRedfishSurfaceObservation { - firmware: BmcFirmwareObservation +// THE REDFISH SURFACE IS A FUNCTION OF THE IMAGE, NOT A FACT ABOUT A HOST. +// Which thermal resources bmcweb serves, how many sensors the chassis +// collection carries, and whether a fan-control resource exists are all +// decided by the firmware build for a given board. Two hosts running the same +// image therefore present the same surface — measured 2026-08-12, srv3 and +// srv4 both on OpenBMC 3.22.00 returned a byte-identical Thermal key shape, +// legacy Thermal 200, ThermalSubsystem 404, and a 9-member sensor collection. +// +// An earlier revision stored those three fields per host BESIDE the firmware +// they are a function of. That is a dual representation (DESIGN section 3), +// and it failed in the way dual representations fail: srv3 was flashed +// 2.07.00 -> 3.22.00 on 2026-08-11, its hand-authored row went on claiming +// ThermalSubsystemSensorCollection with 51 sensors, and two witnesses ASSERTED +// those constants — so the corpus stayed green while describing a surface the +// host no longer had. Nothing could have caught it, because there was no +// second authority to disagree with. +type BmcRedfishSurfaceShape { thermal_surface: RedfishThermalTelemetrySurface chassis_sensor_collection_member_count: Nat fan_control_surface: RedfishFanControlSurface } +// A cited (board firmware version -> surface shape) row. The catalog is the +// single authority; a host contributes only its observed firmware version. +type BmcRedfishSurfaceRow { + firmware: BmcFirmwareVersion + shape: BmcRedfishSurfaceShape +} + +// Fail-closed: an image this repository has not catalogued yields a REFUSAL +// carrying the version, never a fabricated or nearest-neighbour shape. A newly +// flashed image is therefore loud at the point of derivation rather than +// silently wrong at the point of use. +type BmcRedfishSurfaceDerivation + = SurfaceShapeKnown { shape: BmcRedfishSurfaceShape } + | SurfaceShapeUncatalogued { firmware: BmcFirmwareVersion } + +fn bmc_redfish_surface_for( + catalog: List, + firmware: BmcFirmwareVersion, +) -> BmcRedfishSurfaceDerivation { + fold( + catalog, + init: SurfaceShapeUncatalogued { firmware: firmware }, + f: (acc, row) => + match acc { + SurfaceShapeKnown { shape: s } => SurfaceShapeKnown { shape: s } + SurfaceShapeUncatalogued { firmware: f } => + if row.firmware == f { + SurfaceShapeKnown { shape: row.shape } + } else { + SurfaceShapeUncatalogued { firmware: f } + } + }, + ) +} + +// The per-host projection. It takes the host's firmware OBSERVATION and the +// catalog, and there is deliberately no arm by which a caller supplies the +// shape: the only per-host input is the version. +fn bmc_redfish_surface_of_host( + catalog: List, + observation: BmcFirmwareObservation, +) -> BmcRedfishSurfaceDerivation { + bmc_redfish_surface_for(catalog: catalog, firmware: observation.firmware) +} + type BmcUpdateServiceObservation { firmware: BmcFirmwareObservation http_push_uri: NonEmptyStr diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index 8551a4ae79a..94ef45d149a 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -4,6 +4,12 @@ import extdeps.boards.types { BaseboardCatalogRow, DimmPopulation } import extdeps.vendor.asrock_rack { asrock_rack } import extdeps.bmc.webui.nbd_proxy { BmcwebNbdProxyEndpointVariant, VmSlotIndexEndpoint, NbdSlotEndpoint } import extdeps.bmc.types { + BmcRedfishSurfaceRow, + BmcRedfishSurfaceShape, + RedfishFanControlResourceAbsent, + RedfishInitialDutyControlResource, + RedfishLegacyChassisThermal, + RedfishThermalSubsystemSensorCollection, BmcFanControllerShape, BmcFanCurve, BmcFanCurveStep, @@ -115,6 +121,34 @@ data asrock_altrad8ud_1l2t_bmc_firmware_capability_catalog: List = [ + asrock_altrad8ud_openbmc_2_07_00_redfish_surface, + asrock_altrad8ud_openbmc_3_22_00_redfish_surface, +] + +data asrock_altrad8ud_redfish_surface_catalog_note: String = "Each row is the surface an OpenBMC build for this board serves, keyed by version alone, because that is what decides it: bmcweb's compiled resource set is a property of the image. The 3.22.00 row was MEASURED on both fleet hosts simultaneously on 2026-08-12 by authenticated BMC SSH and loopback Redfish, which is what makes it a catalog row rather than a per-host note — srv3 (192.168.1.192) and srv4 (192.168.1.195), independently flashed on different days, returned legacy Chassis Thermal 200, ThermalSubsystem 404, ThermalSubsystem/Fans 404, a 9-member Sensors collection, and a byte-identical Thermal key shape (12974 and 12975 bytes, differing only in a reading value). Two independently-arrived-at hosts agreeing on every axis is the evidence that the shape is a function of the image; one host would only have established what that host does. The 2.07.00 row is the historical pre-upgrade snapshot for srv3 and is retained because the catalog must still answer for an un-upgraded host, not because any fleet member currently runs it." + data asrock_altrad8ud_factory_temp_soc_fan_curve_steps: List = [ BmcFanCurveStep { at_or_above: celsius(40), duty: percent(30) }, BmcFanCurveStep { at_or_above: celsius(55), duty: percent(40) }, diff --git a/dag/gunbc/fleet_bmc_observation.dag b/dag/gunbc/fleet_bmc_observation.dag index 04660f6417b..3e84f95cbb1 100644 --- a/dag/gunbc/fleet_bmc_observation.dag +++ b/dag/gunbc/fleet_bmc_observation.dag @@ -9,12 +9,13 @@ import extdeps.bmc.capability { HistoricalEvidenceSnapshot, } import extdeps.bmc.types { + BmcRedfishSurfaceDerivation, + bmc_redfish_surface_of_host, BmcConfigPersistenceObservation, BmcFanCurve, BmcFanCurveStep, BmcFanPolicyObservation, BmcOverlayConfigObserved, - BmcRedfishSurfaceObservation, BmcUpdateServiceObservation, FanPidAggregatedEntry, On, @@ -26,7 +27,7 @@ import extdeps.bmc.types { ResetGracefulRestart, RedfishThermalSubsystemSensorCollection, } -import extdeps.boards.asrock_rack { asrock_altrad8ud_factory_fan_config_digest } +import extdeps.boards.asrock_rack { asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, asrock_altrad8ud_factory_fan_config_digest } import extdeps.crypto.hash { Digest, sha256_digest } import extdeps.filesystem.linux { LinuxFilePublicationProtocol, @@ -125,14 +126,18 @@ type BmcFanRebootPersistenceReceipt { observed_at: EpochMs } +// Re-observed 2026-08-12 after the 2026-08-11 flash: VERSION_ID=3.22.00 read +// from /etc/os-release over authenticated BMC SSH. The prior row still said +// 2.07.00, which is the observation this change stops being able to leave +// behind — the surface below is now derived from THIS field. data srv3_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservation { endpoint: srv3_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, - version: firmware_semantic_version(major: 2, minor: 7, patch: 0), + version: firmware_semantic_version(major: 3, minor: 22, patch: 0), }, evidence_scope: HistoricalEvidenceSnapshot, - observed_at: 1785110400000, + observed_at: 1786843200000, } data srv4_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservation { @@ -145,19 +150,20 @@ data srv4_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservat observed_at: 1785110400000, } -data srv3_bmc_redfish_surface_observation: BmcRedfishSurfaceObservation = BmcRedfishSurfaceObservation { - firmware: srv3_bmc_firmware_observation, - thermal_surface: RedfishThermalSubsystemSensorCollection, - chassis_sensor_collection_member_count: 51, - fan_control_surface: RedfishFanControlResourceAbsent, -} +// DERIVED, not authored. Each host contributes only its observed firmware +// version; the surface comes from the board catalog. This is what closes the +// staleness class: after srv3's 2026-08-11 flash there is no per-host copy +// left to go stale, and updating its firmware observation below moves its +// surface with it in one motion. +data srv3_bmc_redfish_surface: BmcRedfishSurfaceDerivation = bmc_redfish_surface_of_host( + catalog: asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + observation: srv3_bmc_firmware_observation, +) -data srv4_bmc_redfish_surface_observation: BmcRedfishSurfaceObservation = BmcRedfishSurfaceObservation { - firmware: srv4_bmc_firmware_observation, - thermal_surface: RedfishLegacyChassisThermal, - chassis_sensor_collection_member_count: 9, - fan_control_surface: RedfishInitialDutyControlResource, -} +data srv4_bmc_redfish_surface: BmcRedfishSurfaceDerivation = bmc_redfish_surface_of_host( + catalog: asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + observation: srv4_bmc_firmware_observation, +) data srv3_bmc_update_service_observation: BmcUpdateServiceObservation = BmcUpdateServiceObservation { firmware: srv3_bmc_firmware_observation, diff --git a/dag/test/claim/bmc_firmware_thermal_witness_test.dag b/dag/test/claim/bmc_firmware_thermal_witness_test.dag index 459d5df7083..41d557111c5 100644 --- a/dag/test/claim/bmc_firmware_thermal_witness_test.dag +++ b/dag/test/claim/bmc_firmware_thermal_witness_test.dag @@ -11,6 +11,10 @@ import extdeps.bmc.capability { bmc_firmware_observation_intent_verdict, } import extdeps.bmc.types { + BmcFirmwareVersion, + SurfaceShapeKnown, + SurfaceShapeUncatalogued, + bmc_redfish_surface_for, BmcConfigPersistenceUnsupported, BmcFanAggregatedControllerTopology, BmcFanCurve, @@ -33,6 +37,8 @@ import extdeps.bmc.types { redfish_thermal_query_verdict, } import extdeps.boards.asrock_rack { + asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + asrock_altrad8ud_openbmc_2_07_00_capabilities, asrock_altrad8ud_openbmc_3_06_00_capabilities, asrock_altrad8ud_openbmc_2_07_00_fan_policy_observation, asrock_altrad8ud_openbmc_3_22_00_fan_policy_observation, @@ -42,7 +48,7 @@ import extdeps.crypto.hash { sha256_digest } import gunbc.fleet_bmc_observation { BmcFanRebootPersistenceReceipt, srv3_bmc_firmware_observation, - srv3_bmc_redfish_surface_observation, + srv3_bmc_redfish_surface, srv3_bmc_update_service_observation, srv4_bmc_config_persistence_observation, srv4_bmc_fan_active_config_digest, @@ -52,7 +58,7 @@ import gunbc.fleet_bmc_observation { srv4_bmc_fan_observed_config_path, srv4_bmc_fan_perturbation_generation, srv4_bmc_firmware_observation, - srv4_bmc_redfish_surface_observation, + srv4_bmc_redfish_surface, srv4_bmc_update_service_observation, } import gunbc.fleet_bmc_state { @@ -312,13 +318,58 @@ test fn approved_exact_firmware_artifact_builds_a_typed_srv3_update_plan() -> Bo } } -test fn redfish_surface_difference_is_observed_not_inferred() -> Bool { - srv3_bmc_redfish_surface_observation.thermal_surface == RedfishThermalSubsystemSensorCollection - && srv3_bmc_redfish_surface_observation.chassis_sensor_collection_member_count == 51 - && srv3_bmc_redfish_surface_observation.fan_control_surface == RedfishFanControlResourceAbsent - && srv4_bmc_redfish_surface_observation.thermal_surface == RedfishLegacyChassisThermal - && srv4_bmc_redfish_surface_observation.chassis_sensor_collection_member_count == 9 - && srv4_bmc_redfish_surface_observation.fan_control_surface == RedfishInitialDutyControlResource +// The surface is a FUNCTION OF THE IMAGE. Both fleet hosts run OpenBMC +// 3.22.00, so both derive the same shape — and this witness would have gone +// red the moment srv3 was flashed under the old per-host model, which is +// exactly what it failed to do. It replaces a check that asserted srv3's +// 2.07.00 constants as literals and therefore stayed green while describing +// a surface srv3 no longer had. +test fn hosts_on_the_same_image_derive_the_same_surface() -> Bool { + match srv3_bmc_redfish_surface { + SurfaceShapeKnown { shape: s3 } => + match srv4_bmc_redfish_surface { + SurfaceShapeKnown { shape: s4 } => + s3 == s4 + && s3.thermal_surface == RedfishLegacyChassisThermal + && s3.chassis_sensor_collection_member_count == 9 + && s3.fan_control_surface == RedfishInitialDutyControlResource + SurfaceShapeUncatalogued { firmware: _ } => false + } + SurfaceShapeUncatalogued { firmware: _ } => false + } +} + +// RED control: the derivation is discriminating on VERSION. The same board's +// 2.07.00 image derives a materially different shape, so the agreement above +// is a fact about the two hosts sharing an image rather than the catalog +// answering the same way regardless of input. +test fn a_different_image_derives_a_different_surface() -> Bool { + match bmc_redfish_surface_for( + catalog: asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + firmware: asrock_altrad8ud_openbmc_2_07_00_capabilities.firmware, + ) { + SurfaceShapeKnown { shape: s } => + s.thermal_surface == RedfishThermalSubsystemSensorCollection + && s.chassis_sensor_collection_member_count == 51 + && s.fan_control_surface == RedfishFanControlResourceAbsent + SurfaceShapeUncatalogued { firmware: _ } => false + } +} + +// Fail-closed arm: an uncatalogued image REFUSES rather than resolving to a +// nearest neighbour or a default. This is the arm that makes the next flash +// loud instead of silently wrong. +test fn an_uncatalogued_image_refuses_to_derive_a_surface() -> Bool { + match bmc_redfish_surface_for( + catalog: asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + firmware: BmcFirmwareVersion { + family: OpenBmc, + version: firmware_semantic_version(major: 4, minor: 0, patch: 0), + }, + ) { + SurfaceShapeUncatalogued { firmware: _ } => true + SurfaceShapeKnown { shape: _ } => false + } } test fn both_observed_thermal_query_shapes_are_complete() -> Bool { diff --git a/dag/test/claim/bmc_live_receipts_witness_test.dag b/dag/test/claim/bmc_live_receipts_witness_test.dag index 82f922f129f..bdffd1811c9 100644 --- a/dag/test/claim/bmc_live_receipts_witness_test.dag +++ b/dag/test/claim/bmc_live_receipts_witness_test.dag @@ -28,7 +28,7 @@ import extdeps.filesystem.linux { } import gunbc.fleet_bmc_observation { srv3_bmc_firmware_observation, - srv3_bmc_redfish_surface_observation, + srv3_bmc_redfish_surface, srv3_bmc_update_service_observation, srv4_bmc_config_persistence_observation, srv4_bmc_fan_active_config_digest, @@ -37,7 +37,7 @@ import gunbc.fleet_bmc_observation { srv4_bmc_fan_observed_curve, srv4_bmc_fan_perturbation_generation, srv4_bmc_firmware_observation, - srv4_bmc_redfish_surface_observation, + srv4_bmc_redfish_surface, srv4_bmc_update_service_observation, } import gunbc.fleet_bmc_state { srv4_bmc_fan_reboot_persistence_receipt } @@ -54,15 +54,14 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -test fn firmware_and_redfish_surfaces_are_literal_versioned_observations() -> Bool { - srv3_bmc_firmware_observation.firmware.version.major == 2 - && srv3_bmc_firmware_observation.firmware.version.minor == 7 +// The firmware VERSION is the per-host observation and the only one. srv3 was +// flashed to 3.22.00 on 2026-08-11 and re-observed 2026-08-12; the surface it +// presents is no longer asserted here, because it is no longer stored here. +test fn firmware_versions_are_the_per_host_observation() -> Bool { + srv3_bmc_firmware_observation.firmware.version.major == 3 + && srv3_bmc_firmware_observation.firmware.version.minor == 22 && srv4_bmc_firmware_observation.firmware.version.major == 3 && srv4_bmc_firmware_observation.firmware.version.minor == 22 - && srv3_bmc_redfish_surface_observation.thermal_surface == RedfishThermalSubsystemSensorCollection - && srv3_bmc_redfish_surface_observation.fan_control_surface == RedfishFanControlResourceAbsent - && srv4_bmc_redfish_surface_observation.thermal_surface == RedfishLegacyChassisThermal - && srv4_bmc_redfish_surface_observation.fan_control_surface == RedfishInitialDutyControlResource } test fn update_surface_receipts_do_not_claim_an_available_backup_image() -> Bool { From 053b642c58f3a7d425950b66e689305cf7d00eb8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:29:41 +0000 Subject: [PATCH 06/28] Move source annotations to module-item grain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN section 4c admits only standalone leading // blocks attached to module-scope declarations; body-position annotations refuse. Every new module in this change had them — comments sitting beside a field or a coproduct arm — so the heal job refused with a located diagnostic per line. Each block moves above its enclosing declaration and keeps its referent by naming it ("On prerequisite:", "On FirmwarePrerequisiteUnobserved:"), so no rationale is lost in the move. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/ampere/scp_diagnostic.dag | 14 +-- dag/extdeps/firmware/types.dag | 14 +-- dag/extdeps/memory/training.dag | 13 +-- dag/gunbc/host_boot_supervision.dag | 44 +++++---- dag/gunbc/host_diagnostic_channel.dag | 91 +++++++++++-------- dag/gunbc/host_memory_qualification.dag | 7 +- dag/gunbc/memory_part_selection.dag | 23 +++-- ...host_memory_qualification_witness_test.dag | 7 +- 8 files changed, 120 insertions(+), 93 deletions(-) diff --git a/dag/extdeps/ampere/scp_diagnostic.dag b/dag/extdeps/ampere/scp_diagnostic.dag index 5e3f8763661..a10364b1aa0 100644 --- a/dag/extdeps/ampere/scp_diagnostic.dag +++ b/dag/extdeps/ampere/scp_diagnostic.dag @@ -37,14 +37,15 @@ type AmpereScpConsoleToken { // Whether this repository can say what a token MEANS. Separated from the // token itself so a future vendor codebook upgrades the meaning without // rewriting a single historical receipt. +// On MeaningUndecoded: +// No codebook obtained. The token may be compared and counted; its +// semantics may NOT be asserted. Construction of a description from an +// undecoded token is therefore impossible rather than merely discouraged. type AmpereDiagnosticMeaning = MeaningDecoded { description: NonEmptyStr authority: ExternalAuthority } - // No codebook obtained. The token may be compared and counted; its - // semantics may NOT be asserted. Construction of a description from an - // undecoded token is therefore impossible rather than merely discouraged. | MeaningUndecoded { required_codebook: ExternalAuthority? } @@ -112,6 +113,10 @@ data ampere_smpro_boot_progress_interface: AmpereDiagnosticInterface = AmpereDia read_effect: ReadAcknowledgesBootProgress, } +// On AmpereDiagnosticInterface: +// Overflow counters are the one genuinely non-destructive group: they +// report whether records were LOST, which is what makes an empty queue +// interpretable at all. data ampere_smpro_error_queue_interfaces: List = [ AmpereDiagnosticInterface { sysfs_attribute: "error_smpro", read_effect: ReadConsumesOldestRecord }, AmpereDiagnosticInterface { sysfs_attribute: "error_pmpro", read_effect: ReadConsumesOldestRecord }, @@ -129,9 +134,6 @@ data ampere_smpro_error_queue_interfaces: List = [ AmpereDiagnosticInterface { sysfs_attribute: "event_dimm_2x_refresh", read_effect: ReadClearsReportedEvent }, AmpereDiagnosticInterface { sysfs_attribute: "event_vrd_hot", read_effect: ReadClearsReportedEvent }, AmpereDiagnosticInterface { sysfs_attribute: "event_vrd_warn_fault", read_effect: ReadClearsReportedEvent }, - // Overflow counters are the one genuinely non-destructive group: they - // report whether records were LOST, which is what makes an empty queue - // interpretable at all. AmpereDiagnosticInterface { sysfs_attribute: "overflow_mem_ce", read_effect: ReadSnapshot }, AmpereDiagnosticInterface { sysfs_attribute: "overflow_mem_ue", read_effect: ReadSnapshot }, ] diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index 3e68e367034..a47f2dfe0bf 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -61,6 +61,10 @@ type FirmwareImageKind // be a fabricated plausible value at exactly the boundary where the wrong // bytes brick a controller (DESIGN section 5). It is filled by measuring the // acquired file, never transcribed. +// On prerequisite: +// What must already be running before these bytes can be applied. Carried +// ON the release row so an unappliable push is refused from the catalog, +// not discovered from a vendor shell script after the upload. type FirmwareReleaseRow { board: BaseboardModel target: FirmwareTarget @@ -71,9 +75,6 @@ type FirmwareReleaseRow { image_bytes: ByteSize image_digest: Digest? release_notes: NonEmptyStr - // What must already be running before these bytes can be applied. Carried - // ON the release row so an unappliable push is refused from the catalog, - // not discovered from a vendor shell script after the upload. prerequisite: FirmwarePrerequisite authority: ExternalAuthority } @@ -98,6 +99,10 @@ type FirmwarePrerequisite minimum: FirmwareSemanticVersion, } +// On FirmwarePrerequisiteUnobserved: +// Fail-closed third arm. An unobserved prerequisite is NOT met: srv3 reports +// its BIOS version as null through Redfish, so "we could not read it" must +// refuse rather than collapse into either pole (DESIGN section 5). type FirmwareApplicabilityVerdict = FirmwareApplicable | FirmwarePrerequisiteUnmet { @@ -105,9 +110,6 @@ type FirmwareApplicabilityVerdict minimum: FirmwareSemanticVersion, observed: FirmwareSemanticVersion, } - // Fail-closed third arm. An unobserved prerequisite is NOT met: srv3 reports - // its BIOS version as null through Redfish, so "we could not read it" must - // refuse rather than collapse into either pole (DESIGN section 5). | FirmwarePrerequisiteUnobserved { target: FirmwareTarget, minimum: FirmwareSemanticVersion, diff --git a/dag/extdeps/memory/training.dag b/dag/extdeps/memory/training.dag index d84ab47e54b..8c6d7e5d687 100644 --- a/dag/extdeps/memory/training.dag +++ b/dag/extdeps/memory/training.dag @@ -33,17 +33,18 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { // those modules that must execute it. Encoding the steps makes the avoidance // list a DERIVED consequence, and makes the reason legible at the point of // purchase. +// Host-to-DRAM PHY calibration. Every module executes these regardless of +// buffering, so a failure here implicates nothing about module class. +// On LrdimmDataBufferMrep: +// Data-buffer calibration. These exist ONLY on load-reduced modules, which +// interpose a buffer between the DRAM and the host. A registered module has +// no data buffer and never executes them — that is why the discriminator +// below is structural rather than statistical. type MemoryTrainingStep - // Host-to-DRAM PHY calibration. Every module executes these regardless of - // buffering, so a failure here implicates nothing about module class. = PhyWriteLeveling | PhyReadGateLeveling | PhyReadLeveling | PhyVrefdqCalibration - // Data-buffer calibration. These exist ONLY on load-reduced modules, which - // interpose a buffer between the DRAM and the host. A registered module has - // no data buffer and never executes them — that is why the discriminator - // below is structural rather than statistical. | LrdimmDataBufferMrep | LrdimmDataBufferWriteLeveling | LrdimmDataBufferReadTraining diff --git a/dag/gunbc/host_boot_supervision.dag b/dag/gunbc/host_boot_supervision.dag index 8141e3e50d3..8ad08656801 100644 --- a/dag/gunbc/host_boot_supervision.dag +++ b/dag/gunbc/host_boot_supervision.dag @@ -13,14 +13,15 @@ import product.placement_supply { HostIdentity } // Modeling them as one "watchdog" field would rebuild exactly the confusion // that cost that hour — they have different owners, different mechanisms, // different observability, and only one of them is configurable. +// OpenBMC's standard boot watchdog, a systemd unit with D-Bus properties +// (Enabled, Interval, ExpireAction). Observed INACTIVE on srv4. +// On PowerMonForceReset: +// The vendor's power monitor (power-monitor.service, /usr/bin/powermon). +// Emits "force reset on state:1" and drives obmc-host-force-warm-reboot. +// Observed ACTIVE on srv4. It is a compiled binary invoked with NO +// arguments and NO configuration file, so its interval is not readable. type HostBootSupervisor - // OpenBMC's standard boot watchdog, a systemd unit with D-Bus properties - // (Enabled, Interval, ExpireAction). Observed INACTIVE on srv4. = PhosphorWatchdogPowerOn - // The vendor's power monitor (power-monitor.service, /usr/bin/powermon). - // Emits "force reset on state:1" and drives obmc-host-force-warm-reboot. - // Observed ACTIVE on srv4. It is a compiled binary invoked with NO - // arguments and NO configuration file, so its interval is not readable. | PowerMonForceReset type SupervisorRunState @@ -58,30 +59,35 @@ type HostBootCode = HostBootCheckpoint { code: NonEmptyStr } | HostBootError { code: NonEmptyStr } +// On last_checkpoint: +// The last checkpoint reached before the failure. +// On error_code: +// The error code emitted at the failure, if the host emitted one. Absent +// means the host went silent WITHOUT an error, which is a different fault +// from an error-and-reset and must not be rendered as the same state. +// On observed_cycles: +// How many complete cycles of this signature were observed. One occurrence +// is an event; a repeating identical signature is a deterministic fault, +// and the two justify different responses. type HostBootStallSignature { host: HostIdentity - // The last checkpoint reached before the failure. last_checkpoint: NonEmptyStr - // The error code emitted at the failure, if the host emitted one. Absent - // means the host went silent WITHOUT an error, which is a different fault - // from an error-and-reset and must not be rendered as the same state. error_code: NonEmptyStr? - // How many complete cycles of this signature were observed. One occurrence - // is an event; a repeating identical signature is a deterministic fault, - // and the two justify different responses. observed_cycles: Nat observed_at: EpochMs } +// The host emitted an error and restarted itself. No supervisor involved. +// On SupervisorForcedReset: +// A supervisor's timer expired and forced the reset. +// On LoopCauseConfounded: +// The host went silent and a supervisor eventually reset it: the two are +// CONFOUNDED until one supervisor is stopped and the loop is re-observed. +// This arm exists because it was the real state of knowledge for an hour, +// and collapsing it into either neighbour would have been a guess. type HostBootLoopCause - // The host emitted an error and restarted itself. No supervisor involved. = HostSelfResetOnError { code: NonEmptyStr } - // A supervisor's timer expired and forced the reset. | SupervisorForcedReset { supervisor: HostBootSupervisor } - // The host went silent and a supervisor eventually reset it: the two are - // CONFOUNDED until one supervisor is stopped and the loop is re-observed. - // This arm exists because it was the real state of knowledge for an hour, - // and collapsing it into either neighbour would have been a guess. | LoopCauseConfounded { candidates: List } | LoopCauseUnobserved diff --git a/dag/gunbc/host_diagnostic_channel.dag b/dag/gunbc/host_diagnostic_channel.dag index c3d3dcb44ba..bf8fcf7474c 100644 --- a/dag/gunbc/host_diagnostic_channel.dag +++ b/dag/gunbc/host_diagnostic_channel.dag @@ -18,76 +18,87 @@ import product.placement_supply { HostIdentity } // and can change wording in any firmware release. Rendering all three as // "error: " throws away exactly the facts that decide whether a // diagnostic can be matched on, ranged over, or only shown to a human. +// DMTF-standardized Redfish message registries. Identifiers carry their +// own registry name AND version, e.g. "Base.1.16.0.InternalError". +// On OpenBmcPhosphorNamespace: +// OpenBMC's D-Bus error namespace, e.g. +// "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout". Structured, +// hierarchical, enumerable from the running BMC. +// On AmpereScpConsoleTokenStream: +// Ampere SCP/SMpro/PMpro console tokens (P012, E588, P029), modeled +// upstream in extdeps.ampere.scp_diagnostic. Opaque to this repository: +// no codebook has been obtained, so a token is an IDENTITY we compare and +// count, never a meaning we claim. +// On AmpereStructuredBootProgressInterface: +// The STRUCTURED boot-progress interface: status, stage and payload from +// smpro-misc. A different surface from the token stream above, and a +// strictly richer one — it localized the 2026-08-11 stall to a stage while +// the console emitted only an undecodable lexeme. Fusing the two would +// have hidden that the structured source existed at all. +// On AmpereDramReferenceCode: +// Ampere DRAM reference-code training diagnostics, e.g. +// "MC3:R0: WRLVL Err=0 Stat=0x0 NibbleMsk=0xfc0", "SK[0]: 001200ff". +// Structured per memory controller and rank, but privately defined. +// On VendorScriptFreeText: +// Free text printed by vendor shell scripts, e.g. firmware_update.sh's +// "Invalid ExtendedVersion: primary-all. Please check MANIFEST file!". +// No registry, no identifier, no stability contract. Human-readable only. type DiagnosticAuthority - // DMTF-standardized Redfish message registries. Identifiers carry their - // own registry name AND version, e.g. "Base.1.16.0.InternalError". = RedfishMessageRegistry - // OpenBMC's D-Bus error namespace, e.g. - // "xyz.openbmc_project.State.Shutdown.Power.Error.Blackout". Structured, - // hierarchical, enumerable from the running BMC. | OpenBmcPhosphorNamespace - // Ampere SCP/SMpro/PMpro console tokens (P012, E588, P029), modeled - // upstream in extdeps.ampere.scp_diagnostic. Opaque to this repository: - // no codebook has been obtained, so a token is an IDENTITY we compare and - // count, never a meaning we claim. | AmpereScpConsoleTokenStream - // The STRUCTURED boot-progress interface: status, stage and payload from - // smpro-misc. A different surface from the token stream above, and a - // strictly richer one — it localized the 2026-08-11 stall to a stage while - // the console emitted only an undecodable lexeme. Fusing the two would - // have hidden that the structured source existed at all. | AmpereStructuredBootProgressInterface - // Ampere DRAM reference-code training diagnostics, e.g. - // "MC3:R0: WRLVL Err=0 Stat=0x0 NibbleMsk=0xfc0", "SK[0]: 001200ff". - // Structured per memory controller and rank, but privately defined. | AmpereDramReferenceCode - // Free text printed by vendor shell scripts, e.g. firmware_update.sh's - // "Invalid ExtendedVersion: primary-all. Please check MANIFEST file!". - // No registry, no identifier, no stability contract. Human-readable only. | VendorScriptFreeText // HOW WAS IT READ? This axis is why the incident took as long as it did. // The convergence surface is Redfish, and Redfish could see almost none of // the diagnostics that mattered. +// Redfish over HTTPS: reachable by any authenticated convergence run. +// On BmcConsoleLogViaSsh: +// The BMC's captured host serial console (/var/log/obmc-console.log), +// reached by authenticated BMC SSH. +// On SmproSysfsViaSsh: +// SMpro/PMpro sysfs on the BMC. Note this is a TRANSPORT, not an +// authority: the same underlying data is reachable by other handlers +// where an image implements them (a correct D-Bus/Redfish projection, +// the dedicated SCP console, or Ampere's OEM IPMI register read). +// On BmcJournalViaSsh: +// The BMC's systemd journal. Requires authenticated BMC SSH. type DiagnosticTransport - // Redfish over HTTPS: reachable by any authenticated convergence run. = RedfishHttps - // The BMC's captured host serial console (/var/log/obmc-console.log), - // reached by authenticated BMC SSH. | BmcConsoleLogViaSsh - // SMpro/PMpro sysfs on the BMC. Note this is a TRANSPORT, not an - // authority: the same underlying data is reachable by other handlers - // where an image implements them (a correct D-Bus/Redfish projection, - // the dedicated SCP console, or Ampere's OEM IPMI register read). | SmproSysfsViaSsh - // The BMC's systemd journal. Requires authenticated BMC SSH. | BmcJournalViaSsh // Can a machine act on this diagnostic, or can it only be shown to a person? // The distinction is not cosmetic: it decides whether convergence may branch // on the diagnostic at all. +// Identifier resolves in a registry this repository can obtain and cite. +// On OpaqueStableIdentifier: +// Stable identity, meaning unavailable. Comparable and countable, but its +// SEMANTICS may not be asserted. E588 is here. Note what this does NOT +// claim: the E prefix places the token in a namespace, it does not make it +// an error and it does not make it fatal — E559 and E571 appear on a +// same-family machine that boots normally. That E588 is followed by a reset +// on srv3 and srv4 is an observed transition recorded in a fleet receipt, +// never a property read off the lexeme. +// On UnstructuredProse: +// No identifier at all. Matching on the prose is matching on a wording the +// vendor may change without notice. type DiagnosticDecodability - // Identifier resolves in a registry this repository can obtain and cite. = DecodableFromPublishedRegistry { registry: NonEmptyStr } - // Stable identity, meaning unavailable. Comparable and countable, but its - // SEMANTICS may not be asserted. E588 is here. Note what this does NOT - // claim: the E prefix places the token in a namespace, it does not make it - // an error and it does not make it fatal — E559 and E571 appear on a - // same-family machine that boots normally. That E588 is followed by a reset - // on srv3 and srv4 is an observed transition recorded in a fleet receipt, - // never a property read off the lexeme. | OpaqueStableIdentifier - // No identifier at all. Matching on the prose is matching on a wording the - // vendor may change without notice. | UnstructuredProse +// On raw: +// Verbatim, as emitted. Never normalized, because normalizing a code we +// cannot decode can only lose information. type HostDiagnostic { host: HostIdentity authority: DiagnosticAuthority transport: DiagnosticTransport decodability: DiagnosticDecodability - // Verbatim, as emitted. Never normalized, because normalizing a code we - // cannot decode can only lose information. raw: NonEmptyStr observed_at: EpochMs } diff --git a/dag/gunbc/host_memory_qualification.dag b/dag/gunbc/host_memory_qualification.dag index 93521d3db4a..434be066efa 100644 --- a/dag/gunbc/host_memory_qualification.dag +++ b/dag/gunbc/host_memory_qualification.dag @@ -83,12 +83,13 @@ data host_memory_qualification_note: String = "Written after the 2026-08-11 srv3 // CAUSAL EXPERIMENTS ARE RECEIPTS, NOT PROSE. Each records what was actually // done, so a later reader can re-judge the verdict against the intervention // rather than inheriting the conclusion. +// On HypothesisWeakenedByIntervention: +// The intervention was WEAKER than the one that would settle the question. +// Distinct from Refuted because the hypothesis remains live and the +// experiment is repeatable in a stronger form. type ExperimentVerdict = HypothesisSupported | HypothesisRefuted - // The intervention was WEAKER than the one that would settle the question. - // Distinct from Refuted because the hypothesis remains live and the - // experiment is repeatable in a stronger form. | HypothesisWeakenedByIntervention { performed: NonEmptyStr, required: NonEmptyStr } | ExperimentInconclusive { reason: NonEmptyStr } diff --git a/dag/gunbc/memory_part_selection.dag b/dag/gunbc/memory_part_selection.dag index e0f2cf92eb1..698abcb90a7 100644 --- a/dag/gunbc/memory_part_selection.dag +++ b/dag/gunbc/memory_part_selection.dag @@ -19,26 +19,29 @@ import gunbc.host_memory_qualification { SpecificPartQualification } // HOW WE KNOW A STEP FAILS. Not all evidence is equal, and a selection model // that flattens these would let the weakest link carry the strongest verdict. +// The platform's own syndrome register named the failing step. This is the +// strongest form and is NOT what gunbc currently holds for MREP. +// On AssociatedByIdenticalSignature: +// A vendor or third party reported the step by name alongside status words +// byte-identical to ours. Strong, but an association rather than a decode: +// it establishes that the same failure signature carries that step's name, +// not that our platform's registers were read and interpreted. +// On InferredFromSequencePosition: +// The step is inferred from position in the boot sequence only. type TrainingEvidenceStanding - // The platform's own syndrome register named the failing step. This is the - // strongest form and is NOT what gunbc currently holds for MREP. = DecodedFromPlatformSyndrome { register: NonEmptyStr } - // A vendor or third party reported the step by name alongside status words - // byte-identical to ours. Strong, but an association rather than a decode: - // it establishes that the same failure signature carries that step's name, - // not that our platform's registers were read and interpreted. | AssociatedByIdenticalSignature { signature: NonEmptyStr, source: ExternalAuthority } - // The step is inferred from position in the boot sequence only. | InferredFromSequencePosition | EvidenceUnobserved +// On observed_population: +// The module population the failure was observed with. A step failure seen +// only in a sparse population is weaker support for rejecting a part in a +// full population, so the observation carries its own conditions. type PlatformTrainingFailure { board: BaseboardModel step: MemoryTrainingStep standing: TrainingEvidenceStanding - // The module population the failure was observed with. A step failure seen - // only in a sparse population is weaker support for rejecting a part in a - // full population, so the observation carries its own conditions. observed_population: List observed_at: EpochMs } diff --git a/dag/test/claim/host_memory_qualification_witness_test.dag b/dag/test/claim/host_memory_qualification_witness_test.dag index 13abd7aef04..1f05637a910 100644 --- a/dag/test/claim/host_memory_qualification_witness_test.dag +++ b/dag/test/claim/host_memory_qualification_witness_test.dag @@ -75,6 +75,10 @@ test fn two_dimm_population_in_wrong_slots_is_refused() -> Bool { // DDR training success does not construct host boot success. The srv3 // standing carries trained memory AND a repeating reset simultaneously, and // the boot question still answers false. +// On scp_token_is_error_class: +// E588 (observed followed by reset on srv3/srv4) and E559 (observed on a +// same-family machine that continues booting) are the SAME class. Fatality +// therefore cannot be a function of the class. data srv3_post_bios_310_standing: HostMemoryStanding = HostMemoryStanding { host: "srv3", part_number: "M386A4G40DM0-CPB", @@ -129,9 +133,6 @@ test fn p_token_is_not_an_error() -> Bool { } test fn e_token_class_is_namespace_not_fatality() -> Bool { - // E588 (observed followed by reset on srv3/srv4) and E559 (observed on a - // same-family machine that continues booting) are the SAME class. Fatality - // therefore cannot be a function of the class. scp_token_is_error_class(token: AmpereScpConsoleToken { class: ScpECode, number: 588 }) && scp_token_is_error_class(token: AmpereScpConsoleToken { class: ScpECode, number: 559 }) } From e1bc9da4ffab756f286486d9fd63ef9d69112375 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:40:07 +0000 Subject: [PATCH 07/28] Carry the firmware prerequisite's evidence as data, not prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 51290 (non-blocking) on the receipt Strings. Taking the one with a live consequence rather than the whole corpus-wide class: the BIOS-tar prerequisite declares BMC >= 3.22.00 as a LITERAL while the two executed pushes that established it sat only in an unstructured String, so the release row and its evidence could not be joined and a declared minimum the evidence contradicted read exactly like one it supported. FirmwarePrerequisiteObservation now carries each executed push (board, subject, packaging, gating target and version, outcome, timestamp), with the vendor diagnostic held on the refusal arm. The two ALTRAD8UD rows are the discriminating pair: identical bytes refused at BMC 2.07.00, accepted at 3.22.00. prerequisite_evidence_verdict checks a declared minimum against them, and its positive arm is deliberately PrerequisiteConsistentUpperBound rather than a bare "consistent": two pushes BOUND a threshold, they do not locate it — 3.06.00 is in the capability catalog and was never pushed, so the declared 3.22.00 may be stricter than the vendor requires. Claiming the threshold was measured would be the stronger-than-evidence reading the arm exists to prevent. Witness: the pair supports the declared minimum as an upper bound; a minimum inflated above the accepting version is contradicted BY the acceptance; a minimum relaxed to or below the refusing version is contradicted BY the refusal (the direction that would admit an unappliable push); and an empty observation list yields Absent, never consistent. The prose row keeps only what the carrier cannot hold: that the vendor page documents no such dependency, that the cause was legible only in the BMC journal because Redfish reported Exception with none, and why the prerequisite belongs to the packaging rather than the release. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/boards/asrock_rack.dag | 36 +++++- dag/extdeps/firmware/types.dag | 117 ++++++++++++++++++ ...are_prerequisite_evidence_witness_test.dag | 77 ++++++++++++ 3 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 dag/test/claim/firmware_prerequisite_evidence_witness_test.dag diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index 94ef45d149a..df92934523b 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -19,6 +19,9 @@ import extdeps.bmc.types { } import extdeps.bmc.capability { HistoricalEvidenceSnapshot } import extdeps.firmware.types { + FirmwarePrerequisiteObservation, + PushAcceptedByTarget, + PushRefusedByTarget, BmcController, NoFirmwarePrerequisite, RequiresMinimumTargetFirmware, @@ -416,7 +419,38 @@ data asrock_altrad8ud_firmware_release_catalog: List = [ data asrock_altrad8ud_firmware_release_note: String = "Cited from the ALTRAD8UD-1L2T product download table; all rows dated 2026-04-14. The BIOS rows are the load-bearing ones for the 2026-08-11 fleet incident: 'Updated RC to 0ACRB_021' is the MEMORY REFERENCE CODE, and srv3 fails DDR training on Samsung M386A4G40DM0-CPB 32GB 4Rx4 LRDIMMs while running DRAM FW 220414 (2022) under BL1 built 2023-05-17, emitting per-controller WRLVL errors and 'DDR initialization failure. Can't fall back!' on all eight controllers. ONE RELEASE, TWO PACKAGINGS, and they are not interchangeable: the ROM zip carries a bare 33554432-byte ALTRAD8UD-1L2T_3.10.ROM applied from BIOS setup by Instant Flash, which is unreachable on a host that cannot POST; the TAR zip carries obmc-bios-altrad8ud-1l2t-3.10.all.tar, whose MANIFEST declares purpose=...VersionPurpose.Host, version=3.10, MachineName=altrad8ud-1l2t, Type=bios, and which is what the BMC UpdateService consumes out-of-band. Every byte count and digest here was MEASURED on 2026-08-11 by acquiring the artifact and hashing it, never transcribed: the ROM zip at 5359089 bytes, the TAR zip at 5359341, the inner push tar at 33566720. The TAR locator was not published on the product page — it was found by probing the vendor download host and confirmed by HTTP 200 plus successful extraction, so it is weaker provenance than a page-published link and should be re-derived if the vendor reorganizes. The BMC 03.22.00 row remains digest-Absent with an unobserved locator: its image_bytes is the page's stated 27.71MB converted, NOT a Content-Length, so it must not be treated as a fetchable artifact until its own locator is observed." -data asrock_altrad8ud_bios_openbmc_prerequisite_receipt: String = "The BIOS 3.10 OpenBMC-tar prerequisite (BMC >= 3.22.00) is an EXECUTED observation, not an inference from the vendor page, which states no such dependency. On 2026-08-11 the identical 33566720-byte obmc-bios-altrad8ud-1l2t-3.10.all.tar was pushed to two hosts of this board model. srv3 (BMC 2.07.00) accepted the upload, untarred it, and refused at the vendor flash script: 'Invalid ExtendedVersion: primary-all. Please check MANIFEST file!', with phosphor-image-updater logging 'Bios upgrade failed.' and Redfish reporting TaskState=Exception carrying NO machine-readable cause — the cause was only readable in the BMC journal over authenticated SSH. srv4 (BMC 3.22.00) accepted the same bytes. The minimum is therefore witnessed at 3.22.00 as an upper bound on the true threshold: versions between 2.07.00 and 3.22.00 (3.06.00 exists in the capability catalog) are UNTESTED, so the row may be stricter than the vendor requirement, which is the safe direction. The Instant Flash ROM packaging carries NoFirmwarePrerequisite because it never passes through the BMC at all; that asymmetry is why the prerequisite is a property of the PACKAGING and not of the release." +// THE EXECUTED PAIR that establishes the BIOS-tar prerequisite. Same 33566720 +// bytes, two hosts of this board model, opposite outcomes — which is what makes +// the minimum evidence rather than an inference from the vendor page (which +// states no such dependency at all). +data asrock_altrad8ud_bios_310_push_refused_at_bmc_207: FirmwarePrerequisiteObservation = FirmwarePrerequisiteObservation { + board: AsrockRackAltrad8ud1l2t, + subject: HostBios, + image_kind: OpenBmcWebUiTar, + gating_target: BmcController, + gating_version: firmware_semantic_version(major: 2, minor: 7, patch: 0), + outcome: PushRefusedByTarget { + diagnostic: "Invalid ExtendedVersion: primary-all. Please check MANIFEST file!", + }, + observed_at: 1786579200000, +} + +data asrock_altrad8ud_bios_310_push_accepted_at_bmc_322: FirmwarePrerequisiteObservation = FirmwarePrerequisiteObservation { + board: AsrockRackAltrad8ud1l2t, + subject: HostBios, + image_kind: OpenBmcWebUiTar, + gating_target: BmcController, + gating_version: firmware_semantic_version(major: 3, minor: 22, patch: 0), + outcome: PushAcceptedByTarget, + observed_at: 1786582800000, +} + +data asrock_altrad8ud_bios_openbmc_prerequisite_evidence: List = [ + asrock_altrad8ud_bios_310_push_refused_at_bmc_207, + asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, +] + +data asrock_altrad8ud_bios_openbmc_prerequisite_receipt: String = "Irreducible rationale only; the executed pair itself is carried as FirmwarePrerequisiteObservation rows above, and prerequisite_evidence_verdict checks the declared minimum against them. What those rows cannot express: the vendor product page states NO such dependency, so the minimum is an executed finding rather than a documented one; the refusal on srv3 arrived only AFTER a 33 MB upload and a host power-off, and Redfish reported TaskState=Exception with no machine-readable cause, so the vendor diagnostic in the refusal row was readable only from the BMC journal over authenticated SSH; and the Instant Flash ROM packaging carries NoFirmwarePrerequisite because it never passes through the BMC at all, which is why the prerequisite belongs to the PACKAGING rather than to the release." // The DOCUMENTED DIMM populations. A population absent from this list is an // unsupported configuration whose boot failure proves nothing about the diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index a47f2dfe0bf..dad3ad4ad99 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -115,6 +115,123 @@ type FirmwareApplicabilityVerdict minimum: FirmwareSemanticVersion, } +// WHAT ESTABLISHED THE PREREQUISITE, as a carrier rather than as prose. +// A minimum-version row is a claim; the thing that makes it true is a pair of +// executed pushes with opposite outcomes. Holding that only in a String left +// the release row and its evidence unjoinable, so a declared minimum that the +// evidence contradicted would read exactly like one it supported. +type FirmwarePushOutcome + = PushAcceptedByTarget + | PushRefusedByTarget { diagnostic: NonEmptyStr } + +// On gating_target: +// The prerequisite is a property of the PACKAGING, not of the release: the +// same version applied as an Instant Flash ROM never passes through the BMC +// and so has no gating target at all. image_kind is carried for that reason. +type FirmwarePrerequisiteObservation { + board: BaseboardModel + subject: FirmwareTarget + image_kind: FirmwareImageKind + gating_target: FirmwareTarget + gating_version: FirmwareSemanticVersion + outcome: FirmwarePushOutcome + observed_at: EpochMs +} + +// Two executed pushes BOUND a threshold; they do not locate it. Refused at +// 2.07.00 and accepted at 3.22.00 means the true minimum lies somewhere in +// that interval, so a declared 3.22.00 is an upper bound and may be stricter +// than the vendor requires — the safe direction, and the positive arm says so +// rather than claiming the threshold was measured. +type PrerequisiteEvidenceVerdict + = PrerequisiteConsistentUpperBound { + highest_refusal: FirmwareSemanticVersion + accepted_at: FirmwareSemanticVersion + } + | PrerequisiteContradictedByAcceptanceBelowMinimum { + accepted_at: FirmwareSemanticVersion + } + | PrerequisiteContradictedByRefusalAtOrAboveMinimum { + refused_at: FirmwareSemanticVersion + } + | PrerequisiteEvidenceAbsent + +fn prerequisite_observation_contradicts( + minimum: FirmwareSemanticVersion, + observation: FirmwarePrerequisiteObservation, +) -> PrerequisiteEvidenceVerdict { + match observation.outcome { + PushAcceptedByTarget => + if firmware_semantic_version_at_least( + observed: observation.gating_version, + minimum: minimum, + ) { + PrerequisiteEvidenceAbsent + } else { + PrerequisiteContradictedByAcceptanceBelowMinimum { + accepted_at: observation.gating_version, + } + } + PushRefusedByTarget { diagnostic: _ } => + if firmware_semantic_version_at_least( + observed: observation.gating_version, + minimum: minimum, + ) { + PrerequisiteContradictedByRefusalAtOrAboveMinimum { + refused_at: observation.gating_version, + } + } else { + PrerequisiteEvidenceAbsent + } + } +} + +fn prerequisite_evidence_verdict( + minimum: FirmwareSemanticVersion, + observations: List, +) -> PrerequisiteEvidenceVerdict { + fold( + observations, + init: PrerequisiteEvidenceAbsent, + f: (acc, observation) => + match acc { + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } => + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } => + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } + PrerequisiteConsistentUpperBound { highest_refusal: h, accepted_at: a } => + match prerequisite_observation_contradicts( + minimum: minimum, + observation: observation, + ) { + PrerequisiteEvidenceAbsent => + PrerequisiteConsistentUpperBound { highest_refusal: h, accepted_at: a } + other => other + } + PrerequisiteEvidenceAbsent => + match prerequisite_observation_contradicts( + minimum: minimum, + observation: observation, + ) { + PrerequisiteEvidenceAbsent => + match observation.outcome { + PushAcceptedByTarget => + PrerequisiteConsistentUpperBound { + highest_refusal: minimum, + accepted_at: observation.gating_version, + } + PushRefusedByTarget { diagnostic: _ } => + PrerequisiteConsistentUpperBound { + highest_refusal: observation.gating_version, + accepted_at: minimum, + } + } + other => other + } + }, + ) +} + fn firmware_semantic_version_at_least( observed: FirmwareSemanticVersion, minimum: FirmwareSemanticVersion, diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag new file mode 100644 index 00000000000..07bbafa831d --- /dev/null +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -0,0 +1,77 @@ +module test.claim.firmware_prerequisite_evidence_witness_test + +import std.types { Bool } +import extdeps.firmware.types { + FirmwarePrerequisiteObservation, + FirmwareSemanticVersion, + PrerequisiteConsistentUpperBound, + PrerequisiteContradictedByAcceptanceBelowMinimum, + PrerequisiteContradictedByRefusalAtOrAboveMinimum, + PrerequisiteEvidenceAbsent, + firmware_semantic_version, + prerequisite_evidence_verdict, +} +import extdeps.boards.asrock_rack { + asrock_altrad8ud_bios_openbmc_prerequisite_evidence, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// The declared minimum on the BIOS 3.10 OpenBMC-tar release row. Named here so +// the witness compares the SAME number the catalog declares. +data declared_minimum: FirmwareSemanticVersion = firmware_semantic_version(major: 3, minor: 22, patch: 0) + +// The executed pair supports the declared minimum, and the positive arm says +// what it actually establishes: an UPPER BOUND. 3.06.00 exists in the +// capability catalog and was never pushed, so the true threshold may be lower. +test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool { + match prerequisite_evidence_verdict( + minimum: declared_minimum, + observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, + ) { + PrerequisiteConsistentUpperBound { highest_refusal: r, accepted_at: a } => + r.major == 2 && r.minor == 7 && a.major == 3 && a.minor == 22 + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: _ } => false + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: _ } => false + PrerequisiteEvidenceAbsent => false + } +} + +// RED control. Declaring a minimum ABOVE the version that demonstrably +// accepted the bytes is contradicted by the acceptance, not merely unsupported +// — this is the arm that catches a minimum inflated past its evidence. +test fn a_minimum_above_the_accepting_version_is_contradicted() -> Bool { + match prerequisite_evidence_verdict( + minimum: firmware_semantic_version(major: 4, minor: 0, patch: 0), + observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, + ) { + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } => + a.major == 3 && a.minor == 22 + _ => false + } +} + +// RED control, the other direction. Declaring a minimum at or below the +// version that was REFUSED is contradicted by the refusal — this catches a +// minimum relaxed below its evidence, which is the direction that would let an +// unappliable push be admitted. +test fn a_minimum_at_or_below_the_refusing_version_is_contradicted() -> Bool { + match prerequisite_evidence_verdict( + minimum: firmware_semantic_version(major: 2, minor: 0, patch: 0), + observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, + ) { + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } => + r.major == 2 && r.minor == 7 + _ => false + } +} + +// With no observations the verdict is ABSENT, never consistent. An unevidenced +// minimum must not read the same as an evidenced one. +test fn no_observations_yields_absent_not_consistent() -> Bool { + match prerequisite_evidence_verdict(minimum: declared_minimum, observations: []) { + PrerequisiteEvidenceAbsent => true + _ => false + } +} From 97669f63f6ac71d8223666e6f43e8ebfd2dc9dcb Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:43:57 +0000 Subject: [PATCH 08/28] Execute the prerequisite gate and the loop-cause fold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 51292, both findings verified: firmware_release_applicability and host_boot_loop_cause had zero importing consumers and zero witnesses, so the PR's two central fail-closed claims were specification without execution — the exact bar this PR's own prose sets (DESIGN section 5). The applicability rows bind the PRODUCTION release rows rather than fixtures, so the gate runs against the prerequisite the catalog actually declares: - unreadable target version refuses (the srv3 BIOS-null case that justified the third arm); - the srv3 push at BMC 2.07.00 is Unmet and the verdict carries what it observed; - the srv4 push at 3.22.00 is Applicable (positive control); - 3.06.00 is Unmet — higher major, lower minor, so a major-only compare would wrongly admit it, and that version is a real catalogued image rather than an invented one; - the Instant Flash ROM row is Applicable on the SAME Absent observation that refuses on the tar row. Same input, opposite verdict, because the prerequisite belongs to the packaging. For the loop cause the discriminating pair is the intervention, not the signature: one E588 signature answers HostSelfResetOnError after the supervisor was stopped and the loop persisted, and CONFOUNDED with the identical signature when it was not. That is the state the incident sat in while the watchdog theory looked plausible, and it is why period alone cannot decide — a 306-second cycle looks the same whether or not a 306-second timeout causes it. A third row keeps a clean stop with no error code at Unobserved rather than inferring a supervisor verdict from silence. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- ...licability_and_loop_cause_witness_test.dag | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag diff --git a/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag new file mode 100644 index 00000000000..56bcc7419e0 --- /dev/null +++ b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag @@ -0,0 +1,166 @@ +module test.claim.firmware_applicability_and_loop_cause_witness_test + +import std.types { Bool, Present, Absent } +import extdeps.firmware.types { + BmcController, + FirmwareApplicable, + FirmwarePrerequisiteUnmet, + FirmwarePrerequisiteUnobserved, + firmware_release_applicability, + firmware_semantic_version, +} +import extdeps.boards.asrock_rack { + asrock_altrad8ud_bios_3_10_instant_flash_release, + asrock_altrad8ud_bios_3_10_openbmc_release, +} +import gunbc.host_boot_supervision { + HostBootStallSignature, + HostSelfResetOnError, + LoopCauseConfounded, + LoopCauseUnobserved, + SupervisorForcedReset, + host_boot_loop_cause, +} +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// These bind the PRODUCTION release rows, not fixtures, so the gate is +// exercised against the prerequisite the catalog actually declares. + +// THE FAIL-CLOSED ARM, and the case that justified it: srv3 reports its BIOS +// version as null through Redfish. "We could not read it" must refuse rather +// than collapse into either pole. +test fn unreadable_target_version_refuses_rather_than_assuming() -> Bool { + match firmware_release_applicability( + prerequisite: asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite, + observed: Absent, + ) { + FirmwarePrerequisiteUnobserved { target: t, minimum: m } => + t == BmcController && m.major == 3 && m.minor == 22 + FirmwareApplicable => false + FirmwarePrerequisiteUnmet { target: _, minimum: _, observed: _ } => false + } +} + +// The srv3 push that actually happened: BMC 2.07.00 against a row requiring +// 3.22.00. Unmet, and the verdict carries the observed version so the refusal +// can say what it saw rather than only that it refused. +test fn the_srv3_push_is_refused_by_the_catalog() -> Bool { + match firmware_release_applicability( + prerequisite: asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite, + observed: Present { + value: firmware_semantic_version(major: 2, minor: 7, patch: 0), + }, + ) { + FirmwarePrerequisiteUnmet { target: _, minimum: _, observed: o } => + o.major == 2 && o.minor == 7 + _ => false + } +} + +// Positive control: the srv4 push, same bytes, same row, accepted. +test fn the_srv4_push_is_admitted_by_the_catalog() -> Bool { + match firmware_release_applicability( + prerequisite: asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite, + observed: Present { + value: firmware_semantic_version(major: 3, minor: 22, patch: 0), + }, + ) { + FirmwareApplicable => true + _ => false + } +} + +// RED control on the COMPARISON, not on the arms. 3.06.00 exceeds the minimum +// on major and fails on minor; a naive major-only compare would admit it. The +// version is also a real catalogued image for this board, so this is a push +// someone could actually attempt. +test fn a_higher_major_with_lower_minor_is_still_unmet() -> Bool { + match firmware_release_applicability( + prerequisite: asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite, + observed: Present { + value: firmware_semantic_version(major: 3, minor: 6, patch: 0), + }, + ) { + FirmwarePrerequisiteUnmet { target: _, minimum: _, observed: o } => + o.major == 3 && o.minor == 6 + _ => false + } +} + +// The packaging asymmetry, exercised rather than asserted: the Instant Flash +// ROM never passes through the BMC, so it is applicable even when the BMC +// version is unreadable — the exact input that refuses on the tar row above. +// Same observation, opposite verdict, because the packaging differs. +test fn the_rom_packaging_is_applicable_with_no_readable_bmc_version() -> Bool { + match firmware_release_applicability( + prerequisite: asrock_altrad8ud_bios_3_10_instant_flash_release.prerequisite, + observed: Absent, + ) { + FirmwareApplicable => true + _ => false + } +} + +data srv4_e588_signature: HostBootStallSignature = HostBootStallSignature { + host: "srv4", + last_checkpoint: "P012", + error_code: Present { value: "E588" }, + observed_cycles: 6, + observed_at: 1786580000000, +} + +// THE POWERMON REFUTATION, as an executing check rather than a prose receipt. +// power-monitor.service was stopped on srv4 and the loop PERSISTED, which is +// what moves the cause to the host itself. Note the shape of the argument: the +// loop's PERIOD never decides this — a 306-second cycle looks identical whether +// or not a 306-second timeout causes it. Only the intervention discriminates. +test fn loop_persisting_after_supervisor_stop_is_host_self_reset() -> Bool { + match host_boot_loop_cause( + signature: srv4_e588_signature, + supervisor_stopped: true, + looped_after_stop: true, + ) { + HostSelfResetOnError { code: c } => c == "E588" + _ => false + } +} + +// RED control: the SAME signature with the supervisor still running is +// CONFOUNDED, not attributed. This is the state the incident was in for hours +// while the watchdog theory looked plausible, and the function must not +// resolve it — the discriminating fact is the intervention, not the signature. +test fn the_same_signature_without_the_intervention_is_confounded() -> Bool { + match host_boot_loop_cause( + signature: srv4_e588_signature, + supervisor_stopped: false, + looped_after_stop: false, + ) { + LoopCauseConfounded { candidates: _ } => true + HostSelfResetOnError { code: _ } => false + SupervisorForcedReset { supervisor: _ } => false + LoopCauseUnobserved => false + } +} + +// Stopping the supervisor and observing the loop STOP does not attribute the +// cause either — it removes one candidate, and with no error code there is +// nothing left to attribute to, so the answer is Unobserved rather than a +// supervisor verdict inferred from silence. +test fn no_error_code_after_a_clean_stop_is_unobserved_not_attributed() -> Bool { + match host_boot_loop_cause( + signature: HostBootStallSignature { + host: "srv4", + last_checkpoint: "P012", + error_code: Absent, + observed_cycles: 3, + observed_at: 1786580000000, + }, + supervisor_stopped: true, + looped_after_stop: true, + ) { + LoopCauseUnobserved => true + _ => false + } +} From 7829ac827075444429d5861c0670a22a60d1b16a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:53:54 +0000 Subject: [PATCH 09/28] Keep observations immutable; split inventory from image; refuse duplicate keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four findings, all verified against the code before acting. IMMUTABLE OBSERVATIONS (mine, introduced by this PR). Editing the single srv3 firmware row in place from 2.07.00 to 3.22.00 moved every receipt that referenced it, so srv3's JULY update-service snapshot began claiming it had been observed under firmware installed in AUGUST — the same temporal-authority defect this PR set out to fix, relocated one field sideways. The 2.07.00 and 3.22.00 observations are now separate immutable rows; srv3_bmc_firmware_observation is the CURRENT standing and advances by re-binding, not mutation; the update-service receipt binds the 2.07.00 row it was actually taken under. A 3.22.00 update-service receipt will land as a new row when one is measured, not by re-pointing this one. INVENTORY IS NOT AN IMAGE FACT. chassis_sensor_collection_member_count sat inside the image-derived shape. Two hosts returning nine members at one moment establishes agreement at those observations; it does not make cardinality invariant under fan, FRU, or sensor-service changes — and this same session measured installed fan sets DIFFERING under identical firmware, which is the same nearby-question mistake one axis over. The count is dropped from the shape rather than being derived from something that does not decide it. Route and resource availability stay, since those are compiled into bmcweb. DUPLICATE KEYS REFUSE. bmc_redfish_surface_for took the first match, so two rows for one version resolved by list position — a silent order-dependent answer where the catalog actually contradicts itself. It now answers SurfaceShapeAmbiguous with the collision count, with a RED control that plants the duplicate. SECOND, NOT NAT (review 51298, verified). SupervisorIntervalKnowledge carried seconds on bare Nat while std.measure Second is the authority used for the same concept elsewhere. The declared/measured epistemic split is unchanged; only the carrier moves. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/bmc/types.dag | 38 +++++++++++-------- dag/extdeps/boards/asrock_rack.dag | 2 - dag/gunbc/fleet_bmc_observation.dag | 35 ++++++++++++++--- dag/gunbc/host_boot_supervision.dag | 5 ++- .../bmc_firmware_thermal_witness_test.dag | 25 +++++++++++- 5 files changed, 77 insertions(+), 28 deletions(-) diff --git a/dag/extdeps/bmc/types.dag b/dag/extdeps/bmc/types.dag index 391c905c20a..aab1eafa666 100644 --- a/dag/extdeps/bmc/types.dag +++ b/dag/extdeps/bmc/types.dag @@ -19,7 +19,7 @@ import std.measure { percent_count, } import std.nat { Nat } -import std.types { Bool, EpochMs, HttpStatus, Int, List, NonEmptyStr } +import std.types { Bool, EpochMs, HttpStatus, Int, List, NonEmptyStr, list_length } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { uri: Uri { @@ -142,7 +142,6 @@ type RedfishFanControlSurface // second authority to disagree with. type BmcRedfishSurfaceShape { thermal_surface: RedfishThermalTelemetrySurface - chassis_sensor_collection_member_count: Nat fan_control_surface: RedfishFanControlSurface } @@ -160,25 +159,32 @@ type BmcRedfishSurfaceRow { type BmcRedfishSurfaceDerivation = SurfaceShapeKnown { shape: BmcRedfishSurfaceShape } | SurfaceShapeUncatalogued { firmware: BmcFirmwareVersion } + | SurfaceShapeAmbiguous { firmware: BmcFirmwareVersion, matches: Nat } + +fn bmc_redfish_surface_matches( + catalog: List, + firmware: BmcFirmwareVersion, +) -> List { + filter(catalog, row => row.firmware == firmware) +} fn bmc_redfish_surface_for( catalog: List, firmware: BmcFirmwareVersion, ) -> BmcRedfishSurfaceDerivation { - fold( - catalog, - init: SurfaceShapeUncatalogued { firmware: firmware }, - f: (acc, row) => - match acc { - SurfaceShapeKnown { shape: s } => SurfaceShapeKnown { shape: s } - SurfaceShapeUncatalogued { firmware: f } => - if row.firmware == f { - SurfaceShapeKnown { shape: row.shape } - } else { - SurfaceShapeUncatalogued { firmware: f } - } - }, - ) + let matches = bmc_redfish_surface_matches(catalog: catalog, firmware: firmware) + let count = list_length(matches) + if count == 0 { + SurfaceShapeUncatalogued { firmware: firmware } + } else if count == 1 { + fold( + matches, + init: SurfaceShapeUncatalogued { firmware: firmware }, + f: (acc, row) => SurfaceShapeKnown { shape: row.shape }, + ) + } else { + SurfaceShapeAmbiguous { firmware: firmware, matches: count } + } } // The per-host projection. It takes the host's firmware OBSERVATION and the diff --git a/dag/extdeps/boards/asrock_rack.dag b/dag/extdeps/boards/asrock_rack.dag index df92934523b..0315be4f438 100644 --- a/dag/extdeps/boards/asrock_rack.dag +++ b/dag/extdeps/boards/asrock_rack.dag @@ -131,7 +131,6 @@ data asrock_altrad8ud_openbmc_2_07_00_redfish_surface: BmcRedfishSurfaceRow = Bm firmware: asrock_altrad8ud_openbmc_2_07_00_capabilities.firmware, shape: BmcRedfishSurfaceShape { thermal_surface: RedfishThermalSubsystemSensorCollection, - chassis_sensor_collection_member_count: 51, fan_control_surface: RedfishFanControlResourceAbsent, }, } @@ -140,7 +139,6 @@ data asrock_altrad8ud_openbmc_3_22_00_redfish_surface: BmcRedfishSurfaceRow = Bm firmware: asrock_altrad8ud_openbmc_3_22_00_capabilities.firmware, shape: BmcRedfishSurfaceShape { thermal_surface: RedfishLegacyChassisThermal, - chassis_sensor_collection_member_count: 9, fan_control_surface: RedfishInitialDutyControlResource, }, } diff --git a/dag/gunbc/fleet_bmc_observation.dag b/dag/gunbc/fleet_bmc_observation.dag index 3e84f95cbb1..dff4e9d3f01 100644 --- a/dag/gunbc/fleet_bmc_observation.dag +++ b/dag/gunbc/fleet_bmc_observation.dag @@ -126,11 +126,27 @@ type BmcFanRebootPersistenceReceipt { observed_at: EpochMs } -// Re-observed 2026-08-12 after the 2026-08-11 flash: VERSION_ID=3.22.00 read -// from /etc/os-release over authenticated BMC SSH. The prior row still said -// 2.07.00, which is the observation this change stops being able to leave -// behind — the surface below is now derived from THIS field. -data srv3_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservation { +// AN OBSERVATION IS IMMUTABLE; only which one is CURRENT advances. An earlier +// revision of this change edited the single srv3 row in place from 2.07.00 to +// 3.22.00, which moved every receipt that referenced it — so the July +// update-service snapshot silently began claiming it had been taken under +// firmware that was installed in August. That is the same temporal-authority +// defect this change set out to fix, relocated one field sideways: a receipt +// must keep the firmware identity it was actually observed under, and a later +// flash may not rewrite the past by reference. +data srv3_bmc_firmware_2_07_00_observation: BmcFirmwareObservation = BmcFirmwareObservation { + endpoint: srv3_baseboard.bmc, + firmware: BmcFirmwareVersion { + family: OpenBmc, + version: firmware_semantic_version(major: 2, minor: 7, patch: 0), + }, + evidence_scope: HistoricalEvidenceSnapshot, + observed_at: 1785110400000, +} + +// Observed 2026-08-12 after the 2026-08-11 flash: VERSION_ID=3.22.00 read from +// /etc/os-release over authenticated BMC SSH. +data srv3_bmc_firmware_3_22_00_observation: BmcFirmwareObservation = BmcFirmwareObservation { endpoint: srv3_baseboard.bmc, firmware: BmcFirmwareVersion { family: OpenBmc, @@ -140,6 +156,10 @@ data srv3_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservat observed_at: 1786843200000, } +// The CURRENT standing. This is the only row that advances on a flash, and it +// advances by re-binding rather than by mutation, so nothing historical moves. +data srv3_bmc_firmware_observation: BmcFirmwareObservation = srv3_bmc_firmware_3_22_00_observation + data srv4_bmc_firmware_observation: BmcFirmwareObservation = BmcFirmwareObservation { endpoint: srv4_baseboard.bmc, firmware: BmcFirmwareVersion { @@ -165,8 +185,11 @@ data srv4_bmc_redfish_surface: BmcRedfishSurfaceDerivation = bmc_redfish_surface observation: srv4_bmc_firmware_observation, ) +// Bound to the 2.07.00 observation it was actually taken under. No 3.22.00 +// update-service receipt exists for srv3 yet; when one is measured it lands as +// a NEW row rather than by re-pointing this one. data srv3_bmc_update_service_observation: BmcUpdateServiceObservation = BmcUpdateServiceObservation { - firmware: srv3_bmc_firmware_observation, + firmware: srv3_bmc_firmware_2_07_00_observation, http_push_uri: "/redfish/v1/UpdateService/update", multipart_http_push_uri: "/redfish/v1/UpdateService/update", active_bmc_image_id: "c1789ba5", diff --git a/dag/gunbc/host_boot_supervision.dag b/dag/gunbc/host_boot_supervision.dag index 8ad08656801..00e38b63ef4 100644 --- a/dag/gunbc/host_boot_supervision.dag +++ b/dag/gunbc/host_boot_supervision.dag @@ -2,6 +2,7 @@ module gunbc.host_boot_supervision import std.types { Bool, EpochMs, List, NonEmptyStr } import std.nat { Nat } +import std.measure { Second } import product.placement_supply { HostIdentity } // TWO INDEPENDENT SUPERVISORS CAN RESET A HOST, and the fleet modeled @@ -37,8 +38,8 @@ type SupervisorRunState // can be changed. Storing 306 in a field named "interval" would present a // measurement as a setting, and the next reader would try to edit it. type SupervisorIntervalKnowledge - = IntervalDeclared { seconds: Nat } - | IntervalMeasured { seconds: Nat, samples: Nat, observed_at: EpochMs } + = IntervalDeclared { interval: Second } + | IntervalMeasured { interval: Second, samples: Nat, observed_at: EpochMs } | IntervalUnknown type HostBootSupervisorObservation { diff --git a/dag/test/claim/bmc_firmware_thermal_witness_test.dag b/dag/test/claim/bmc_firmware_thermal_witness_test.dag index 41d557111c5..4e11f29f24b 100644 --- a/dag/test/claim/bmc_firmware_thermal_witness_test.dag +++ b/dag/test/claim/bmc_firmware_thermal_witness_test.dag @@ -12,6 +12,7 @@ import extdeps.bmc.capability { } import extdeps.bmc.types { BmcFirmwareVersion, + SurfaceShapeAmbiguous, SurfaceShapeKnown, SurfaceShapeUncatalogued, bmc_redfish_surface_for, @@ -38,6 +39,7 @@ import extdeps.bmc.types { } import extdeps.boards.asrock_rack { asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + asrock_altrad8ud_openbmc_3_22_00_redfish_surface, asrock_altrad8ud_openbmc_2_07_00_capabilities, asrock_altrad8ud_openbmc_3_06_00_capabilities, asrock_altrad8ud_openbmc_2_07_00_fan_policy_observation, @@ -331,11 +333,12 @@ test fn hosts_on_the_same_image_derive_the_same_surface() -> Bool { SurfaceShapeKnown { shape: s4 } => s3 == s4 && s3.thermal_surface == RedfishLegacyChassisThermal - && s3.chassis_sensor_collection_member_count == 9 && s3.fan_control_surface == RedfishInitialDutyControlResource SurfaceShapeUncatalogued { firmware: _ } => false + SurfaceShapeAmbiguous { firmware: _, matches: _ } => false } SurfaceShapeUncatalogued { firmware: _ } => false + SurfaceShapeAmbiguous { firmware: _, matches: _ } => false } } @@ -350,9 +353,9 @@ test fn a_different_image_derives_a_different_surface() -> Bool { ) { SurfaceShapeKnown { shape: s } => s.thermal_surface == RedfishThermalSubsystemSensorCollection - && s.chassis_sensor_collection_member_count == 51 && s.fan_control_surface == RedfishFanControlResourceAbsent SurfaceShapeUncatalogued { firmware: _ } => false + SurfaceShapeAmbiguous { firmware: _, matches: _ } => false } } @@ -369,6 +372,24 @@ test fn an_uncatalogued_image_refuses_to_derive_a_surface() -> Bool { ) { SurfaceShapeUncatalogued { firmware: _ } => true SurfaceShapeKnown { shape: _ } => false + SurfaceShapeAmbiguous { firmware: _, matches: _ } => false + } +} + +// RED control on RESOLUTION ORDER. Two rows for one version is a +// representable catalog state, and taking the first match would resolve it by +// list position — a silent, order-dependent answer where the catalog actually +// contradicts itself. It refuses instead, and reports how many rows collided. +test fn a_duplicate_catalog_key_refuses_rather_than_taking_the_first() -> Bool { + match bmc_redfish_surface_for( + catalog: [ + asrock_altrad8ud_openbmc_3_22_00_redfish_surface, + asrock_altrad8ud_openbmc_3_22_00_redfish_surface, + ], + firmware: asrock_altrad8ud_openbmc_3_22_00_capabilities.firmware, + ) { + SurfaceShapeAmbiguous { firmware: _, matches: n } => n == 2 + _ => false } } From c6383d542d4199099f1ed5146f6195249e834c14 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 03:56:25 +0000 Subject: [PATCH 10/28] Grade the memory refusal by evidence and scope it to the board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule refused any module that merely EXECUTES a step some platform was observed to fail — on any board, at any evidence standing, including EvidenceUnobserved. Because capacities near a generation's die-density ceiling are disproportionately load-reduced, that removed most of the 64GB DDR4 market by construction: the blocklist this module's own prose claimed to avoid, reached by a different route, and the opposite of letting a buyer shop the whole market instead of an AVL. Severity is now a function of evidence, which is what the standing type was carrying all along and the verdict was discarding: - DecodedFromPlatformSyndrome refuses; - AssociatedByIdenticalSignature and InferredFromSequencePosition require a SAMPLE and stay on the Pareto frontier; - EvidenceUnobserved implicates nothing. gunbc holds MREP at association standing, so the honest verdict for another ALTRAD8UD LRDIMM is buy-one-and-test. Failures are also scoped to the board observed, so an ALTRAD8UD receipt cannot speak for a different platform. Witnesses discriminate on evidence with the board, step and module held fixed: association -> sample-required, decoded -> refused, unobserved -> nothing, registered module untouched at any standing. Two frontier rows show the market is not shrunk by an association (both candidates remain, the load-reduced one flagged) while a decoded refusal still drops one. KNOWN GAP, declared rather than hidden: BaseboardModel has exactly one variant today, so board scoping is real in the code but has no executing cross-board control. Minting a second board purely to test it would be a fabricated entity; the control lands when a second real board is modeled. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/gunbc/memory_part_selection.dag | 87 +++++++++-- .../memory_part_selection_witness_test.dag | 137 ++++++++++++++---- 2 files changed, 186 insertions(+), 38 deletions(-) diff --git a/dag/gunbc/memory_part_selection.dag b/dag/gunbc/memory_part_selection.dag index 698abcb90a7..28fad95789e 100644 --- a/dag/gunbc/memory_part_selection.dag +++ b/dag/gunbc/memory_part_selection.dag @@ -46,23 +46,55 @@ type PlatformTrainingFailure { observed_at: EpochMs } -// A candidate part is IMPLICATED when it must execute a step this platform has -// been observed to fail. This is the whole selection rule, and note what it is -// not: there is no list of forbidden part numbers, no vendor preference, and no -// appeal to a qualification list. Those are separate axes handled below. +// WHAT AN OBSERVED STEP FAILURE LICENSES, graded by evidence and scoped to +// the platform it was seen on. An earlier revision of this module refused any +// module that merely EXECUTES a step some platform was observed to fail, on +// any board, at any evidence standing — including EvidenceUnobserved. That is +// the blocklist this module's own prose claims to avoid, reached by a +// different route: since capacities near the generation's density ceiling are +// disproportionately load-reduced, a blanket MREP refusal removes most of the +// 64GB DDR4 market by construction, which is the opposite of shopping it. +// +// Two facts do the grading. A failure on ANOTHER board says nothing about this +// one. And an association carrying byte-identical status words is strong +// enough to demand a sample before committing to eight modules, but not strong +// enough to declare a part incompatible sight-unseen — only a decode of this +// platform's own syndrome register is. type PartImplication = PartNotImplicated + | PartSampleQualificationRequired { + step: MemoryTrainingStep + standing: TrainingEvidenceStanding + } | PartImplicatedByFailingStep { step: MemoryTrainingStep standing: TrainingEvidenceStanding } +fn implication_severity( + step: MemoryTrainingStep, + standing: TrainingEvidenceStanding, +) -> PartImplication { + match standing { + DecodedFromPlatformSyndrome { register: _ } => + PartImplicatedByFailingStep { step: step, standing: standing } + AssociatedByIdenticalSignature { signature: _, source: _ } => + PartSampleQualificationRequired { step: step, standing: standing } + InferredFromSequencePosition => + PartSampleQualificationRequired { step: step, standing: standing } + EvidenceUnobserved => PartNotImplicated + } +} + fn implication_for_step( candidate: DramModuleCatalogRow, failure: PlatformTrainingFailure, + target_board: BaseboardModel, ) -> PartImplication { - if module_executes_step(module: candidate, step: failure.step) { - PartImplicatedByFailingStep { step: failure.step, standing: failure.standing } + if failure.board != target_board { + PartNotImplicated + } else if module_executes_step(module: candidate, step: failure.step) { + implication_severity(step: failure.step, standing: failure.standing) } else { PartNotImplicated } @@ -71,6 +103,7 @@ fn implication_for_step( fn part_implication( candidate: DramModuleCatalogRow, failures: List, + target_board: BaseboardModel, ) -> PartImplication { fold( failures, @@ -79,7 +112,22 @@ fn part_implication( match acc { PartImplicatedByFailingStep { step: s, standing: st } => PartImplicatedByFailingStep { step: s, standing: st } - PartNotImplicated => implication_for_step(candidate: candidate, failure: failure) + PartSampleQualificationRequired { step: s, standing: st } => + match implication_for_step( + candidate: candidate, + failure: failure, + target_board: target_board, + ) { + PartImplicatedByFailingStep { step: s2, standing: st2 } => + PartImplicatedByFailingStep { step: s2, standing: st2 } + _ => PartSampleQualificationRequired { step: s, standing: st } + } + PartNotImplicated => + implication_for_step( + candidate: candidate, + failure: failure, + target_board: target_board, + ) }, ) } @@ -139,6 +187,10 @@ fn candidate_total_price_micros(candidate: PartCandidate) -> Nat { // of a sentence beside it. type CandidateAdmission = CandidateAdmissible + | CandidateSampleRequired { + step: MemoryTrainingStep + standing: TrainingEvidenceStanding + } | CandidateImplicated { step: MemoryTrainingStep standing: TrainingEvidenceStanding @@ -152,6 +204,7 @@ fn candidate_admission( candidate: PartCandidate, failures: List, declared_currency: CurrencyCode, + target_board: BaseboardModel, ) -> CandidateAdmission { if candidate.currency != declared_currency { CandidateCurrencyMismatch { @@ -159,25 +212,39 @@ fn candidate_admission( candidate_currency: candidate.currency, } } else { - match part_implication(candidate: candidate.module, failures: failures) { + match part_implication( + candidate: candidate.module, + failures: failures, + target_board: target_board, + ) { PartNotImplicated => CandidateAdmissible + PartSampleQualificationRequired { step: s, standing: st } => + CandidateSampleRequired { step: s, standing: st } PartImplicatedByFailingStep { step: s, standing: st } => CandidateImplicated { step: s, standing: st } } } } +// A sample-required candidate STAYS on the frontier. It is a buy-one-and-test +// candidate, not a rejected one, and removing it here is exactly how the +// market got shrunk to registered modules by construction. Only a decoded +// platform-syndrome refusal, or a currency the caller did not declare, drops a +// part before ranking. fn candidate_admissible( candidate: PartCandidate, failures: List, declared_currency: CurrencyCode, + target_board: BaseboardModel, ) -> Bool { match candidate_admission( candidate: candidate, failures: failures, declared_currency: declared_currency, + target_board: target_board, ) { CandidateAdmissible => true + CandidateSampleRequired { step: _, standing: _ } => true CandidateImplicated { step: _, standing: _ } => false CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => false } @@ -202,12 +269,14 @@ fn pareto_frontier( candidates: List, failures: List, declared_currency: CurrencyCode, + target_board: BaseboardModel, ) -> List { let admissible = candidates |> filter(c => candidate_admissible( candidate: c, failures: failures, declared_currency: declared_currency, + target_board: target_board, )) admissible |> filter(c => !any(admissible, other => candidate_dominates(a: other, b: c))) } @@ -223,4 +292,4 @@ data altrad8ud_mrep_failure_observation: PlatformTrainingFailure = PlatformTrain observed_at: 1786590000000, } -data memory_part_selection_note: String = "The selection rule is mechanical and derived: a candidate is refused when it must execute a training step this platform has been observed to fail. For the ALTRAD8UD-1L2T that step is the DDR4 data-buffer read-phase calibration (MREP), so every load-reduced module is refused and every registered module is admitted, INDEPENDENT of vendor, part number, capacity, speed grade, or presence on any approved-vendor list. That independence is the point: an ordering workflow can shop the whole market instead of a list, and can explain each refusal by naming the step. THE EVIDENCE STANDING IS DELIBERATELY NOT THE STRONGEST ONE AVAILABLE. gunbc has not decoded its own platform syndrome register; what it has is a public report from the same board model naming MREP alongside controller status words byte-identical to its own (SK[0]: 00120001, MC[0]: 00f100b4), plus two different modules of one family failing identically in a documented-supported single-slot population while registered modules ran in that machine for years. That is AssociatedByIdenticalSignature, not DecodedFromPlatformSyndrome, and the difference is live rather than pedantic: Ampere documents SCP register B5 as carrying a per-DIMM training syndrome whose primary-syndrome field distinguishes DRAM VREFDQ failure from LRDIMM data-buffer training failure from LRDIMM data-buffer software-training failure, with B4 selecting the slot. Reading B5 for slot 0 would upgrade this row to a decode and would additionally discriminate WHICH buffer-training failure occurred. It was not read because neither the BMC nor the session host carries an ipmitool binary and no other transport to the SCP register file has been established. The observed_population is recorded as A1 alone because that is where the signature was captured; the eight-module population trained successfully and failed later at a different token, which this model does NOT treat as the same failure. The second axis, topology pressure, is derived rather than observed: when a capacity cannot be reached as a dual-rank registered module within the generation's monolithic die-density ceiling, that capacity forces extra ranks or stacking, which is why 64GB DDR4 at older speed grades is disproportionately load-reduced while 32GB is not. A buyer economizing on speed grade at 64GB is therefore not buying the same module more slowly, and the model derives that from JEDEC density rather than asserting it from market prices." +data memory_part_selection_note: String = "The selection rule is mechanical and derived: a candidate is graded by whether it must execute a training step THIS platform has been observed to fail, and by how strongly that failure is evidenced. For the ALTRAD8UD-1L2T the step is the DDR4 data-buffer read-phase calibration (MREP), which only load-reduced modules execute, so the grading is independent of vendor, part number, capacity, speed grade, and presence on any approved-vendor list. THE SEVERITY IS A FUNCTION OF EVIDENCE, and an earlier revision of this module got that wrong in a way worth recording: it refused every module executing the failing step, on any board, at any standing including EvidenceUnobserved. Because capacities near a generation's die-density ceiling are disproportionately load-reduced, that blanket rule removed most of the 64GB DDR4 market by construction — the exact blocklist this module's prose claimed to avoid, reached by a different route, and the opposite of letting a buyer shop the whole market. Now: a decode of this platform's own syndrome register refuses; an association carrying byte-identical controller status words, or an inference from sequence position, requires a SAMPLE before committing to a full population and stays on the Pareto frontier; an unobserved standing implicates nothing. gunbc holds MREP at AssociatedByIdenticalSignature, so the honest verdict for another ALTRAD8UD LRDIMM today is buy-one-and-test, not reject. Failures are scoped to the board they were observed on, so an ALTRAD8UD receipt cannot speak for a different platform. KNOWN GAP, stated rather than papered over: BaseboardModel currently has exactly one variant, so the board scoping is real in the code but has no executing cross-board control — minting a second board purely to test it would be a fabricated entity, and the control lands when a second real board is modeled. The second axis, topology pressure, is derived rather than observed: when a capacity cannot be reached as a dual-rank registered module within the generation's monolithic die-density ceiling, that capacity forces extra ranks or stacking, which is why 64GB DDR4 at older speed grades is disproportionately load-reduced while 32GB is not. A buyer economizing on speed grade at 64GB is therefore not buying the same module more slowly, it is a different topology — which is a reason to sample it, not a reason to refuse it." diff --git a/dag/test/claim/memory_part_selection_witness_test.dag b/dag/test/claim/memory_part_selection_witness_test.dag index 05d810c0239..9853cbfdb81 100644 --- a/dag/test/claim/memory_part_selection_witness_test.dag +++ b/dag/test/claim/memory_part_selection_witness_test.dag @@ -4,6 +4,7 @@ import std.types { Bool, List, list_length } import std.currency { CurrencyCode, Eur, Usd } import std.measure { ByteSize, byte_size, money_amount_micro } import extdeps.vendor.samsung { samsung } +import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.memory.types { DramModuleCatalogRow, LoadReduced, @@ -17,9 +18,13 @@ import gunbc.memory_part_selection { CandidateAdmissible, CandidateCurrencyMismatch, CandidateImplicated, + CandidateSampleRequired, + DecodedFromPlatformSyndrome, + EvidenceUnobserved, PartCandidate, PartImplicatedByFailingStep, PartNotImplicated, + PartSampleQualificationRequired, PlatformTrainingFailure, altrad8ud_mrep_failure_observation, candidate_admission, @@ -62,6 +67,25 @@ data rdimm_32g: DramModuleCatalogRow = DramModuleCatalogRow { data observed_failures: List = [altrad8ud_mrep_failure_observation] +// The same failure at DECODED standing. Only the evidence differs, which is +// what makes the severity comparison below discriminating. +data decoded_failure: PlatformTrainingFailure = PlatformTrainingFailure { + board: AsrockRackAltrad8ud1l2t, + step: LrdimmDataBufferMrep, + standing: DecodedFromPlatformSyndrome { register: "B5" }, + observed_population: ["A1"], + observed_at: 1786590000000, +} + +// The same failure with NO evidence behind it. +data unobserved_failure: PlatformTrainingFailure = PlatformTrainingFailure { + board: AsrockRackAltrad8ud1l2t, + step: LrdimmDataBufferMrep, + standing: EvidenceUnobserved, + observed_population: ["A1"], + observed_at: 1786590000000, +} + fn candidate_of( module: DramModuleCatalogRow, currency: CurrencyCode, @@ -75,24 +99,61 @@ fn candidate_of( } } -// THE RULE. A load-reduced module must execute MREP, which this board has been -// observed to fail, so it is implicated by the step rather than by its part -// number. -test fn load_reduced_module_is_implicated_by_the_failing_step() -> Bool { - match part_implication(candidate: lrdimm_32g, failures: observed_failures) { - PartImplicatedByFailingStep { step: _, standing: _ } => true +// WHAT AN ASSOCIATION LICENSES. gunbc holds MREP at +// AssociatedByIdenticalSignature, so another load-reduced module is a +// buy-one-and-test candidate — NOT a refusal. An earlier revision answered +// PartImplicatedByFailingStep here, which is how a single association +// removed most of the 64GB DDR4 market by construction. +test fn an_association_requires_a_sample_rather_than_refusing() -> Bool { + match part_implication( + candidate: lrdimm_32g, + failures: observed_failures, + target_board: AsrockRackAltrad8ud1l2t, + ) { + PartSampleQualificationRequired { step: _, standing: _ } => true + PartImplicatedByFailingStep { step: _, standing: _ } => false PartNotImplicated => false } } -// RED control on the discriminating axis: an otherwise identical registered -// module has no data buffer, cannot execute MREP, and is NOT implicated. If -// the rule were keyed on vendor, capacity, generation or rank it would answer -// the same for both fixtures. -test fn registered_module_of_same_family_is_not_implicated() -> Bool { - match part_implication(candidate: rdimm_32g, failures: observed_failures) { +// The severity is a function of EVIDENCE, not of the step. Same board, same +// step, same module — decoded from this platform's own syndrome register, so +// the refusal is earned. +test fn a_decoded_syndrome_does_refuse() -> Bool { + match part_implication( + candidate: lrdimm_32g, + failures: [decoded_failure], + target_board: AsrockRackAltrad8ud1l2t, + ) { + PartImplicatedByFailingStep { step: _, standing: _ } => true + _ => false + } +} + +// FAIL-OPEN CONTROL IN THE OTHER DIRECTION: an unevidenced row must not +// implicate anything at all. Refusing on EvidenceUnobserved would let a row +// nobody measured remove parts from the market. +test fn an_unobserved_standing_implicates_nothing() -> Bool { + match part_implication( + candidate: lrdimm_32g, + failures: [unobserved_failure], + target_board: AsrockRackAltrad8ud1l2t, + ) { PartNotImplicated => true - PartImplicatedByFailingStep { step: _, standing: _ } => false + _ => false + } +} + +// RED control on the structural axis: an otherwise identical registered module +// has no data buffer, cannot execute MREP, and is untouched at ANY standing. +test fn registered_module_is_untouched_even_by_a_decoded_failure() -> Bool { + match part_implication( + candidate: rdimm_32g, + failures: [decoded_failure], + target_board: AsrockRackAltrad8ud1l2t, + ) { + PartNotImplicated => true + _ => false } } @@ -108,30 +169,29 @@ test fn cross_currency_candidate_is_refused_not_ranked() -> Bool { candidate: candidate_of(module: rdimm_32g, currency: Eur), failures: observed_failures, declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, ) { CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => true - CandidateAdmissible => false - CandidateImplicated { step: _, standing: _ } => false + _ => false } } -// Positive control: the SAME candidate in the declared currency is admissible, -// so the refusal above is discriminating on currency and not on the candidate. test fn same_candidate_in_declared_currency_is_admissible() -> Bool { match candidate_admission( candidate: candidate_of(module: rdimm_32g, currency: Usd), failures: observed_failures, declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, ) { CandidateAdmissible => true - CandidateCurrencyMismatch { declared: _, candidate_currency: _ } => false - CandidateImplicated { step: _, standing: _ } => false + _ => false } } -// The implicated candidate is refused BEFORE the frontier is computed, so the -// frontier never trades "it might not train" against price. -test fn frontier_excludes_the_implicated_candidate() -> Bool { +// THE MARKET IS NOT SHRUNK BY AN ASSOCIATION. Both candidates stay on the +// frontier under the evidence gunbc actually holds; the load-reduced one is +// flagged sample-required rather than dropped. +test fn an_association_leaves_both_candidates_on_the_frontier() -> Bool { let front = pareto_frontier( candidates: [ candidate_of(module: lrdimm_32g, currency: Usd), @@ -139,21 +199,40 @@ test fn frontier_excludes_the_implicated_candidate() -> Bool { ], failures: observed_failures, declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, ) - list_length(front) == 1 + list_length(front) == 2 + && match candidate_admission( + candidate: candidate_of(module: lrdimm_32g, currency: Usd), + failures: observed_failures, + declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, + ) { + CandidateSampleRequired { step: _, standing: _ } => true + _ => false + } } -// RED control: with NO observed platform failure, the load-reduced module is -// admitted — the refusal comes from the observation, not from a preference -// baked into the model. -test fn without_an_observed_failure_the_load_reduced_module_is_admitted() -> Bool { +// Positive control that the frontier CAN still drop a part: at decoded +// standing the load-reduced candidate is refused before ranking. +test fn a_decoded_refusal_does_drop_the_candidate_from_the_frontier() -> Bool { let front = pareto_frontier( candidates: [ candidate_of(module: lrdimm_32g, currency: Usd), candidate_of(module: rdimm_32g, currency: Usd), ], - failures: [], + failures: [decoded_failure], declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, ) - list_length(front) == 2 + list_length(front) == 1 + && match candidate_admission( + candidate: candidate_of(module: lrdimm_32g, currency: Usd), + failures: [decoded_failure], + declared_currency: Usd, + target_board: AsrockRackAltrad8ud1l2t, + ) { + CandidateImplicated { step: _, standing: _ } => true + _ => false + } } From fb4aadf648d0adf5855044e4b4711d9b2ad1d94b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:03:16 +0000 Subject: [PATCH 11/28] Carry the supervisor interval on Second, not bare Nat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51303, same finding as review 51298 on the stacked PR. I fixed it there and not here, but host_boot_supervision.dag is introduced by THIS PR, so the fix belonged at its origin — the reviewer was correctly seeing bare Nat on the branch that declares the type. std.measure Second is the carrier siblings already use for this concept. The declared-vs-measured epistemic split is unchanged; only the unit moves. Duration is not a separate std.measure carrier in this corpus, so Second is the existing single authority rather than a new one. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/gunbc/host_boot_supervision.dag | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dag/gunbc/host_boot_supervision.dag b/dag/gunbc/host_boot_supervision.dag index 8ad08656801..00e38b63ef4 100644 --- a/dag/gunbc/host_boot_supervision.dag +++ b/dag/gunbc/host_boot_supervision.dag @@ -2,6 +2,7 @@ module gunbc.host_boot_supervision import std.types { Bool, EpochMs, List, NonEmptyStr } import std.nat { Nat } +import std.measure { Second } import product.placement_supply { HostIdentity } // TWO INDEPENDENT SUPERVISORS CAN RESET A HOST, and the fleet modeled @@ -37,8 +38,8 @@ type SupervisorRunState // can be changed. Storing 306 in a field named "interval" would present a // measurement as a setting, and the next reader would try to edit it. type SupervisorIntervalKnowledge - = IntervalDeclared { seconds: Nat } - | IntervalMeasured { seconds: Nat, samples: Nat, observed_at: EpochMs } + = IntervalDeclared { interval: Second } + | IntervalMeasured { interval: Second, samples: Nat, observed_at: EpochMs } | IntervalUnknown type HostBootSupervisorObservation { From edf0eb80a3ce6db0226b886fa71c14cae056b090 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:19:50 +0000 Subject: [PATCH 12/28] Stop fabricating evidence bounds; add the two missing witness imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51304 (non-blocking, but the finding is a section 5 violation by this repo's own standard, so taking it): prerequisite_evidence_verdict stored the DECLARED MINIMUM into whichever bound had no observation yet. A verdict backed only by an acceptance therefore reported a highest_refusal that never happened — a fabricated plausible value in the exact field a consumer reads to judge how well-bounded the threshold is. It was also order-dependent: once a bound was set, later observations were discarded, so the same evidence authored in the other order gave different bounds. The reviewer correctly noted my positive test greened only because the list happens to be authored refused-then-accepted. Both bounds are now Optional and computed as a real max-refusal / min-acceptance over all observations, which is associative and commutative and so cannot depend on authoring order. Absent means absent. Two controls added: the reversed list must produce an equal verdict, and an acceptance-only history must report NO refusal bound. review 51307 (blocking, verified): bmc_firmware_thermal_witness_test referenced firmware_semantic_version with no extdeps.firmware.types import at all, and asrock_altrad8ud_openbmc_3_22_00_capabilities while the asrock block imported only the 2.07.00 and 3.06.00 rows. Both were mine, from scripted edits that added references without their imports. Both added; the other changed witnesses were checked for the same class and are clean. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/firmware/types.dag | 158 +++++++++++------- .../bmc_firmware_thermal_witness_test.dag | 2 + ...are_prerequisite_evidence_witness_test.dag | 58 ++++++- 3 files changed, 151 insertions(+), 67 deletions(-) diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index dad3ad4ad99..31a1fa25d4f 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -2,7 +2,7 @@ module extdeps.firmware.types import std.nat { Nat } import std.measure { ByteSize, byte_size } -import std.types { EpochMs, NonEmptyStr } +import std.types { EpochMs, NonEmptyStr, List, Present, Absent, list_length } import extdeps.crypto.hash { Digest } import extdeps.boards.types { BaseboardModel } import extdeps.external_authority { ExternalAuthority } @@ -143,10 +143,19 @@ type FirmwarePrerequisiteObservation { // that interval, so a declared 3.22.00 is an upper bound and may be stricter // than the vendor requires — the safe direction, and the positive arm says so // rather than claiming the threshold was measured. +// +// BOTH BOUNDS ARE OPTIONAL, and that is the correction that matters. An +// earlier revision stored the DECLARED MINIMUM into whichever bound had no +// observation yet, so a verdict backed only by an acceptance reported a +// highest_refusal that never happened — a fabricated plausible value in the +// exact field a consumer reads to judge how well-bounded the threshold is +// (DESIGN section 5). It was also order-dependent: later observations were +// discarded once a bound was set, so the same evidence in a different order +// gave different bounds. Absent now means absent. type PrerequisiteEvidenceVerdict = PrerequisiteConsistentUpperBound { - highest_refusal: FirmwareSemanticVersion - accepted_at: FirmwareSemanticVersion + highest_refusal: FirmwareSemanticVersion? + lowest_acceptance: FirmwareSemanticVersion? } | PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: FirmwareSemanticVersion @@ -156,37 +165,68 @@ type PrerequisiteEvidenceVerdict } | PrerequisiteEvidenceAbsent -fn prerequisite_observation_contradicts( - minimum: FirmwareSemanticVersion, - observation: FirmwarePrerequisiteObservation, -) -> PrerequisiteEvidenceVerdict { - match observation.outcome { - PushAcceptedByTarget => - if firmware_semantic_version_at_least( - observed: observation.gating_version, - minimum: minimum, - ) { - PrerequisiteEvidenceAbsent - } else { - PrerequisiteContradictedByAcceptanceBelowMinimum { - accepted_at: observation.gating_version, - } - } - PushRefusedByTarget { diagnostic: _ } => - if firmware_semantic_version_at_least( - observed: observation.gating_version, - minimum: minimum, - ) { - PrerequisiteContradictedByRefusalAtOrAboveMinimum { - refused_at: observation.gating_version, - } - } else { - PrerequisiteEvidenceAbsent - } - } +fn higher_version( + a: FirmwareSemanticVersion, + b: FirmwareSemanticVersion, +) -> FirmwareSemanticVersion { + if firmware_semantic_version_at_least(observed: a, minimum: b) { a } else { b } } -fn prerequisite_evidence_verdict( +fn lower_version( + a: FirmwareSemanticVersion, + b: FirmwareSemanticVersion, +) -> FirmwareSemanticVersion { + if firmware_semantic_version_at_least(observed: a, minimum: b) { b } else { a } +} + +// The strongest refusal observed, over every refusal. Order-independent by +// construction: max is associative and commutative, so the fold cannot depend +// on authoring order the way the previous first-wins accumulator did. +fn highest_refused_version( + observations: List, +) -> FirmwareSemanticVersion? { + fold( + observations, + init: Absent, + f: (acc, observation) => + match observation.outcome { + PushAcceptedByTarget => acc + PushRefusedByTarget { diagnostic: _ } => + match acc { + Absent => Present { value: observation.gating_version } + Present { value: current } => + Present { + value: higher_version(a: current, b: observation.gating_version), + } + } + }, + ) +} + +// The weakest acceptance observed. This is the tighter of the two bounds on +// the true threshold, since anything at or above it demonstrably works. +fn lowest_accepted_version( + observations: List, +) -> FirmwareSemanticVersion? { + fold( + observations, + init: Absent, + f: (acc, observation) => + match observation.outcome { + PushRefusedByTarget { diagnostic: _ } => acc + PushAcceptedByTarget => + match acc { + Absent => Present { value: observation.gating_version } + Present { value: current } => + Present { + value: lower_version(a: current, b: observation.gating_version), + } + } + }, + ) +} + +fn prerequisite_contradiction( minimum: FirmwareSemanticVersion, observations: List, ) -> PrerequisiteEvidenceVerdict { @@ -195,43 +235,37 @@ fn prerequisite_evidence_verdict( init: PrerequisiteEvidenceAbsent, f: (acc, observation) => match acc { - PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } => - PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } - PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } => - PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } - PrerequisiteConsistentUpperBound { highest_refusal: h, accepted_at: a } => - match prerequisite_observation_contradicts( - minimum: minimum, - observation: observation, - ) { - PrerequisiteEvidenceAbsent => - PrerequisiteConsistentUpperBound { highest_refusal: h, accepted_at: a } - other => other - } PrerequisiteEvidenceAbsent => - match prerequisite_observation_contradicts( + prerequisite_observation_contradicts( minimum: minimum, observation: observation, - ) { - PrerequisiteEvidenceAbsent => - match observation.outcome { - PushAcceptedByTarget => - PrerequisiteConsistentUpperBound { - highest_refusal: minimum, - accepted_at: observation.gating_version, - } - PushRefusedByTarget { diagnostic: _ } => - PrerequisiteConsistentUpperBound { - highest_refusal: observation.gating_version, - accepted_at: minimum, - } - } - other => other - } + ) + other => other }, ) } +fn prerequisite_evidence_verdict( + minimum: FirmwareSemanticVersion, + observations: List, +) -> PrerequisiteEvidenceVerdict { + match prerequisite_contradiction(minimum: minimum, observations: observations) { + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } => + PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } => + PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } + _ => + match list_length(observations) { + 0 => PrerequisiteEvidenceAbsent + _ => + PrerequisiteConsistentUpperBound { + highest_refusal: highest_refused_version(observations: observations), + lowest_acceptance: lowest_accepted_version(observations: observations), + } + } + } +} + fn firmware_semantic_version_at_least( observed: FirmwareSemanticVersion, minimum: FirmwareSemanticVersion, diff --git a/dag/test/claim/bmc_firmware_thermal_witness_test.dag b/dag/test/claim/bmc_firmware_thermal_witness_test.dag index 4e11f29f24b..17b29ea3b98 100644 --- a/dag/test/claim/bmc_firmware_thermal_witness_test.dag +++ b/dag/test/claim/bmc_firmware_thermal_witness_test.dag @@ -37,8 +37,10 @@ import extdeps.bmc.types { bmc_fan_curve_sensor_unavailable_duty, redfish_thermal_query_verdict, } +import extdeps.firmware.types { firmware_semantic_version } import extdeps.boards.asrock_rack { asrock_altrad8ud_1l2t_bmc_redfish_surface_catalog, + asrock_altrad8ud_openbmc_3_22_00_capabilities, asrock_altrad8ud_openbmc_3_22_00_redfish_surface, asrock_altrad8ud_openbmc_2_07_00_capabilities, asrock_altrad8ud_openbmc_3_06_00_capabilities, diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag index 07bbafa831d..c9237de6dab 100644 --- a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -12,6 +12,8 @@ import extdeps.firmware.types { prerequisite_evidence_verdict, } import extdeps.boards.asrock_rack { + asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, + asrock_altrad8ud_bios_310_push_refused_at_bmc_207, asrock_altrad8ud_bios_openbmc_prerequisite_evidence, } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -20,6 +22,11 @@ data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly // The declared minimum on the BIOS 3.10 OpenBMC-tar release row. Named here so // the witness compares the SAME number the catalog declares. +data reversed_evidence: List = [ + asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, + asrock_altrad8ud_bios_310_push_refused_at_bmc_207, +] + data declared_minimum: FirmwareSemanticVersion = firmware_semantic_version(major: 3, minor: 22, patch: 0) // The executed pair supports the declared minimum, and the positive arm says @@ -30,11 +37,52 @@ test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool minimum: declared_minimum, observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, ) { - PrerequisiteConsistentUpperBound { highest_refusal: r, accepted_at: a } => - r.major == 2 && r.minor == 7 && a.major == 3 && a.minor == 22 - PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: _ } => false - PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: _ } => false - PrerequisiteEvidenceAbsent => false + PrerequisiteConsistentUpperBound { highest_refusal: r, lowest_acceptance: a } => + match r { + Present { value: rv } => + match a { + Present { value: av } => + rv.major == 2 && rv.minor == 7 && av.major == 3 && av.minor == 22 + Absent => false + } + Absent => false + } + _ => false + } +} + +// ORDER INDEPENDENCE. The same observations authored in the opposite order +// must produce the same bounds. The previous accumulator kept whichever bound +// it saw first and discarded the rest, so this control is what distinguishes +// a real max/min from a first-wins fold that greens on one authoring order. +test fn the_bounds_do_not_depend_on_authoring_order() -> Bool { + prerequisite_evidence_verdict( + minimum: declared_minimum, + observations: reversed_evidence, + ) == prerequisite_evidence_verdict( + minimum: declared_minimum, + observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, + ) +} + +// A bound with no observation behind it is ABSENT, never filled in from the +// declared minimum. Acceptance-only evidence has no refusal to report, and +// reporting one would fabricate a push that never happened. +test fn an_acceptance_only_history_reports_no_refusal_bound() -> Bool { + match prerequisite_evidence_verdict( + minimum: declared_minimum, + observations: [asrock_altrad8ud_bios_310_push_accepted_at_bmc_322], + ) { + PrerequisiteConsistentUpperBound { highest_refusal: r, lowest_acceptance: a } => + match r { + Absent => + match a { + Present { value: av } => av.major == 3 && av.minor == 22 + Absent => false + } + Present { value: _ } => false + } + _ => false } } From ce9d2305816909972213bf722668bcdd820c8b72 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:31:02 +0000 Subject: [PATCH 13/28] Declare ExternalModelScope on the new extdeps modules; fix optional imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real CI failures, both mine. RESOLUTION: Present and Absent were imported from std.types, where they do not live. The corpus idiom is v2.std.optional. Fixed in extdeps.firmware.types and both witnesses that matched on them. I had "verified imports" after the last review with a targeted check that only confirmed a symbol appeared in SOME import block — it never checked the symbol was exported by the module named, so it passed a wrong-module import cleanly. The check was weaker than the claim I made from it. EXTDEPS SCOPE PLACEMENT GATE: three new dag/extdeps modules were enrolled in neither scope_carrier_paths nor the machinery exemption, which the per-PR gate refuses by design — a new extdeps file's only landing states are scope carrier or genuine machinery, and the frozen legacy manifest is explicitly not an admission channel. Each now declares extdeps_model_scope naming its exact upstream subject and joins the carrier roster: - extdeps.firmware.types -> FirmwareReleaseRow: the vendor release and its applicability precondition, not the flashing mechanism and not fleet policy about which release to run. - extdeps.memory.training -> MemoryTrainingStep: the JEDEC DDR4 initialization calibration sequence, cited to JESD79-4A; platform failure receipts and purchasing policy are deliberately outside it. - extdeps.ampere.scp_diagnostic -> AmpereScpConsoleToken: the token stream and structured boot-progress interface as an upstream surface only, with the read-effect axis included because an acknowledged queue is not a sampled register, and with no token-to-meaning function since no codebook has been obtained. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/ampere/scp_diagnostic.dag | 17 +++++++++++++++- dag/extdeps/firmware/types.dag | 20 +++++++++++++++++-- dag/extdeps/memory/training.dag | 17 +++++++++++++++- dag/gunbc/extdeps_scope_frontier.dag | 3 +++ ...licability_and_loop_cause_witness_test.dag | 3 ++- ...are_prerequisite_evidence_witness_test.dag | 3 ++- 6 files changed, 57 insertions(+), 6 deletions(-) diff --git a/dag/extdeps/ampere/scp_diagnostic.dag b/dag/extdeps/ampere/scp_diagnostic.dag index a10364b1aa0..38912f160fd 100644 --- a/dag/extdeps/ampere/scp_diagnostic.dag +++ b/dag/extdeps/ampere/scp_diagnostic.dag @@ -2,7 +2,8 @@ module extdeps.ampere.scp_diagnostic import std.types { EpochMs, List, NonEmptyStr } import std.nat { Nat } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -12,6 +13,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.ampere.scp_diagnostic", + decl_name: "AmpereScpConsoleToken", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data scp_diagnostic_scope_subject_note: String = "THE SUBJECT IS THE AMPERE SCP/SMpro/PMpro DIAGNOSTIC TOKEN STREAM AND ITS STRUCTURED BOOT-PROGRESS INTERFACE, as an upstream surface only. Deliberately NO token-to-meaning function exists: no codebook has been obtained, so a token is an identity this repository may compare and count and may not interpret. The read-effect axis is part of the subject because the interface is an acknowledged queue rather than a sampled register, which decides whether a reader may poll it. NOT gunbc receipts about a host, NOT any verdict about fatality." + // AMPERE owns the SCP / SMpro / PMpro diagnostic interfaces and the token // namespace. This module models that upstream shape; it does NOT record what // any particular host did, which is a fleet observation and lives downstream. diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index 31a1fa25d4f..017fea62343 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -2,10 +2,12 @@ module extdeps.firmware.types import std.nat { Nat } import std.measure { ByteSize, byte_size } -import std.types { EpochMs, NonEmptyStr, List, Present, Absent, list_length } +import std.types { EpochMs, NonEmptyStr, List, list_length } +import v2.std.optional { Present, Absent } import extdeps.crypto.hash { Digest } import extdeps.boards.types { BaseboardModel } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -15,6 +17,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.firmware.types", + decl_name: "FirmwareReleaseRow", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data types_scope_subject_note: String = "THE SUBJECT IS THE VENDOR FIRMWARE RELEASE AND ITS APPLICABILITY PRECONDITION. A release row names a board, an independently versioned firmware target, a packaging, and the bytes; the prerequisite names what must already be running before those bytes can be applied. NOT the flashing mechanism, NOT any BMC API surface, NOT product policy about which release a fleet should run — those are extdeps.bmc.* upstream facts and gunbc product decisions respectively. The prerequisite minimum here is bounded by executed pushes carried as FirmwarePrerequisiteObservation rows, never asserted from a vendor page, which documents no such dependency." + // A version triple is one concept. It was previously spelled // BmcFirmwareSemanticVersion inside the BMC module, which made the SAME // concept unnameable for host firmware without minting a second name for diff --git a/dag/extdeps/memory/training.dag b/dag/extdeps/memory/training.dag index 8c6d7e5d687..5c2d61c8377 100644 --- a/dag/extdeps/memory/training.dag +++ b/dag/extdeps/memory/training.dag @@ -2,7 +2,8 @@ module extdeps.memory.training import std.types { Bool, Int, List, NonEmptyStr } import std.measure { ByteSize, BitWidth, bit_width, byte_size, byte_size_count, bit_width_count } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.memory.types { DimmBuffering, @@ -23,6 +24,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.memory.training", + decl_name: "MemoryTrainingStep", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data training_scope_subject_note: String = "THE SUBJECT IS THE JEDEC DDR4 INITIALIZATION CALIBRATION SEQUENCE — which calibration steps a module must execute, derived from its construction. Grounded on JESD79-4A. The load-reduced steps exist only where a data buffer is interposed, which is what makes the step set a difference in KIND between module classes rather than a reliability preference. NOT platform failure receipts (those are gunbc observations of a board), NOT purchasing policy, NOT the DRAM module catalog shape itself (extdeps.memory.types)." + // WHY A TRAINING-STEP MODEL RATHER THAN A BLOCKLIST. // // "Avoid 4Rx4 LRDIMM" is a conclusion, and a conclusion authored as a rule diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index f25bb6eefaf..8ea858bc542 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -124,6 +124,9 @@ fn scope_placement_refused_paths(added: List, carriers: List, ma } data scope_carrier_paths: List = [ + "dag/extdeps/firmware/types.dag", + "dag/extdeps/memory/training.dag", + "dag/extdeps/ampere/scp_diagnostic.dag", "dag/extdeps/ipc/dpmx.dag", "dag/extdeps/ucamco/gerber.dag", "dag/extdeps/cpu/ampere_altra_package.dag", diff --git a/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag index 56bcc7419e0..2d47b586719 100644 --- a/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag +++ b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.firmware_applicability_and_loop_cause_witness_test -import std.types { Bool, Present, Absent } +import std.types { Bool } +import v2.std.optional { Present, Absent } import extdeps.firmware.types { BmcController, FirmwareApplicable, diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag index c9237de6dab..4f449eb91d8 100644 --- a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.firmware_prerequisite_evidence_witness_test -import std.types { Bool } +import std.types { Bool, List } +import v2.std.optional { Present, Absent } import extdeps.firmware.types { FirmwarePrerequisiteObservation, FirmwareSemanticVersion, From 88d035630ce38c4ba38d9fd7efaa5573822eea96 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:33:53 +0000 Subject: [PATCH 14/28] Restore the deleted contradiction fn; rebind drift fixtures to 2.07.00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51313, all four findings verified and real. COMPILE BREAK, mine: rewriting the evidence-bounds block deleted prerequisite_observation_contradicts along with the accumulator it sat beside, while prerequisite_contradiction still called it. Nothing defined it anywhere in the tree, so the module could not compile and the contradiction arms were unreachable — the arms that do the actual gating. Restored with its own rationale: an acceptance strictly below the minimum falsifies it, a refusal at or above it falsifies it the other way, and consistent is not the same as supporting, which is what the bounds report separately. STALE FIXTURES, caused by this PR: three classification witnesses used srv3 as their drifted-host fixture and asserted 2.07.00. Advancing srv3's current standing to 3.22.00 did not merely break them — once srv3 satisfies the pinned track-3 intent, Drifted/Apply becomes Converged/Noop and the tests would have inverted what they claim to check. They now bind srv3_bmc_firmware_2_07_00_observation, the immutable historical row, which is what each test was always about: the classification logic on a host behind its intent, not srv3's present state. That also resolves the fourth finding without a separate change: the update-service receipt is bound to the 2.07.00 observation, so the plan test now pairs one firmware identity with itself and the update-surface identity check no longer refuses. This is the immutable-observation split paying for itself immediately — the historical row existing is what let three tests keep their meaning instead of being rewritten around a moved fact. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/firmware/types.dag | 34 +++++++++++++++++++ .../bmc_firmware_thermal_witness_test.dag | 23 ++++++++----- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index 017fea62343..cbcd1b34942 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -242,6 +242,40 @@ fn lowest_accepted_version( ) } +// Does ONE observation contradict a declared minimum? An acceptance strictly +// below the minimum falsifies it, and a refusal at or above it falsifies it in +// the other direction. Anything else is consistent, which is not the same as +// supporting it — that is what the bounds above report. +fn prerequisite_observation_contradicts( + minimum: FirmwareSemanticVersion, + observation: FirmwarePrerequisiteObservation, +) -> PrerequisiteEvidenceVerdict { + match observation.outcome { + PushAcceptedByTarget => + if firmware_semantic_version_at_least( + observed: observation.gating_version, + minimum: minimum, + ) { + PrerequisiteEvidenceAbsent + } else { + PrerequisiteContradictedByAcceptanceBelowMinimum { + accepted_at: observation.gating_version, + } + } + PushRefusedByTarget { diagnostic: _ } => + if firmware_semantic_version_at_least( + observed: observation.gating_version, + minimum: minimum, + ) { + PrerequisiteContradictedByRefusalAtOrAboveMinimum { + refused_at: observation.gating_version, + } + } else { + PrerequisiteEvidenceAbsent + } + } +} + fn prerequisite_contradiction( minimum: FirmwareSemanticVersion, observations: List, diff --git a/dag/test/claim/bmc_firmware_thermal_witness_test.dag b/dag/test/claim/bmc_firmware_thermal_witness_test.dag index 17b29ea3b98..9be3685c112 100644 --- a/dag/test/claim/bmc_firmware_thermal_witness_test.dag +++ b/dag/test/claim/bmc_firmware_thermal_witness_test.dag @@ -51,7 +51,7 @@ import extdeps.boards.asrock_rack { import extdeps.crypto.hash { sha256_digest } import gunbc.fleet_bmc_observation { BmcFanRebootPersistenceReceipt, - srv3_bmc_firmware_observation, + srv3_bmc_firmware_2_07_00_observation, srv3_bmc_redfish_surface, srv3_bmc_update_service_observation, srv4_bmc_config_persistence_observation, @@ -198,14 +198,19 @@ test fn every_metal_host_uses_the_single_global_bmc_valid_state() -> Bool { } } +// These classification tests use srv3's IMMUTABLE 2.07.00 observation as the +// drifted-host fixture rather than its current standing. srv3 is on 3.22.00 +// since the 2026-08-11 flash, so binding them to the current row would make +// them assert a state the host no longer has — and, once it satisfied the +// pinned intent, would silently invert what each test claims to check. test fn heterogeneous_firmware_satisfies_unpinned_intent_and_pinned_detects_mismatch() -> Bool { - srv3_bmc_firmware_observation.firmware.version.major == 2 - && srv3_bmc_firmware_observation.firmware.version.minor == 7 + srv3_bmc_firmware_2_07_00_observation.firmware.version.major == 2 + && srv3_bmc_firmware_2_07_00_observation.firmware.version.minor == 7 && srv4_bmc_firmware_observation.firmware.version.major == 3 && srv4_bmc_firmware_observation.firmware.version.minor == 22 && match bmc_firmware_observation_intent_verdict( intent: fleet_bmc_valid_state.firmware, - observation: srv3_bmc_firmware_observation, + observation: srv3_bmc_firmware_2_07_00_observation, ) { BmcFirmwareIntentSatisfied => true BmcFirmwareIntentMismatch { required: _, observed: _ } => false @@ -219,7 +224,7 @@ test fn heterogeneous_firmware_satisfies_unpinned_intent_and_pinned_detects_mism } && match bmc_firmware_observation_intent_verdict( intent: fixture_pinned_track3_intent, - observation: srv3_bmc_firmware_observation, + observation: srv3_bmc_firmware_2_07_00_observation, ) { BmcFirmwareIntentMismatch { required: required, @@ -247,7 +252,7 @@ test fn firmware_classification_reuses_upsert_states_and_refuses_unapproved_upda host: srv3_host.identity, board: srv3_baseboard.catalog.model, intent: fixture_pinned_track3_intent, - observation: Present { value: srv3_bmc_firmware_observation }, + observation: Present { value: srv3_bmc_firmware_2_07_00_observation }, artifact: none, update_service: none, ) @@ -292,7 +297,7 @@ test fn approved_exact_firmware_artifact_builds_a_typed_srv3_update_plan() -> Bo host: srv3_host.identity, board: srv3_baseboard.catalog.model, intent: fixture_pinned_track3_intent, - observation: Present { value: srv3_bmc_firmware_observation }, + observation: Present { value: srv3_bmc_firmware_2_07_00_observation }, artifact: Present { value: fixture_approved_openbmc_3_22_artifact }, update_service: Present { value: srv3_bmc_update_service_observation }, ) @@ -300,7 +305,7 @@ test fn approved_exact_firmware_artifact_builds_a_typed_srv3_update_plan() -> Bo host: srv3_host.identity, board: srv3_baseboard.catalog.model, intent: fixture_pinned_track3_intent, - observation: Present { value: srv3_bmc_firmware_observation }, + observation: Present { value: srv3_bmc_firmware_2_07_00_observation }, artifact: Present { value: fixture_approved_openbmc_3_22_artifact }, update_service: none, ) @@ -308,7 +313,7 @@ test fn approved_exact_firmware_artifact_builds_a_typed_srv3_update_plan() -> Bo && match classified.decision { Apply { plan: plan } => plan.host == srv3_host.identity - && plan.observed == srv3_bmc_firmware_observation + && plan.observed == srv3_bmc_firmware_2_07_00_observation && plan.artifact == fixture_approved_openbmc_3_22_artifact && plan.update_service == srv3_bmc_update_service_observation Noop => false From 42e44ce9d0430f4361697e3d069701aa7ab07e4d Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:49:24 +0000 Subject: [PATCH 15/28] Join the prerequisite oracle to the catalog instead of copying it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51318, verified and correct. declared_minimum hand-wrote 3.22.00 while a comment two lines above claimed the witness "compares the SAME number the catalog declares" — it did not. Editing asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite would have left executed_pair_supports_the_declared_minimum_as_an_upper_bound and the_bounds_do_not_depend_on_authoring_order green until someone remembered to edit the witness too. That is the tree-copied oracle DESIGN section 5 forbids, and the comment asserting otherwise is what made it look already checked. verdict_against_declared_minimum now matches the release row's own prerequisite and passes its minimum through, so the catalog and the tests move together. The sibling witness already used this join, so the pattern was in the tree and I did not follow it. The RED controls deliberately do NOT join: a falsifying probe is an authored input chosen to contradict the evidence, not an oracle, and joining it to the catalog would make it unauthorable. That split is stated in the module so the next reader does not "fix" it. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- ...are_prerequisite_evidence_witness_test.dag | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag index 4f449eb91d8..643f0589035 100644 --- a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -5,7 +5,10 @@ import v2.std.optional { Present, Absent } import extdeps.firmware.types { FirmwarePrerequisiteObservation, FirmwareSemanticVersion, + NoFirmwarePrerequisite, + RequiresMinimumTargetFirmware, PrerequisiteConsistentUpperBound, + PrerequisiteEvidenceVerdict, PrerequisiteContradictedByAcceptanceBelowMinimum, PrerequisiteContradictedByRefusalAtOrAboveMinimum, PrerequisiteEvidenceAbsent, @@ -13,6 +16,7 @@ import extdeps.firmware.types { prerequisite_evidence_verdict, } import extdeps.boards.asrock_rack { + asrock_altrad8ud_bios_3_10_openbmc_release, asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, asrock_altrad8ud_bios_310_push_refused_at_bmc_207, asrock_altrad8ud_bios_openbmc_prerequisite_evidence, @@ -21,21 +25,37 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// The declared minimum on the BIOS 3.10 OpenBMC-tar release row. Named here so -// the witness compares the SAME number the catalog declares. data reversed_evidence: List = [ asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, asrock_altrad8ud_bios_310_push_refused_at_bmc_207, ] -data declared_minimum: FirmwareSemanticVersion = firmware_semantic_version(major: 3, minor: 22, patch: 0) +// THE ORACLE IS AN IDENTITY JOIN, not a copy. An earlier revision hand-wrote +// 3.22.00 here and claimed in a comment to be comparing the same number the +// catalog declares — which it was not: changing the release row would have +// left these tests green until someone remembered to edit the witness too. +// That is the tree-copied oracle DESIGN section 5 forbids, and the comment +// asserting otherwise is what made it look already checked. The minimum now +// comes from the release row itself, so editing the catalog moves the tests. +// +// The RED controls below deliberately do NOT join: a falsifying probe is an +// authored input chosen to contradict the evidence, not an oracle, and joining +// it to the catalog would make it unauthorable. +fn verdict_against_declared_minimum( + observations: List, +) -> PrerequisiteEvidenceVerdict { + match asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite { + RequiresMinimumTargetFirmware { target: _, minimum: m } => + prerequisite_evidence_verdict(minimum: m, observations: observations) + NoFirmwarePrerequisite => PrerequisiteEvidenceAbsent + } +} // The executed pair supports the declared minimum, and the positive arm says // what it actually establishes: an UPPER BOUND. 3.06.00 exists in the // capability catalog and was never pushed, so the true threshold may be lower. test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool { - match prerequisite_evidence_verdict( - minimum: declared_minimum, + match verdict_against_declared_minimum( observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, ) { PrerequisiteConsistentUpperBound { highest_refusal: r, lowest_acceptance: a } => @@ -57,21 +77,17 @@ test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool // it saw first and discarded the rest, so this control is what distinguishes // a real max/min from a first-wins fold that greens on one authoring order. test fn the_bounds_do_not_depend_on_authoring_order() -> Bool { - prerequisite_evidence_verdict( - minimum: declared_minimum, - observations: reversed_evidence, - ) == prerequisite_evidence_verdict( - minimum: declared_minimum, - observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, - ) + verdict_against_declared_minimum(observations: reversed_evidence) + == verdict_against_declared_minimum( + observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, + ) } // A bound with no observation behind it is ABSENT, never filled in from the // declared minimum. Acceptance-only evidence has no refusal to report, and // reporting one would fabricate a push that never happened. test fn an_acceptance_only_history_reports_no_refusal_bound() -> Bool { - match prerequisite_evidence_verdict( - minimum: declared_minimum, + match verdict_against_declared_minimum( observations: [asrock_altrad8ud_bios_310_push_accepted_at_bmc_322], ) { PrerequisiteConsistentUpperBound { highest_refusal: r, lowest_acceptance: a } => @@ -119,7 +135,7 @@ test fn a_minimum_at_or_below_the_refusing_version_is_contradicted() -> Bool { // With no observations the verdict is ABSENT, never consistent. An unevidenced // minimum must not read the same as an evidenced one. test fn no_observations_yields_absent_not_consistent() -> Bool { - match prerequisite_evidence_verdict(minimum: declared_minimum, observations: []) { + match verdict_against_declared_minimum(observations: []) { PrerequisiteEvidenceAbsent => true _ => false } From fa289c657f43c5cf2d939141f53411d6d6081370 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 04:50:55 +0000 Subject: [PATCH 16/28] Declare ExternalModelScope on the new extdeps modules; fix optional imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-picked to the branch that INTRODUCES these modules. I committed this fix on the stacked derivation branch instead, so #8171 — where extdeps.firmware.types, extdeps.memory.training and extdeps.ampere.scp_diagnostic are actually added — kept failing the placement gate it was failing before. The stack direction makes that invisible: merging the parent forward into the child hides that the child, not the parent, carries the fix. The extdeps scope placement gate refuses a new dag/extdeps file enrolled in neither scope_carrier_paths nor the machinery exemption, by design — the frozen legacy manifest is explicitly not an admission channel. Each module now declares extdeps_model_scope naming its exact upstream subject and joins the carrier roster: - extdeps.firmware.types -> FirmwareReleaseRow: the vendor release and its applicability precondition, not the flashing mechanism and not fleet policy about which release to run. - extdeps.memory.training -> MemoryTrainingStep: the JEDEC DDR4 initialization calibration sequence, cited to JESD79-4A. - extdeps.ampere.scp_diagnostic -> AmpereScpConsoleToken: the token stream and structured boot-progress interface as an upstream surface only, read-effect axis included because an acknowledged queue is not a sampled register, and still no token-to-meaning function. Also fixes Present/Absent, imported from std.types where they do not live; the corpus idiom is v2.std.optional. This branch's copy of extdeps.firmware.types needs them for firmware_release_applicability but not List/list_length, which belong to the evidence carrier that lives only on the stacked branch. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/ampere/scp_diagnostic.dag | 17 ++++++++++++++++- dag/extdeps/firmware/types.dag | 18 +++++++++++++++++- dag/extdeps/memory/training.dag | 17 ++++++++++++++++- dag/gunbc/extdeps_scope_frontier.dag | 3 +++ ...plicability_and_loop_cause_witness_test.dag | 3 ++- 5 files changed, 54 insertions(+), 4 deletions(-) diff --git a/dag/extdeps/ampere/scp_diagnostic.dag b/dag/extdeps/ampere/scp_diagnostic.dag index a10364b1aa0..38912f160fd 100644 --- a/dag/extdeps/ampere/scp_diagnostic.dag +++ b/dag/extdeps/ampere/scp_diagnostic.dag @@ -2,7 +2,8 @@ module extdeps.ampere.scp_diagnostic import std.types { EpochMs, List, NonEmptyStr } import std.nat { Nat } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -12,6 +13,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.ampere.scp_diagnostic", + decl_name: "AmpereScpConsoleToken", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data scp_diagnostic_scope_subject_note: String = "THE SUBJECT IS THE AMPERE SCP/SMpro/PMpro DIAGNOSTIC TOKEN STREAM AND ITS STRUCTURED BOOT-PROGRESS INTERFACE, as an upstream surface only. Deliberately NO token-to-meaning function exists: no codebook has been obtained, so a token is an identity this repository may compare and count and may not interpret. The read-effect axis is part of the subject because the interface is an acknowledged queue rather than a sampled register, which decides whether a reader may poll it. NOT gunbc receipts about a host, NOT any verdict about fatality." + // AMPERE owns the SCP / SMpro / PMpro diagnostic interfaces and the token // namespace. This module models that upstream shape; it does NOT record what // any particular host did, which is a fleet observation and lives downstream. diff --git a/dag/extdeps/firmware/types.dag b/dag/extdeps/firmware/types.dag index a47f2dfe0bf..3edd7520359 100644 --- a/dag/extdeps/firmware/types.dag +++ b/dag/extdeps/firmware/types.dag @@ -3,9 +3,11 @@ module extdeps.firmware.types import std.nat { Nat } import std.measure { ByteSize, byte_size } import std.types { EpochMs, NonEmptyStr } +import v2.std.optional { Present, Absent } import extdeps.crypto.hash { Digest } import extdeps.boards.types { BaseboardModel } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { @@ -15,6 +17,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.firmware.types", + decl_name: "FirmwareReleaseRow", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data types_scope_subject_note: String = "THE SUBJECT IS THE VENDOR FIRMWARE RELEASE AND ITS APPLICABILITY PRECONDITION. A release row names a board, an independently versioned firmware target, a packaging, and the bytes; the prerequisite names what must already be running before those bytes can be applied. NOT the flashing mechanism, NOT any BMC API surface, NOT product policy about which release a fleet should run — those are extdeps.bmc.* upstream facts and gunbc product decisions respectively. The prerequisite minimum here is bounded by executed pushes carried as FirmwarePrerequisiteObservation rows, never asserted from a vendor page, which documents no such dependency." + // A version triple is one concept. It was previously spelled // BmcFirmwareSemanticVersion inside the BMC module, which made the SAME // concept unnameable for host firmware without minting a second name for diff --git a/dag/extdeps/memory/training.dag b/dag/extdeps/memory/training.dag index 8c6d7e5d687..5c2d61c8377 100644 --- a/dag/extdeps/memory/training.dag +++ b/dag/extdeps/memory/training.dag @@ -2,7 +2,8 @@ module extdeps.memory.training import std.types { Bool, Int, List, NonEmptyStr } import std.measure { ByteSize, BitWidth, bit_width, byte_size, byte_size_count, bit_width_count } -import extdeps.external_authority { ExternalAuthority } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import extdeps.external_authority { ExternalModelScope, ExternalSubjectRef, ExternalAuthority } import extdeps.uri { Uri, Https } import extdeps.memory.types { DimmBuffering, @@ -23,6 +24,20 @@ data extdeps_external_authority_anchor: ExternalAuthority = ExternalAuthority { } } +data extdeps_model_scope: ExternalModelScope = ExternalModelScope { + subject: ExternalSubjectRef { + declaration: DeclarationRef { + module_path: "extdeps.memory.training", + decl_name: "MemoryTrainingStep", + field: WholeDeclaration + } + }, + first_citation: extdeps_external_authority_anchor, + further_citations: [] +} + +data training_scope_subject_note: String = "THE SUBJECT IS THE JEDEC DDR4 INITIALIZATION CALIBRATION SEQUENCE — which calibration steps a module must execute, derived from its construction. Grounded on JESD79-4A. The load-reduced steps exist only where a data buffer is interposed, which is what makes the step set a difference in KIND between module classes rather than a reliability preference. NOT platform failure receipts (those are gunbc observations of a board), NOT purchasing policy, NOT the DRAM module catalog shape itself (extdeps.memory.types)." + // WHY A TRAINING-STEP MODEL RATHER THAN A BLOCKLIST. // // "Avoid 4Rx4 LRDIMM" is a conclusion, and a conclusion authored as a rule diff --git a/dag/gunbc/extdeps_scope_frontier.dag b/dag/gunbc/extdeps_scope_frontier.dag index f25bb6eefaf..8ea858bc542 100644 --- a/dag/gunbc/extdeps_scope_frontier.dag +++ b/dag/gunbc/extdeps_scope_frontier.dag @@ -124,6 +124,9 @@ fn scope_placement_refused_paths(added: List, carriers: List, ma } data scope_carrier_paths: List = [ + "dag/extdeps/firmware/types.dag", + "dag/extdeps/memory/training.dag", + "dag/extdeps/ampere/scp_diagnostic.dag", "dag/extdeps/ipc/dpmx.dag", "dag/extdeps/ucamco/gerber.dag", "dag/extdeps/cpu/ampere_altra_package.dag", diff --git a/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag index 56bcc7419e0..2d47b586719 100644 --- a/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag +++ b/dag/test/claim/firmware_applicability_and_loop_cause_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.firmware_applicability_and_loop_cause_witness_test -import std.types { Bool, Present, Absent } +import std.types { Bool } +import v2.std.optional { Present, Absent } import extdeps.firmware.types { BmcController, FirmwareApplicable, From 7a75586b406792f9934c428821b3483ba5b2e3ce Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 05:08:17 +0000 Subject: [PATCH 17/28] Drop two dead surface imports from the live-receipts witness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51322, verified: srv3_bmc_redfish_surface and srv4_bmc_redfish_surface appear only on their own import lines in bmc_live_receipts_witness_test, left behind when the per-host surface assertions were replaced by the derivation. Removing these is NOT the case I declined on the earlier PR citing the #6985 Class B hazard, and the distinction is worth stating since I invoked that rule already. Class B is about a stripped file's bare cross-module references resolving only by pool-membership coincidence — the danger is dropping a MODULE from the assembled closure. Here gunbc.fleet_bmc_observation stays imported via ten other symbols, so the closure is unchanged and only two dead names go. The derivation itself keeps its real consumers in bmc_firmware_thermal_witness_test, which is where the surface is actually asserted. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/test/claim/bmc_live_receipts_witness_test.dag | 2 -- 1 file changed, 2 deletions(-) diff --git a/dag/test/claim/bmc_live_receipts_witness_test.dag b/dag/test/claim/bmc_live_receipts_witness_test.dag index bdffd1811c9..d67bcbe0809 100644 --- a/dag/test/claim/bmc_live_receipts_witness_test.dag +++ b/dag/test/claim/bmc_live_receipts_witness_test.dag @@ -28,7 +28,6 @@ import extdeps.filesystem.linux { } import gunbc.fleet_bmc_observation { srv3_bmc_firmware_observation, - srv3_bmc_redfish_surface, srv3_bmc_update_service_observation, srv4_bmc_config_persistence_observation, srv4_bmc_fan_active_config_digest, @@ -37,7 +36,6 @@ import gunbc.fleet_bmc_observation { srv4_bmc_fan_observed_curve, srv4_bmc_fan_perturbation_generation, srv4_bmc_firmware_observation, - srv4_bmc_redfish_surface, srv4_bmc_update_service_observation, } import gunbc.fleet_bmc_state { srv4_bmc_fan_reboot_persistence_receipt } From 8c7335ad1ad2309ba985b5944fe7e18f9a3d1605 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 05:10:08 +0000 Subject: [PATCH 18/28] Make population admission true multiset equality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51323. The finding as stated does not reproduce, and the defect underneath it is real but mirrored — both worth recording. NOT REPRODUCED: the cited candidate ["A1","A1","B1","E1"] is REFUSED today. population_slots_match folds `a` over `b`, and the call site passes a = documented row, b = candidate — so the subset test runs documented-into-candidate, not candidate-into-documented. A duplicated candidate slot shrinks its distinct set, so a same-length documented row with four distinct slots cannot be covered. Simulated over all four documented rows before changing anything. REAL, AND LATENT: the guard was equal-length-plus-subset, not multiset equality, and it relied on an unstated precondition that no DOCUMENTED row carries a duplicate. Violate that and it fails in the opposite direction from the report: documented ["A1","A1"] admits candidate ["A1","B1"], since both documented entries are found and lengths agree. No board row has a duplicate today, which is why it was latent — and why leaving correctness resting on that staying true is the wrong place to stop. Now compares occurrence counts, which is multiset equality given equal lengths and removes the precondition. Three controls: the duplicated candidate stays refused (the property callers rely on, previously true only incidentally); a synthetic duplicated documented row does NOT admit a distinct pair — the case that was actually broken; and that same row still admits its own multiset, so the refusal is discrimination rather than a blanket rejection of duplicated rows. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/boards/types.dag | 24 +++++++++- ...host_memory_qualification_witness_test.dag | 47 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/dag/extdeps/boards/types.dag b/dag/extdeps/boards/types.dag index 7b8f4685208..c85dde76a96 100644 --- a/dag/extdeps/boards/types.dag +++ b/dag/extdeps/boards/types.dag @@ -1,6 +1,6 @@ module extdeps.boards.types -import std.types { Bool, List, NonEmptyStr } +import std.types { Bool, Int, List, NonEmptyStr } import std.bmc { BmcFirmwareFamily, BmcEndpoint } import extdeps.vendor { Vendor } import extdeps.hardware { Hardware } @@ -42,9 +42,29 @@ type PopulationAdmission = PopulationSupported | PopulationUnsupported { slots: List } +fn slot_occurrence_count(slots: List, slot: NonEmptyStr) -> Int { + fold(slots, init: 0, f: (acc, s) => if s == slot { acc + 1 } else { acc }) +} + +// MULTISET EQUALITY, not equal-length-plus-subset. The previous form folded +// the DOCUMENTED row over the candidate, which happens to refuse a duplicated +// candidate slot — a duplicate shrinks the candidate's distinct set, so the +// subset test fails. But it relied on an unstated precondition that no +// documented row carries a duplicate: documented ["A1","A1"] would admit +// candidate ["A1","B1"], since both documented entries are found and the +// lengths agree. No documented row has a duplicate today, so this was latent +// rather than live; comparing occurrence counts removes the precondition +// instead of leaving correctness resting on it. fn population_slots_match(a: List, b: List) -> Bool { count(a) == count(b) - && fold(a, init: true, f: (acc, s) => acc && any(b, p => p == s)) + && fold( + a, + init: true, + f: (acc, s) => + acc + && slot_occurrence_count(slots: a, slot: s) + == slot_occurrence_count(slots: b, slot: s), + ) } // Fail-closed: a population absent from the documented list is REFUSED rather diff --git a/dag/test/claim/host_memory_qualification_witness_test.dag b/dag/test/claim/host_memory_qualification_witness_test.dag index 1f05637a910..7bcf5bdaefe 100644 --- a/dag/test/claim/host_memory_qualification_witness_test.dag +++ b/dag/test/claim/host_memory_qualification_witness_test.dag @@ -151,3 +151,50 @@ test fn boot_progress_read_is_not_a_snapshot() -> Bool { ampere_smpro_boot_progress_interface.read_effect == ReadAcknowledgesBootProgress && ampere_smpro_boot_progress_interface.read_effect != ReadSnapshot } + +// A duplicated slot is refused. This passed under the previous +// equal-length-plus-subset form too, but for an incidental reason — a +// duplicate shrinks the candidate's distinct set, so the subset test failed. +// It is kept because the property is the one callers rely on. +test fn a_duplicated_candidate_slot_is_refused() -> Bool { + match admit_population( + candidate: ["A1", "A1", "B1", "E1"], + documented: asrock_altrad8ud_documented_dimm_populations, + ) { + PopulationUnsupported { slots: _ } => true + PopulationSupported => false + } +} + +// THE CONTROL FOR THE CASE THAT WAS ACTUALLY BROKEN. Under equal-length-plus- +// subset, a DOCUMENTED row carrying a duplicate would admit a candidate that +// merely covers its distinct slots: ["A1","A1"] admitted ["A1","B1"], because +// both documented entries are found and the lengths agree. Multiset equality +// refuses it. Synthetic on purpose — no real board row has a duplicate, which +// is exactly why the defect was latent and why the guard must not depend on +// that staying true. +data duplicated_documented_population: DimmPopulation = DimmPopulation { + board: AsrockRackAltrad8ud1l2t, + slots: ["A1", "A1"], + authority: ampere_altra_avl_authority, +} + +test fn a_duplicated_documented_row_does_not_admit_a_distinct_pair() -> Bool { + match admit_population( + candidate: ["A1", "B1"], + documented: [duplicated_documented_population], + ) { + PopulationUnsupported { slots: _ } => true + PopulationSupported => false + } +} + +// Positive control on the same synthetic row: the multiset it really names is +// still admitted, so the refusal above is multiset discrimination rather than +// a blanket rejection of duplicated rows. +test fn a_duplicated_documented_row_still_admits_its_own_multiset() -> Bool { + admit_population( + candidate: ["A1", "A1"], + documented: [duplicated_documented_population], + ) == PopulationSupported +} From 3da7e58c25bed580e2861718981405515fc1fb35 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 05:19:22 +0000 Subject: [PATCH 19/28] Flatten the multiset comparison onto one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse error I shipped in the previous commit: expected RParen, found EqEq at dag/extdeps/boards/types.dag. The comparison was written with a line break BEFORE the ==, inside fold's argument list, so the expression terminated at the newline and the parser then wanted the arg list to close. The corpus does carry `&& x == 5` unparenthesized, so the operator combination was never the problem — the continuation position was. Leading && and || continuations at statement level are used throughout and parse fine; a leading == inside a call's argument list is the shape that fails. Now one line, parenthesized, matching the style of slot_occurrence_count directly above it. Swept the rest of the change for the same shape: this was the only occurrence. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/boards/types.dag | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dag/extdeps/boards/types.dag b/dag/extdeps/boards/types.dag index c85dde76a96..1bb6c9879c0 100644 --- a/dag/extdeps/boards/types.dag +++ b/dag/extdeps/boards/types.dag @@ -60,10 +60,7 @@ fn population_slots_match(a: List, b: List) -> Bool { && fold( a, init: true, - f: (acc, s) => - acc - && slot_occurrence_count(slots: a, slot: s) - == slot_occurrence_count(slots: b, slot: s), + f: (acc, s) => acc && (slot_occurrence_count(slots: a, slot: s) == slot_occurrence_count(slots: b, slot: s)), ) } From 3799d4fc7fcf036878e6bcd68e397a7702b6a9f2 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 06:36:50 +0000 Subject: [PATCH 20/28] Stop conflating admitted with undominated in the frontier witness MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The witness floor caught a real error, and it was in my test design rather than in the model: an_association_leaves_both_candidates_on_the _frontier returned false because both fixtures carried the SAME price and capacity, leaving data rate as the only difference. The registered module therefore strictly DOMINATED the load-reduced one on the Pareto axes and the frontier correctly dropped it — I read that dominance as an admission failure. Admitted and undominated are different properties, and a fixture pair differing on exactly one axis cannot distinguish them. The pair now differs on two: the load-reduced part is cheaper and slower, the registered part dearer and faster, so neither dominates and frontier membership is decided by admission alone — which is what the test claims to check. That also matches the market fact the module models, where the part near the density ceiling is the cheap one. Verified both frontier assertions by simulating the dominance relation over the new fixtures before pushing: association -> 2, decoded -> 1. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- .../memory_part_selection_witness_test.dag | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/dag/test/claim/memory_part_selection_witness_test.dag b/dag/test/claim/memory_part_selection_witness_test.dag index 9853cbfdb81..2cd741f3d82 100644 --- a/dag/test/claim/memory_part_selection_witness_test.dag +++ b/dag/test/claim/memory_part_selection_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.memory_part_selection_witness_test import std.types { Bool, List, list_length } +import std.nat { Nat } import std.currency { CurrencyCode, Eur, Usd } import std.measure { ByteSize, byte_size, money_amount_micro } import extdeps.vendor.samsung { samsung } @@ -86,19 +87,47 @@ data unobserved_failure: PlatformTrainingFailure = PlatformTrainingFailure { observed_at: 1786590000000, } -fn candidate_of( +// PRICE IS A PARAMETER because the Pareto axes are real. An earlier revision +// gave both fixtures the same price and capacity, leaving data rate as the +// only difference — so the registered module strictly DOMINATED the +// load-reduced one and the frontier legitimately dropped it. The test then +// read that dominance as an admission failure. Admitted and undominated are +// different properties, and a fixture pair that differs on exactly one axis +// cannot distinguish them. +// +// The prices below also match the market fact this module models: at +// capacities near the density ceiling the load-reduced part is the CHEAP one. +// So neither candidate dominates — cheaper-but-slower against +// faster-but-dearer — and frontier membership is decided by admission alone. +fn candidate_at_price( module: DramModuleCatalogRow, currency: CurrencyCode, + unit_price_micros: Nat, ) -> PartCandidate { PartCandidate { module: module, - unit_price: money_amount_micro(count: 100000000), + unit_price: money_amount_micro(count: unit_price_micros), currency: currency, sticks_required: 8, qualification: PartQualificationUnobserved, } } +fn cheap_lrdimm_candidate(currency: CurrencyCode) -> PartCandidate { + candidate_at_price(module: lrdimm_32g, currency: currency, unit_price_micros: 60000000) +} + +fn dearer_rdimm_candidate(currency: CurrencyCode) -> PartCandidate { + candidate_at_price(module: rdimm_32g, currency: currency, unit_price_micros: 100000000) +} + +fn candidate_of( + module: DramModuleCatalogRow, + currency: CurrencyCode, +) -> PartCandidate { + candidate_at_price(module: module, currency: currency, unit_price_micros: 100000000) +} + // WHAT AN ASSOCIATION LICENSES. gunbc holds MREP at // AssociatedByIdenticalSignature, so another load-reduced module is a // buy-one-and-test candidate — NOT a refusal. An earlier revision answered @@ -194,8 +223,8 @@ test fn same_candidate_in_declared_currency_is_admissible() -> Bool { test fn an_association_leaves_both_candidates_on_the_frontier() -> Bool { let front = pareto_frontier( candidates: [ - candidate_of(module: lrdimm_32g, currency: Usd), - candidate_of(module: rdimm_32g, currency: Usd), + cheap_lrdimm_candidate(currency: Usd), + dearer_rdimm_candidate(currency: Usd), ], failures: observed_failures, declared_currency: Usd, @@ -203,7 +232,7 @@ test fn an_association_leaves_both_candidates_on_the_frontier() -> Bool { ) list_length(front) == 2 && match candidate_admission( - candidate: candidate_of(module: lrdimm_32g, currency: Usd), + candidate: cheap_lrdimm_candidate(currency: Usd), failures: observed_failures, declared_currency: Usd, target_board: AsrockRackAltrad8ud1l2t, @@ -218,8 +247,8 @@ test fn an_association_leaves_both_candidates_on_the_frontier() -> Bool { test fn a_decoded_refusal_does_drop_the_candidate_from_the_frontier() -> Bool { let front = pareto_frontier( candidates: [ - candidate_of(module: lrdimm_32g, currency: Usd), - candidate_of(module: rdimm_32g, currency: Usd), + cheap_lrdimm_candidate(currency: Usd), + dearer_rdimm_candidate(currency: Usd), ], failures: [decoded_failure], declared_currency: Usd, @@ -227,7 +256,7 @@ test fn a_decoded_refusal_does_drop_the_candidate_from_the_frontier() -> Bool { ) list_length(front) == 1 && match candidate_admission( - candidate: candidate_of(module: lrdimm_32g, currency: Usd), + candidate: cheap_lrdimm_candidate(currency: Usd), failures: [decoded_failure], declared_currency: Usd, target_board: AsrockRackAltrad8ud1l2t, From 48e48b353b59b68d9861f153d73c370cb791302f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 06:48:24 +0000 Subject: [PATCH 21/28] Take the money carrier at the helper's parameter, not inside it review 51333, verified: candidate_at_price took unit_price_micros as a bare Nat and wrapped it in money_amount_micro internally, so the unit lived inside the helper rather than at its boundary. Every caller was free to pass a raw number meaning anything. Now the parameter is MoneyAmountMicro and callers construct the carrier at the call site. Same class as the earlier ByteSize / MoneyAmountMicro / Second fixes in this stack; I introduced this one two commits ago while fixing the frontier fixtures and reached for a scalar out of habit, which is precisely why the rule is worth having as a wall rather than a preference. std.nat is dropped from this witness: Nat became unused with the parameter retyped, and I had added that import in the same commit that introduced the scalar, so this returns the file to its prior import set rather than stripping anything pre-existing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- .../memory_part_selection_witness_test.dag | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/dag/test/claim/memory_part_selection_witness_test.dag b/dag/test/claim/memory_part_selection_witness_test.dag index 2cd741f3d82..73c50c6d656 100644 --- a/dag/test/claim/memory_part_selection_witness_test.dag +++ b/dag/test/claim/memory_part_selection_witness_test.dag @@ -1,9 +1,8 @@ module test.claim.memory_part_selection_witness_test import std.types { Bool, List, list_length } -import std.nat { Nat } import std.currency { CurrencyCode, Eur, Usd } -import std.measure { ByteSize, byte_size, money_amount_micro } +import std.measure { ByteSize, MoneyAmountMicro, byte_size, money_amount_micro } import extdeps.vendor.samsung { samsung } import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.memory.types { @@ -102,11 +101,11 @@ data unobserved_failure: PlatformTrainingFailure = PlatformTrainingFailure { fn candidate_at_price( module: DramModuleCatalogRow, currency: CurrencyCode, - unit_price_micros: Nat, + unit_price: MoneyAmountMicro, ) -> PartCandidate { PartCandidate { module: module, - unit_price: money_amount_micro(count: unit_price_micros), + unit_price: unit_price, currency: currency, sticks_required: 8, qualification: PartQualificationUnobserved, @@ -114,18 +113,30 @@ fn candidate_at_price( } fn cheap_lrdimm_candidate(currency: CurrencyCode) -> PartCandidate { - candidate_at_price(module: lrdimm_32g, currency: currency, unit_price_micros: 60000000) + candidate_at_price( + module: lrdimm_32g, + currency: currency, + unit_price: money_amount_micro(count: 60000000), + ) } fn dearer_rdimm_candidate(currency: CurrencyCode) -> PartCandidate { - candidate_at_price(module: rdimm_32g, currency: currency, unit_price_micros: 100000000) + candidate_at_price( + module: rdimm_32g, + currency: currency, + unit_price: money_amount_micro(count: 100000000), + ) } fn candidate_of( module: DramModuleCatalogRow, currency: CurrencyCode, ) -> PartCandidate { - candidate_at_price(module: module, currency: currency, unit_price_micros: 100000000) + candidate_at_price( + module: module, + currency: currency, + unit_price: money_amount_micro(count: 100000000), + ) } // WHAT AN ASSOCIATION LICENSES. gunbc holds MREP at From cfe597ea0d81f45f33d09acd57a8d5b64c32e14a Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 07:08:51 +0000 Subject: [PATCH 22/28] Import Present/Absent explicitly in host_boot_supervision MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 51335 asked for this on the ground that the file "has unresolved constructors and won't compile". That reasoning is wrong, and the check is cheap: host_boot_loop_cause executed GREEN in CI run 31566167444 — all three of its witnesses passed, including the arms that match Present and Absent. Bare Absent resolves in this corpus without the import; extdeps.bmc.capability and bmc_fan_converge_witness_test both do the same and are green on main. Taking the change anyway, because it is right for the OTHER reason. That resolution works by pool-membership coincidence — the target module is in the assembled closure only because some unrelated import elsewhere dragged it there — which is exactly the #6985 Class B hazard DESIGN records, and the remedy that thread names is making the binding closure-independent with an explicit import. asrock_live_probe_witness carries the same repair for the same reason. So this is not a no-op: it removes a dependency on accidental closure membership that would break the next time an unrelated lane changes its imports. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/gunbc/host_boot_supervision.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/dag/gunbc/host_boot_supervision.dag b/dag/gunbc/host_boot_supervision.dag index 00e38b63ef4..7e78b46c872 100644 --- a/dag/gunbc/host_boot_supervision.dag +++ b/dag/gunbc/host_boot_supervision.dag @@ -3,6 +3,7 @@ module gunbc.host_boot_supervision import std.types { Bool, EpochMs, List, NonEmptyStr } import std.nat { Nat } import std.measure { Second } +import v2.std.optional { Present, Absent } import product.placement_supply { HostIdentity } // TWO INDEPENDENT SUPERVISORS CAN RESET A HOST, and the fleet modeled From 51cc0cf2b48bdf4c395c59258b0bf243a9a688f1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Wed, 12 Aug 2026 10:09:42 +0000 Subject: [PATCH 23/28] Fix the parse error CI found, and make the order control discriminating CI's located cause: expected expression, found EqEq in firmware_prerequisite_evidence_witness_test.dag. A line break before == terminates the expression, so the operator began a new statement. Same class as the earlier fold-argument break; leading && is fine, leading == is not, so the comparison is now one line. This file had never been parsed by CI. It reached main's floor only because #8171 merged and GitHub retargeted this PR, which is what a stacked branch buys: approvals on a branch that no required check was ever configured to run. Reading the failing test to fix it showed it did not test what it claimed. It compared two orderings of a two-element list holding ONE refusal and ONE acceptance -- an ordering over which first-wins and max/min agree, so it could not distinguish them, though its comment said that was its whole purpose. Both lists now carry a second, lower refusal at 1.00.00: a first-wins fold reports 1.00.00 on the reversed ordering and reds. Simulated in both directions before pushing; the expected bounds are held in one helper so a disagreeing ordering cannot be absorbed by a second copy of the numbers. Also drops FirmwareSemanticVersion, unused here before this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- ...are_prerequisite_evidence_witness_test.dag | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag index 643f0589035..e7c1a8db2f8 100644 --- a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -2,10 +2,14 @@ module test.claim.firmware_prerequisite_evidence_witness_test import std.types { Bool, List } import v2.std.optional { Present, Absent } +import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.firmware.types { + BmcController, FirmwarePrerequisiteObservation, - FirmwareSemanticVersion, + HostBios, NoFirmwarePrerequisite, + OpenBmcWebUiTar, + PushRefusedByTarget, RequiresMinimumTargetFirmware, PrerequisiteConsistentUpperBound, PrerequisiteEvidenceVerdict, @@ -25,8 +29,30 @@ import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// A SECOND, LOWER REFUSAL, authored rather than observed — the fixture that +// makes order-independence testable at all. With one refusal and one +// acceptance a first-wins accumulator and a real max/min return the SAME +// answer, so a two-element list cannot distinguish them however it is +// ordered. Only a second refusal forces the fold to choose. +data synthetic_lower_refusal: FirmwarePrerequisiteObservation = FirmwarePrerequisiteObservation { + board: AsrockRackAltrad8ud1l2t, + subject: HostBios, + image_kind: OpenBmcWebUiTar, + gating_target: BmcController, + gating_version: firmware_semantic_version(major: 1, minor: 0, patch: 0), + outcome: PushRefusedByTarget { diagnostic: "authored probe; no such push was executed" }, + observed_at: 1786579200000, +} + +data forward_evidence: List = [ + asrock_altrad8ud_bios_310_push_refused_at_bmc_207, + synthetic_lower_refusal, + asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, +] + data reversed_evidence: List = [ asrock_altrad8ud_bios_310_push_accepted_at_bmc_322, + synthetic_lower_refusal, asrock_altrad8ud_bios_310_push_refused_at_bmc_207, ] @@ -51,13 +77,14 @@ fn verdict_against_declared_minimum( } } -// The executed pair supports the declared minimum, and the positive arm says -// what it actually establishes: an UPPER BOUND. 3.06.00 exists in the -// capability catalog and was never pushed, so the true threshold may be lower. -test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool { - match verdict_against_declared_minimum( - observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, - ) { +// The bounds the EXECUTED pair establishes: refused at 2.07.00, accepted at +// 3.22.00. Held once and applied to several orderings below, so an ordering +// that answered differently could not be papered over by a second copy of the +// expected numbers. +fn bounds_are_the_executed_pair( + observations: List, +) -> Bool { + match verdict_against_declared_minimum(observations: observations) { PrerequisiteConsistentUpperBound { highest_refusal: r, lowest_acceptance: a } => match r { Present { value: rv } => @@ -72,15 +99,22 @@ test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool } } -// ORDER INDEPENDENCE. The same observations authored in the opposite order -// must produce the same bounds. The previous accumulator kept whichever bound -// it saw first and discarded the rest, so this control is what distinguishes -// a real max/min from a first-wins fold that greens on one authoring order. +// The executed pair supports the declared minimum, and the positive arm says +// what it actually establishes: an UPPER BOUND. 3.06.00 exists in the +// capability catalog and was never pushed, so the true threshold may be lower. +test fn executed_pair_supports_the_declared_minimum_as_an_upper_bound() -> Bool { + bounds_are_the_executed_pair(observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence) +} + +// ORDER INDEPENDENCE, and it took a third observation to make it real. The +// claim is that the accumulator takes the HIGHEST refusal rather than +// whichever it met first. An earlier revision compared two orderings of the +// two-element executed pair, which cannot decide that: with exactly one +// refusal in the list, first-wins and max return the same version whatever +// the order. Both lists below carry a second, LOWER refusal at 1.00.00, so a +// first-wins fold reports 1.00.00 on the reversed ordering and fails here. test fn the_bounds_do_not_depend_on_authoring_order() -> Bool { - verdict_against_declared_minimum(observations: reversed_evidence) - == verdict_against_declared_minimum( - observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, - ) + bounds_are_the_executed_pair(observations: forward_evidence) && bounds_are_the_executed_pair(observations: reversed_evidence) } // A bound with no observation behind it is ABSENT, never filled in from the From b28b73cab8933809f2db46669e140e87d2348f37 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 13 Aug 2026 04:59:47 +0000 Subject: [PATCH 24/28] Reclassify srv3's install path as unestablished, not refused-forever CI's 13 reds were one derivation, and they were the tripwire in os_install_mechanism working exactly as written: "if srv3's BMC is upgraded and its evidence row follows, this witness reds instead of the fleet silently planning a delivery mechanism the firmware no longer implements." srv3 was measured live on BMC 3.22.00, BIOS 3.10, host Running; re-binding its firmware standing to that made the tripwire fire. WHAT THESE WITNESSES NOW SAY, and what they deliberately do not. The catalogued 3.22.00 row lists neither Redfish virtual media nor the bmcweb nbd-proxy, and no host has an established network-boot delivery, so the solver has nothing to select and refuses with InstallNoVirtualMediaAndNetworkBootUnestablished. That cause names UNESTABLISHED. It is NOT a claim that the host cannot be installed -- srv4 runs this same firmware and was installed over the configfs USB-gadget transport, which extdeps.bmc.virtual_media already models as ConfigfsGadgetTransport and which this solver cannot see because OsInstallMechanism predates that authority. WHAT I DID NOT DO, having started it and backed it out. The first attempt added CapabilityConfigfsVirtualMedia plus a matching OsInstallMechanism variant. That would have minted a third name for a transport VirtualMediaTransport already names -- the section 3 nicknaming violation -- and it surfaced that CapabilityNbdProxyVirtualMedia is already that same fork for NbdProxyWebsocatTransport. Widening a fork to turn CI green is the wrong trade. Reverted whole; the real repair is the install lane consuming VirtualMediaIntent/VirtualMediaTransport, which is INSTALL-0, not this PR. Each rewritten witness keeps its original load-bearing claim rather than being flipped to expect a refusal: no silent PXE fallback -- previously "is nbd-proxy, no fallback"; the fallback arm is now exactly where a defective solver would land, so refusing is the proof it does not plan is solver-derived -- a hardcoded plan would still be Ready after the firmware change; both halves refusing is strictly more discriminating than the old version, which could not tell a derived plan from a hardcoded one while the solve agreed with it boot target follows mechanism -- the mapping is pinned on the mechanisms directly, where the claim always lived prep is not install -- credential wiring and the seeded ISO path hold; the plan and seed halves refuse with the solve, so nothing here can read as a solved install srv4 has none -- srv3 has now joined it; both hosts run 3.22.00 and refuse with the same cause, so asserting a difference would assert a fact that stopped being true Also corrects srv3_install_mechanism_board_default_residue, which still said the legacy row "agrees TODAY" and cited a witness by its old name -- a stale citation in the note that predicted this very divergence. The target facts the plan witnesses used to assert are not lost: they are what INSTALL-0 must reproduce once srv3 has an established path, at which point those witnesses red and are replaced rather than silently passing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/gunbc/host_standup.dag | 15 +++-- dag/gunbc/os_install_mechanism.dag | 2 +- dag/gunbc/srv3_os_install_actuate.dag | 10 ++- dag/gunbc/srv3_os_install_actuate_scope.dag | 45 ++++++++----- ...vidence_install_mechanism_witness_test.dag | 52 ++++++++++----- .../bmc_onboarding_lifecycle_witness_test.dag | 17 ++++- .../claim/host_standup_spine_witness_test.dag | 2 +- ...oxy_virtual_media_install_witness_test.dag | 36 +++++----- ..._os_install_actuate_scope_witness_test.dag | 28 ++++---- .../srv3_os_install_actuate_witness_test.dag | 19 +++--- .../claim/srv3_os_install_witness_test.dag | 65 ++++++++----------- 11 files changed, 165 insertions(+), 126 deletions(-) diff --git a/dag/gunbc/host_standup.dag b/dag/gunbc/host_standup.dag index 7cd3b3e7f5c..64a5b282739 100644 --- a/dag/gunbc/host_standup.dag +++ b/dag/gunbc/host_standup.dag @@ -648,13 +648,16 @@ fn assimilation_complete_all_satisfied( } } -fn srv3_install_mechanism_is_nbd_proxy_no_fallback() -> Bool { +// NO SILENT FALLBACK — the same claim as before, now carried by the refusal. +// This read "is nbd-proxy, no fallback" while srv3 ran 2.07.00; the load-bearing +// half was always the second one, that a solver finding no virtual media must not +// quietly answer PXE. srv3 on 3.22.00 has no catalogued delivery mechanism, so +// the fallback arm is exactly where a defective solver would land — and refusing +// is the proof it does not. A Selected of any kind here means the fallback fired. +fn srv3_install_mechanism_refuses_rather_than_falling_back() -> Bool { match srv3_install_mechanism_solution { - OsInstallMechanismSelected { mechanism: NbdProxyVirtualMediaInstall } => true - OsInstallMechanismSelected { mechanism: VirtualMediaInstall } => false - OsInstallMechanismSelected { mechanism: PxeHttpInstall } => false - OsInstallMechanismSelected { mechanism: FirmwareUpdateThenVirtualMedia } => false - OsInstallMechanismRefused { cause: _ } => false + OsInstallMechanismRefused { cause: _ } => true + OsInstallMechanismSelected { mechanism: _ } => false } } diff --git a/dag/gunbc/os_install_mechanism.dag b/dag/gunbc/os_install_mechanism.dag index 690ab7d9d02..ba61ed995a1 100644 --- a/dag/gunbc/os_install_mechanism.dag +++ b/dag/gunbc/os_install_mechanism.dag @@ -301,7 +301,7 @@ data srv4_install_mechanism_solution: OsInstallMechanismSolution = solve_install firmware_transition: fleet_firmware_transition_standing ) -data srv3_install_mechanism_board_default_residue: String = "🟡 dissolve-on: srv3_install_mechanism is the last pre-repair row reading the BOARD's shipped-firmware capability row (asrock_altrad8ud_shipped_bmc_firmware_capabilities) instead of a host's own firmware evidence. Its consumer set is now exactly ONE: gunbc.os_install fleet_install_server_specs, which feeds gunbc.generated_artifact — the generated-artifact registry is a total List with no representation for a producer refusal, so migrating that last row requires the registry itself to become a standing (GENERATED-REGISTRY-0). srv3_install_boot_target was DELETED with this note's narrowing: gunbc.bmc_onboarding was its last consumer, and it did not need it — BmcOnboardingPlan carried os_install_boot_target while no production consumer read it, which is how srv4 came to carry srv3's board-default CD target despite having no derived install path at all. The remaining row agrees with srv3_install_mechanism_solution TODAY only because srv3's observed firmware equals the board's shipped version; srv3_legacy_row_agrees_with_evidence_derived_solution executes that coincidence so it cannot rot silently, and there is deliberately no srv4 legacy row because it would be wrong. DISSOLVES WHEN the registry becomes a standing and fleet_install_server_specs takes the evidence-derived solution; at that point the row, the coincidence witness and this note are all DELETED." +data srv3_install_mechanism_board_default_residue: String = "🟡 dissolve-on: srv3_install_mechanism is the last pre-repair row reading the BOARD's shipped-firmware capability row (asrock_altrad8ud_shipped_bmc_firmware_capabilities) instead of a host's own firmware evidence. Its consumer set is now exactly ONE: gunbc.os_install fleet_install_server_specs, which feeds gunbc.generated_artifact — the generated-artifact registry is a total List with no representation for a producer refusal, so migrating that last row requires the registry itself to become a standing (GENERATED-REGISTRY-0). srv3_install_boot_target was DELETED with this note's narrowing: gunbc.bmc_onboarding was its last consumer, and it did not need it — BmcOnboardingPlan carried os_install_boot_target while no production consumer read it, which is how srv4 came to carry srv3's board-default CD target despite having no derived install path at all. The remaining row NO LONGER agrees with srv3_install_mechanism_solution, and that is this note's own prediction coming true rather than a new defect: it reads the board's shipped 2.07.00 capabilities and still answers NbdProxyVirtualMediaInstall, while srv3's BMC is now 3.22.00 and the evidence-derived solution refuses with InstallNoVirtualMediaAndNetworkBootUnestablished. legacy_board_default_row_no_longer_agrees_with_evidence executes that divergence so the stale row cannot be read as a current plan, and there is deliberately no srv4 legacy row because it would be wrong. The refusal is UNESTABLISHED, not impossible: srv4 runs this same firmware and was installed over the configfs USB-gadget transport that extdeps.bmc.virtual_media models as ConfigfsGadgetTransport, which this solver cannot select because OsInstallMechanism predates that authority — teaching it to consume VirtualMediaTransport is the INSTALL-0 repair. DISSOLVES WHEN the registry becomes a standing and fleet_install_server_specs takes the evidence-derived solution; at that point the row, the coincidence witness and this note are all DELETED." data srv3_install_mechanism: OsInstallMechanism = solve_install_mechanism( current: asrock_altrad8ud_shipped_bmc_firmware_capabilities, diff --git a/dag/gunbc/srv3_os_install_actuate.dag b/dag/gunbc/srv3_os_install_actuate.dag index 24f5648a7ab..afaa1a22de2 100644 --- a/dag/gunbc/srv3_os_install_actuate.dag +++ b/dag/gunbc/srv3_os_install_actuate.dag @@ -113,10 +113,14 @@ func srv3_nbd_proxy_serve() -> ProcessExit ) } -fn srv3_os_install_actuator_step_count_is_three() -> Bool { +// The step count was never the subject — that the plan is DERIVED from the solved +// mechanism was. srv3's BMC upgrade propagates through that derivation and the +// plan now refuses, which is the observable that distinguishes a derived plan +// from a hardcoded three-step one: a hardcoded plan would still be Ready. +fn srv3_os_install_actuator_plan_refuses_on_unestablished_path() -> Bool { match srv3_os_install_actuator_plan_standing { - ActuatorPlanReady { plan: p, intents: _ } => os_install_actuator_step_count(plan: p) == 3 - ActuatorPlanRefused { cause: _ } => false + ActuatorPlanRefused { cause: _ } => true + ActuatorPlanReady { plan: _, intents: _ } => false } } diff --git a/dag/gunbc/srv3_os_install_actuate_scope.dag b/dag/gunbc/srv3_os_install_actuate_scope.dag index a4f839b1ec7..712bbfa8ab0 100644 --- a/dag/gunbc/srv3_os_install_actuate_scope.dag +++ b/dag/gunbc/srv3_os_install_actuate_scope.dag @@ -67,13 +67,27 @@ fn srv3_autoinstall_seed_delivery_refusal() -> AutoinstallSeedDeliveryRefusal? { } } -fn srv3_autoinstall_seed_gap_is_fail_closed() -> Bool { +// THE SEED GAP IS ONLY ASKABLE ONCE A MECHANISM IS SELECTED. This asserted the +// refusal was Present and fail-closed, which held while srv3 solved to +// nbd-proxy. srv3_autoinstall_seed_delivery_refusal answers none when the +// mechanism itself refuses — correctly, because there is no selected path whose +// seed could be unreachable — so on 3.22.00 the honest answer is Absent. Keeping +// the old assertion would have required a seed refusal describing the seed of a +// path that was never selected. +fn srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() -> Bool { match srv3_autoinstall_seed_delivery_refusal() { - Present { value: refusal } => - refusal.install_server_count == 0 - && string_contains(s: refusal.refusal as String, pattern: "fail-closed") - && string_contains(s: refusal.seed_url as String, pattern: "install.srv3.lan") - Absent => false + Absent => true + Present { value: _ } => false + } +} + +// The seeded plan derives from the same solve, so it refuses with it. The +// NoCloudLocal delivery this used to assert is not readable off a plan that does +// not exist, and reading it anyway is the substitution this lane keeps finding. +fn srv3_seeded_plan_refuses_with_the_solve() -> Bool { + match srv3_seeded_os_install_plan_standing { + OsInstallPlanRefused { cause: _ } => true + OsInstallPlanReady { plan: _ } => false } } @@ -106,16 +120,6 @@ fn srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() -> Bool { bmc_srv3_admin_secret_ref.secret == (bmc_srv3_secret_name as NonEmptyStr) } -fn srv3_actuator_prep_uses_seeded_autoinstall_delivery() -> Bool { - match srv3_seeded_os_install_plan_standing { - OsInstallPlanRefused { cause: _ } => false - OsInstallPlanReady { plan: p } => - match srv3_os_install_plan_autoinstall(plan: p).delivery { - NoCloudLocal => true - NoCloudNet { seed_url: _ } => false - } - } -} fn srv3_actuator_prep_binds_seeded_iso_path() -> Bool { srv3_actuator_virtual_media_iso_install_path == srv3_seeded_ubuntu_install_media_install_path @@ -125,10 +129,15 @@ fn srv3_actuator_prep_binds_seeded_iso_path() -> Bool { ) } +// PREP IS NOT INSTALL — still the claim, and now demonstrated more sharply. The +// credential wiring and the seeded ISO path are prep facts and hold regardless of +// whether a delivery mechanism is selectable; the plan and seed halves refuse +// with the solve. That split is exactly the point: prep survives, solving does +// not, so nothing here can be mistaken for a solved install. fn srv3_os_install_actuate_is_actuator_prep_not_install_solved() -> Bool { - srv3_autoinstall_seed_gap_is_fail_closed() + srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() && srv3_os_install_actuate_credential_resolution_is_wired() && srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() - && srv3_actuator_prep_uses_seeded_autoinstall_delivery() + && srv3_seeded_plan_refuses_with_the_solve() && srv3_actuator_prep_binds_seeded_iso_path() } diff --git a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag index 77afa8a82d9..9e71a5eedc9 100644 --- a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag +++ b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag @@ -158,28 +158,48 @@ test fn srv4_evidence_derived_refuses_while_board_default_selects_nbd_proxy() -> } } -test fn srv3_evidence_derived_mechanism_is_nbd_proxy() -> Bool { +// srv3's BMC upgrade to 3.22.00 took its catalogued delivery mechanism with it, +// and this witness says so by execution. The predecessor asserted the mechanism +// IS nbd-proxy; it reddened the moment srv3's evidence row followed the upgrade, +// which is exactly what the tripwire above it was written to do. +// +// READ THE CAUSE, NOT THE ARM. UNESTABLISHED is not impossible. The catalogued +// 3.22.00 row lists neither Redfish virtual media nor the bmcweb nbd-proxy, and +// no host has an established network-boot delivery — so this solver has nothing +// left to select. It does NOT establish that the host cannot be installed: srv4 +// runs this same firmware and was installed over the configfs USB-gadget +// transport, which extdeps.bmc.virtual_media already models as +// ConfigfsGadgetTransport and which this solver cannot see because its +// OsInstallMechanism enum predates that authority. Teaching it to consume +// VirtualMediaTransport is the INSTALL-0 repair; an omitted capability read as an +// observed absence is the defect, not the hardware. +test fn srv3_reinstall_path_is_unestablished_on_3_22_00() -> Bool { match srv3_install_mechanism_solution { - OsInstallMechanismSelected { mechanism: m } => - match m { - NbdProxyVirtualMediaInstall => true - PxeHttpInstall => false - VirtualMediaInstall => false - FirmwareUpdateThenVirtualMedia => false + OsInstallMechanismRefused { cause: c } => + match c { + InstallNoVirtualMediaAndNetworkBootUnestablished { firmware: fw, missing: _ } => + fw.version.major == 3 && fw.version.minor == 22 + _ => false } - OsInstallMechanismRefused { cause: _ } => false + OsInstallMechanismSelected { mechanism: _ } => false } } -// The retained legacy row agrees with the evidence-derived solution for srv3 ONLY because -// srv3's observed firmware equals the board's shipped version. Executing that coincidence is -// what keeps srv3_install_mechanism_board_default_residue honest: if srv3's BMC is upgraded -// and its evidence row follows, this witness reds instead of the fleet silently planning a -// delivery mechanism the firmware no longer implements. -test fn srv3_legacy_row_agrees_with_evidence_derived_solution() -> Bool { +// The retained legacy row NO LONGER agrees with the evidence-derived solution, +// and executing that divergence is the point. srv3_install_mechanism reads the +// board's SHIPPED 2.07.00 capabilities, so it still answers +// NbdProxyVirtualMediaInstall; the evidence-derived solution follows srv3's +// ACTUAL firmware and refuses. Pinning both halves is what stops the stale +// board-default row being read as a current plan — the failure its predecessor +// existed to catch, now caught and held rather than re-greened. +test fn legacy_board_default_row_no_longer_agrees_with_evidence() -> Bool { match srv3_install_mechanism_solution { - OsInstallMechanismSelected { mechanism: m } => m == srv3_install_mechanism - OsInstallMechanismRefused { cause: _ } => false + OsInstallMechanismRefused { cause: _ } => + match srv3_install_mechanism { + NbdProxyVirtualMediaInstall => true + _ => false + } + OsInstallMechanismSelected { mechanism: _ } => false } } diff --git a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag index 8ed91985882..145cf30a8d3 100644 --- a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag +++ b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag @@ -195,10 +195,21 @@ test fn credential_probe_maps_to_correct_phase() -> Bool { // srv3_bmc_credential_resolve and the bmc_onboard tools use bmc / factory_login / secret_name), // so forcing credential-only operations to unwrap an installation plan would have been a second // fusion, not a repair. The boot target now lives with the evidence that derives it. -test fn install_boot_target_is_per_host_and_srv4_has_none() -> Bool { +// srv3 HAS JOINED srv4. This previously read "per-host, and srv4 has none", +// pinning srv3 to a Cd target while srv4 refused — a real difference at the time, +// because srv3 still ran 2.07.00. Both hosts now run 3.22.00 and both refuse with +// the same cause, so asserting a difference would be asserting a fact that +// stopped being true when srv3's BMC was upgraded. The per-host machinery is +// unchanged and still reads each host's own evidence; the two hosts simply agree +// now, and the witness says which state that is rather than which it used to be. +test fn install_boot_target_refuses_alike_for_both_3_22_00_hosts() -> Bool { match srv3_install_boot_target_standing { - InstallBootTargetSelected { target: t } => redfish_boot_target_wire(t: t) == "Cd" - InstallBootTargetRefused { cause: _ } => false + InstallBootTargetRefused { cause: c3 } => + match c3 { + InstallNoVirtualMediaAndNetworkBootUnestablished { firmware: _, missing: _ } => true + _ => false + } + InstallBootTargetSelected { target: _ } => false } && match srv4_install_boot_target_standing { InstallBootTargetRefused { cause: c } => diff --git a/dag/test/claim/host_standup_spine_witness_test.dag b/dag/test/claim/host_standup_spine_witness_test.dag index 113ee531e53..aaf5c3c0510 100644 --- a/dag/test/claim/host_standup_spine_witness_test.dag +++ b/dag/test/claim/host_standup_spine_witness_test.dag @@ -64,7 +64,7 @@ test fn p1b_build_cache_provision_spine_row_landed() -> Bool { test fn srv3_target_and_install_mechanism_are_grounded() -> Bool { srv3_host_standup_target == operator_host_srv3 - && srv3_install_mechanism_is_nbd_proxy_no_fallback() + && srv3_install_mechanism_refuses_rather_than_falling_back() } test fn srv3_live_fleet_offer_carries_ci() -> Bool { diff --git a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index 21b18ac2844..afc0e22bc39 100644 --- a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -3,31 +3,31 @@ module test.claim.nbd_proxy_virtual_media_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -test fn srv3_install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { +// The mapping is the subject here and it is unchanged: a boot target is derived +// from the mechanism, never hardcoded to Pxe. What changed is that srv3 no +// longer REACHES the mapping — its 3.22.00 evidence refuses before a mechanism +// is selected — so the host half asserts the refusal and the mapping is pinned +// on the mechanisms directly, which is where the claim always actually lived. +test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { match srv3_install_mechanism_solution { - OsInstallMechanismRefused { cause: _ } => false - OsInstallMechanismSelected { mechanism: m } => - match m { - NbdProxyVirtualMediaInstall => install_boot_target_for(mechanism: m) == BootTargetCd - PxeHttpInstall => install_boot_target_for(mechanism: m) == BootTargetPxe - VirtualMediaInstall => install_boot_target_for(mechanism: m) == BootTargetCd - FirmwareUpdateThenVirtualMedia => install_boot_target_for(mechanism: m) == BootTargetCd - } + OsInstallMechanismRefused { cause: _ } => true + OsInstallMechanismSelected { mechanism: _ } => false } && install_boot_target_for(mechanism: NbdProxyVirtualMediaInstall) == BootTargetCd && install_boot_target_for(mechanism: PxeHttpInstall) == BootTargetPxe + && install_boot_target_for(mechanism: VirtualMediaInstall) == BootTargetCd + && install_boot_target_for(mechanism: FirmwareUpdateThenVirtualMedia) == BootTargetCd } -test fn srv3_nbd_proxy_actuator_plan_is_solve_driven() -> Bool { +// SOLVE-DRIVEN IS STILL THE CLAIM, and the refusal is what proves it. If the +// plan were hardcoded to nbd-proxy it would still be Ready after srv3's BMC +// upgrade; because it is derived from the solved mechanism, the upgrade +// propagates and the plan refuses. A plan that survived the firmware change +// would be the defect this witness exists to catch. +test fn srv3_actuator_plan_is_solve_driven_and_follows_the_refusal() -> Bool { match srv3_os_install_actuator_plan_standing { - ActuatorPlanRefused { cause: _ } => false - ActuatorPlanReady { plan: p } => - match p.mechanism { - NbdProxyVirtualMediaInstall => os_install_actuator_step_count(plan: p) == 3 - PxeHttpInstall => false - VirtualMediaInstall => false - FirmwareUpdateThenVirtualMedia => false - } + ActuatorPlanRefused { cause: _ } => true + ActuatorPlanReady { plan: _ } => false } } diff --git a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag index 5fa56ae8b8a..3c7e499fdd3 100644 --- a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag @@ -3,16 +3,17 @@ module test.claim.srv3_os_install_actuate_scope data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -test fn srv3_autoinstall_seed_gap_is_explicitly_fail_closed() -> Bool { - match srv3_autoinstall_seed_delivery_refusal() { - Present { value: refusal } => - fleet_install_server_specs.length() == 0 - && refusal.install_server_count == 0 - && string_contains(s: refusal.refusal as String, pattern: "fail-closed") - && string_contains(s: refusal.refusal as String, pattern: "NoCloudNet") - && srv3_autoinstall_seed_gap_is_fail_closed() - Absent => false - } +// The seed gap is not askable while no mechanism is selected, and the +// discriminating half is the conjunction: Absent ALONE would also be produced by +// a selected path whose seed was fine, so the mechanism refusal is asserted +// beside it. A mechanism becoming selectable again makes the refusal Present and +// reds this. +test fn srv3_autoinstall_seed_gap_is_unasked_while_the_mechanism_refuses() -> Bool { + srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() + && match srv3_install_mechanism_solution { + OsInstallMechanismRefused { cause: _ } => true + OsInstallMechanismSelected { mechanism: _ } => false + } } test fn srv3_os_install_actuate_credential_resolution_is_wired_in_scope() -> Bool { @@ -20,8 +21,11 @@ test fn srv3_os_install_actuate_credential_resolution_is_wired_in_scope() -> Boo && srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() } -test fn srv3_actuator_prep_binds_seeded_delivery_and_iso_path() -> Bool { - srv3_actuator_prep_uses_seeded_autoinstall_delivery() +// The ISO-path binding is a prep fact and is unchanged; the delivery half now +// refuses with the solve rather than reporting NoCloudLocal off a plan that does +// not exist. Both are asserted so prep and solving stay visibly separate. +test fn srv3_actuator_prep_binds_iso_path_while_plan_refuses() -> Bool { + srv3_seeded_plan_refuses_with_the_solve() && srv3_actuator_prep_binds_seeded_iso_path() } diff --git a/dag/test/claim/srv3_os_install_actuate_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_witness_test.dag index 3a3e3fa2e9a..261333f37fa 100644 --- a/dag/test/claim/srv3_os_install_actuate_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_witness_test.dag @@ -3,17 +3,16 @@ module test.claim.srv3_os_install_actuate data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -test fn srv3_actuator_plan_has_three_steps() -> Bool { - srv3_os_install_actuator_step_count_is_three() +// Was "has three steps". The count only ever mattered as a proxy for the plan +// being derived from the solved mechanism, and srv3's BMC upgrade now exercises +// that derivation directly: the solve refuses, so the plan refuses with it. A +// plan still offering three nbd-proxy steps for a host whose firmware does not +// implement nbd-proxy is the failure this replaces. +test fn srv3_actuator_plan_follows_the_solve_and_refuses() -> Bool { + srv3_os_install_actuator_plan_refuses_on_unestablished_path() && match srv3_os_install_actuator_plan_standing { - ActuatorPlanRefused { cause: _ } => false - ActuatorPlanReady { plan: p, intents: _ } => - match p.mechanism { - NbdProxyVirtualMediaInstall => true - PxeHttpInstall => false - VirtualMediaInstall => false - FirmwareUpdateThenVirtualMedia => false - } + ActuatorPlanRefused { cause: _ } => true + ActuatorPlanReady { plan: _, intents: _ } => false } } diff --git a/dag/test/claim/srv3_os_install_witness_test.dag b/dag/test/claim/srv3_os_install_witness_test.dag index 315922cd3bb..d86d9a8cab3 100644 --- a/dag/test/claim/srv3_os_install_witness_test.dag +++ b/dag/test/claim/srv3_os_install_witness_test.dag @@ -3,14 +3,15 @@ module test.claim.srv3_os_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -test fn srv3_bootloader_artifact_is_arch_grounded() -> Bool { +// ARCH-GROUNDED is still the claim and is asserted directly on the mapping, +// which is where it lives. The srv3 clause that used to read the architecture off +// a Ready plan is now the plan's refusal: with no delivery mechanism selectable +// on 3.22.00 there is no plan to take an architecture from, and taking one anyway +// would be reading a field off a plan that does not exist. +test fn bootloader_artifact_is_arch_grounded() -> Bool { match srv3_os_install_plan_standing { - OsInstallPlanRefused { cause: _ } => false - OsInstallPlanReady { plan: p } => - match network_uefi_bootloader_artifact(arch: p.host_architecture) { - Present { value: a } => a == "grubnetaa64.efi" - Absent => false - } + OsInstallPlanRefused { cause: _ } => true + OsInstallPlanReady { plan: _ } => false } && match network_uefi_bootloader_artifact(arch: X86_64) { Present { value: x } => x == "grubnetx64.efi" @@ -48,44 +49,32 @@ test fn srv3_survey_forces_proxy_dhcp() -> Bool { // Now asserted through the standing, so a refusal cannot read as a satisfied plan: the plan // arm must be Ready AND its delivery virtual-media, both derived from srv3's firmware evidence. -test fn srv3_plan_is_solver_derived_nbd_proxy_virtual_media() -> Bool { +// SOLVER-DERIVED is the claim, and the two halves moving TOGETHER is the proof. +// A plan hardcoded to nbd-proxy virtual media would still be Ready now that the +// solve refuses; because it is derived, both refuse. Asserting the pair is +// strictly more discriminating than the old version, which could not tell a +// derived plan from a hardcoded one while the solve happened to agree with it. +test fn srv3_plan_is_solver_derived_and_refuses_with_the_solve() -> Bool { match srv3_os_install_plan_standing { - OsInstallPlanRefused { cause: _ } => false - OsInstallPlanReady { plan: p } => - match p.delivery { - VirtualMediaIso { autoinstall: _ } => true - NetworkBoot { delivery: _ } => false - } + OsInstallPlanRefused { cause: _ } => true + OsInstallPlanReady { plan: _ } => false } && match srv3_install_mechanism_solution { - OsInstallMechanismSelected { mechanism: NbdProxyVirtualMediaInstall } => true - OsInstallMechanismSelected { mechanism: VirtualMediaInstall } => false - OsInstallMechanismSelected { mechanism: PxeHttpInstall } => false - OsInstallMechanismSelected { mechanism: FirmwareUpdateThenVirtualMedia } => false - OsInstallMechanismRefused { cause: _ } => false + OsInstallMechanismRefused { cause: _ } => true + OsInstallMechanismSelected { mechanism: _ } => false } && fleet_install_server_specs.length() == 0 } -test fn srv3_plan_targets_ubuntu_2404_aarch64_nbd_proxy_boot_once() -> Bool { +// The plan's TARGET facts — Ubuntu 24.04, Cd, Once, fully-automated seed — are +// not assertable while no plan exists, and this witness now says that instead of +// describing one. They are not lost: they are what the INSTALL-0 lane must +// reproduce once srv3 has an established delivery path again, at which point this +// witness reds and is replaced by the real target assertions rather than +// silently continuing to pass. +test fn srv3_plan_target_facts_are_unassertable_while_the_plan_refuses() -> Bool { match srv3_os_install_plan_standing { - OsInstallPlanRefused { cause: _ } => false - OsInstallPlanReady { plan: p } => - match p.os_target { - UbuntuOs { distro: d } => - match d { - NobleNumbat2404Lts => true - } - WindowsOs { distro: _ } => false - MacosOs { distro: _ } => false - } - && redfish_boot_target_wire(t: p.bmc_boot_target) == "Cd" - && redfish_boot_override_enabled_wire(e: p.bmc_boot_enabled) == "Once" - && srv3_os_install_plan_autoinstall(plan: p).fully_automated - && srv3_os_install_plan_autoinstall(plan: p).autoinstall_version == 1 - && match srv3_os_install_plan_autoinstall(plan: p).delivery { - NoCloudNet { seed_url: _ } => true - NoCloudLocal => false - } + OsInstallPlanRefused { cause: _ } => true + OsInstallPlanReady { plan: _ } => false } } From ab627ddfa6cd26903e758db260cc0aa76105cd31 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 13 Aug 2026 05:40:33 +0000 Subject: [PATCH 25/28] Bisect the annotation gate: strip this branch's new comment blocks DELIBERATELY INFORMATION-SEEKING, not a final state. The compile-clean gate refuses with "source annotation sits inside a declaration body" and carries NO file, line, or declaration name -- 18 identical diagnostics with nothing to locate them by. That is itself a floor gap: section 5 requires a refusal to be typed AND located, and this one is typed only. What I ruled out first, by measurement rather than by guessing, so this strip is a last resort and not a first reflex: brace-depth scan of every changed file, with string literals stripped and comment lines excluded from the count -- zero comments inside any declaration body unattached and blank-separated comment blocks -- none; every block is a standalone leading block immediately above a module-scope declaration em-dashes in comments -- present in 306 files on main double quotes in comments -- present in 132 files on main scope widening -- compile-clean units moved 1316 -> 1319, so the gate is not newly reaching pre-existing corpus files; it passed at 51cc0cf2b and fails here, which places the cause in this commit So the violation is real and mine, and I cannot see it. This strips exactly the 93 comment lines b28b73cab added, across all nine files it touched, to answer one question the log cannot: are the new annotations the cause at all. Green means yes and the prose comes back placed correctly; still red means the diagnostic is pointing somewhere other than my annotations and I look elsewhere. The prose is not lost -- it is in b28b73cab, and restoring it is the next commit either way. Structure re-verified after the strip: every touched file returns to brace depth 0 with no orphaned comment block. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/gunbc/host_standup.dag | 6 ----- dag/gunbc/srv3_os_install_actuate.dag | 4 ---- dag/gunbc/srv3_os_install_actuate_scope.dag | 15 ------------- ...vidence_install_mechanism_witness_test.dag | 22 ------------------- .../bmc_onboarding_lifecycle_witness_test.dag | 7 ------ ...oxy_virtual_media_install_witness_test.dag | 10 --------- ..._os_install_actuate_scope_witness_test.dag | 8 ------- .../srv3_os_install_actuate_witness_test.dag | 5 ----- .../claim/srv3_os_install_witness_test.dag | 16 -------------- 9 files changed, 93 deletions(-) diff --git a/dag/gunbc/host_standup.dag b/dag/gunbc/host_standup.dag index 64a5b282739..4e8d9a6d281 100644 --- a/dag/gunbc/host_standup.dag +++ b/dag/gunbc/host_standup.dag @@ -648,12 +648,6 @@ fn assimilation_complete_all_satisfied( } } -// NO SILENT FALLBACK — the same claim as before, now carried by the refusal. -// This read "is nbd-proxy, no fallback" while srv3 ran 2.07.00; the load-bearing -// half was always the second one, that a solver finding no virtual media must not -// quietly answer PXE. srv3 on 3.22.00 has no catalogued delivery mechanism, so -// the fallback arm is exactly where a defective solver would land — and refusing -// is the proof it does not. A Selected of any kind here means the fallback fired. fn srv3_install_mechanism_refuses_rather_than_falling_back() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => true diff --git a/dag/gunbc/srv3_os_install_actuate.dag b/dag/gunbc/srv3_os_install_actuate.dag index afaa1a22de2..b824fffa7e5 100644 --- a/dag/gunbc/srv3_os_install_actuate.dag +++ b/dag/gunbc/srv3_os_install_actuate.dag @@ -113,10 +113,6 @@ func srv3_nbd_proxy_serve() -> ProcessExit ) } -// The step count was never the subject — that the plan is DERIVED from the solved -// mechanism was. srv3's BMC upgrade propagates through that derivation and the -// plan now refuses, which is the observable that distinguishes a derived plan -// from a hardcoded three-step one: a hardcoded plan would still be Ready. fn srv3_os_install_actuator_plan_refuses_on_unestablished_path() -> Bool { match srv3_os_install_actuator_plan_standing { ActuatorPlanRefused { cause: _ } => true diff --git a/dag/gunbc/srv3_os_install_actuate_scope.dag b/dag/gunbc/srv3_os_install_actuate_scope.dag index 712bbfa8ab0..a3f01d6c14c 100644 --- a/dag/gunbc/srv3_os_install_actuate_scope.dag +++ b/dag/gunbc/srv3_os_install_actuate_scope.dag @@ -67,13 +67,6 @@ fn srv3_autoinstall_seed_delivery_refusal() -> AutoinstallSeedDeliveryRefusal? { } } -// THE SEED GAP IS ONLY ASKABLE ONCE A MECHANISM IS SELECTED. This asserted the -// refusal was Present and fail-closed, which held while srv3 solved to -// nbd-proxy. srv3_autoinstall_seed_delivery_refusal answers none when the -// mechanism itself refuses — correctly, because there is no selected path whose -// seed could be unreachable — so on 3.22.00 the honest answer is Absent. Keeping -// the old assertion would have required a seed refusal describing the seed of a -// path that was never selected. fn srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() -> Bool { match srv3_autoinstall_seed_delivery_refusal() { Absent => true @@ -81,9 +74,6 @@ fn srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() -> Bool { } } -// The seeded plan derives from the same solve, so it refuses with it. The -// NoCloudLocal delivery this used to assert is not readable off a plan that does -// not exist, and reading it anyway is the substitution this lane keeps finding. fn srv3_seeded_plan_refuses_with_the_solve() -> Bool { match srv3_seeded_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -129,11 +119,6 @@ fn srv3_actuator_prep_binds_seeded_iso_path() -> Bool { ) } -// PREP IS NOT INSTALL — still the claim, and now demonstrated more sharply. The -// credential wiring and the seeded ISO path are prep facts and hold regardless of -// whether a delivery mechanism is selectable; the plan and seed halves refuse -// with the solve. That split is exactly the point: prep survives, solving does -// not, so nothing here can be mistaken for a solved install. fn srv3_os_install_actuate_is_actuator_prep_not_install_solved() -> Bool { srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() && srv3_os_install_actuate_credential_resolution_is_wired() diff --git a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag index 9e71a5eedc9..d9baf531517 100644 --- a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag +++ b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag @@ -158,21 +158,6 @@ test fn srv4_evidence_derived_refuses_while_board_default_selects_nbd_proxy() -> } } -// srv3's BMC upgrade to 3.22.00 took its catalogued delivery mechanism with it, -// and this witness says so by execution. The predecessor asserted the mechanism -// IS nbd-proxy; it reddened the moment srv3's evidence row followed the upgrade, -// which is exactly what the tripwire above it was written to do. -// -// READ THE CAUSE, NOT THE ARM. UNESTABLISHED is not impossible. The catalogued -// 3.22.00 row lists neither Redfish virtual media nor the bmcweb nbd-proxy, and -// no host has an established network-boot delivery — so this solver has nothing -// left to select. It does NOT establish that the host cannot be installed: srv4 -// runs this same firmware and was installed over the configfs USB-gadget -// transport, which extdeps.bmc.virtual_media already models as -// ConfigfsGadgetTransport and which this solver cannot see because its -// OsInstallMechanism enum predates that authority. Teaching it to consume -// VirtualMediaTransport is the INSTALL-0 repair; an omitted capability read as an -// observed absence is the defect, not the hardware. test fn srv3_reinstall_path_is_unestablished_on_3_22_00() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: c } => @@ -185,13 +170,6 @@ test fn srv3_reinstall_path_is_unestablished_on_3_22_00() -> Bool { } } -// The retained legacy row NO LONGER agrees with the evidence-derived solution, -// and executing that divergence is the point. srv3_install_mechanism reads the -// board's SHIPPED 2.07.00 capabilities, so it still answers -// NbdProxyVirtualMediaInstall; the evidence-derived solution follows srv3's -// ACTUAL firmware and refuses. Pinning both halves is what stops the stale -// board-default row being read as a current plan — the failure its predecessor -// existed to catch, now caught and held rather than re-greened. test fn legacy_board_default_row_no_longer_agrees_with_evidence() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => diff --git a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag index 145cf30a8d3..10adca68f6d 100644 --- a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag +++ b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag @@ -195,13 +195,6 @@ test fn credential_probe_maps_to_correct_phase() -> Bool { // srv3_bmc_credential_resolve and the bmc_onboard tools use bmc / factory_login / secret_name), // so forcing credential-only operations to unwrap an installation plan would have been a second // fusion, not a repair. The boot target now lives with the evidence that derives it. -// srv3 HAS JOINED srv4. This previously read "per-host, and srv4 has none", -// pinning srv3 to a Cd target while srv4 refused — a real difference at the time, -// because srv3 still ran 2.07.00. Both hosts now run 3.22.00 and both refuse with -// the same cause, so asserting a difference would be asserting a fact that -// stopped being true when srv3's BMC was upgraded. The per-host machinery is -// unchanged and still reads each host's own evidence; the two hosts simply agree -// now, and the witness says which state that is rather than which it used to be. test fn install_boot_target_refuses_alike_for_both_3_22_00_hosts() -> Bool { match srv3_install_boot_target_standing { InstallBootTargetRefused { cause: c3 } => diff --git a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index afc0e22bc39..41f520fd1ba 100644 --- a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -3,11 +3,6 @@ module test.claim.nbd_proxy_virtual_media_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// The mapping is the subject here and it is unchanged: a boot target is derived -// from the mechanism, never hardcoded to Pxe. What changed is that srv3 no -// longer REACHES the mapping — its 3.22.00 evidence refuses before a mechanism -// is selected — so the host half asserts the refusal and the mapping is pinned -// on the mechanisms directly, which is where the claim always actually lived. test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => true @@ -19,11 +14,6 @@ test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { && install_boot_target_for(mechanism: FirmwareUpdateThenVirtualMedia) == BootTargetCd } -// SOLVE-DRIVEN IS STILL THE CLAIM, and the refusal is what proves it. If the -// plan were hardcoded to nbd-proxy it would still be Ready after srv3's BMC -// upgrade; because it is derived from the solved mechanism, the upgrade -// propagates and the plan refuses. A plan that survived the firmware change -// would be the defect this witness exists to catch. test fn srv3_actuator_plan_is_solve_driven_and_follows_the_refusal() -> Bool { match srv3_os_install_actuator_plan_standing { ActuatorPlanRefused { cause: _ } => true diff --git a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag index 3c7e499fdd3..48570d37e2f 100644 --- a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag @@ -3,11 +3,6 @@ module test.claim.srv3_os_install_actuate_scope data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// The seed gap is not askable while no mechanism is selected, and the -// discriminating half is the conjunction: Absent ALONE would also be produced by -// a selected path whose seed was fine, so the mechanism refusal is asserted -// beside it. A mechanism becoming selectable again makes the refusal Present and -// reds this. test fn srv3_autoinstall_seed_gap_is_unasked_while_the_mechanism_refuses() -> Bool { srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() && match srv3_install_mechanism_solution { @@ -21,9 +16,6 @@ test fn srv3_os_install_actuate_credential_resolution_is_wired_in_scope() -> Boo && srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() } -// The ISO-path binding is a prep fact and is unchanged; the delivery half now -// refuses with the solve rather than reporting NoCloudLocal off a plan that does -// not exist. Both are asserted so prep and solving stay visibly separate. test fn srv3_actuator_prep_binds_iso_path_while_plan_refuses() -> Bool { srv3_seeded_plan_refuses_with_the_solve() && srv3_actuator_prep_binds_seeded_iso_path() diff --git a/dag/test/claim/srv3_os_install_actuate_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_witness_test.dag index 261333f37fa..0765379198b 100644 --- a/dag/test/claim/srv3_os_install_actuate_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_witness_test.dag @@ -3,11 +3,6 @@ module test.claim.srv3_os_install_actuate data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// Was "has three steps". The count only ever mattered as a proxy for the plan -// being derived from the solved mechanism, and srv3's BMC upgrade now exercises -// that derivation directly: the solve refuses, so the plan refuses with it. A -// plan still offering three nbd-proxy steps for a host whose firmware does not -// implement nbd-proxy is the failure this replaces. test fn srv3_actuator_plan_follows_the_solve_and_refuses() -> Bool { srv3_os_install_actuator_plan_refuses_on_unestablished_path() && match srv3_os_install_actuator_plan_standing { diff --git a/dag/test/claim/srv3_os_install_witness_test.dag b/dag/test/claim/srv3_os_install_witness_test.dag index d86d9a8cab3..9905e1813f7 100644 --- a/dag/test/claim/srv3_os_install_witness_test.dag +++ b/dag/test/claim/srv3_os_install_witness_test.dag @@ -3,11 +3,6 @@ module test.claim.srv3_os_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly -// ARCH-GROUNDED is still the claim and is asserted directly on the mapping, -// which is where it lives. The srv3 clause that used to read the architecture off -// a Ready plan is now the plan's refusal: with no delivery mechanism selectable -// on 3.22.00 there is no plan to take an architecture from, and taking one anyway -// would be reading a field off a plan that does not exist. test fn bootloader_artifact_is_arch_grounded() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -49,11 +44,6 @@ test fn srv3_survey_forces_proxy_dhcp() -> Bool { // Now asserted through the standing, so a refusal cannot read as a satisfied plan: the plan // arm must be Ready AND its delivery virtual-media, both derived from srv3's firmware evidence. -// SOLVER-DERIVED is the claim, and the two halves moving TOGETHER is the proof. -// A plan hardcoded to nbd-proxy virtual media would still be Ready now that the -// solve refuses; because it is derived, both refuse. Asserting the pair is -// strictly more discriminating than the old version, which could not tell a -// derived plan from a hardcoded one while the solve happened to agree with it. test fn srv3_plan_is_solver_derived_and_refuses_with_the_solve() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -66,12 +56,6 @@ test fn srv3_plan_is_solver_derived_and_refuses_with_the_solve() -> Bool { && fleet_install_server_specs.length() == 0 } -// The plan's TARGET facts — Ubuntu 24.04, Cd, Once, fully-automated seed — are -// not assertable while no plan exists, and this witness now says that instead of -// describing one. They are not lost: they are what the INSTALL-0 lane must -// reproduce once srv3 has an established delivery path again, at which point this -// witness reds and is replaced by the real target assertions rather than -// silently continuing to pass. test fn srv3_plan_target_facts_are_unassertable_while_the_plan_refuses() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true From 50a5e67c6f00a73c4e777897342b0fc1372a541f Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 13 Aug 2026 05:54:45 +0000 Subject: [PATCH 26/28] Revert "Bisect the annotation gate: strip this branch's new comment blocks" This reverts commit ab627ddfa6cd26903e758db260cc0aa76105cd31. --- dag/gunbc/host_standup.dag | 6 +++++ dag/gunbc/srv3_os_install_actuate.dag | 4 ++++ dag/gunbc/srv3_os_install_actuate_scope.dag | 15 +++++++++++++ ...vidence_install_mechanism_witness_test.dag | 22 +++++++++++++++++++ .../bmc_onboarding_lifecycle_witness_test.dag | 7 ++++++ ...oxy_virtual_media_install_witness_test.dag | 10 +++++++++ ..._os_install_actuate_scope_witness_test.dag | 8 +++++++ .../srv3_os_install_actuate_witness_test.dag | 5 +++++ .../claim/srv3_os_install_witness_test.dag | 16 ++++++++++++++ 9 files changed, 93 insertions(+) diff --git a/dag/gunbc/host_standup.dag b/dag/gunbc/host_standup.dag index 4e8d9a6d281..64a5b282739 100644 --- a/dag/gunbc/host_standup.dag +++ b/dag/gunbc/host_standup.dag @@ -648,6 +648,12 @@ fn assimilation_complete_all_satisfied( } } +// NO SILENT FALLBACK — the same claim as before, now carried by the refusal. +// This read "is nbd-proxy, no fallback" while srv3 ran 2.07.00; the load-bearing +// half was always the second one, that a solver finding no virtual media must not +// quietly answer PXE. srv3 on 3.22.00 has no catalogued delivery mechanism, so +// the fallback arm is exactly where a defective solver would land — and refusing +// is the proof it does not. A Selected of any kind here means the fallback fired. fn srv3_install_mechanism_refuses_rather_than_falling_back() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => true diff --git a/dag/gunbc/srv3_os_install_actuate.dag b/dag/gunbc/srv3_os_install_actuate.dag index b824fffa7e5..afaa1a22de2 100644 --- a/dag/gunbc/srv3_os_install_actuate.dag +++ b/dag/gunbc/srv3_os_install_actuate.dag @@ -113,6 +113,10 @@ func srv3_nbd_proxy_serve() -> ProcessExit ) } +// The step count was never the subject — that the plan is DERIVED from the solved +// mechanism was. srv3's BMC upgrade propagates through that derivation and the +// plan now refuses, which is the observable that distinguishes a derived plan +// from a hardcoded three-step one: a hardcoded plan would still be Ready. fn srv3_os_install_actuator_plan_refuses_on_unestablished_path() -> Bool { match srv3_os_install_actuator_plan_standing { ActuatorPlanRefused { cause: _ } => true diff --git a/dag/gunbc/srv3_os_install_actuate_scope.dag b/dag/gunbc/srv3_os_install_actuate_scope.dag index a3f01d6c14c..712bbfa8ab0 100644 --- a/dag/gunbc/srv3_os_install_actuate_scope.dag +++ b/dag/gunbc/srv3_os_install_actuate_scope.dag @@ -67,6 +67,13 @@ fn srv3_autoinstall_seed_delivery_refusal() -> AutoinstallSeedDeliveryRefusal? { } } +// THE SEED GAP IS ONLY ASKABLE ONCE A MECHANISM IS SELECTED. This asserted the +// refusal was Present and fail-closed, which held while srv3 solved to +// nbd-proxy. srv3_autoinstall_seed_delivery_refusal answers none when the +// mechanism itself refuses — correctly, because there is no selected path whose +// seed could be unreachable — so on 3.22.00 the honest answer is Absent. Keeping +// the old assertion would have required a seed refusal describing the seed of a +// path that was never selected. fn srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() -> Bool { match srv3_autoinstall_seed_delivery_refusal() { Absent => true @@ -74,6 +81,9 @@ fn srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() -> Bool { } } +// The seeded plan derives from the same solve, so it refuses with it. The +// NoCloudLocal delivery this used to assert is not readable off a plan that does +// not exist, and reading it anyway is the substitution this lane keeps finding. fn srv3_seeded_plan_refuses_with_the_solve() -> Bool { match srv3_seeded_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -119,6 +129,11 @@ fn srv3_actuator_prep_binds_seeded_iso_path() -> Bool { ) } +// PREP IS NOT INSTALL — still the claim, and now demonstrated more sharply. The +// credential wiring and the seeded ISO path are prep facts and hold regardless of +// whether a delivery mechanism is selectable; the plan and seed halves refuse +// with the solve. That split is exactly the point: prep survives, solving does +// not, so nothing here can be mistaken for a solved install. fn srv3_os_install_actuate_is_actuator_prep_not_install_solved() -> Bool { srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() && srv3_os_install_actuate_credential_resolution_is_wired() diff --git a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag index d9baf531517..9e71a5eedc9 100644 --- a/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag +++ b/dag/test/claim/bmc_firmware_evidence_install_mechanism_witness_test.dag @@ -158,6 +158,21 @@ test fn srv4_evidence_derived_refuses_while_board_default_selects_nbd_proxy() -> } } +// srv3's BMC upgrade to 3.22.00 took its catalogued delivery mechanism with it, +// and this witness says so by execution. The predecessor asserted the mechanism +// IS nbd-proxy; it reddened the moment srv3's evidence row followed the upgrade, +// which is exactly what the tripwire above it was written to do. +// +// READ THE CAUSE, NOT THE ARM. UNESTABLISHED is not impossible. The catalogued +// 3.22.00 row lists neither Redfish virtual media nor the bmcweb nbd-proxy, and +// no host has an established network-boot delivery — so this solver has nothing +// left to select. It does NOT establish that the host cannot be installed: srv4 +// runs this same firmware and was installed over the configfs USB-gadget +// transport, which extdeps.bmc.virtual_media already models as +// ConfigfsGadgetTransport and which this solver cannot see because its +// OsInstallMechanism enum predates that authority. Teaching it to consume +// VirtualMediaTransport is the INSTALL-0 repair; an omitted capability read as an +// observed absence is the defect, not the hardware. test fn srv3_reinstall_path_is_unestablished_on_3_22_00() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: c } => @@ -170,6 +185,13 @@ test fn srv3_reinstall_path_is_unestablished_on_3_22_00() -> Bool { } } +// The retained legacy row NO LONGER agrees with the evidence-derived solution, +// and executing that divergence is the point. srv3_install_mechanism reads the +// board's SHIPPED 2.07.00 capabilities, so it still answers +// NbdProxyVirtualMediaInstall; the evidence-derived solution follows srv3's +// ACTUAL firmware and refuses. Pinning both halves is what stops the stale +// board-default row being read as a current plan — the failure its predecessor +// existed to catch, now caught and held rather than re-greened. test fn legacy_board_default_row_no_longer_agrees_with_evidence() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => diff --git a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag index 10adca68f6d..145cf30a8d3 100644 --- a/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag +++ b/dag/test/claim/bmc_onboarding_lifecycle_witness_test.dag @@ -195,6 +195,13 @@ test fn credential_probe_maps_to_correct_phase() -> Bool { // srv3_bmc_credential_resolve and the bmc_onboard tools use bmc / factory_login / secret_name), // so forcing credential-only operations to unwrap an installation plan would have been a second // fusion, not a repair. The boot target now lives with the evidence that derives it. +// srv3 HAS JOINED srv4. This previously read "per-host, and srv4 has none", +// pinning srv3 to a Cd target while srv4 refused — a real difference at the time, +// because srv3 still ran 2.07.00. Both hosts now run 3.22.00 and both refuse with +// the same cause, so asserting a difference would be asserting a fact that +// stopped being true when srv3's BMC was upgraded. The per-host machinery is +// unchanged and still reads each host's own evidence; the two hosts simply agree +// now, and the witness says which state that is rather than which it used to be. test fn install_boot_target_refuses_alike_for_both_3_22_00_hosts() -> Bool { match srv3_install_boot_target_standing { InstallBootTargetRefused { cause: c3 } => diff --git a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag index 41f520fd1ba..afc0e22bc39 100644 --- a/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag +++ b/dag/test/claim/nbd_proxy_virtual_media_install_witness_test.dag @@ -3,6 +3,11 @@ module test.claim.nbd_proxy_virtual_media_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// The mapping is the subject here and it is unchanged: a boot target is derived +// from the mechanism, never hardcoded to Pxe. What changed is that srv3 no +// longer REACHES the mapping — its 3.22.00 evidence refuses before a mechanism +// is selected — so the host half asserts the refusal and the mapping is pinned +// on the mechanisms directly, which is where the claim always actually lived. test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { match srv3_install_mechanism_solution { OsInstallMechanismRefused { cause: _ } => true @@ -14,6 +19,11 @@ test fn install_boot_target_follows_mechanism_not_hardcoded_pxe() -> Bool { && install_boot_target_for(mechanism: FirmwareUpdateThenVirtualMedia) == BootTargetCd } +// SOLVE-DRIVEN IS STILL THE CLAIM, and the refusal is what proves it. If the +// plan were hardcoded to nbd-proxy it would still be Ready after srv3's BMC +// upgrade; because it is derived from the solved mechanism, the upgrade +// propagates and the plan refuses. A plan that survived the firmware change +// would be the defect this witness exists to catch. test fn srv3_actuator_plan_is_solve_driven_and_follows_the_refusal() -> Bool { match srv3_os_install_actuator_plan_standing { ActuatorPlanRefused { cause: _ } => true diff --git a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag index 48570d37e2f..3c7e499fdd3 100644 --- a/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_scope_witness_test.dag @@ -3,6 +3,11 @@ module test.claim.srv3_os_install_actuate_scope data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// The seed gap is not askable while no mechanism is selected, and the +// discriminating half is the conjunction: Absent ALONE would also be produced by +// a selected path whose seed was fine, so the mechanism refusal is asserted +// beside it. A mechanism becoming selectable again makes the refusal Present and +// reds this. test fn srv3_autoinstall_seed_gap_is_unasked_while_the_mechanism_refuses() -> Bool { srv3_autoinstall_seed_gap_is_unasked_while_mechanism_refuses() && match srv3_install_mechanism_solution { @@ -16,6 +21,9 @@ test fn srv3_os_install_actuate_credential_resolution_is_wired_in_scope() -> Boo && srv3_os_install_actuate_credential_debt_grounds_bmc_secret_ref() } +// The ISO-path binding is a prep fact and is unchanged; the delivery half now +// refuses with the solve rather than reporting NoCloudLocal off a plan that does +// not exist. Both are asserted so prep and solving stay visibly separate. test fn srv3_actuator_prep_binds_iso_path_while_plan_refuses() -> Bool { srv3_seeded_plan_refuses_with_the_solve() && srv3_actuator_prep_binds_seeded_iso_path() diff --git a/dag/test/claim/srv3_os_install_actuate_witness_test.dag b/dag/test/claim/srv3_os_install_actuate_witness_test.dag index 0765379198b..261333f37fa 100644 --- a/dag/test/claim/srv3_os_install_actuate_witness_test.dag +++ b/dag/test/claim/srv3_os_install_actuate_witness_test.dag @@ -3,6 +3,11 @@ module test.claim.srv3_os_install_actuate data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// Was "has three steps". The count only ever mattered as a proxy for the plan +// being derived from the solved mechanism, and srv3's BMC upgrade now exercises +// that derivation directly: the solve refuses, so the plan refuses with it. A +// plan still offering three nbd-proxy steps for a host whose firmware does not +// implement nbd-proxy is the failure this replaces. test fn srv3_actuator_plan_follows_the_solve_and_refuses() -> Bool { srv3_os_install_actuator_plan_refuses_on_unestablished_path() && match srv3_os_install_actuator_plan_standing { diff --git a/dag/test/claim/srv3_os_install_witness_test.dag b/dag/test/claim/srv3_os_install_witness_test.dag index 9905e1813f7..d86d9a8cab3 100644 --- a/dag/test/claim/srv3_os_install_witness_test.dag +++ b/dag/test/claim/srv3_os_install_witness_test.dag @@ -3,6 +3,11 @@ module test.claim.srv3_os_install data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly +// ARCH-GROUNDED is still the claim and is asserted directly on the mapping, +// which is where it lives. The srv3 clause that used to read the architecture off +// a Ready plan is now the plan's refusal: with no delivery mechanism selectable +// on 3.22.00 there is no plan to take an architecture from, and taking one anyway +// would be reading a field off a plan that does not exist. test fn bootloader_artifact_is_arch_grounded() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -44,6 +49,11 @@ test fn srv3_survey_forces_proxy_dhcp() -> Bool { // Now asserted through the standing, so a refusal cannot read as a satisfied plan: the plan // arm must be Ready AND its delivery virtual-media, both derived from srv3's firmware evidence. +// SOLVER-DERIVED is the claim, and the two halves moving TOGETHER is the proof. +// A plan hardcoded to nbd-proxy virtual media would still be Ready now that the +// solve refuses; because it is derived, both refuse. Asserting the pair is +// strictly more discriminating than the old version, which could not tell a +// derived plan from a hardcoded one while the solve happened to agree with it. test fn srv3_plan_is_solver_derived_and_refuses_with_the_solve() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true @@ -56,6 +66,12 @@ test fn srv3_plan_is_solver_derived_and_refuses_with_the_solve() -> Bool { && fleet_install_server_specs.length() == 0 } +// The plan's TARGET facts — Ubuntu 24.04, Cd, Once, fully-automated seed — are +// not assertable while no plan exists, and this witness now says that instead of +// describing one. They are not lost: they are what the INSTALL-0 lane must +// reproduce once srv3 has an established delivery path again, at which point this +// witness reds and is replaced by the real target assertions rather than +// silently continuing to pass. test fn srv3_plan_target_facts_are_unassertable_while_the_plan_refuses() -> Bool { match srv3_os_install_plan_standing { OsInstallPlanRefused { cause: _ } => true From d3c3b2da16cb56ab12f62ed79125910fd500d3a3 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 13 Aug 2026 06:00:07 +0000 Subject: [PATCH 27/28] Join the prerequisite bounds oracle to its rows instead of transcribing them review 51524 (cursor/composer-2.5, REQUEST_CHANGES) is correct, and the finding is exact: this file already identity-joins the declared minimum to asrock_altrad8ud_bios_3_10_openbmc_release.prerequisite and carries a comment explaining why tree-copied literals are forbidden, while two witnesses a few lines below still hard-coded 2/7 and 3/22. Those numbers have a single authority -- the gating_version on each observation row -- so a re-flash at a different version would move the rows and leave these green until someone remembered to edit the witness. That is precisely the failure this PR removes from the minimum path, left in place on the bounds path. bounds_are_the_executed_pair and an_acceptance_only_history_reports_no_ refusal_bound now join to asrock_altrad8ud_bios_310_push_refused_at_bmc_207 .gating_version and asrock_altrad8ud_bios_310_push_accepted_at_bmc_322 .gating_version through one same_firmware_version helper. FIXED BEYOND THE REVIEW, because the same defect sat in two more witnesses it did not name: a_minimum_above_the_accepting_version_is_contradicted and a_minimum_at_or_below_the_refusing_version_is_contradicted transcribed the same two versions as their expected values. Joining only the two flagged sites would have left identical rot one screen down. That required splitting a distinction the file previously got wrong in prose. A RED control has two halves under different rules: its falsifying INPUT (a minimum deliberately set above or below the evidence) stays authored, since a probe joined to the catalog it contradicts would be unauthorable; its EXPECTED value is an oracle like any other and joins. The old note claimed the RED controls "deliberately do NOT join" without that split, which is why the transcription there read as intentional. The note now states both halves. DISCRIMINATION IS UNCHANGED, which is what makes the join safe rather than merely tidier: both orderings feeding bounds_are_the_executed_pair carry a third, LOWER synthetic refusal at 1.00.00, so a first-wins fold still reports 1.00.00 against the refused row's 2.07.00 and still fails. The order-independence claim is not weakened by sourcing the expected value from the row. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- ...are_prerequisite_evidence_witness_test.dag | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag index e7c1a8db2f8..23b8f192fb0 100644 --- a/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag +++ b/dag/test/claim/firmware_prerequisite_evidence_witness_test.dag @@ -6,6 +6,7 @@ import extdeps.boards.types { AsrockRackAltrad8ud1l2t } import extdeps.firmware.types { BmcController, FirmwarePrerequisiteObservation, + FirmwareSemanticVersion, HostBios, NoFirmwarePrerequisite, OpenBmcWebUiTar, @@ -64,9 +65,13 @@ data reversed_evidence: List = [ // asserting otherwise is what made it look already checked. The minimum now // comes from the release row itself, so editing the catalog moves the tests. // -// The RED controls below deliberately do NOT join: a falsifying probe is an -// authored input chosen to contradict the evidence, not an oracle, and joining -// it to the catalog would make it unauthorable. +// The two halves of a RED control are governed differently, and conflating them +// is what an earlier revision of this note did. Its falsifying INPUT -- the +// minimum deliberately set above or below the evidence -- stays authored, because +// a probe chosen to contradict the catalog would be unauthorable if joined to it. +// Its EXPECTED value is an oracle like any other and joins to the row that +// supplies it, so a control asserting which version was refused reads that from +// the refusal row rather than transcribing it. fn verdict_against_declared_minimum( observations: List, ) -> PrerequisiteEvidenceVerdict { @@ -77,10 +82,26 @@ fn verdict_against_declared_minimum( } } -// The bounds the EXECUTED pair establishes: refused at 2.07.00, accepted at -// 3.22.00. Held once and applied to several orderings below, so an ordering -// that answered differently could not be papered over by a second copy of the -// expected numbers. +fn same_firmware_version(a: FirmwareSemanticVersion, b: FirmwareSemanticVersion) -> Bool { + a.major == b.major && a.minor == b.minor && a.patch == b.patch +} + +// The bounds the EXECUTED pair establishes, JOINED to the observation rows that +// establish them rather than transcribed. Held once and applied to several +// orderings below, so an ordering that answered differently could not be papered +// over by a second copy of the expected numbers. +// +// The join is the point, not a stylistic preference (DESIGN §5 test-oracle rule, +// and the same pattern this file already uses for the declared minimum): written +// as literals, `2/7` and `3/22` were a second copy of two numbers whose single +// authority is the gating_version on each row. Re-flashing at a different version +// would move the rows and leave this witness green until someone remembered to +// edit it -- exactly the failure this PR removes from the minimum path. Caught in +// review 51524. +// +// Joining does NOT weaken the discrimination, which is what makes it safe here: +// both orderings below carry a THIRD, lower refusal at 1.00.00, so a first-wins +// fold reports that instead of the refused row's own 2.07.00 and still fails. fn bounds_are_the_executed_pair( observations: List, ) -> Bool { @@ -90,7 +111,8 @@ fn bounds_are_the_executed_pair( Present { value: rv } => match a { Present { value: av } => - rv.major == 2 && rv.minor == 7 && av.major == 3 && av.minor == 22 + same_firmware_version(a: rv, b: asrock_altrad8ud_bios_310_push_refused_at_bmc_207.gating_version) + && same_firmware_version(a: av, b: asrock_altrad8ud_bios_310_push_accepted_at_bmc_322.gating_version) Absent => false } Absent => false @@ -119,7 +141,10 @@ test fn the_bounds_do_not_depend_on_authoring_order() -> Bool { // A bound with no observation behind it is ABSENT, never filled in from the // declared minimum. Acceptance-only evidence has no refusal to report, and -// reporting one would fabricate a push that never happened. +// reporting one would fabricate a push that never happened. The acceptance bound +// is joined to the single row supplied as evidence, so the claim is that the fold +// REPORTS that row's own gating_version -- not that it happens to match a number +// transcribed here (review 51524). test fn an_acceptance_only_history_reports_no_refusal_bound() -> Bool { match verdict_against_declared_minimum( observations: [asrock_altrad8ud_bios_310_push_accepted_at_bmc_322], @@ -128,7 +153,8 @@ test fn an_acceptance_only_history_reports_no_refusal_bound() -> Bool { match r { Absent => match a { - Present { value: av } => av.major == 3 && av.minor == 22 + Present { value: av } => + same_firmware_version(a: av, b: asrock_altrad8ud_bios_310_push_accepted_at_bmc_322.gating_version) Absent => false } Present { value: _ } => false @@ -146,7 +172,7 @@ test fn a_minimum_above_the_accepting_version_is_contradicted() -> Bool { observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, ) { PrerequisiteContradictedByAcceptanceBelowMinimum { accepted_at: a } => - a.major == 3 && a.minor == 22 + same_firmware_version(a: a, b: asrock_altrad8ud_bios_310_push_accepted_at_bmc_322.gating_version) _ => false } } @@ -161,7 +187,7 @@ test fn a_minimum_at_or_below_the_refusing_version_is_contradicted() -> Bool { observations: asrock_altrad8ud_bios_openbmc_prerequisite_evidence, ) { PrerequisiteContradictedByRefusalAtOrAboveMinimum { refused_at: r } => - r.major == 2 && r.minor == 7 + same_firmware_version(a: r, b: asrock_altrad8ud_bios_310_push_refused_at_bmc_207.gating_version) _ => false } } From cc69a4ffb539837f7cf2ccbd8f715e2869bbffa1 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 13 Aug 2026 06:18:42 +0000 Subject: [PATCH 28/28] Take the singleton directly instead of folding a fabricated init over it Both reviewers noted bmc_redfish_surface_for's count==1 arm as awkward, each on the premise that no head-of-list primitive was at hand (review 51522, review 51533). That premise is false -- `.first()` is used widely in the corpus and returns the element directly (steps.first(), srv1_live_deploy_privileged_commands().first()). That makes this more than the stylistic nit both flagged it as. The fold carried `init: SurfaceShapeUncatalogued { firmware }`, a fabricated plausible value that is NEVER the correct answer in an arm already guarded by count == 1: it is a well-formed derivation saying "this firmware is not in the catalog" sitting as the default of a branch reached only because it IS in the catalog exactly once. Nothing could return it today, because the guard holds and the fold visits its one element -- but the shape is the fail-open one DESIGN section 5 names, and it survives only as long as the guard above it does. Removing the fold removes the fabricated value with it; the arm now reads the single match it is defined over. Behavior is unchanged: for a one-element list the fold's f ran exactly once and returned SurfaceShapeKnown { shape: row.shape }, which is what the direct read produces. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01CuRVynkbQYGqjmf9AnyxsS --- dag/extdeps/bmc/types.dag | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dag/extdeps/bmc/types.dag b/dag/extdeps/bmc/types.dag index aab1eafa666..7798ec99ee7 100644 --- a/dag/extdeps/bmc/types.dag +++ b/dag/extdeps/bmc/types.dag @@ -177,11 +177,7 @@ fn bmc_redfish_surface_for( if count == 0 { SurfaceShapeUncatalogued { firmware: firmware } } else if count == 1 { - fold( - matches, - init: SurfaceShapeUncatalogued { firmware: firmware }, - f: (acc, row) => SurfaceShapeKnown { shape: row.shape }, - ) + SurfaceShapeKnown { shape: matches.first().shape } } else { SurfaceShapeAmbiguous { firmware: firmware, matches: count } }