Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
128c805
CLI invocation emission design note: the tree is the authority, argv …
Aug 18, 2026
94320fb
Design note corrections: policy-indexed jq decoding, sealed CLI tree,…
Aug 18, 2026
99c4199
Design note: orthogonal observation axes, sealed carriers, subject-gr…
Aug 18, 2026
9b40c21
Remote jq argument-boundary loss is source-proven, not a risk: no quo…
Aug 18, 2026
1de10ed
Wave 0: the grammar's token stream survives as argument boundaries, p…
Aug 18, 2026
61e63ec
Delete the two unconsumed decls, and correct a sealing claim that was…
Aug 19, 2026
8e07969
Wave 0 corrected: group fragments into arguments, and stop proving a …
Aug 19, 2026
05f1c91
Wave 1A: jq semantic invocation layer, lowered to argv through the sh…
Aug 19, 2026
736612f
Correct rung inflation in this lane's own authority: the CliSurface s…
Aug 19, 2026
d807a3a
Fix two stale claims in cli_surface's own mint-hole note, one of whic…
Aug 19, 2026
cf1ee9f
Record the spelling-seam extraction: right finding, forbidden shape, …
Aug 19, 2026
76d4393
Name the import jq was resolving by pool coincidence, and record the …
Aug 19, 2026
96133bb
Four review corrections, three of which my passing tests were concealing
Aug 19, 2026
d6db90c
Update the entry-point claims those corrections just falsified
Aug 19, 2026
17560cb
Name two more imports jq was resolving by pool coincidence (review 53…
Aug 19, 2026
eef165b
Delete five declarations sitting ahead of their consumers (side-chat …
Aug 19, 2026
012ef23
BLOCKER, proven by execution: the argv transport silently joins compu…
Aug 19, 2026
ce7694a
Correct the blocker's conclusion: a construction exists, it is not bl…
Aug 19, 2026
273fcb7
Wave 1B: cut the first argv site over to the jq invocation layer, and…
Aug 19, 2026
b3b4f06
Rename the witness-local ExpectedOutcome: it collided with gunbc.outp…
Aug 19, 2026
00556e7
Admit the wet argv receipt properly: excluded from hermetic discovery…
Aug 19, 2026
50fb645
Classify the wet receipt row BinWitnessWet, not OfflineLocalRecipe
Aug 19, 2026
3d303be
Merge remote-tracking branch 'origin/main' into session/sharp-ant-396
Aug 19, 2026
5ea765c
Exclude the wet receipt through the mechanism the floor actually cons…
Aug 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 67 additions & 48 deletions dag/extdeps/bmc/openbmc_fan_control.dag
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module extdeps.bmc.openbmc_fan_control

import std.algebra { trim }
import std.algebra { Empty, trim }
import v2.std.diagnostic { Accepted, Rejected }

import extdeps.external_authority { ExternalAuthority }
import extdeps.uri { Https, Uri }
Expand All @@ -18,7 +19,21 @@ import extdeps.languages.json.emit {
json_string,
serialize_json,
}
import extdeps.tools.jq { jq_authority }
import extdeps.tools.jq {
JqInputStdin,
JqInvocation,
JqProgram,
JqProgramExit,
JqRawOutput,
jq_authority,
JqOutcome,
JqOutputAbsent,
JqOutputPresent,
JqExecutionRefused,
jq_classify_observation,
jq_execute_admitted_plan,
jq_invocation_process_plan
}
import extdeps.systemd.systemctl { systemctl_authority }
import extdeps.bmc.openbmc_operation {
OpenBmcCopyFilePreservingMetadata,
Expand Down Expand Up @@ -792,23 +807,6 @@ service openbmc.JsonProjection {
}
}

operation SensorIntegerValue {
input { content: String }
output {
value: String from "stdout"
success: Bool from "exit_success"
stderr: String from "stderr"
}
readonly
transport shell {
argv: ["jq", "-r", "if (.data | type) == \"number\" then (.data | round) else empty end"]
stdin: content
}
exit {
0 => Unit
nonzero => String "sensor integer projection failed"
}
}
}

fn openbmc_read_file(
Expand Down Expand Up @@ -881,23 +879,56 @@ type OpenBmcSensorValueResult
| OpenBmcSensorValueAbsent
| OpenBmcSensorValueRefused { reason: String }

// exit_code, not `success: Bool`. Two fields for one fact is a second representation: success is
// exactly exit_code == 0, derived here where the three states are decided rather than carried
// alongside them.
// THE PROGRAM IS THE DOMAIN'S, THE FLAGS ARE JQ'S. This module states what it wants extracted;
// it never spells -r, --raw-output or an argv position. `else empty end` is deliberate and
// load-bearing: a non-numeric reading emits NOTHING and exits zero, which the decoder below reads
// as OpenBmcSensorValueAbsent -- a real third state beside Refused and Observed. That is why this
// path must NOT carry jq's --exit-status: it would collapse Absent into Refused on a live sensor.
data openbmc_sensor_integer_program: String = "if (.data | type) == \"number\" then (.data | round) else empty end"

// One place builds the sensor projection invocation, so both call sites cannot drift apart.
fn openbmc_sensor_integer_projection(content: String) -> OpenBmcSensorValueResult {
match jq_invocation_process_plan(invocation: JqInvocation {
program: JqProgram { source: openbmc_sensor_integer_program },
bindings: Empty,
input: JqInputStdin { content: content },
output_encoding: JqRawOutput,
exit_policy: JqProgramExit,
}) {
Rejected { diagnostics: _ } =>
OpenBmcSensorValueRefused { reason: "sensor value projection could not be lowered to a jq invocation" }
Accepted { value: plan, diagnostics: _ } => {
let observed = jq_execute_admitted_plan(plan: plan)
openbmc_sensor_integer_projection_result(
outcome: jq_classify_observation(plan: plan, observation: observed),
)
}
}
}

// THIS DECODER NO LONGER READS AN EXIT CODE, and that is the point of the arm it matches instead.
// It previously took exit_code/stdout/stderr and mapped every nonzero exit to a refusal -- correct
// under JqProgramExit, silently wrong under any other exit policy, and nothing connected the two.
// jq_classify_observation now owns that reading, against the policy carried on the plan, so this
// function decides only what OpenBMC alone can decide: whether a present projection is an integer.
fn openbmc_sensor_integer_projection_result(
success: Bool,
value: String,
stderr: String,
outcome: JqOutcome,
) -> OpenBmcSensorValueResult {
if !success {
OpenBmcSensorValueRefused { reason: stderr }
} else if trim(value) == "" {
OpenBmcSensorValueAbsent
} else {
match parse_int(s: trim(value)) {
Absent =>
OpenBmcSensorValueRefused {
reason: "sensor value projection is not an integer",
}
Present { value: parsed } => OpenBmcSensorValueObserved { value: parsed }
}
match outcome {
JqExecutionRefused { exit_code: _, stderr: stderr } =>
OpenBmcSensorValueRefused { reason: stderr }
JqOutputAbsent => OpenBmcSensorValueAbsent
JqOutputPresent { text: text } =>
match parse_int(s: trim(text)) {
Absent =>
OpenBmcSensorValueRefused {
reason: "sensor value projection is not an integer",
}
Present { value: parsed } => OpenBmcSensorValueObserved { value: parsed }
}
}
}

Expand Down Expand Up @@ -1025,14 +1056,7 @@ fn openbmc_sensor_value(
if !remote.success {
OpenBmcSensorValueRefused { reason: remote.stderr }
} else {
let extracted = openbmc.JsonProjection.SensorIntegerValue(
content: remote.stdout,
)
openbmc_sensor_integer_projection_result(
success: extracted.success,
value: extracted.value,
stderr: extracted.stderr,
)
openbmc_sensor_integer_projection(content: remote.stdout)
}
}

Expand All @@ -1059,12 +1083,7 @@ fn openbmc_sensor_threshold_result(
if !remote.success {
OpenBmcSensorValueRefused { reason: remote.stderr }
} else {
let extracted = openbmc.JsonProjection.SensorIntegerValue(content: remote.stdout)
openbmc_sensor_integer_projection_result(
success: extracted.success,
value: extracted.value,
stderr: extracted.stderr,
)
openbmc_sensor_integer_projection(content: remote.stdout)
}
}

Expand Down
Loading