Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
180 changes: 180 additions & 0 deletions dag/gunbc/self_host_artifact_materialization.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
module gunbc.self_host_artifact_materialization

import std.types { Bool, FilePath, Int, NonEmptyStr, String }
import std.content_hash { Sha512Digest }
import extdeps.cargo_message {
CargoArtifactSelection,
CargoArtifactSelected,
CargoArtifactAbsent,
CargoArtifactAmbiguous,
CargoArtifactWithoutExecutable,
CargoArtifactExecutablePathEmpty,
CargoArtifactExecutableNotAString,
CargoMessageExecutableAbsent,
CargoMessageLineUnparseable,
CargoMessageLineNotAnObject,
CargoMessageReasonAbsent,
CargoMessageReasonNotAString,
CargoMessageMemberDuplicated,
CargoMessageTargetNotAnObject,
CargoMessageTargetNameAbsent,
CargoMessageTargetNameNotAString,
cargo_select_artifact_executable
}
import extdeps.tools.sha512sum {
Sha512FileObservation,
Sha512FileObserved,
Sha512FileObservationRefused
}
import v2.compiler.self_host.generation {
ArtifactIdentity,
ArtifactMaterialized,
ArtifactNotMaterialized,
observed_artifact_digest
}

data artifact_materialization_subject_note: String = "SH-C: turning a cargo build into the artifact axis of a compiler generation, with no step that predicts a path instead of observing one. The chain is build -> the path cargo NAMED -> a digest of THAT path -> ArtifactIdentity. The load-bearing property is that the digest and every later execution consume ONE path value, produced by extdeps.cargo_message from cargo's own --message-format=json output. A convention-derived path breaks that at both ends simultaneously: it digests one file while executing another, with each operation individually honest and the composition silently wrong, and it also makes the artifact axis a FUNCTION of the build-configuration axis, collapsing two of the six independent axes v2.compiler.self_host.generation keeps apart."

data artifact_materialization_decomposition_note: String = "The decision logic is deliberately split into two PURE folds with a thin wet composition between them, rather than one wet function. select_artifact_path decides what the build reported; materialization_from_digest decides what the digest observation means. Both are total folds over typed inputs, so both are witnessed hermetically on every PR, and the only part that needs a wet cadence is the three service calls that produce their inputs. The alternative — one wet function carrying the decisions — would put every refusal arm behind a cadence that does not run per PR, which is how a decision nobody executes reads as a decision that holds."

data artifact_materialization_unbuilt_note: String = "There is NO arm here that yields ArtifactMaterialized without a digest observation in hand. That is the whole point of the carrier: v2.compiler.self_host.generation makes ObservedArtifactDigest a sole_constructor over a validated Sha512Digest precisely so an artifact identity cannot be authored, and this module is the first producer that has to respect it. Every failure — build nonzero, no artifact message, ambiguous target, unreadable stream, digest refused — lands on its own arm and projects ArtifactNotMaterialized, which promotion admission already refuses BY NAME rather than treating as equivalent."

type ArtifactPathOutcome
= ArtifactPathReported { executable: FilePath }
| ArtifactPathBuildFailed { target_name: String }
| ArtifactPathRefused { selection: CargoArtifactSelection }

type ArtifactMaterializationOutcome
= ArtifactMaterializationObserved {
executable: FilePath
digest: Sha512Digest
}
| ArtifactMaterializationBuildFailed { target_name: String }
| ArtifactMaterializationPathRefused { selection: CargoArtifactSelection }
| ArtifactMaterializationDigestRefused {
executable: FilePath
cause: NonEmptyStr
}

// A build that exited nonzero is its own arm and never reaches the stream decode.
// The distinction matters downstream: "the candidate did not build" and "the candidate
// built but we cannot say what it produced" have different remedies, and the brief this
// lane answers requires build failure to stay separate from every other refusal so a
// later behavioural comparison cannot read one as the other.
fn select_artifact_path(
build_success: Bool,
build_stdout: String,
target_name: String
) -> ArtifactPathOutcome {
if !build_success {
ArtifactPathBuildFailed { target_name: target_name }
} else {
match cargo_select_artifact_executable(
stdout: build_stdout,
target_name: target_name
) {
CargoArtifactSelected { executable: exe } =>
ArtifactPathReported { executable: exe }
CargoArtifactAbsent { target_name: t } =>
ArtifactPathRefused { selection: CargoArtifactAbsent { target_name: t } }
CargoArtifactAmbiguous { target_name: t, count: c } =>
ArtifactPathRefused {
selection: CargoArtifactAmbiguous { target_name: t, count: c }
}
CargoArtifactWithoutExecutable { target_name: t } =>
ArtifactPathRefused {
selection: CargoArtifactWithoutExecutable { target_name: t }
}
CargoArtifactExecutablePathEmpty { target_name: t, line_number: n } =>
ArtifactPathRefused {
selection: CargoArtifactExecutablePathEmpty { target_name: t, line_number: n }
}
CargoArtifactExecutableNotAString { target_name: t, line_number: n } =>
ArtifactPathRefused {
selection: CargoArtifactExecutableNotAString { target_name: t, line_number: n }
}
CargoMessageExecutableAbsent { line_number: n } =>
ArtifactPathRefused {
selection: CargoMessageExecutableAbsent { line_number: n }
}
CargoMessageLineUnparseable { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageLineUnparseable { line_number: n } }
CargoMessageLineNotAnObject { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageLineNotAnObject { line_number: n } }
CargoMessageReasonAbsent { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageReasonAbsent { line_number: n } }
CargoMessageReasonNotAString { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageReasonNotAString { line_number: n } }
CargoMessageMemberDuplicated { line_number: n, key: k } =>
ArtifactPathRefused {
selection: CargoMessageMemberDuplicated { line_number: n, key: k }
}
CargoMessageTargetNotAnObject { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageTargetNotAnObject { line_number: n } }
CargoMessageTargetNameAbsent { line_number: n } =>
ArtifactPathRefused { selection: CargoMessageTargetNameAbsent { line_number: n } }
CargoMessageTargetNameNotAString { line_number: n } =>
ArtifactPathRefused {
selection: CargoMessageTargetNameNotAString { line_number: n }
}
}
}
}

// Takes the path the previous fold reported ALONGSIDE the observation made of that same
// path. The two parameters are not independent inputs a caller may mix: the wet
// composition below obtains the observation by digesting exactly this executable, and
// the executable is carried into the result so a consumer can prove the digest and any
// later invocation refer to one file.
fn materialization_from_digest(
executable: FilePath,
observation: Sha512FileObservation
) -> ArtifactMaterializationOutcome {
match observation {
Sha512FileObserved { digest: d } =>
ArtifactMaterializationObserved { executable: executable, digest: d }
Sha512FileObservationRefused { cause: c } =>
ArtifactMaterializationDigestRefused { executable: executable, cause: c }
}
}

fn materialization_outcome_from_path(
path: ArtifactPathOutcome,
observation: Sha512FileObservation
) -> ArtifactMaterializationOutcome {
match path {
ArtifactPathBuildFailed { target_name: t } =>
ArtifactMaterializationBuildFailed { target_name: t }
ArtifactPathRefused { selection: s } =>
ArtifactMaterializationPathRefused { selection: s }
ArtifactPathReported { executable: exe } =>
materialization_from_digest(executable: exe, observation: observation)
}
}

// The projection into the generation carrier. ArtifactMaterialized is reachable from
// exactly one arm, and only through observed_artifact_digest, whose argument is a
// Sha512Digest that existed only because sha512sum reported it for this path.
fn artifact_identity_of(outcome: ArtifactMaterializationOutcome) -> ArtifactIdentity {
match outcome {
ArtifactMaterializationObserved { executable: _, digest: d } =>
ArtifactMaterialized { digest: observed_artifact_digest(observed: d) }
ArtifactMaterializationBuildFailed { target_name: _ } => ArtifactNotMaterialized
ArtifactMaterializationPathRefused { selection: _ } => ArtifactNotMaterialized
ArtifactMaterializationDigestRefused { executable: _, cause: _ } =>
ArtifactNotMaterialized
}
}

// The executable a consumer may invoke, available ONLY when the artifact was observed.
// A consumer that wants to run the candidate takes the path from here, so the file it
// executes is the file that was digested, by construction rather than by discipline.
fn materialized_executable(outcome: ArtifactMaterializationOutcome) -> FilePath? {
match outcome {
ArtifactMaterializationObserved { executable: exe, digest: _ } =>
Present { value: exe }
ArtifactMaterializationBuildFailed { target_name: _ } => none
ArtifactMaterializationPathRefused { selection: _ } => none
ArtifactMaterializationDigestRefused { executable: _, cause: _ } => none
}
}
171 changes: 171 additions & 0 deletions dag/test/claim/self_host_artifact_materialization_witness_test.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
module test.claim.self_host_artifact_materialization_witness

data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly

data artifact_materialization_witness_note: String = "These claims execute the two PURE folds that carry every decision in gunbc.self_host_artifact_materialization, which is why the decisions are witnessed on every PR instead of behind a wet cadence. The discriminating claims are not the happy path: they are that a build failure never reads as a path refusal, that no failure arm produces ArtifactMaterialized, and that the executable carried out of the fold is the one the cargo stream NAMED. That last one is the whole lane in a single assertion — a convention-derived path would satisfy every other claim here while pointing at a different file than the one digested."

data valid_hex: String = "cf03d459f24df996cd9f067bb9ef1eb98b4110a3566bc0db54d6949e5371e7861511488e25a86c5177a6940af6e97e7a337e987ce464b55408fe325df9993be9"

data artifact_stream: String = "\{\"reason\":\"compiler-artifact\",\"target\":\{\"name\":\"gunbc\",\"kind\":[\"bin\"]\},\"executable\":\"/w/target/release/gunbc\"\}\n\{\"reason\":\"build-finished\",\"success\":true\}"

data ambiguous_stream: String = "\{\"reason\":\"compiler-artifact\",\"target\":\{\"name\":\"gunbc\",\"kind\":[\"bin\"]\},\"executable\":\"/w/target/release/gunbc\"\}\n\{\"reason\":\"compiler-artifact\",\"target\":\{\"name\":\"gunbc\",\"kind\":[\"bin\"]\},\"executable\":\"/w/target/release/other\"\}"

data observed_digest_note: String = "valid_hex is the REAL sha512 of the ASCII string gunbc-artifact-materialization-fixture, reproducible with printf | sha512sum, rather than hand-typed hex. The first draft here WAS hand-typed and was 130 characters instead of 128; the refinement rejected it, good_observation became a refusal, and the three positive claims went red — which is the wall doing its job and is why this note exists. Built through the validating mint rather than by labelling text a digest: sha512_hex_digest applies the length and lowercase-hex refinement, so if this fixture hex is ever wrong the observation becomes a REFUSAL and the positive claims below go red, rather than a bad digest silently flowing into the carrier. That is the same construction wall the artifact axis relies on, exercised by the witness that depends on it."

fn good_observation() -> Sha512FileObservation {
match std.content_hash.sha512_hex_digest(hex: valid_hex) {
Present { value: d } => Sha512FileObserved { digest: d }
Absent =>
Sha512FileObservationRefused { cause: "fixture hex failed the refinement" as NonEmptyStr }
}
}

fn path_of(build_success: Bool, stdout: String) -> ArtifactPathOutcome {
gunbc.self_host_artifact_materialization.select_artifact_path(
build_success: build_success,
build_stdout: stdout,
target_name: "gunbc"
)
}

fn reported_path_or_empty(outcome: ArtifactPathOutcome) -> String {
match outcome {
ArtifactPathReported { executable: exe } => exe
ArtifactPathBuildFailed { target_name: _ } => ""
ArtifactPathRefused { selection: _ } => ""
}
}

test fn the_reported_path_is_the_one_the_stream_named() -> Bool {
let reported = reported_path_or_empty(
outcome: path_of(build_success: true, stdout: artifact_stream)
)
reported == "/w/target/release/gunbc"
}

test fn a_failed_build_is_its_own_arm_not_a_path_refusal() -> Bool {
match path_of(build_success: false, stdout: artifact_stream) {
ArtifactPathBuildFailed { target_name: t } => t == "gunbc"
ArtifactPathReported { executable: _ } => false
ArtifactPathRefused { selection: _ } => false
}
}

test fn a_failed_build_does_not_consult_the_stream_at_all() -> Bool {
match path_of(build_success: false, stdout: "not json at all") {
ArtifactPathBuildFailed { target_name: t } => t == "gunbc"
ArtifactPathReported { executable: _ } => false
ArtifactPathRefused { selection: _ } => false
}
}

test fn an_ambiguous_stream_refuses_and_carries_the_selection() -> Bool {
match path_of(build_success: true, stdout: ambiguous_stream) {
ArtifactPathRefused { selection: s } =>
match s {
CargoArtifactAmbiguous { target_name: t, count: c } => t == "gunbc" && c == 2
CargoArtifactSelected { executable: _ } => false
CargoArtifactAbsent { target_name: _ } => false
CargoArtifactWithoutExecutable { target_name: _ } => false
CargoArtifactExecutablePathEmpty { target_name: _, line_number: _ } => false
CargoArtifactExecutableNotAString { target_name: _, line_number: _ } => false
CargoMessageExecutableAbsent { line_number: _ } => false
CargoMessageLineUnparseable { line_number: _ } => false
CargoMessageLineNotAnObject { line_number: _ } => false
CargoMessageReasonAbsent { line_number: _ } => false
CargoMessageReasonNotAString { line_number: _ } => false
CargoMessageMemberDuplicated { line_number: _, key: _ } => false
CargoMessageTargetNotAnObject { line_number: _ } => false
CargoMessageTargetNameAbsent { line_number: _ } => false
CargoMessageTargetNameNotAString { line_number: _ } => false
}
ArtifactPathReported { executable: _ } => false
ArtifactPathBuildFailed { target_name: _ } => false
}
}

test fn an_observed_digest_materializes_and_keeps_the_named_path() -> Bool {
let outcome = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: artifact_stream),
observation: good_observation()
)
match outcome {
ArtifactMaterializationObserved { executable: exe, digest: _ } =>
exe == "/w/target/release/gunbc"
ArtifactMaterializationBuildFailed { target_name: _ } => false
ArtifactMaterializationPathRefused { selection: _ } => false
ArtifactMaterializationDigestRefused { executable: _, cause: _ } => false
}
}

test fn a_refused_digest_does_not_materialize() -> Bool {
let outcome = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: artifact_stream),
observation: Sha512FileObservationRefused {
cause: "sha512sum DigestFile failed" as NonEmptyStr
}
)
match gunbc.self_host_artifact_materialization.artifact_identity_of(outcome: outcome) {
ArtifactNotMaterialized => true
ArtifactMaterialized { digest: _ } => false
}
}

test fn a_failed_build_does_not_materialize_even_with_a_good_digest() -> Bool {
let outcome = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: false, stdout: artifact_stream),
observation: good_observation()
)
match gunbc.self_host_artifact_materialization.artifact_identity_of(outcome: outcome) {
ArtifactNotMaterialized => true
ArtifactMaterialized { digest: _ } => false
}
}

test fn an_ambiguous_stream_does_not_materialize_even_with_a_good_digest() -> Bool {
let outcome = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: ambiguous_stream),
observation: good_observation()
)
match gunbc.self_host_artifact_materialization.artifact_identity_of(outcome: outcome) {
ArtifactNotMaterialized => true
ArtifactMaterialized { digest: _ } => false
}
}

test fn only_an_observed_outcome_yields_an_invocable_path() -> Bool {
let good = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: artifact_stream),
observation: good_observation()
)
let bad = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: ambiguous_stream),
observation: good_observation()
)
let good_path = match gunbc.self_host_artifact_materialization.materialized_executable(outcome: good) {
Present { value: p } => p
Absent => ""
}
let bad_path = match gunbc.self_host_artifact_materialization.materialized_executable(outcome: bad) {
Present { value: p } => p
Absent => ""
}
good_path == "/w/target/release/gunbc" && bad_path == ""
}

test fn the_digested_path_and_the_invocable_path_are_one_value() -> Bool {
let outcome = gunbc.self_host_artifact_materialization.materialization_outcome_from_path(
path: path_of(build_success: true, stdout: artifact_stream),
observation: good_observation()
)
match outcome {
ArtifactMaterializationObserved { executable: exe, digest: _ } =>
match gunbc.self_host_artifact_materialization.materialized_executable(outcome: outcome) {
Present { value: p } => p == exe
Absent => false
}
ArtifactMaterializationBuildFailed { target_name: _ } => false
ArtifactMaterializationPathRefused { selection: _ } => false
ArtifactMaterializationDigestRefused { executable: _, cause: _ } => false
}
}
Loading