Skip to main content

logicaffeine_kernel/interface/lean/
package.rs

1//! The portable proof package (`logos-lean-package/1` and `/2`) and its replay.
2//!
3//! Pure: a package is a map from published relative path to bytes. Writing it to disk atomically
4//! and running [`replay`] in a fresh process are the caller's (`largo lean`); everything a replay
5//! decides is decided here, from the bytes alone, with no importer state.
6//!
7//! # Two proof contracts
8//!
9//! The export decides which contract a package states, never the manifest. When the target's
10//! closure, as LOGOS checks it, re-derives Lean's `native_decide` steps by kernel evaluation
11//! ([`native_decide`](super::native_decide)), the package is `logos-lean-package/2`; otherwise it
12//! is `logos-lean-package/1`.
13//!
14//! * `/1`, the export's proof as exported. `lean_print_axioms`, when the producing run observed
15//!   Lean, names exactly the `transitive` assumptions.
16//! * `/2`, the export's proof with every native decision of the target's closure re-derived. Its
17//!   two assumption sets are kept apart: `lean_print_axioms` is Lean's own report for the proof it
18//!   replaced, required, naming [`ELIMINATED_AXIOM`](super::native_decide::ELIMINATED_AXIOM); and
19//!   `transitive` is what the checked replacement rests on, which is exactly Lean's report without
20//!   that escape. Every declaration holding re-derived steps is disclosed as one
21//!   [`INFORMATION_CATEGORY`](super::native_decide::INFORMATION_CATEGORY) information change.
22//!
23//! The schema is the result identity's domain, so one set of files never carries both contracts'
24//! identities.
25//!
26//! # What a package holds
27//!
28//! ```text
29//! manifest.json          schema, result id, the SHA-256 of every other file, source identity,
30//!                        target (Lean name, kernel name, kind, universe parameters, statement,
31//!                        LOGOS name), toolchain, transitive assumptions, information losses, the
32//!                        trusted computing base, and the producing run's receipt
33//! <Target>.lg            the LOGOS document: literate prose, English forms and kernel vernacular
34//! certificate.json       the environment certificate (`logos-environment-certificate/1` or `/2`)
35//! correspondence.json    Lean name ↔ kernel name ↔ LOGOS name (and document lines)
36//! source/export.ndjson   the exact export the result was produced from
37//! source/*.lean          the original Lean source, when the producing run had it
38//! ```
39//!
40//! # What a replay establishes, and what it does not
41//!
42//! A replay reads nothing but the package. Before any checking work it enforces the file-count,
43//! byte, path and term limits, verifies every digest and the result identity, and requires every
44//! JSON file to be in its one canonical encoding. Then:
45//!
46//! * `primary_check` — the certificate's environment is rebuilt from the bare kernel, every
47//!   inductive, constructor, definition, recursor, opaque theorem, structure η and quotient
48//!   primitive kernel-validated under exact (not cumulative) conversion, with no obligation left.
49//! * `independent_recheck` — every checked term was also inferred by the second kernel
50//!   (`recheck`) to its declared type. A term outside that checker's fragment makes the stage
51//!   `unsupported`, never `passed`.
52//! * `translated` — the statement is BOUND to the source: the reference translator (which shares no
53//!   code with the importer) reads the target's type from `source/export.ndjson`, and it must be
54//!   exactly convertible with the certificate's statement. Every other certificate declaration
55//!   with a source counterpart is bound the same way, and the correspondence must agree. The
56//!   manifest states the contract the carried export decides: its schema, and exactly the
57//!   native re-derivations recomputed from the export.
58//! * `assumption_policy` — the transitive assumptions of statement AND proof, traced through opaque
59//!   theorems and definitions, are exactly Lean's `propext`, `Quot.sound` and `Classical.choice` in
60//!   their canonical form; an admission (`sorryAx` or anything of its universal-inhabitant shape)
61//!   and an axiom stating the goal are refused by structure, not by spelling. Lean's recorded
62//!   report agrees with them as the contract says, and the manifest lists exactly them.
63//! * `emission_reparsed` — the LOGOS document is exactly what the emitter writes for the rebuilt
64//!   environment, it reparses in a fresh LOGOS session, and its target states the reference
65//!   translation.
66//! * `fresh_replay` — every stage above passed, from the package bytes alone.
67//!
68//! `lean_accepted` is `not_run`: a replay never runs Lean. The producing run's receipt is in the
69//! manifest, and a replay repeats what it says without adopting it.
70
71use super::native_decide::{Elimination, NativeDecisions};
72use super::receipt::{Receipt, Stage, StageStatus, STAGES};
73use super::reference::{Reference, ReferenceKind};
74use super::{declined, rejected, DiagnosticCode, Result, Verdict};
75use crate::certificate::{
76    read_certificate, AnyCertificate, CaptureRequest, ConversionDiscipline, EnvironmentBase,
77    EnvironmentCertificate, EnvironmentEntry, EnvironmentError, EnvironmentErrorCode,
78    EnvironmentLimits, IndependentVerdict, RebuiltEnvironment, ENVIRONMENT_SCHEMA,
79    OPAQUE_ENVIRONMENT_SCHEMA, PRELUDE_VERSION,
80};
81use crate::{Term, Universe};
82use serde::{Deserialize, Serialize};
83use std::collections::{BTreeMap, BTreeSet};
84
85/// The schema of a package whose proof is the export's own, checked as exported.
86pub const PACKAGE_SCHEMA: &str = "logos-lean-package/1";
87/// The schema of a package whose proof is the export's with the native decisions of the target's
88/// closure re-derived by kernel evaluation.
89pub const NATIVE_REDERIVED_SCHEMA: &str = "logos-lean-package/2";
90/// Every package schema this crate writes and replays.
91pub const PACKAGE_SCHEMAS: [&str; 2] = [PACKAGE_SCHEMA, NATIVE_REDERIVED_SCHEMA];
92pub const REPLAY_SCHEMA: &str = "logos-lean-replay/1";
93pub const CORRESPONDENCE_SCHEMA: &str = "logos-lean-correspondence/1";
94
95/// A package's files, keyed by published relative path.
96pub type PackageFiles = BTreeMap<String, Vec<u8>>;
97
98pub const MANIFEST: &str = "manifest.json";
99pub const CERTIFICATE: &str = "certificate.json";
100pub const CORRESPONDENCE: &str = "correspondence.json";
101pub const EXPORT: &str = "source/export.ndjson";
102
103/// Lean's three standard axioms, in the kernel's spelling of their Lean names — the only
104/// assumptions a verified result may rest on.
105pub const ALLOWED_AXIOMS: [(&str, &str); 3] = [
106    ("propext", "/s:\"propext\""),
107    ("Quot.sound", "/s:\"Quot\"/s:\"sound\""),
108    ("Classical.choice", "/s:\"Classical\"/s:\"choice\""),
109];
110
111/// The trusted computing base shared by the checks a package's receipt names. Two checkers that
112/// share this much are two implementations of typing, not two independent theorem provers, and the
113/// manifest says so in every package.
114pub const TRUSTED_BASE: [&str; 8] = [
115    "The LOGOS kernel crate (logicaffeine-kernel): its Term, Universe and Context representations, reduction and definitional equality, and the primary type checker.",
116    "The independent re-checker (`recheck`) is a separately written de Bruijn type checker in the same crate. It shares the Term, Universe and Context representations and the environment the primary check built; it corroborates typing, it is not an independent theorem prover.",
117    "Exact (Lean) conversion is enforced by the Lean reader's exact typing traversal over the kernel's own reduction.",
118    "The statement binding reads the export with the reference translator, which shares no code with the importer's conversion, sharing or caching machinery; it shares the kernel's Term type and the serde_json parser.",
119    "Structure η, quotient primitives and the kernel's literal Nat are admitted only in their checked canonical forms.",
120    "The LOGOS document is checked by the LOGOS parser and `Repl::execute_document` over the same kernel.",
121    "SHA-256 digests identify bytes; they establish no semantic correspondence. The binding and the kernel do.",
122    "That the pinned Lean accepted the source is recorded by the producing run and is not re-established by replay.",
123];
124
125// -------------------------------------------------------------------------------------------------
126// SHA-256 (FIPS 180-4), for content digests. Kept here so the kernel stays free of dependencies.
127// -------------------------------------------------------------------------------------------------
128
129mod sha256 {
130    const K: [u32; 64] = [
131        0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4,
132        0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe,
133        0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f,
134        0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
135        0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc,
136        0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
137        0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116,
138        0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
139        0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
140        0xc67178f2,
141    ];
142    const H0: [u32; 8] = [
143        0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab,
144        0x5be0cd19,
145    ];
146
147    fn compress(h: &mut [u32; 8], block: &[u8]) {
148        let mut w = [0u32; 64];
149        for (i, word) in block.chunks_exact(4).enumerate() {
150            w[i] = u32::from_be_bytes([word[0], word[1], word[2], word[3]]);
151        }
152        for i in 16..64 {
153            let s0 = w[i - 15].rotate_right(7) ^ w[i - 15].rotate_right(18) ^ (w[i - 15] >> 3);
154            let s1 = w[i - 2].rotate_right(17) ^ w[i - 2].rotate_right(19) ^ (w[i - 2] >> 10);
155            w[i] = w[i - 16]
156                .wrapping_add(s0)
157                .wrapping_add(w[i - 7])
158                .wrapping_add(s1);
159        }
160        let [mut a, mut b, mut c, mut d, mut e, mut f, mut g, mut hh] = *h;
161        for i in 0..64 {
162            let s1 = e.rotate_right(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
163            let ch = (e & f) ^ (!e & g);
164            let t1 = hh
165                .wrapping_add(s1)
166                .wrapping_add(ch)
167                .wrapping_add(K[i])
168                .wrapping_add(w[i]);
169            let s0 = a.rotate_right(2) ^ a.rotate_right(13) ^ a.rotate_right(22);
170            let maj = (a & b) ^ (a & c) ^ (b & c);
171            let t2 = s0.wrapping_add(maj);
172            hh = g;
173            g = f;
174            f = e;
175            e = d.wrapping_add(t1);
176            d = c;
177            c = b;
178            b = a;
179            a = t1.wrapping_add(t2);
180        }
181        for (slot, value) in h.iter_mut().zip([a, b, c, d, e, f, g, hh]) {
182            *slot = slot.wrapping_add(value);
183        }
184    }
185
186    pub fn digest(message: &[u8]) -> [u8; 32] {
187        let mut h = H0;
188        let mut chunks = message.chunks_exact(64);
189        for block in chunks.by_ref() {
190            compress(&mut h, block);
191        }
192        let rest = chunks.remainder();
193        let mut tail = [0u8; 128];
194        tail[..rest.len()].copy_from_slice(rest);
195        tail[rest.len()] = 0x80;
196        let length = if rest.len() < 56 { 64 } else { 128 };
197        tail[length - 8..length]
198            .copy_from_slice(&((message.len() as u64).wrapping_mul(8)).to_be_bytes());
199        for block in tail[..length].chunks_exact(64) {
200            compress(&mut h, block);
201        }
202        let mut out = [0u8; 32];
203        for (i, word) in h.iter().enumerate() {
204            out[i * 4..i * 4 + 4].copy_from_slice(&word.to_be_bytes());
205        }
206        out
207    }
208}
209
210/// The lowercase hexadecimal SHA-256 of `bytes`.
211pub fn sha256_hex(bytes: &[u8]) -> String {
212    sha256::digest(bytes)
213        .iter()
214        .map(|b| format!("{b:02x}"))
215        .collect()
216}
217
218// -------------------------------------------------------------------------------------------------
219// The manifest and correspondence formats.
220// -------------------------------------------------------------------------------------------------
221
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223#[serde(deny_unknown_fields)]
224pub struct Manifest {
225    pub schema: String,
226    /// `sha256:<hex>` of the target's Lean name and every other file's path and digest.
227    pub result_id: String,
228    /// The SHA-256 of every file in the package except this manifest, by path.
229    pub files: BTreeMap<String, String>,
230    pub source: SourceIdentity,
231    pub target: TargetIdentity,
232    /// The LOGOS document's path, or `None` when the emitter could not write one.
233    pub document: Option<String>,
234    pub emission: EmissionRecord,
235    pub toolchain: Toolchain,
236    pub assumptions: AssumptionRecord,
237    pub information_losses: Vec<InformationLoss>,
238    pub trusted_base: Vec<String>,
239    /// The producing run's receipt, stage by stage.
240    pub receipt: BTreeMap<String, StageRecord>,
241}
242
243#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
244#[serde(deny_unknown_fields)]
245pub struct SourceIdentity {
246    pub export: String,
247    pub export_sha256: String,
248    pub format: String,
249    pub lean_version: String,
250    pub lean_githash: Option<String>,
251    pub exporter: String,
252    /// Original Lean source files carried in the package (under `source/`).
253    pub lean_sources: Vec<String>,
254}
255
256#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
257#[serde(deny_unknown_fields)]
258pub struct TargetIdentity {
259    /// The Lean name, as Lean writes it.
260    pub lean: String,
261    /// The kernel's spelling, the certificate's target.
262    pub kernel: String,
263    /// `thm`, `def` or `opaque`.
264    pub kind: String,
265    pub level_params: Vec<String>,
266    /// The statement, rendered for reading (the certificate carries the term).
267    pub statement: String,
268    /// The LOGOS name in the document, when there is one.
269    pub logos: Option<String>,
270}
271
272#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
273#[serde(deny_unknown_fields)]
274pub struct EmissionRecord {
275    /// `emitted`, `unsupported` or `failed`.
276    pub status: String,
277    pub detail: String,
278    /// Declarations whose literate form was omitted, with reasons.
279    pub omissions: Vec<String>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283#[serde(deny_unknown_fields)]
284pub struct Toolchain {
285    pub logos: String,
286    pub prelude: String,
287    pub certificate_schema: String,
288    pub lean: String,
289    pub exporter: String,
290}
291
292#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
293#[serde(deny_unknown_fields)]
294pub struct AssumptionRecord {
295    /// The policy: the assumptions a verified result may rest on (Lean names).
296    pub allowed: Vec<String>,
297    /// The transitive assumptions of the statement and the checked proof (kernel names), sorted —
298    /// Lean's `#print axioms` semantics: an axiom reached counts, the axioms its own TYPE mentions
299    /// do not; definitions and opaque theorems are traced through their types and bodies.
300    pub transitive: Vec<String>,
301    /// What the pinned Lean's own `#print axioms` reported for the target (Lean names), when the
302    /// producing run compiled the source: the original evidence, recorded as Lean wrote it. Under
303    /// `logos-lean-package/1` it names exactly `transitive`; under `/2` it is required, and it names
304    /// `transitive` and the eliminated escape.
305    pub lean_print_axioms: Option<Vec<String>>,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
309#[serde(deny_unknown_fields)]
310pub struct InformationLoss {
311    pub category: String,
312    pub detail: String,
313}
314
315#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
316#[serde(deny_unknown_fields)]
317pub struct StageRecord {
318    pub status: String,
319    pub code: Option<String>,
320    pub detail: String,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
324#[serde(deny_unknown_fields)]
325pub struct CorrespondenceFile {
326    pub schema: String,
327    /// The target's kernel name.
328    pub target: String,
329    pub rows: Vec<CorrespondenceRow>,
330}
331
332#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
333#[serde(deny_unknown_fields)]
334pub struct CorrespondenceRow {
335    /// The Lean name as Lean writes it (for a reader-introduced helper, the reader's own name).
336    pub lean: String,
337    /// The kernel name, as the certificate declares it.
338    pub kernel: String,
339    /// The LOGOS name in the document, when the document declares it.
340    pub logos: Option<String>,
341    pub kind: String,
342    /// `authored`, `helper`, `synthesized` or `prelude`.
343    pub origin: String,
344    /// For a helper, the authored declaration it serves.
345    pub helper_of: Option<String>,
346    /// `literate`, `vernacular` or `prelude`, when the document carries it.
347    pub form: Option<String>,
348    /// Universe parameters: (kernel name, LOGOS name).
349    pub levels: Vec<(String, String)>,
350    /// The document's 1-based inclusive line range.
351    pub lines: Option<(usize, usize)>,
352}
353
354/// The one canonical encoding of a package JSON file: pretty-printed, newline-terminated.
355pub fn canonical_json<T: Serialize>(value: &T) -> Vec<u8> {
356    let mut bytes = serde_json::to_vec_pretty(value).expect("package JSON serializes");
357    bytes.push(b'\n');
358    bytes
359}
360
361/// The certificate's canonical encoding: compact, newline-terminated.
362pub fn canonical_certificate(certificate: &EnvironmentCertificate) -> Vec<u8> {
363    let mut bytes = serde_json::to_vec(certificate).expect("a certificate serializes");
364    bytes.push(b'\n');
365    bytes
366}
367
368/// The result identity of a `logos-lean-package/1` package: the target and every non-manifest
369/// file's path and digest.
370pub fn result_id(target_lean: &str, files: &BTreeMap<String, String>) -> String {
371    result_id_in(PACKAGE_SCHEMA, target_lean, files)
372}
373
374/// The result identity under `schema`, which is the preimage's domain: the same files under two
375/// contracts have two identities.
376pub fn result_id_in(schema: &str, target_lean: &str, files: &BTreeMap<String, String>) -> String {
377    let mut preimage = format!("{schema}\0{target_lean}\0");
378    for (path, digest) in files {
379        preimage.push_str(path);
380        preimage.push('\0');
381        preimage.push_str(digest);
382        preimage.push('\n');
383    }
384    format!("sha256:{}", sha256_hex(preimage.as_bytes()))
385}
386
387/// The document's file name for a Lean target: its name with anything outside letters, digits,
388/// `_`, `-` and `.` written as `_`, then `.lg`.
389pub fn document_path(target_lean: &str) -> String {
390    let stem: String = target_lean
391        .chars()
392        .map(|c| {
393            if c.is_alphanumeric() || matches!(c, '_' | '-' | '.') {
394                c
395            } else {
396                '_'
397            }
398        })
399        .collect();
400    let stem = stem.trim_start_matches('.');
401    format!("{}.lg", if stem.is_empty() { "target" } else { stem })
402}
403
404// -------------------------------------------------------------------------------------------------
405// Limits.
406// -------------------------------------------------------------------------------------------------
407
408/// Limits enforced on a package before any work.
409#[derive(Clone, Copy, Debug, PartialEq, Eq)]
410pub struct PackageLimits {
411    pub max_files: usize,
412    pub max_file_bytes: usize,
413    pub max_total_bytes: usize,
414    pub max_path_bytes: usize,
415    pub environment: EnvironmentLimits,
416}
417
418impl Default for PackageLimits {
419    fn default() -> Self {
420        PackageLimits {
421            max_files: 64,
422            max_file_bytes: 512 * 1024 * 1024,
423            max_total_bytes: 1024 * 1024 * 1024,
424            max_path_bytes: 255,
425            environment: EnvironmentLimits::default(),
426        }
427    }
428}
429
430/// Why a path cannot be a package path, or `None` when it can.
431fn unsafe_path(path: &str, limits: &PackageLimits) -> Option<&'static str> {
432    if path.is_empty() {
433        return Some("an empty path");
434    }
435    if path.len() > limits.max_path_bytes {
436        return Some("a path longer than the limit");
437    }
438    if path.starts_with('/') || path.contains('\\') || path.contains('\0') || path.contains(':') {
439        return Some("an absolute or non-portable path");
440    }
441    if path
442        .split('/')
443        .any(|c| c.is_empty() || c == "." || c == ".." || c.starts_with('.'))
444    {
445        return Some("a path with an empty, dot or hidden component");
446    }
447    if path.chars().any(char::is_control) {
448        return Some("a path with a control character");
449    }
450    None
451}
452
453// -------------------------------------------------------------------------------------------------
454// Building a package.
455// -------------------------------------------------------------------------------------------------
456
457/// What the producing run knows that the export does not say.
458#[derive(Clone, Debug, Default)]
459pub struct BuildOptions {
460    /// Original Lean source files, by project-relative path (published under `source/`).
461    pub lean_sources: BTreeMap<String, Vec<u8>>,
462    /// The pinned Lean's verdict on the source, when the producing run compiled it.
463    pub lean_run: Option<LeanRun>,
464}
465
466/// A pinned Lean toolchain's run over the original source.
467#[derive(Clone, Debug)]
468pub struct LeanRun {
469    pub toolchain: String,
470    pub exporter: String,
471    /// Whether Lean accepted the source (compiled, with the target's `#print axioms` within policy).
472    pub accepted: bool,
473    pub detail: String,
474}
475
476/// A built package and the producing run's receipt. A build never claims more than its receipt:
477/// [`assemble`] is the door that refuses to hand out a package whose receipt is not clean.
478pub struct Build {
479    pub files: PackageFiles,
480    pub receipt: Receipt,
481    /// The assumption policy over the checked proof alone: the audit of what it rests on, before
482    /// the package's contract holds that against Lean's report. The receipt's `assumption_policy`
483    /// is this audit and then the contract's agreement.
484    pub audit: Stage,
485}
486
487/// Build the package for `target` from a Lean export, and require that every stage the producing
488/// run can establish passed. A package is returned only when it would replay clean.
489pub fn assemble(export: &str, target: &str) -> Result<PackageFiles> {
490    assemble_with(export, target, &BuildOptions::default())
491}
492
493/// [`assemble`] with the producing run's own evidence.
494pub fn assemble_with(export: &str, target: &str, options: &BuildOptions) -> Result<PackageFiles> {
495    assemble_observed(export, target, options, None)
496}
497
498/// [`assemble_with`], holding the result's assumptions against what the pinned Lean's own
499/// `#print axioms` reported for the target (Lean names), as the package's contract says: any
500/// difference in either direction, and a native-rederived proof without Lean's report, is an
501/// `assumption_disagreement`, and no package is handed out.
502pub fn assemble_observed(
503    export: &str,
504    target: &str,
505    options: &BuildOptions,
506    lean_print_axioms: Option<&[String]>,
507) -> Result<PackageFiles> {
508    publishable(build_observed(export, target, options, lean_print_axioms)?)
509}
510
511/// The package of `build`, when every stage the producing run can establish passed and the
512/// package replays clean in this process; otherwise the first stage that did not pass, as a refusal.
513pub fn publishable(build: Build) -> Result<PackageFiles> {
514    // In the order a failure explains the rest: a stage not run behind a failed one is not the news.
515    for stage in [
516        "primary_check",
517        "independent_recheck",
518        "translated",
519        "assumption_policy",
520        "emission_reparsed",
521    ] {
522        let record = build
523            .receipt
524            .stage(stage)
525            .expect("every stage is on the receipt");
526        if record.status != StageStatus::Passed {
527            let code = match (stage, record.status) {
528                _ if record.code.as_deref() == Some("resource_limit") => {
529                    DiagnosticCode::ResourceLimit
530                }
531                ("assumption_policy", StageStatus::Failed)
532                    if record.code.as_deref() == Some("assumption_disagreement") =>
533                {
534                    DiagnosticCode::AssumptionDisagreement
535                }
536                ("assumption_policy", StageStatus::Failed) => DiagnosticCode::DisallowedAssumption,
537                (_, StageStatus::Unsupported | StageStatus::NotRun) => {
538                    DiagnosticCode::UnsupportedFeature
539                }
540                (_, _) => DiagnosticCode::InvalidProof,
541            };
542            let message = format!("{stage} {}: {}", record.status.as_str(), record.detail);
543            return Err(
544                if matches!(
545                    code,
546                    DiagnosticCode::UnsupportedFeature | DiagnosticCode::ResourceLimit
547                ) {
548                    declined(code, message)
549                } else {
550                    rejected(code, message)
551                },
552            );
553        }
554    }
555    let check = replay(&build.files);
556    if !check.replayed() {
557        let failure = check
558            .first_failure()
559            .map(|s| format!("{}: {}", s.name, s.detail))
560            .unwrap_or_default();
561        return Err(rejected(
562            DiagnosticCode::InvalidProof,
563            format!("the assembled package does not replay: {failure}"),
564        ));
565    }
566    Ok(build.files)
567}
568
569/// Capture `kernel_target`'s environment certificate from ONE import that retained every checked
570/// proof (an opaque theorem publishes only its header) and carried Lean's binder names. A proof the
571/// capture needs and the import did not retain is `MissingEvidence`, never an axiom.
572fn capture_with_proofs(
573    imported: &super::ImportedContext,
574    kernel_target: &str,
575) -> Result<(EnvironmentCertificate, BTreeMap<String, Term>)> {
576    let proofs: BTreeMap<String, Term> = imported
577        .retained_declarations()
578        .map(|(name, _, body)| (name.to_string(), body.clone()))
579        .collect();
580    let request = CaptureRequest {
581        context: imported,
582        base: EnvironmentBase::Kernel,
583        conversion: ConversionDiscipline::Exact,
584        target: kernel_target,
585        opaque_proofs: &proofs,
586    };
587    let certificate = EnvironmentCertificate::capture(&request).map_err(environment_verdict)?;
588    // Keep only the proofs the certificate carries: re-emission reads exactly these.
589    let carried: BTreeSet<&str> = certificate
590        .entries
591        .iter()
592        .filter_map(|e| match e {
593            EnvironmentEntry::Theorem { name, .. } | EnvironmentEntry::Opaque { name, .. } => {
594                Some(name.as_str())
595            }
596            _ => None,
597        })
598        .collect();
599    let proofs = proofs
600        .into_iter()
601        .filter(|(name, _)| carried.contains(name.as_str()))
602        .collect();
603    Ok((certificate, proofs))
604}
605
606fn environment_verdict(error: EnvironmentError) -> Verdict {
607    let code = match error.code {
608        EnvironmentErrorCode::ResourceLimit => DiagnosticCode::ResourceLimit,
609        EnvironmentErrorCode::Unsupported | EnvironmentErrorCode::MissingEvidence => {
610            DiagnosticCode::UnsupportedFeature
611        }
612        _ => DiagnosticCode::InvalidProof,
613    };
614    let message = error.to_string();
615    if matches!(code, DiagnosticCode::InvalidProof) {
616        rejected(code, message)
617    } else {
618        declined(code, message)
619    }
620}
621
622/// Build a package and its producing receipt. Refuses only when no package can be formed at all
623/// (an export the importer does not accept, a target it does not declare); a package whose checks
624/// fail is returned with the failure on its receipt, so a refusal can be inspected and replayed.
625pub fn build(export: &str, target: &str, options: &BuildOptions) -> Result<Build> {
626    build_observed(export, target, options, None)
627}
628
629/// [`build`], with the pinned Lean's `#print axioms` for the target when the producing run has it.
630pub fn build_observed(
631    export: &str,
632    target: &str,
633    options: &BuildOptions,
634    lean_print_axioms: Option<&[String]>,
635) -> Result<Build> {
636    // Source terms and their drops need the same bounded worker as certificate checking.
637    on_kernel_stack(|| build_observed_on_stack(export, target, options, lean_print_axioms))?
638}
639
640fn build_observed_on_stack(
641    export: &str,
642    target: &str,
643    options: &BuildOptions,
644    lean_print_axioms: Option<&[String]>,
645) -> Result<Build> {
646    let header: serde_json::Value = serde_json::from_str(export.lines().next().unwrap_or(""))
647        .map_err(|e| {
648            declined(
649                DiagnosticCode::MalformedExport,
650                format!("export header: {e}"),
651            )
652        })?;
653    let reference = Reference::read(export)?;
654    let source_limits = EnvironmentLimits::default();
655    reference.preflight(
656        &[(target.to_string(), false)],
657        source_limits.max_depth,
658        source_limits.max_tree_nodes,
659    )?;
660    let kernel_target = reference.source_name(target)?;
661    let mut source_work = SourceWork::new(&source_limits)?;
662    let (imported, report) = super::import_with(
663        export,
664        super::ImportRequest {
665            audit: true,
666            retain: super::RetainSource::EveryProof,
667            lean_binder_names: true,
668        },
669    )?;
670    let declaration = source_work.declaration(&*imported, &reference, &kernel_target)?;
671    let kind = match declaration.kind {
672        ReferenceKind::Theorem => "thm",
673        ReferenceKind::Definition => "def",
674        ReferenceKind::Opaque => "opaque",
675        other => {
676            return Err(rejected(
677                DiagnosticCode::InvalidProof,
678                format!(
679                    "{} is a Lean {}, and a package proves a theorem or definition",
680                    declaration.lean_name,
681                    other.as_str()
682                ),
683            ))
684        }
685    };
686    for (name, bytes) in &options.lean_sources {
687        // Project-relative, so a multi-module project keeps its layout (`Demo/Defs.lean`).
688        if unsafe_path(&format!("source/{name}"), &PackageLimits::default()).is_some()
689            || !name.ends_with(".lean")
690        {
691            return Err(declined(
692                DiagnosticCode::Infrastructure,
693                format!("{name:?} is not a project-relative Lean source path"),
694            ));
695        }
696        if std::str::from_utf8(bytes).is_err() {
697            return Err(declined(
698                DiagnosticCode::MalformedExport,
699                format!("{name} is not UTF-8"),
700            ));
701        }
702    }
703    let (certificate, proofs) = capture_with_proofs(&imported, &declaration.name)?;
704    let mut receipt = Receipt::unstarted();
705    match &options.lean_run {
706        Some(run) => receipt.set(
707            "lean_accepted",
708            if run.accepted {
709                StageStatus::Passed
710            } else {
711                StageStatus::Failed
712            },
713            format!("{} with {}: {}", run.toolchain, run.exporter, run.detail),
714        ),
715        None => receipt.set(
716            "lean_accepted",
717            StageStatus::NotRun,
718            "this run read a pre-exported artifact and did not run Lean",
719        ),
720    }
721    source_binding_metrics(
722        &reference,
723        &certificate,
724        &declaration.name,
725        lean_print_axioms.unwrap_or(&[]),
726        &source_limits,
727    )?;
728    let native = NativeDecisions::read(export).closure_of(&declaration.name);
729    let schema = contract_schema(&native);
730    let mut checks = check_environment(
731        &certificate,
732        &reference,
733        &declaration.name,
734        &source_limits,
735        &mut source_work,
736    );
737    checks.rebuilt = None;
738    let audit = stage("assumption_policy", &checks.policy);
739    if checks.primary.0 == StageStatus::Passed {
740        if let Err(detail) =
741            agree_with_lean(&reference, &native, lean_print_axioms, &checks.transitive)
742        {
743            checks.policy = failed("assumption_disagreement", detail);
744        }
745    }
746    checks.record(&mut receipt);
747
748    // The LOGOS document, written by the emitter from the imported environment and checked in a
749    // fresh LOGOS session against the reference translation.
750    let proof_of = |key: &str| proofs.get(key).cloned();
751    let document = match super::emit::emit_context(&*imported, &proof_of, &declaration.name) {
752        Ok(document) => match super::emit::check_statement(&document, &declaration) {
753            Ok(()) => {
754                receipt.set("emission_reparsed", StageStatus::Passed, "the document reparsed in a fresh LOGOS session and states the reference translation");
755                Some(document)
756            }
757            Err(fault) => {
758                receipt.set_with_code(
759                    "emission_reparsed",
760                    StageStatus::Failed,
761                    "emission_mismatch",
762                    fault.to_string(),
763                );
764                None
765            }
766        },
767        Err(verdict) => {
768            let unsupported = matches!(verdict, Verdict::Decline(_));
769            receipt.set_with_code(
770                "emission_reparsed",
771                if unsupported {
772                    StageStatus::Unsupported
773                } else {
774                    StageStatus::Failed
775                },
776                verdict.reason().code().as_str(),
777                verdict.reason().message().to_string(),
778            );
779            None
780        }
781    };
782    receipt.set(
783        "fresh_replay",
784        StageStatus::NotRun,
785        "established only by a replay of the package in a fresh process",
786    );
787
788    let mut files = PackageFiles::new();
789    files.insert(CERTIFICATE.into(), canonical_certificate(&certificate));
790    files.insert(EXPORT.into(), export.as_bytes().to_vec());
791    let rows = correspondence_rows(&certificate, &reference, document.as_ref())?;
792    let correspondence = CorrespondenceFile {
793        schema: CORRESPONDENCE_SCHEMA.into(),
794        target: declaration.name.clone(),
795        rows,
796    };
797    files.insert(CORRESPONDENCE.into(), canonical_json(&correspondence));
798    let document_file = document
799        .as_ref()
800        .map(|_| document_path(&declaration.lean_name));
801    if let (Some(path), Some(document)) = (&document_file, &document) {
802        files.insert(path.clone(), document.source.as_bytes().to_vec());
803    }
804    let mut lean_sources = Vec::new();
805    for (name, bytes) in &options.lean_sources {
806        let path = format!("source/{name}");
807        files.insert(path.clone(), bytes.clone());
808        lean_sources.push(path);
809    }
810    let digests: BTreeMap<String, String> = files
811        .iter()
812        .map(|(p, b)| (p.clone(), sha256_hex(b)))
813        .collect();
814    let emission = match (&document, receipt.stage("emission_reparsed")) {
815        (Some(doc), _) => EmissionRecord {
816            status: "emitted".into(),
817            detail: "written by the emitter and reparsed".into(),
818            omissions: doc.omissions.clone(),
819        },
820        (None, Some(stage)) => EmissionRecord {
821            status: if stage.status == StageStatus::Unsupported {
822                "unsupported".into()
823            } else {
824                "failed".into()
825            },
826            detail: stage.detail.clone(),
827            omissions: Vec::new(),
828        },
829        (None, None) => unreachable!("every stage is on the receipt"),
830    };
831    let mut losses: Vec<InformationLoss> = report
832        .metadata_losses
833        .iter()
834        .map(|loss| InformationLoss {
835            category: "SOURCE_METADATA".into(),
836            detail: format!(
837                "expression {}: {} = {}",
838                loss.expression, loss.key, loss.value
839            ),
840        })
841        .collect();
842    losses.extend(
843        native
844            .information_changes()
845            .into_iter()
846            .map(|(category, detail)| InformationLoss { category, detail }),
847    );
848    losses.push(InformationLoss {
849        category: "SOURCE_POSITIONS".into(),
850        detail: "a Lean export records no source positions, so the correspondence maps names, not spans, on the Lean side".into(),
851    });
852    let meta = &header["meta"];
853    let manifest = Manifest {
854        schema: schema.into(),
855        result_id: result_id_in(schema, &declaration.lean_name, &digests),
856        source: SourceIdentity {
857            export: EXPORT.into(),
858            export_sha256: digests[EXPORT].clone(),
859            format: meta["format"]["version"]
860                .as_str()
861                .unwrap_or_default()
862                .into(),
863            lean_version: meta["lean"]["version"].as_str().unwrap_or_default().into(),
864            lean_githash: meta["lean"]["githash"].as_str().map(str::to_string),
865            exporter: format!(
866                "{} {}",
867                meta["exporter"]["name"].as_str().unwrap_or("unknown"),
868                meta["exporter"]["version"].as_str().unwrap_or("unknown")
869            ),
870            lean_sources,
871        },
872        files: digests,
873        target: TargetIdentity {
874            lean: declaration.lean_name.clone(),
875            kernel: declaration.name.clone(),
876            kind: kind.into(),
877            level_params: declaration.level_params.clone(),
878            statement: declaration.ty.to_string(),
879            logos: document.as_ref().map(|d| d.target.clone()),
880        },
881        document: document_file,
882        emission,
883        toolchain: Toolchain {
884            logos: env!("CARGO_PKG_VERSION").into(),
885            prelude: PRELUDE_VERSION.into(),
886            certificate_schema: certificate.schema.clone(),
887            lean: options
888                .lean_run
889                .as_ref()
890                .map(|r| r.toolchain.clone())
891                .unwrap_or_else(|| {
892                    format!(
893                        "leanprover/lean4:v{}",
894                        meta["lean"]["version"].as_str().unwrap_or("unknown")
895                    )
896                }),
897            exporter: options
898                .lean_run
899                .as_ref()
900                .map(|r| r.exporter.clone())
901                .unwrap_or_else(|| {
902                    meta["exporter"]["name"]
903                        .as_str()
904                        .unwrap_or("unknown")
905                        .to_string()
906                }),
907        },
908        assumptions: AssumptionRecord {
909            allowed: ALLOWED_AXIOMS
910                .iter()
911                .map(|(lean, _)| lean.to_string())
912                .collect(),
913            transitive: checks.transitive.clone(),
914            lean_print_axioms: lean_print_axioms.map(|a| a.to_vec()),
915        },
916        information_losses: losses,
917        trusted_base: TRUSTED_BASE.iter().map(|s| s.to_string()).collect(),
918        receipt: receipt_records(&receipt),
919    };
920    files.insert(MANIFEST.into(), canonical_json(&manifest));
921    Ok(Build {
922        files,
923        receipt,
924        audit,
925    })
926}
927
928/// The schema of the contract `native`, the re-derivations of a target's closure, decides.
929fn contract_schema(native: &Elimination) -> &'static str {
930    if native.eliminates() {
931        NATIVE_REDERIVED_SCHEMA
932    } else {
933        PACKAGE_SCHEMA
934    }
935}
936
937/// The manifest states the contract its export decides: the schema, and exactly the re-derivations
938/// recomputed from the export as its information changes.
939fn states_contract(
940    manifest: &Manifest,
941    native: &Elimination,
942) -> std::result::Result<(), (&'static str, String)> {
943    let schema = contract_schema(native);
944    if manifest.schema != schema {
945        return Err((
946            "native_rederivation_schema",
947            if native.eliminates() {
948                format!(
949                "the export's proof of the target re-derives native decisions, so its package is {schema}, but the manifest declares {}",
950                manifest.schema
951            )
952            } else {
953                format!(
954                "the export's proof of the target re-derives no native decision, so its package is {schema}, but the manifest declares {}",
955                manifest.schema
956            )
957            },
958        ));
959    }
960    native
961        .disclosed_by(
962            manifest
963                .information_losses
964                .iter()
965                .map(|loss| (loss.category.as_str(), loss.detail.as_str())),
966        )
967        .map_err(|detail| ("native_rederivation_disclosure", detail))
968}
969
970/// Lean's `#print axioms` for the target (Lean names), held against the transitive assumptions the
971/// kernel computed for the checked proof (kernel names). Each name Lean reports is read through the
972/// export's own name table, so a numeric component or an escaped string cannot be misread by
973/// splitting on dots, and must be declared there, once.
974///
975/// A proof checked as exported rests on exactly what Lean reports, and a run that did not observe
976/// Lean holds nothing against it. A proof whose native decisions were re-derived (`native`) rests on
977/// Lean's report without the eliminated escape, and Lean's report must name it: that report is the
978/// only evidence of what the replaced proof rested on, so its absence is a disagreement too.
979fn agree_with_lean(
980    reference: &Reference,
981    native: &Elimination,
982    lean: Option<&[String]>,
983    transitive: &[String],
984) -> std::result::Result<(), String> {
985    let Some(lean) = lean else {
986        if !native.eliminates() {
987            return Ok(());
988        }
989        let replaced: Vec<&str> = native
990            .rederivations
991            .iter()
992            .map(|r| r.lean.as_str())
993            .collect();
994        return Err(format!(
995            "the proof re-derives the native decisions of {}, and a native-rederived package states what Lean's own proof \
996             rested on, but this run recorded no #print axioms from Lean",
997            replaced.join(", ")
998        ));
999    };
1000    let mut kernel_of: BTreeMap<&str, String> = BTreeMap::new();
1001    let mut reported = BTreeSet::new();
1002    for axiom in lean {
1003        let kernel = reference.source_lean_name(axiom).map_err(|_| {
1004            format!("Lean's #print axioms names {axiom}, which the export does not declare")
1005        })?;
1006        if !reported.insert(kernel.clone()) {
1007            return Err(format!("Lean's #print axioms names {axiom} twice"));
1008        }
1009        kernel_of.insert(axiom, kernel);
1010    }
1011    let expected_lean = native.lean_after_rederivation(lean)?;
1012    let expected: BTreeSet<String> = expected_lean
1013        .iter()
1014        .map(|axiom| kernel_of[axiom.as_str()].clone())
1015        .collect();
1016    let computed: BTreeSet<String> = transitive.iter().cloned().collect();
1017    if expected == computed {
1018        return Ok(());
1019    }
1020    let lean_names = |names: &mut dyn Iterator<Item = &String>| {
1021        names
1022            .map(|n| super::intake::lean_name(n))
1023            .collect::<Vec<_>>()
1024    };
1025    let rests_on = lean_names(&mut computed.iter());
1026    let only_lean = lean_names(&mut expected.difference(&computed));
1027    let only_here = lean_names(&mut computed.difference(&expected));
1028    let rederived = if native.eliminates() {
1029        format!(", so with its native decisions re-derived the statement and proof must rest on {expected_lean:?}")
1030    } else {
1031        String::new()
1032    };
1033    Err(format!(
1034        "Lean's #print axioms reports {lean:?}{rederived}, but the statement and proof rest on {rests_on:?} (only in \
1035         Lean: {only_lean:?}; only here: {only_here:?})"
1036    ))
1037}
1038
1039/// A receipt stage of `outcome`.
1040fn stage(name: &'static str, (status, code, detail): &StageOutcome) -> Stage {
1041    Stage {
1042        name,
1043        status: *status,
1044        detail: detail.clone(),
1045        code: code.clone(),
1046    }
1047}
1048
1049fn receipt_records(receipt: &Receipt) -> BTreeMap<String, StageRecord> {
1050    receipt
1051        .stages
1052        .iter()
1053        .map(|s| {
1054            (
1055                s.name.to_string(),
1056                StageRecord {
1057                    status: s.status.as_str().into(),
1058                    code: s.code.clone(),
1059                    detail: s.detail.clone(),
1060                },
1061            )
1062        })
1063        .collect()
1064}
1065
1066/// The correspondence rows: the emitter's, when there is a document; otherwise one row per
1067/// certificate declaration, Lean name ↔ kernel name.
1068fn correspondence_rows(
1069    certificate: &EnvironmentCertificate,
1070    reference: &Reference,
1071    document: Option<&super::emit::EmittedDocument>,
1072) -> Result<Vec<CorrespondenceRow>> {
1073    use super::emit::{Form, Origin};
1074    if let Some(document) = document {
1075        return Ok(document
1076            .correspondence
1077            .iter()
1078            .map(|c| {
1079                let (origin, helper_of) = match &c.origin {
1080                    Origin::Authored => ("authored", None),
1081                    Origin::Helper { of } => ("helper", Some(of.clone())),
1082                    Origin::Synthesized => ("synthesized", None),
1083                    Origin::Prelude => ("prelude", None),
1084                };
1085                CorrespondenceRow {
1086                    lean: c.lean.clone(),
1087                    kernel: c.importer_key.clone(),
1088                    logos: Some(c.logos.clone()),
1089                    kind: c.kind.as_str().into(),
1090                    origin: origin.into(),
1091                    helper_of,
1092                    form: Some(
1093                        match c.form {
1094                            Form::Literate => "literate",
1095                            Form::Vernacular => "vernacular",
1096                            Form::Prelude => "prelude",
1097                        }
1098                        .into(),
1099                    ),
1100                    levels: c.levels.clone(),
1101                    lines: c.lines,
1102                }
1103            })
1104            .collect());
1105    }
1106    let mut rows = Vec::new();
1107    for entry in &certificate.entries {
1108        for name in entry.names() {
1109            let (lean, origin) = match reference.source_spelling(name) {
1110                Ok((_, lean)) => (lean, "authored"),
1111                Err(_) => (name.to_string(), "synthesized"),
1112            };
1113            rows.push(CorrespondenceRow {
1114                lean,
1115                kernel: name.to_string(),
1116                logos: None,
1117                kind: entry_kind(entry, name).into(),
1118                origin: origin.into(),
1119                helper_of: None,
1120                form: None,
1121                levels: Vec::new(),
1122                lines: None,
1123            });
1124        }
1125    }
1126    Ok(rows)
1127}
1128
1129fn entry_kind(entry: &EnvironmentEntry, name: &str) -> &'static str {
1130    match entry {
1131        EnvironmentEntry::Axiom { .. } => "axiom",
1132        EnvironmentEntry::Definition { .. } => "definition",
1133        EnvironmentEntry::Theorem { .. } => "theorem",
1134        EnvironmentEntry::Opaque { .. } => "opaque",
1135        EnvironmentEntry::Inductive { types, .. } => {
1136            if types.iter().any(|t| t.name == name) {
1137                "inductive"
1138            } else {
1139                "constructor"
1140            }
1141        }
1142        EnvironmentEntry::Structure { .. } => "structure",
1143        EnvironmentEntry::Quotient { .. } => "quotient",
1144    }
1145}
1146
1147// -------------------------------------------------------------------------------------------------
1148// The environment checks a build and a replay share.
1149// -------------------------------------------------------------------------------------------------
1150
1151/// The outcome of rebuilding, binding and auditing one certificate.
1152struct EnvironmentChecks {
1153    /// `primary_check`, `independent_recheck`, `translated`, `assumption_policy`, as
1154    /// `(status, code, detail)`.
1155    primary: (StageStatus, Option<String>, String),
1156    independent: (StageStatus, Option<String>, String),
1157    translated: (StageStatus, Option<String>, String),
1158    policy: (StageStatus, Option<String>, String),
1159    /// The transitive assumptions of the statement and proof, sorted kernel names.
1160    transitive: Vec<String>,
1161    /// The rebuilt target's proofs of opaque theorems, for re-emission.
1162    rebuilt: Option<RebuiltSummary>,
1163}
1164
1165struct RebuiltSummary {
1166    context: crate::Context,
1167    proofs: BTreeMap<String, Term>,
1168}
1169
1170impl EnvironmentChecks {
1171    fn record(&self, receipt: &mut Receipt) {
1172        for (stage, (status, code, detail)) in [
1173            ("primary_check", &self.primary),
1174            ("independent_recheck", &self.independent),
1175            ("translated", &self.translated),
1176            ("assumption_policy", &self.policy),
1177        ] {
1178            match code {
1179                Some(code) => receipt.set_with_code(stage, *status, code, detail.clone()),
1180                None => receipt.set(stage, *status, detail.clone()),
1181            }
1182        }
1183    }
1184}
1185
1186fn failed(code: &str, detail: impl Into<String>) -> (StageStatus, Option<String>, String) {
1187    (StageStatus::Failed, Some(code.to_string()), detail.into())
1188}
1189
1190fn not_run(why: &str) -> (StageStatus, Option<String>, String) {
1191    (StageStatus::NotRun, None, why.to_string())
1192}
1193
1194/// Run `work` on a thread whose stack holds the kernel's deepest admissible term.
1195fn on_kernel_stack<R: Send>(work: impl FnOnce() -> R + Send) -> Result<R> {
1196    let rungs = EnvironmentLimits::default()
1197        .max_depth
1198        .saturating_mul(3)
1199        .saturating_add(crate::term_store::INLINE_DEPTH);
1200    std::thread::scope(|scope| {
1201        std::thread::Builder::new()
1202            .name("lean-package-build".into())
1203            .stack_size(crate::term_store::stack_bytes_for_depth(rungs))
1204            .spawn_scoped(scope, work)
1205            .map(|handle| {
1206                handle
1207                    .join()
1208                    .unwrap_or_else(|p| std::panic::resume_unwind(p))
1209            })
1210            .map_err(|e| {
1211                declined(
1212                    DiagnosticCode::Infrastructure,
1213                    format!("cannot reserve the package stack: {e}"),
1214                )
1215            })
1216    })
1217}
1218
1219/// Bound source syntax before any reference term is allocated. Reader helpers have no
1220/// source counterpart; recursors have a source type but no transparent `value` field.
1221fn source_binding_metrics(
1222    reference: &Reference,
1223    certificate: &EnvironmentCertificate,
1224    target: &str,
1225    other_source_reads: &[String],
1226    limits: &EnvironmentLimits,
1227) -> Result<super::reference::SourceMetrics> {
1228    let source_names: BTreeMap<_, _> = reference.names()?.into_iter().collect();
1229    let mut declarations = BTreeMap::<String, bool>::new();
1230    for entry in &certificate.entries {
1231        for name in entry.names() {
1232            if let Some(kind) = source_names.get(name) {
1233                let body = matches!(
1234                    (entry, kind),
1235                    (
1236                        EnvironmentEntry::Definition { .. } | EnvironmentEntry::Opaque { .. },
1237                        ReferenceKind::Definition | ReferenceKind::Opaque
1238                    )
1239                );
1240                declarations
1241                    .entry(name.to_string())
1242                    .and_modify(|existing| *existing |= body)
1243                    .or_insert(body);
1244            }
1245        }
1246    }
1247    // A missing target remains the ordinary source-binding error after this term-free pass.
1248    if source_names.contains_key(target) {
1249        declarations.entry(target.to_string()).or_insert(false);
1250    }
1251    // Manifest correspondence and reported axioms also call Reference::declaration.
1252    // Resolve names without lowering; absent names keep their existing later diagnostic.
1253    for name in other_source_reads {
1254        if let Ok(name) = reference.source_lean_name(name) {
1255            declarations.entry(name).or_insert(false);
1256        }
1257    }
1258    reference.preflight_kernel(
1259        &declarations.into_iter().collect::<Vec<_>>(),
1260        limits.max_depth,
1261        limits.max_tree_nodes,
1262    )
1263}
1264
1265/// Rebuild the certificate, then bind its statement to the export and audit its assumptions.
1266fn check_environment(
1267    certificate: &EnvironmentCertificate,
1268    reference: &Reference,
1269    kernel_target: &str,
1270    limits: &EnvironmentLimits,
1271    source_work: &mut SourceWork,
1272) -> EnvironmentChecks {
1273    let blocked = "the environment did not rebuild";
1274    let mut canonical = BTreeMap::new();
1275    let mut body_binding = reference
1276        .names()
1277        .map(|names| names.into_iter().collect::<BTreeMap<_, _>>())
1278        .map_err(|error| {
1279            binding_error(
1280                error,
1281                "missing_source_evidence",
1282                "source declaration inventory",
1283            )
1284        });
1285    let mut first_binding_failure = body_binding.as_ref().err().cloned();
1286    let rebuilt =
1287        match certificate.rebuild_observing_values(limits, &mut |prefix, name, levels, ty, body| {
1288            if first_binding_failure.is_none() {
1289                if let Ok(source_names) = &mut body_binding {
1290                    first_binding_failure = bind_source_body(
1291                        prefix,
1292                        reference,
1293                        source_names,
1294                        &mut canonical,
1295                        name,
1296                        levels,
1297                        ty,
1298                        body,
1299                        source_work,
1300                    )
1301                    .err();
1302                }
1303            }
1304        }) {
1305            Ok(rebuilt) => rebuilt,
1306            Err(error) => {
1307                let code = error.code.as_str();
1308                let (primary, independent) =
1309                    if error.code == EnvironmentErrorCode::IndependentDisagreement {
1310                        (
1311                        failed(
1312                            "rebuild_incomplete",
1313                            "the environment did not finish rebuilding: the two kernels disagreed",
1314                        ),
1315                        failed(code, error.message.clone()),
1316                    )
1317                    } else {
1318                        (failed(code, error.message.clone()), not_run(blocked))
1319                    };
1320                return EnvironmentChecks {
1321                    primary,
1322                    independent,
1323                    translated: first_binding_failure
1324                        .map(binding_failure)
1325                        .unwrap_or_else(|| not_run(blocked)),
1326                    policy: not_run(blocked),
1327                    transitive: Vec::new(),
1328                    rebuilt: None,
1329                };
1330            }
1331        };
1332    let primary = (
1333        StageStatus::Passed,
1334        None,
1335        format!(
1336            "{} declarations rebuilt from the bare kernel, each kernel-validated under exact conversion; no obligation owed",
1337            certificate.entries.len()
1338        ),
1339    );
1340    let unsupported: Vec<String> = rebuilt
1341        .independent
1342        .iter()
1343        .filter_map(|(name, verdict)| match verdict {
1344            IndependentVerdict::Unsupported(why) => Some(format!("{name} ({why})")),
1345            IndependentVerdict::Agreed => None,
1346        })
1347        .collect();
1348    let independent = if unsupported.is_empty() {
1349        (
1350            StageStatus::Passed,
1351            None,
1352            format!(
1353                "{} checked terms re-inferred by the independent kernel to their declared types",
1354                rebuilt.independent.len()
1355            ),
1356        )
1357    } else {
1358        (
1359            StageStatus::Unsupported,
1360            Some("outside_recheck_fragment".to_string()),
1361            format!(
1362                "the independent kernel does not cover {} term(s): {}",
1363                unsupported.len(),
1364                unsupported.join("; ")
1365            ),
1366        )
1367    };
1368    let translated = match first_binding_failure {
1369        Some(error) => binding_failure(error),
1370        None => match bind(certificate, &rebuilt, reference, kernel_target, source_work) {
1371            Ok(detail) => (StageStatus::Passed, None, detail),
1372            Err(error) => binding_failure(error),
1373        },
1374    };
1375    let transitive: Vec<String> = rebuilt
1376        .axioms
1377        .axioms
1378        .iter()
1379        .chain(rebuilt.axioms.unresolved.iter())
1380        .cloned()
1381        .collect();
1382    let policy = match audit_assumptions(&rebuilt) {
1383        Ok(detail) => (StageStatus::Passed, None, detail),
1384        Err((code, detail)) => failed(code, detail),
1385    };
1386    let proofs = rebuilt.proofs.clone();
1387    EnvironmentChecks {
1388        primary,
1389        independent,
1390        translated,
1391        policy,
1392        transitive,
1393        rebuilt: Some(RebuiltSummary {
1394            context: rebuilt.context,
1395            proofs,
1396        }),
1397    }
1398}
1399
1400type BindingError = (&'static str, String);
1401
1402// Conversion/resource incompleteness is evidence of an unfinished comparison, never inequality.
1403fn binding_error(error: Verdict, mismatch: &'static str, what: &str) -> BindingError {
1404    let code = match error.reason().code() {
1405        DiagnosticCode::ResourceLimit
1406        | DiagnosticCode::UnsupportedFeature
1407        | DiagnosticCode::RecheckRefused
1408        | DiagnosticCode::AuditIncomplete
1409        | DiagnosticCode::Infrastructure => error.reason().code().as_str(),
1410        _ => mismatch,
1411    };
1412    (code, format!("{what}: {}", error.reason().message()))
1413}
1414
1415fn binding_failure((code, detail): BindingError) -> StageOutcome {
1416    let status = if matches!(
1417        code,
1418        "unsupported_feature" | "recheck_refused" | "audit_incomplete"
1419    ) {
1420        StageStatus::Unsupported
1421    } else {
1422        StageStatus::Failed
1423    };
1424    (status, Some(code.to_string()), detail)
1425}
1426
1427fn source_levels(
1428    name: &str,
1429    levels: &[String],
1430    terms: &[&Term],
1431    work: &mut SourceWork,
1432) -> std::result::Result<(), BindingError> {
1433    if !work
1434        .levels(levels, terms)
1435        .map_err(|error| binding_error(error, "statement_mismatch", name))?
1436    {
1437        return Err((
1438            "statement_mismatch",
1439            format!("{name} has duplicate or undeclared source universe parameters"),
1440        ));
1441    }
1442    Ok(())
1443}
1444
1445/// Check source syntax itself, including dead subterms, with both kernels.
1446fn source_type(
1447    prefix: &crate::Context,
1448    name: &str,
1449    levels: &[String],
1450    ty: &Term,
1451    work: &mut SourceWork,
1452) -> std::result::Result<(), BindingError> {
1453    source_levels(name, levels, &[ty], work)?;
1454    let main = super::lean_type(prefix, ty)
1455        .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1456    let sort = super::normal(prefix, &main)
1457        .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1458    if !matches!(sort, Term::Sort(_)) {
1459        return Err((
1460            "statement_mismatch",
1461            format!("{name}'s source type does not inhabit a sort"),
1462        ));
1463    }
1464    let independent = crate::recheck(prefix, ty).map_err(|error| {
1465        binding_error(
1466            super::independent_verdict(&error, name),
1467            "statement_mismatch",
1468            name,
1469        )
1470    })?;
1471    super::independent_comparison(super::exact(prefix, &independent, &main), name)
1472        .map_err(|error| binding_error(error, "statement_mismatch", name))
1473}
1474
1475/// Called before each candidate value is registered. A failure is sticky for the entire package;
1476/// continuing the checked rebuild cannot turn a failed source comparison into source evidence.
1477fn bind_source_body(
1478    prefix: &crate::Context,
1479    reference: &Reference,
1480    source_names: &BTreeMap<String, ReferenceKind>,
1481    canonical: &mut BTreeMap<String, super::source_recursor::CanonicalRecursor>,
1482    name: &str,
1483    levels: &[String],
1484    ty: &Term,
1485    body: &Term,
1486    work: &mut SourceWork,
1487) -> std::result::Result<(), BindingError> {
1488    work.lookup_name(name, source_names.len())
1489        .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1490    let Some(kind) = source_names.get(name) else {
1491        // Absence is established from the complete raw inventory, not a failed translation.
1492        // Private checked helpers are permitted only while all sourced observations are bound.
1493        return if reader_introduced(name) {
1494            Ok(())
1495        } else {
1496            Err((
1497                "unsourced_declaration",
1498                format!("the certificate value {name} has no source declaration"),
1499            ))
1500        };
1501    };
1502    if *kind == ReferenceKind::Recursor {
1503        work.lookup_name(name, canonical.len())
1504            .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1505        if !canonical.contains_key(name) {
1506            let block = work
1507                .block(prefix, reference, name)
1508                .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1509            let derived = super::source_recursor::derive_bounded(prefix, &block, &mut work.budget)
1510                .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1511            // Cache the COMPLETE block at its first recursor, while no carried sibling exists.
1512            for recursor in derived {
1513                let key = work
1514                    .cache_name(&recursor.name)
1515                    .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1516                work.lookup_name(&key, canonical.len())
1517                    .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1518                if canonical.insert(key, recursor).is_some() {
1519                    return Err((
1520                        "definition_mismatch",
1521                        "canonical source recursor blocks overlap".into(),
1522                    ));
1523                }
1524            }
1525        }
1526        work.lookup_name(name, canonical.len())
1527            .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1528        let expected = canonical.get(name).ok_or_else(|| {
1529            (
1530                "missing_source_evidence",
1531                format!("the independently derived block has no {name}"),
1532            )
1533        })?;
1534        if expected.levels != levels {
1535            return Err((
1536                "statement_mismatch",
1537                format!("{name} changes the canonical source universe telescope"),
1538            ));
1539        }
1540        super::exact(prefix, &expected.ty, ty)
1541            .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1542        return super::exact(prefix, &expected.body, body)
1543            .map_err(|error| binding_error(error, "definition_mismatch", name));
1544    }
1545    if !matches!(kind, ReferenceKind::Definition | ReferenceKind::Opaque) {
1546        return Err((
1547            "kind_mismatch",
1548            format!("{name} is a source {} but carries a value", kind.as_str()),
1549        ));
1550    }
1551    let source = work
1552        .declaration(prefix, reference, name)
1553        .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1554    if source.level_params != levels {
1555        return Err((
1556            "statement_mismatch",
1557            format!("{name} changes the source universe telescope"),
1558        ));
1559    }
1560    source_type(prefix, name, levels, &source.ty, work)?;
1561    super::exact(prefix, &source.ty, ty)
1562        .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1563    let source_body = work
1564        .value(prefix, reference, name)
1565        .map_err(|error| binding_error(error, "missing_source_evidence", name))?;
1566    source_levels(name, levels, &[&source_body], work)?;
1567    let main = super::lean_type(prefix, &source_body)
1568        .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1569    super::exact(prefix, &main, &source.ty)
1570        .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1571    let independent = crate::recheck(prefix, &source_body).map_err(|error| {
1572        binding_error(
1573            super::independent_verdict(&error, name),
1574            "definition_mismatch",
1575            name,
1576        )
1577    })?;
1578    super::independent_comparison(super::exact(prefix, &independent, &source.ty), name)
1579        .map_err(|error| binding_error(error, "definition_mismatch", name))?;
1580    // Opaque declarations remain sealed: compare their supplied values in the prefix, without
1581    // registering or unfolding either candidate under its own name. Theorem proofs are excluded.
1582    super::exact(prefix, &source_body, body)
1583        .map_err(|error| binding_error(error, "definition_mismatch", name))
1584}
1585
1586/// Bind the certificate to the export: its target is the requested Lean declaration and states the
1587/// reference translation of its Lean type, and every other declaration with a source counterpart
1588/// is that counterpart.
1589fn bind(
1590    certificate: &EnvironmentCertificate,
1591    rebuilt: &RebuiltEnvironment,
1592    reference: &Reference,
1593    kernel_target: &str,
1594    work: &mut SourceWork,
1595) -> std::result::Result<String, (&'static str, String)> {
1596    if certificate.target != kernel_target {
1597        return Err((
1598            "target_mismatch",
1599            format!(
1600                "the certificate proves {}, not the requested {kernel_target}",
1601                certificate.target
1602            ),
1603        ));
1604    }
1605    let ctx = &rebuilt.context;
1606    let target = work
1607        .declaration(ctx, reference, kernel_target)
1608        .map_err(|v| binding_error(v, "missing_source_declaration", kernel_target))?;
1609    let kind_ok = matches!(
1610        (
1611            target.kind,
1612            rebuilt.target_is_theorem,
1613            rebuilt.target_is_opaque
1614        ),
1615        (ReferenceKind::Theorem, true, false)
1616            | (ReferenceKind::Opaque, false, true)
1617            | (ReferenceKind::Definition, false, false)
1618    );
1619    if !kind_ok {
1620        return Err((
1621            "kind_mismatch",
1622            format!(
1623                "{} is a Lean {} but the certificate states a {}",
1624                target.lean_name,
1625                target.kind.as_str(),
1626                if rebuilt.target_is_opaque {
1627                    "opaque"
1628                } else if rebuilt.target_is_theorem {
1629                    "theorem"
1630                } else {
1631                    "definition"
1632                }
1633            ),
1634        ));
1635    }
1636    if target.level_params != rebuilt.target_level_params {
1637        return Err((
1638            "statement_mismatch",
1639            format!(
1640                "{} has universe parameters {:?} in the export and {:?} in the certificate",
1641                target.lean_name, target.level_params, rebuilt.target_level_params
1642            ),
1643        ));
1644    }
1645    source_type(ctx, kernel_target, &target.level_params, &target.ty, work)?;
1646    super::exact(ctx, &target.ty, &rebuilt.target_type)
1647        .map_err(|error| binding_error(error, "statement_mismatch", kernel_target))?;
1648    let mut bound = 0usize;
1649    let source_names: BTreeMap<_, _> = reference
1650        .names()
1651        .map_err(|error| binding_error(error, "missing_source_evidence", "source inventory"))?
1652        .into_iter()
1653        .collect();
1654    for entry in &certificate.entries {
1655        for name in entry.names() {
1656            if name == kernel_target {
1657                continue;
1658            }
1659            match work.declaration(ctx, reference, name) {
1660                Ok(source) => {
1661                    let expected_kind = match entry {
1662                        EnvironmentEntry::Axiom { .. } => {
1663                            matches!(source.kind, ReferenceKind::Axiom)
1664                        }
1665                        EnvironmentEntry::Definition { .. } => matches!(
1666                            source.kind,
1667                            ReferenceKind::Definition | ReferenceKind::Recursor
1668                        ),
1669                        EnvironmentEntry::Theorem { .. } => {
1670                            matches!(source.kind, ReferenceKind::Theorem)
1671                        }
1672                        EnvironmentEntry::Opaque { .. } => {
1673                            matches!(source.kind, ReferenceKind::Opaque)
1674                        }
1675                        EnvironmentEntry::Inductive { types, .. } => {
1676                            let is_type = types.iter().any(|t| t.name == name);
1677                            matches!(source.kind, ReferenceKind::Inductive) && is_type
1678                                || matches!(source.kind, ReferenceKind::Constructor) && !is_type
1679                        }
1680                        EnvironmentEntry::Quotient { .. } => {
1681                            matches!(source.kind, ReferenceKind::Quotient)
1682                        }
1683                        EnvironmentEntry::Structure { .. } => false,
1684                    };
1685                    if !expected_kind {
1686                        return Err((
1687                            "kind_mismatch",
1688                            format!(
1689                                "{} is a Lean {} but the certificate declares it as a {}",
1690                                source.lean_name,
1691                                source.kind.as_str(),
1692                                entry_kind(entry, name)
1693                            ),
1694                        ));
1695                    }
1696                    if source.kind == ReferenceKind::Inductive {
1697                        work.family(ctx, reference, name).map_err(|error| {
1698                            binding_error(error, "family_metadata_mismatch", name)
1699                        })?;
1700                    }
1701                    let declared = ctx
1702                        .poly_declaration(name)
1703                        .or_else(|| ctx.get_global(name).map(|t| (&[][..], t)));
1704                    let Some((levels, ty)) = declared else {
1705                        return Err((
1706                            "missing_source_declaration",
1707                            format!("{name} is not in the rebuilt environment"),
1708                        ));
1709                    };
1710                    if levels != source.level_params.as_slice() {
1711                        return Err(("statement_mismatch", format!("{} has universe parameters {:?} in the export and {levels:?} in the certificate", source.lean_name, source.level_params)));
1712                    }
1713                    source_type(ctx, name, &source.level_params, &source.ty, work)?;
1714                    super::exact(ctx, &source.ty, ty)
1715                        .map_err(|error| binding_error(error, "statement_mismatch", name))?;
1716                    bound += 1;
1717                }
1718                Err(error) => {
1719                    if source_names.contains_key(name) {
1720                        return Err(binding_error(error, "missing_source_evidence", name));
1721                    }
1722                    if !matches!(entry, EnvironmentEntry::Definition { .. })
1723                        || !reader_introduced(name)
1724                    {
1725                        return Err(("unsourced_declaration", format!(
1726                            "the certificate declares the {} {name}, which the export does not declare",
1727                            entry_kind(entry, name)
1728                        )));
1729                    }
1730                }
1731            }
1732        }
1733    }
1734    let detail = format!(
1735        "{}'s statement is exactly convertible with the reference translation of its Lean type; {bound} further declaration types and every sourced transparent or opaque value bound to their source",
1736        target.lean_name
1737    );
1738    Ok(detail)
1739}
1740
1741/// Names the Lean reader introduces itself: sharing helpers and structure projections. They are
1742/// checked definitions like any other; they simply have no Lean spelling.
1743fn reader_introduced(name: &str) -> bool {
1744    name.starts_with("arena.")
1745}
1746
1747// -------------------------------------------------------------------------------------------------
1748// The assumption policy.
1749// -------------------------------------------------------------------------------------------------
1750
1751fn v(name: &str) -> Term {
1752    Term::Var(name.into())
1753}
1754fn g(name: &str) -> Term {
1755    Term::Global(name.into())
1756}
1757fn pi(name: &str, domain: Term, body: Term) -> Term {
1758    Term::Pi {
1759        param: name.into(),
1760        param_type: Box::new(domain),
1761        body_type: Box::new(body),
1762    }
1763}
1764fn app(f: Term, args: Vec<Term>) -> Term {
1765    args.into_iter()
1766        .fold(f, |f, a| Term::App(Box::new(f), Box::new(a)))
1767}
1768fn at(name: &str, levels: Vec<Universe>) -> Term {
1769    Term::Const {
1770        name: name.into(),
1771        levels,
1772    }
1773}
1774fn prop() -> Term {
1775    Term::Sort(Universe::Prop)
1776}
1777
1778const EQ: &str = "/s:\"Eq\"";
1779const EQ_REFL: &str = "/s:\"Eq\"/s:\"refl\"";
1780const IFF: &str = "/s:\"Iff\"";
1781const IFF_INTRO: &str = "/s:\"Iff\"/s:\"intro\"";
1782const NONEMPTY: &str = "/s:\"Nonempty\"";
1783const NONEMPTY_INTRO: &str = "/s:\"Nonempty\"/s:\"intro\"";
1784
1785/// Whether `name` is declared in `ctx` with exactly the given level telescope size, arity, one
1786/// constructor and constructor type — the canonical shape of an inductive an allowed axiom's
1787/// meaning depends on.
1788fn canonical_inductive(
1789    ctx: &crate::Context,
1790    name: &str,
1791    arity: impl Fn(&[String]) -> Term,
1792    num_params: usize,
1793    constructor: &str,
1794    constructor_type: impl Fn(&[String]) -> Term,
1795) -> bool {
1796    if !ctx.is_inductive(name) || ctx.inductive_num_params(name) != num_params {
1797        return false;
1798    }
1799    let levels = ctx
1800        .poly_declaration(name)
1801        .map(|(p, _)| p.to_vec())
1802        .unwrap_or_default();
1803    let constructors = ctx.get_constructors(name);
1804    constructors.len() == 1
1805        && constructors[0].0 == constructor
1806        && ctx
1807            .get_global(name)
1808            .is_some_and(|t| crate::defeq_for_test(ctx, t, &arity(&levels)))
1809        && crate::defeq_for_test(ctx, constructors[0].1, &constructor_type(&levels))
1810}
1811
1812fn level(levels: &[String], i: usize) -> Universe {
1813    levels
1814        .get(i)
1815        .map(|l| Universe::Var(l.clone()))
1816        .unwrap_or(Universe::Prop)
1817}
1818
1819fn canonical_eq(ctx: &crate::Context) -> bool {
1820    canonical_inductive(
1821        ctx,
1822        EQ,
1823        |l| {
1824            pi(
1825                "α",
1826                Term::Sort(level(l, 0)),
1827                pi("a", v("α"), pi("b", v("α"), prop())),
1828            )
1829        },
1830        2,
1831        EQ_REFL,
1832        |l| {
1833            pi(
1834                "α",
1835                Term::Sort(level(l, 0)),
1836                pi(
1837                    "a",
1838                    v("α"),
1839                    app(at(EQ, vec![level(l, 0)]), vec![v("α"), v("a"), v("a")]),
1840                ),
1841            )
1842        },
1843    ) && ctx.poly_declaration(EQ).is_some_and(|(p, _)| p.len() == 1)
1844}
1845
1846fn canonical_iff(ctx: &crate::Context) -> bool {
1847    canonical_inductive(
1848        ctx,
1849        IFF,
1850        |_| pi("a", prop(), pi("b", prop(), prop())),
1851        2,
1852        IFF_INTRO,
1853        |_| {
1854            pi(
1855                "a",
1856                prop(),
1857                pi(
1858                    "b",
1859                    prop(),
1860                    pi(
1861                        "mp",
1862                        pi("x", v("a"), v("b")),
1863                        pi(
1864                            "mpr",
1865                            pi("y", v("b"), v("a")),
1866                            app(g(IFF), vec![v("a"), v("b")]),
1867                        ),
1868                    ),
1869                ),
1870            )
1871        },
1872    )
1873}
1874
1875fn canonical_nonempty(ctx: &crate::Context) -> bool {
1876    canonical_inductive(
1877        ctx,
1878        NONEMPTY,
1879        |l| pi("α", Term::Sort(level(l, 0)), prop()),
1880        1,
1881        NONEMPTY_INTRO,
1882        |l| {
1883            pi(
1884                "α",
1885                Term::Sort(level(l, 0)),
1886                pi(
1887                    "val",
1888                    v("α"),
1889                    app(at(NONEMPTY, vec![level(l, 0)]), vec![v("α")]),
1890                ),
1891            )
1892        },
1893    ) && ctx
1894        .poly_declaration(NONEMPTY)
1895        .is_some_and(|(p, _)| p.len() == 1)
1896}
1897
1898/// The canonical statement of an allowed axiom at its own universe parameters, and whether the
1899/// inductives its meaning depends on are canonical in `ctx`.
1900fn allowed_statement(ctx: &crate::Context, name: &str, levels: &[String]) -> Option<Term> {
1901    match name {
1902        "/s:\"propext\"" if levels.is_empty() && canonical_eq(ctx) && canonical_iff(ctx) => {
1903            Some(pi(
1904                "a",
1905                prop(),
1906                pi(
1907                    "b",
1908                    prop(),
1909                    pi(
1910                        "h",
1911                        app(g(IFF), vec![v("a"), v("b")]),
1912                        app(
1913                            at(EQ, vec![Universe::Type(0)]),
1914                            vec![prop(), v("a"), v("b")],
1915                        ),
1916                    ),
1917                ),
1918            ))
1919        }
1920        "/s:\"Quot\"/s:\"sound\"" if levels.len() == 1 && canonical_eq(ctx) => {
1921            let u = Universe::Var(levels[0].clone());
1922            let quotient = app(
1923                at("kernel.quot.type", vec![u.clone()]),
1924                vec![v("α"), v("r")],
1925            );
1926            let mk = |x: Term| {
1927                app(
1928                    at("kernel.quot.mk", vec![u.clone()]),
1929                    vec![v("α"), v("r"), x],
1930                )
1931            };
1932            Some(pi(
1933                "α",
1934                Term::Sort(u.clone()),
1935                pi(
1936                    "r",
1937                    pi("x", v("α"), pi("y", v("α"), prop())),
1938                    pi(
1939                        "a",
1940                        v("α"),
1941                        pi(
1942                            "b",
1943                            v("α"),
1944                            pi(
1945                                "h",
1946                                app(v("r"), vec![v("a"), v("b")]),
1947                                app(
1948                                    at(EQ, vec![u.clone()]),
1949                                    vec![quotient, mk(v("a")), mk(v("b"))],
1950                                ),
1951                            ),
1952                        ),
1953                    ),
1954                ),
1955            ))
1956        }
1957        "/s:\"Classical\"/s:\"choice\"" if levels.len() == 1 && canonical_nonempty(ctx) => {
1958            let u = Universe::Var(levels[0].clone());
1959            Some(pi(
1960                "α",
1961                Term::Sort(u.clone()),
1962                pi("h", app(at(NONEMPTY, vec![u]), vec![v("α")]), v("α")),
1963            ))
1964        }
1965        _ => None,
1966    }
1967}
1968
1969/// An assumption that inhabits every type of some sort — `Π (α : Sort u) …, α`, whose other
1970/// premises say nothing about `α` — proves anything, whatever it is called. This is the structure
1971/// of `sorryAx` (its one other premise is a `Bool`) and of every alias of it. A premise that
1972/// constrains `α`, as `Nonempty α` does for `Classical.choice`, is what separates a choice
1973/// principle from an admission.
1974fn universal_inhabitant(ctx: &crate::Context, ty: &Term) -> bool {
1975    let Ok(mut t) = crate::normalize_checked(ctx, ty) else {
1976        return false;
1977    };
1978    let mut sorts: BTreeSet<String> = BTreeSet::new();
1979    let mut counter = 0usize;
1980    loop {
1981        match t {
1982            Term::Pi {
1983                param,
1984                param_type,
1985                body_type,
1986            } => {
1987                let fresh = format!("policy.binder.{counter}");
1988                counter += 1;
1989                let domain = crate::normalize_checked(ctx, &param_type).unwrap_or(*param_type);
1990                if matches!(domain, Term::Sort(_)) {
1991                    sorts.insert(fresh.clone());
1992                } else if sorts
1993                    .iter()
1994                    .any(|s| crate::term_store::occurs_free_in_term(&domain, s))
1995                {
1996                    return false;
1997                }
1998                t = crate::substitute(&body_type, &param, &Term::Var(fresh));
1999                if let Ok(normal) = crate::normalize_checked(ctx, &t) {
2000                    t = normal;
2001                }
2002            }
2003            Term::Var(ref name) => return sorts.contains(name),
2004            _ => return false,
2005        }
2006    }
2007}
2008
2009/// Audit the transitive assumptions of the target's statement and proof.
2010fn audit_assumptions(
2011    rebuilt: &RebuiltEnvironment,
2012) -> std::result::Result<String, (&'static str, String)> {
2013    let ctx = &rebuilt.context;
2014    if !rebuilt.axioms.unresolved.is_empty() {
2015        return Err((
2016            "unresolved_assumption",
2017            format!(
2018                "the proof rests on names the environment does not declare: {:?}",
2019                rebuilt.axioms.unresolved
2020            ),
2021        ));
2022    }
2023    let declared: BTreeMap<&str, (&Vec<String>, &Term)> = rebuilt
2024        .assumptions
2025        .iter()
2026        .map(|(n, l, t)| (n.as_str(), (l, t)))
2027        .collect();
2028    let mut admitted = Vec::new();
2029    let mut goal = Vec::new();
2030    let mut disallowed = Vec::new();
2031    let mut allowed = Vec::new();
2032    for name in &rebuilt.axioms.axioms {
2033        let Some((levels, ty)) = declared.get(name.as_str()).copied() else {
2034            disallowed.push(format!(
2035                "{name} (not an assumption this environment declares)"
2036            ));
2037            continue;
2038        };
2039        // A standard axiom in its canonical form, over canonical datatypes, is allowed — even when a
2040        // theorem restates it: it is not new. Its name alone earns nothing.
2041        if allowed_statement(ctx, name, levels)
2042            .is_some_and(|expected| crate::defeq_for_test(ctx, ty, &expected))
2043        {
2044            allowed.push(name.clone());
2045            continue;
2046        }
2047        let admitted_by_name = crate::certificate::ADMITTED_AXIOMS.iter().any(|lean| {
2048            let components: Vec<&str> = lean.split('.').collect();
2049            super::reference::kernel_name_of_components(&components) == *name || lean == name
2050        });
2051        if admitted_by_name || universal_inhabitant(ctx, ty) {
2052            admitted.push(name.clone());
2053            continue;
2054        }
2055        let states_goal = if levels.len() == rebuilt.target_level_params.len() {
2056            let renamed = crate::instantiate_universes(
2057                ty,
2058                &levels
2059                    .iter()
2060                    .cloned()
2061                    .zip(
2062                        rebuilt
2063                            .target_level_params
2064                            .iter()
2065                            .cloned()
2066                            .map(Universe::Var),
2067                    )
2068                    .collect(),
2069            );
2070            crate::defeq_for_test(ctx, &renamed, &rebuilt.target_type)
2071        } else {
2072            false
2073        };
2074        if states_goal {
2075            goal.push(name.clone());
2076        } else {
2077            disallowed.push(name.clone());
2078        }
2079    }
2080    if !admitted.is_empty() {
2081        return Err(("admitted_proof", format!(
2082            "the proof rests on an admission — {} — reached through the transitive closure of its statement and proof (opaque theorems and definitions included); an admitted step proves anything and is never a solution",
2083            admitted.join(", ")
2084        )));
2085    }
2086    if !goal.is_empty() {
2087        return Err(("goal_axiom", format!(
2088            "the proof rests on an axiom stating the goal itself — {} — and an assumed goal is never a solution",
2089            goal.join(", ")
2090        )));
2091    }
2092    if !disallowed.is_empty() {
2093        return Err(("disallowed_assumption", format!(
2094            "the proof rests on assumptions outside the policy (propext, Quot.sound, Classical.choice in canonical form): {}",
2095            disallowed.join(", ")
2096        )));
2097    }
2098    Ok(if allowed.is_empty() {
2099        "the statement and proof rest on no assumption".to_string()
2100    } else {
2101        format!(
2102            "the statement and proof rest only on the allowed, canonical {}",
2103            allowed.join(", ")
2104        )
2105    })
2106}
2107
2108// -------------------------------------------------------------------------------------------------
2109// Replay.
2110// -------------------------------------------------------------------------------------------------
2111
2112/// Replay a package from its bytes alone, under the default limits.
2113pub fn replay(files: &PackageFiles) -> Receipt {
2114    replay_with(files, &PackageLimits::default())
2115}
2116
2117/// Replay a package from its bytes alone, under `limits`.
2118pub fn replay_with(files: &PackageFiles, limits: &PackageLimits) -> Receipt {
2119    let mut receipt = Receipt::unstarted();
2120    let admitted = match admit(files, limits) {
2121        Ok(admitted) => admitted,
2122        Err((code, detail)) => {
2123            receipt.set(
2124                "lean_accepted",
2125                StageStatus::NotRun,
2126                "a replay never runs Lean",
2127            );
2128            receipt.set_with_code("fresh_replay", StageStatus::Failed, code, detail);
2129            finish_unreached(&mut receipt);
2130            return receipt;
2131        }
2132    };
2133    let producing = admitted
2134        .manifest
2135        .receipt
2136        .get("lean_accepted")
2137        .map(|r| r.status.clone())
2138        .unwrap_or_else(|| "absent".into());
2139    receipt.set(
2140        "lean_accepted",
2141        StageStatus::NotRun,
2142        format!("a replay never runs Lean; the producing run recorded lean_accepted = {producing}"),
2143    );
2144    // Every limit on the certificate's terms is enforced here, before any checking work.
2145    let depth = match admitted.certificate.admitted_depth(&limits.environment) {
2146        Ok(depth) => depth,
2147        Err(error) => {
2148            receipt.set_with_code(
2149                "fresh_replay",
2150                StageStatus::Failed,
2151                error.code.as_str(),
2152                error.message,
2153            );
2154            finish_unreached(&mut receipt);
2155            return receipt;
2156        }
2157    };
2158    // Reference::read holds raw JSON only. Preflight every source root that binding will
2159    // lower before allocating a Term or choosing the replay worker's stack size.
2160    let source = Reference::read(&admitted.export).and_then(|reference| {
2161        let target = reference.source_lean_name(&admitted.manifest.target.lean)?;
2162        let mut reads: Vec<String> = admitted
2163            .correspondence
2164            .rows
2165            .iter()
2166            .filter(|row| {
2167                row.origin == "authored" || row.origin == "prelude" && !row.lean.is_empty()
2168            })
2169            .map(|row| row.lean.clone())
2170            .collect();
2171        if let Some(axioms) = &admitted.manifest.assumptions.lean_print_axioms {
2172            reads.extend(axioms.iter().cloned());
2173        }
2174        source_binding_metrics(
2175            &reference,
2176            &admitted.certificate,
2177            &target,
2178            &reads,
2179            &limits.environment,
2180        )
2181        .map(|metrics| (reference, metrics))
2182    });
2183    let (reference, source_metrics) = match source {
2184        Ok(source) => source,
2185        Err(verdict) => {
2186            let code = verdict.reason().code().as_str();
2187            let detail = verdict.reason().message().to_string();
2188            receipt.set_with_code("translated", StageStatus::Failed, code, detail.clone());
2189            receipt.set_with_code("fresh_replay", StageStatus::Failed, code, detail);
2190            finish_unreached(&mut receipt);
2191            return receipt;
2192        }
2193    };
2194    let source_depth = if source_metrics.has_projection || source_metrics.has_generated {
2195        // Projections and canonical recursors may generate deeper terms than their source
2196        // expressions. Their generators must enforce this output envelope before allocation.
2197        // Reserve it for all subsequent checking and term destruction as well.
2198        source_metrics.depth.max(limits.environment.max_depth)
2199    } else {
2200        source_metrics.depth
2201    };
2202    let depth = depth.max(source_depth);
2203    let rungs = depth
2204        .saturating_mul(3)
2205        .saturating_add(crate::term_store::INLINE_DEPTH);
2206    let run = std::thread::scope(|scope| {
2207        std::thread::Builder::new()
2208            .name("lean-package-replay".into())
2209            .stack_size(crate::term_store::stack_bytes_for_depth(rungs))
2210            .spawn_scoped(scope, || replay_admitted(&admitted, &reference, limits))
2211            .map(|handle| {
2212                handle
2213                    .join()
2214                    .unwrap_or_else(|p| std::panic::resume_unwind(p))
2215            })
2216    });
2217    match run {
2218        Ok(stages) => {
2219            for (name, (status, code, detail)) in stages {
2220                match code {
2221                    Some(code) => receipt.set_with_code(name, status, &code, detail),
2222                    None => receipt.set(name, status, detail),
2223                }
2224            }
2225        }
2226        Err(e) => receipt.set_with_code(
2227            "fresh_replay",
2228            StageStatus::Failed,
2229            "infrastructure",
2230            format!("cannot reserve the replay stack: {e}"),
2231        ),
2232    }
2233    let blocking = STAGES
2234        .iter()
2235        .filter(|s| !matches!(**s, "lean_accepted" | "fresh_replay"))
2236        .map(|s| (*s, receipt.status(s)))
2237        .find(|(_, status)| *status != StageStatus::Passed);
2238    finish_unreached(&mut receipt);
2239    match blocking {
2240        None => receipt.set(
2241            "fresh_replay",
2242            StageStatus::Passed,
2243            "every replay stage passed from the package bytes alone, with no importer state",
2244        ),
2245        Some((stage, StageStatus::Unsupported)) => receipt.set_with_code(
2246            "fresh_replay",
2247            StageStatus::Unsupported,
2248            "incomplete_replay",
2249            format!("{stage} is unsupported, so the replay is not complete"),
2250        ),
2251        Some((stage, status)) => receipt.set_with_code(
2252            "fresh_replay",
2253            StageStatus::Failed,
2254            "stage_failed",
2255            format!("{stage} {}", status.as_str()),
2256        ),
2257    }
2258    receipt
2259}
2260
2261/// Say of every stage the replay never reached that it was not reached, and why.
2262fn finish_unreached(receipt: &mut Receipt) {
2263    let stop = receipt
2264        .first_failure()
2265        .map(|s| format!("{} failed", s.name));
2266    for name in STAGES {
2267        let stage = receipt.stage(name).expect("every stage");
2268        if stage.status == StageStatus::NotRun && stage.detail.is_empty() {
2269            let why = match &stop {
2270                Some(stop) => format!("not reached: the replay stopped when {stop}"),
2271                None => "not reached".to_string(),
2272            };
2273            receipt.set(name, StageStatus::NotRun, why);
2274        }
2275    }
2276}
2277
2278/// A package that passed admission: limits, file set, digests, canonical encodings, formats.
2279struct Admitted {
2280    manifest: Manifest,
2281    certificate: EnvironmentCertificate,
2282    correspondence: CorrespondenceFile,
2283    export: String,
2284    document: Option<String>,
2285}
2286
2287/// An unknown field is an extension this replay does not implement; anything else is malformed.
2288fn json_refusal(error: &serde_json::Error) -> &'static str {
2289    if error.to_string().contains("unknown field") {
2290        "unsupported_format"
2291    } else {
2292        "malformed"
2293    }
2294}
2295
2296fn admit(
2297    files: &PackageFiles,
2298    limits: &PackageLimits,
2299) -> std::result::Result<Admitted, (&'static str, String)> {
2300    if files.len() > limits.max_files {
2301        return Err((
2302            "resource_limit",
2303            format!(
2304                "{} files exceed the limit of {}",
2305                files.len(),
2306                limits.max_files
2307            ),
2308        ));
2309    }
2310    let mut total = 0usize;
2311    for (path, bytes) in files {
2312        if let Some(why) = unsafe_path(path, limits) {
2313            return Err(("unsafe_path", format!("{path:?} is {why}")));
2314        }
2315        if bytes.len() > limits.max_file_bytes {
2316            return Err((
2317                "resource_limit",
2318                format!(
2319                    "{path} is {} bytes, beyond the limit of {}",
2320                    bytes.len(),
2321                    limits.max_file_bytes
2322                ),
2323            ));
2324        }
2325        total = total.saturating_add(bytes.len());
2326    }
2327    if total > limits.max_total_bytes {
2328        return Err((
2329            "resource_limit",
2330            format!(
2331                "the package is {total} bytes, beyond the limit of {}",
2332                limits.max_total_bytes
2333            ),
2334        ));
2335    }
2336    let manifest_bytes = files
2337        .get(MANIFEST)
2338        .ok_or(("missing_file", format!("the package has no {MANIFEST}")))?;
2339    let manifest_value: serde_json::Value = serde_json::from_slice(manifest_bytes)
2340        .map_err(|e| ("malformed", format!("{MANIFEST}: {e}")))?;
2341    match manifest_value.get("schema").and_then(|s| s.as_str()) {
2342        Some(schema) if PACKAGE_SCHEMAS.contains(&schema) => {}
2343        Some(other) => {
2344            return Err((
2345                "unsupported_format",
2346                format!(
2347                    "{MANIFEST} declares {other:?}; this replay reads {}",
2348                    PACKAGE_SCHEMAS.join(" and ")
2349                ),
2350            ))
2351        }
2352        None => {
2353            return Err((
2354                "unsupported_format",
2355                format!("{MANIFEST} declares no schema"),
2356            ))
2357        }
2358    }
2359    let manifest: Manifest = serde_json::from_value(manifest_value)
2360        .map_err(|e| (json_refusal(&e), format!("{MANIFEST}: {e}")))?;
2361    if canonical_json(&manifest) != *manifest_bytes {
2362        return Err((
2363            "non_canonical_encoding",
2364            format!("{MANIFEST} is not in its canonical encoding"),
2365        ));
2366    }
2367    // The file set is exactly the manifest's, and every digest matches.
2368    let listed: BTreeSet<&String> = manifest.files.keys().collect();
2369    let present: BTreeSet<&String> = files.keys().filter(|p| *p != MANIFEST).collect();
2370    if let Some(extra) = present.difference(&listed).next() {
2371        return Err((
2372            "unknown_file",
2373            format!("{extra} is not listed in the manifest"),
2374        ));
2375    }
2376    if let Some(missing) = listed.difference(&present).next() {
2377        return Err((
2378            "missing_file",
2379            format!("the manifest lists {missing}, which the package does not contain"),
2380        ));
2381    }
2382    for (path, digest) in &manifest.files {
2383        if sha256_hex(&files[path]) != *digest {
2384            return Err((
2385                "digest_mismatch",
2386                format!("{path}'s SHA-256 is not the manifest's"),
2387            ));
2388        }
2389    }
2390    if result_id_in(&manifest.schema, &manifest.target.lean, &manifest.files) != manifest.result_id
2391    {
2392        return Err((
2393            "result_id_mismatch",
2394            "the result identity does not match the schema, target and file digests".into(),
2395        ));
2396    }
2397    // Every listed file has a declared role.
2398    let mut roles: BTreeSet<String> = [CERTIFICATE, CORRESPONDENCE, EXPORT]
2399        .iter()
2400        .map(|s| s.to_string())
2401        .collect();
2402    roles.extend(manifest.document.iter().cloned());
2403    roles.extend(manifest.source.lean_sources.iter().cloned());
2404    for path in manifest.files.keys() {
2405        if !roles.contains(path) {
2406            return Err((
2407                "unknown_file",
2408                format!("{path} has no role in {}", manifest.schema),
2409            ));
2410        }
2411    }
2412    for role in &roles {
2413        if !manifest.files.contains_key(role) {
2414            return Err((
2415                "missing_file",
2416                format!("the package declares {role} but does not list it"),
2417            ));
2418        }
2419    }
2420    if let Some(document) = &manifest.document {
2421        if document.contains('/') || !document.ends_with(".lg") {
2422            return Err((
2423                "unsafe_path",
2424                format!("the document {document:?} is not a top-level .lg file"),
2425            ));
2426        }
2427    }
2428    for source in &manifest.source.lean_sources {
2429        if !source.starts_with("source/") || !source.ends_with(".lean") {
2430            return Err((
2431                "unsafe_path",
2432                format!("{source:?} is not a Lean source under source/"),
2433            ));
2434        }
2435    }
2436    if manifest.source.export != EXPORT
2437        || manifest.files.get(EXPORT) != Some(&manifest.source.export_sha256)
2438    {
2439        return Err((
2440            "manifest_mismatch",
2441            "the manifest's source identity does not name the carried export".into(),
2442        ));
2443    }
2444    if manifest.toolchain.prelude != PRELUDE_VERSION
2445        || ![OPAQUE_ENVIRONMENT_SCHEMA, ENVIRONMENT_SCHEMA]
2446            .contains(&manifest.toolchain.certificate_schema.as_str())
2447    {
2448        return Err(("unsupported_format", format!(
2449            "the package was produced for prelude {} and certificate schema {}; this replay is {PRELUDE_VERSION} and {OPAQUE_ENVIRONMENT_SCHEMA}",
2450            manifest.toolchain.prelude, manifest.toolchain.certificate_schema
2451        )));
2452    }
2453    let certificate_bytes = &files[CERTIFICATE];
2454    let certificate_text = std::str::from_utf8(certificate_bytes)
2455        .map_err(|_| ("malformed", format!("{CERTIFICATE} is not UTF-8")))?;
2456    let certificate = match read_certificate(certificate_text) {
2457        Ok(AnyCertificate::Environment(certificate)) => certificate,
2458        Ok(_) => {
2459            return Err((
2460                "unsupported_format",
2461                format!("{CERTIFICATE} is not an environment certificate"),
2462            ))
2463        }
2464        Err(error) => {
2465            let code = match error.code {
2466                EnvironmentErrorCode::UnsupportedSchema
2467                | EnvironmentErrorCode::IncompatiblePrelude => {
2468                    if error.code == EnvironmentErrorCode::IncompatiblePrelude {
2469                        "incompatible_prelude"
2470                    } else {
2471                        "unsupported_format"
2472                    }
2473                }
2474                _ if error.message.contains("unknown field") => "unsupported_format",
2475                _ => "malformed",
2476            };
2477            return Err((code, format!("{CERTIFICATE}: {}", error.message)));
2478        }
2479    };
2480    if manifest.toolchain.certificate_schema != certificate.schema {
2481        return Err((
2482            "manifest_mismatch",
2483            "the manifest's certificate schema differs from the carried certificate".into(),
2484        ));
2485    }
2486    if canonical_certificate(&certificate) != *certificate_bytes {
2487        return Err((
2488            "non_canonical_encoding",
2489            format!("{CERTIFICATE} is not in its canonical encoding"),
2490        ));
2491    }
2492    let correspondence_bytes = &files[CORRESPONDENCE];
2493    let correspondence: CorrespondenceFile = serde_json::from_slice(correspondence_bytes)
2494        .map_err(|e| (json_refusal(&e), format!("{CORRESPONDENCE}: {e}")))?;
2495    if correspondence.schema != CORRESPONDENCE_SCHEMA {
2496        return Err((
2497            "unsupported_format",
2498            format!("{CORRESPONDENCE} declares {:?}", correspondence.schema),
2499        ));
2500    }
2501    if canonical_json(&correspondence) != *correspondence_bytes {
2502        return Err((
2503            "non_canonical_encoding",
2504            format!("{CORRESPONDENCE} is not in its canonical encoding"),
2505        ));
2506    }
2507    let export = String::from_utf8(files[EXPORT].clone())
2508        .map_err(|_| ("malformed", format!("{EXPORT} is not UTF-8")))?;
2509    let header: serde_json::Value = serde_json::from_str(export.lines().next().unwrap_or(""))
2510        .map_err(|e| ("malformed", format!("{EXPORT} header: {e}")))?;
2511    if header["meta"]["format"]["version"] != "3.1.0" || manifest.source.format != "3.1.0" {
2512        return Err((
2513            "unsupported_format",
2514            format!("{EXPORT} is not a format 3.1.0 export"),
2515        ));
2516    }
2517    let document = match &manifest.document {
2518        Some(path) => Some(
2519            String::from_utf8(files[path].clone())
2520                .map_err(|_| ("malformed", format!("{path} is not UTF-8")))?,
2521        ),
2522        None => None,
2523    };
2524    Ok(Admitted {
2525        manifest,
2526        certificate,
2527        correspondence,
2528        export,
2529        document,
2530    })
2531}
2532
2533type StageOutcome = (StageStatus, Option<String>, String);
2534
2535/// Everything after admission, on a stack sized for certificate and source terms.
2536fn replay_admitted(
2537    admitted: &Admitted,
2538    reference: &Reference,
2539    limits: &PackageLimits,
2540) -> Vec<(&'static str, StageOutcome)> {
2541    let manifest = &admitted.manifest;
2542    let mut stages: Vec<(&'static str, StageOutcome)> = Vec::new();
2543    let target = match reference.source_lean_name(&manifest.target.lean) {
2544        Ok(name) => name,
2545        Err(verdict) => {
2546            stages.push((
2547                "translated",
2548                binding_failure(binding_error(
2549                    verdict,
2550                    "missing_source_declaration",
2551                    &manifest.target.lean,
2552                )),
2553            ));
2554            return stages;
2555        }
2556    };
2557    let mut source_work = match SourceWork::new(&limits.environment) {
2558        Ok(work) => work,
2559        Err(verdict) => {
2560            stages.push((
2561                "translated",
2562                binding_failure(binding_error(verdict, "missing_source_evidence", &target)),
2563            ));
2564            return stages;
2565        }
2566    };
2567    let checks = check_environment(
2568        &admitted.certificate,
2569        reference,
2570        &target,
2571        &limits.environment,
2572        &mut source_work,
2573    );
2574    let declaration = match &checks.rebuilt {
2575        Some(rebuilt) => source_work.declaration(&rebuilt.context, reference, &target),
2576        None => {
2577            stages.push(("primary_check", checks.primary));
2578            stages.push(("independent_recheck", checks.independent));
2579            stages.push(("translated", checks.translated));
2580            stages.push(("assumption_policy", checks.policy));
2581            stages.push((
2582                "emission_reparsed",
2583                not_run("the environment did not rebuild"),
2584            ));
2585            return stages;
2586        }
2587    };
2588    let declaration = match declaration {
2589        Ok(declaration) => declaration,
2590        Err(verdict) => {
2591            stages.push(("primary_check", checks.primary));
2592            stages.push(("independent_recheck", checks.independent));
2593            // An earlier body mismatch remains the first source failure.
2594            let translated = if checks.translated.0 == StageStatus::Passed {
2595                binding_failure(binding_error(
2596                    verdict,
2597                    "missing_source_declaration",
2598                    &target,
2599                ))
2600            } else {
2601                checks.translated
2602            };
2603            stages.push(("translated", translated));
2604            stages.push(("assumption_policy", checks.policy));
2605            stages.push((
2606                "emission_reparsed",
2607                not_run("the source target did not decode"),
2608            ));
2609            return stages;
2610        }
2611    };
2612    let mut translated = checks.translated.clone();
2613    if translated.0 == StageStatus::Passed {
2614        if let Err((code, detail)) = bind_manifest(
2615            manifest,
2616            &declaration,
2617            &admitted.correspondence,
2618            &admitted.certificate,
2619            &reference,
2620        ) {
2621            translated = failed(code, detail);
2622        }
2623    }
2624    // The contract is recomputed from the carried export and the bound target, never read from the
2625    // manifest or the producing run's report.
2626    let native = NativeDecisions::read(&admitted.export).closure_of(&target);
2627    if translated.0 == StageStatus::Passed
2628        && checks.primary.0 == StageStatus::Passed
2629        && checks.independent.0 == StageStatus::Passed
2630    {
2631        if let Err((code, detail)) = states_contract(manifest, &native) {
2632            translated = failed(code, detail);
2633        }
2634    }
2635    let mut policy = checks.policy.clone();
2636    if checks.primary.0 == StageStatus::Passed {
2637        if let Err(detail) = agree_with_lean(
2638            &reference,
2639            &native,
2640            manifest.assumptions.lean_print_axioms.as_deref(),
2641            &checks.transitive,
2642        ) {
2643            policy = failed("assumption_disagreement", detail);
2644        }
2645    }
2646    if policy.0 == StageStatus::Passed && manifest.assumptions.transitive != checks.transitive {
2647        policy = failed(
2648            "manifest_mismatch",
2649            format!(
2650                "the manifest lists the assumptions {:?}, but the proof rests on {:?}",
2651                manifest.assumptions.transitive, checks.transitive
2652            ),
2653        );
2654    }
2655    stages.push(("primary_check", checks.primary.clone()));
2656    stages.push(("independent_recheck", checks.independent.clone()));
2657    stages.push(("translated", translated));
2658    stages.push(("assumption_policy", policy));
2659    let emission = match (&checks.rebuilt, &admitted.document) {
2660        (None, _) => not_run("the environment did not rebuild"),
2661        (Some(_), None) => (
2662            StageStatus::Unsupported,
2663            Some("no_document".into()),
2664            format!(
2665                "the package carries no LOGOS document: {}",
2666                manifest.emission.detail
2667            ),
2668        ),
2669        (Some(rebuilt), Some(document)) => {
2670            replay_emission(rebuilt, document, &declaration, &reference, admitted)
2671        }
2672    };
2673    stages.push(("emission_reparsed", emission));
2674    stages
2675}
2676
2677/// The manifest's claims about the target, and the correspondence, agree with the certificate and
2678/// the export.
2679fn bind_manifest(
2680    manifest: &Manifest,
2681    declaration: &super::reference::ReferenceDeclaration,
2682    correspondence: &CorrespondenceFile,
2683    certificate: &EnvironmentCertificate,
2684    reference: &Reference,
2685) -> std::result::Result<(), (&'static str, String)> {
2686    let kind = match declaration.kind {
2687        ReferenceKind::Theorem => "thm",
2688        ReferenceKind::Definition => "def",
2689        ReferenceKind::Opaque => "opaque",
2690        _ => "",
2691    };
2692    if manifest.target.kernel != declaration.name
2693        || certificate.target != declaration.name
2694        || manifest.target.kind != kind
2695        || manifest.target.level_params != declaration.level_params
2696    {
2697        return Err((
2698            "manifest_mismatch",
2699            format!(
2700                "the manifest's target ({}, {}, {}) is not the export's {}",
2701                manifest.target.kernel,
2702                manifest.target.kind,
2703                manifest.target.lean,
2704                declaration.lean_name
2705            ),
2706        ));
2707    }
2708    if correspondence.target != declaration.name {
2709        return Err((
2710            "correspondence_mismatch",
2711            format!(
2712                "the correspondence's target is {}, not {}",
2713                correspondence.target, declaration.name
2714            ),
2715        ));
2716    }
2717    let declared: BTreeSet<&str> = certificate.entries.iter().flat_map(|e| e.names()).collect();
2718    let mut kernels = BTreeSet::new();
2719    let mut logos_names = BTreeSet::new();
2720    let mut target_row = false;
2721    for row in &correspondence.rows {
2722        if let Some(logos) = &row.logos {
2723            if !logos_names.insert(logos.as_str()) {
2724                return Err((
2725                    "correspondence_mismatch",
2726                    format!("the LOGOS name {logos} carries two declarations"),
2727                ));
2728            }
2729        }
2730        // A constant the fresh LOGOS context derives itself (a recursor family member, an equation)
2731        // or holds in its prelude has no certificate declaration and no Lean name of its own. It is
2732        // bound by the emission stage: the document must be exactly the emitter's writing of the
2733        // rebuilt environment, and the emitter's verification re-derives every such constant.
2734        let derived =
2735            matches!(row.origin.as_str(), "synthesized" | "prelude") && row.kernel.is_empty();
2736        if derived {
2737            if !row.lean.is_empty() || row.logos.is_none() {
2738                return Err(("correspondence_mismatch", "a derived correspondence row must name its LOGOS constant and no Lean declaration".into()));
2739            }
2740            continue;
2741        }
2742        if row.kernel.is_empty() || !kernels.insert(row.kernel.as_str()) {
2743            return Err((
2744                "correspondence_mismatch",
2745                format!(
2746                    "{:?} has no kernel name, or two correspondence rows",
2747                    row.kernel
2748                ),
2749            ));
2750        }
2751        if row.origin != "prelude" && !declared.contains(row.kernel.as_str()) {
2752            return Err((
2753                "correspondence_mismatch",
2754                format!(
2755                    "the correspondence maps {}, which the certificate does not declare",
2756                    row.kernel
2757                ),
2758            ));
2759        }
2760        if row.origin == "authored" || row.origin == "prelude" && !row.lean.is_empty() {
2761            match reference.source_lean_name(&row.lean) {
2762                Ok(source) if source == row.kernel => {}
2763                Ok(source) => {
2764                    return Err((
2765                        "correspondence_mismatch",
2766                        format!(
2767                            "the correspondence maps Lean {} to {}, but the export's {} is {}",
2768                            row.lean, row.kernel, row.lean, source
2769                        ),
2770                    ))
2771                }
2772                Err(_) => {
2773                    return Err((
2774                        "correspondence_mismatch",
2775                        format!(
2776                            "the correspondence names Lean {}, which the export does not declare",
2777                            row.lean
2778                        ),
2779                    ))
2780                }
2781            }
2782        }
2783        if row.kernel == declaration.name {
2784            target_row = true;
2785            if row.lean != declaration.lean_name || row.logos != manifest.target.logos {
2786                return Err((
2787                    "correspondence_mismatch",
2788                    "the correspondence's target row is not the manifest's target".into(),
2789                ));
2790            }
2791        }
2792    }
2793    if !target_row {
2794        return Err((
2795            "correspondence_mismatch",
2796            "the correspondence has no row for the target".into(),
2797        ));
2798    }
2799    Ok(())
2800}
2801
2802/// The document is exactly the emitter's writing of the rebuilt environment, its correspondence is
2803/// the correspondence file, it reparses in a fresh LOGOS session, and its target states the
2804/// reference translation of the Lean type.
2805fn replay_emission(
2806    rebuilt: &RebuiltSummary,
2807    document: &str,
2808    declaration: &super::reference::ReferenceDeclaration,
2809    reference: &Reference,
2810    admitted: &Admitted,
2811) -> StageOutcome {
2812    let proof_of = |key: &str| rebuilt.proofs.get(key).cloned();
2813    let emitted = match super::emit::emit_context(&rebuilt.context, &proof_of, &declaration.name) {
2814        Ok(emitted) => emitted,
2815        Err(verdict) => {
2816            return (
2817                if matches!(verdict, Verdict::Decline(_)) {
2818                    StageStatus::Unsupported
2819                } else {
2820                    StageStatus::Failed
2821                },
2822                Some(verdict.reason().code().as_str().to_string()),
2823                format!(
2824                    "the rebuilt environment could not be written as LOGOS: {}",
2825                    verdict.reason().message()
2826                ),
2827            )
2828        }
2829    };
2830    if emitted.source != document {
2831        return failed(
2832            "emission_mismatch",
2833            "the package's LOGOS document is not the emitter's writing of the rebuilt environment",
2834        );
2835    }
2836    let rows = match correspondence_rows(&admitted.certificate, reference, Some(&emitted)) {
2837        Ok(rows) => rows,
2838        Err(verdict) => return failed("emission_mismatch", verdict.reason().message().to_string()),
2839    };
2840    if rows != admitted.correspondence.rows {
2841        return failed(
2842            "correspondence_mismatch",
2843            "the correspondence file is not the document's correspondence",
2844        );
2845    }
2846    if admitted.manifest.target.logos.as_deref() != Some(emitted.target.as_str()) {
2847        return failed(
2848            "manifest_mismatch",
2849            "the manifest's LOGOS target is not the document's",
2850        );
2851    }
2852    match super::emit::check_statement(&emitted, declaration) {
2853        Ok(()) => (
2854            StageStatus::Passed,
2855            None,
2856            format!(
2857                "{} reparsed in a fresh LOGOS session; {} states the reference translation of {}",
2858                admitted.manifest.document.as_deref().unwrap_or(""),
2859                emitted.target,
2860                declaration.lean_name
2861            ),
2862        ),
2863        Err(fault) => failed("emission_mismatch", fault.to_string()),
2864    }
2865}
2866
2867// -------------------------------------------------------------------------------------------------
2868// The package on disk.
2869// -------------------------------------------------------------------------------------------------
2870
2871/// Read a package directory, enforcing the file-count, size and path limits before reading any
2872/// content. Symbolic links and special files are refused.
2873pub fn read_directory(
2874    dir: &std::path::Path,
2875    limits: &PackageLimits,
2876) -> std::result::Result<PackageFiles, String> {
2877    let mut files = PackageFiles::new();
2878    let mut total = 0usize;
2879    let mut work = vec![(dir.to_path_buf(), String::new())];
2880    while let Some((path, prefix)) = work.pop() {
2881        let entries = std::fs::read_dir(&path).map_err(|e| format!("{}: {e}", path.display()))?;
2882        for entry in entries {
2883            let entry = entry.map_err(|e| format!("{}: {e}", path.display()))?;
2884            let name = entry
2885                .file_name()
2886                .into_string()
2887                .map_err(|n| format!("{n:?} is not a UTF-8 file name"))?;
2888            let relative = if prefix.is_empty() {
2889                name.clone()
2890            } else {
2891                format!("{prefix}/{name}")
2892            };
2893            let metadata =
2894                std::fs::symlink_metadata(entry.path()).map_err(|e| format!("{relative}: {e}"))?;
2895            if metadata.file_type().is_symlink() {
2896                return Err(format!("{relative} is a symbolic link"));
2897            }
2898            if metadata.is_dir() {
2899                work.push((entry.path(), relative));
2900                continue;
2901            }
2902            if !metadata.is_file() {
2903                return Err(format!("{relative} is not a regular file"));
2904            }
2905            if let Some(why) = unsafe_path(&relative, limits) {
2906                return Err(format!("{relative:?} is {why}"));
2907            }
2908            if files.len() + 1 > limits.max_files {
2909                return Err(format!("more than {} files", limits.max_files));
2910            }
2911            let size = usize::try_from(metadata.len()).unwrap_or(usize::MAX);
2912            if size > limits.max_file_bytes {
2913                return Err(format!(
2914                    "{relative} is {size} bytes, beyond the limit of {}",
2915                    limits.max_file_bytes
2916                ));
2917            }
2918            total = total.saturating_add(size);
2919            if total > limits.max_total_bytes {
2920                return Err(format!(
2921                    "the package exceeds {} bytes",
2922                    limits.max_total_bytes
2923                ));
2924            }
2925            let bytes = std::fs::read(entry.path()).map_err(|e| format!("{relative}: {e}"))?;
2926            if bytes.len() != size {
2927                return Err(format!("{relative} changed while it was read"));
2928            }
2929            files.insert(relative, bytes);
2930        }
2931    }
2932    Ok(files)
2933}
2934
2935/// Write a package to `dir` atomically: every file is written and synced in a staging directory
2936/// beside it, which is then renamed into place. An existing `dir` is never replaced — a published
2937/// result is immutable — and a failure leaves nothing behind.
2938pub fn publish(dir: &std::path::Path, files: &PackageFiles) -> std::io::Result<()> {
2939    use std::io::Write;
2940    if dir.exists() {
2941        return Err(std::io::Error::new(
2942            std::io::ErrorKind::AlreadyExists,
2943            format!("{} already exists", dir.display()),
2944        ));
2945    }
2946    let limits = PackageLimits::default();
2947    for path in files.keys() {
2948        if let Some(why) = unsafe_path(path, &limits) {
2949            return Err(std::io::Error::new(
2950                std::io::ErrorKind::InvalidInput,
2951                format!("{path:?} is {why}"),
2952            ));
2953        }
2954    }
2955    let parent = dir
2956        .parent()
2957        .filter(|p| !p.as_os_str().is_empty())
2958        .unwrap_or(std::path::Path::new("."));
2959    let stem = dir
2960        .file_name()
2961        .and_then(|n| n.to_str())
2962        .unwrap_or("package");
2963    let nonce = std::time::SystemTime::now()
2964        .duration_since(std::time::UNIX_EPOCH)
2965        .map(|d| d.as_nanos())
2966        .unwrap_or(0);
2967    let staging = parent.join(format!(".{stem}.staging-{}-{nonce}", std::process::id()));
2968    let result = (|| {
2969        std::fs::create_dir(&staging)?;
2970        for (path, bytes) in files {
2971            let target = staging.join(path);
2972            if let Some(dir) = target.parent() {
2973                std::fs::create_dir_all(dir)?;
2974            }
2975            let mut file = std::fs::File::create(&target)?;
2976            file.write_all(bytes)?;
2977            file.sync_all()?;
2978        }
2979        std::fs::rename(&staging, dir)
2980    })();
2981    if result.is_err() {
2982        let _ = std::fs::remove_dir_all(&staging);
2983    }
2984    result
2985}
2986
2987/// The replay receipt as a `logos-lean-replay/1` document.
2988pub fn receipt_document(receipt: &Receipt, manifest_sha256: Option<&str>) -> serde_json::Value {
2989    serde_json::json!({
2990        "schema": REPLAY_SCHEMA,
2991        "replayed": receipt.replayed(),
2992        "verified": receipt.verified(),
2993        "manifest_sha256": manifest_sha256,
2994        "stages": receipt.to_json(),
2995        "trusted_base": TRUSTED_BASE,
2996    })
2997}
2998
2999#[path = "package_source_work.rs"]
3000mod source_work;
3001use source_work::SourceWork;
3002
3003#[cfg(test)]
3004#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/package/native_disclosure_controls.rs"]
3005mod native_disclosure_controls;
3006
3007#[cfg(test)]
3008#[path = "../../../../logicaffeine_tests/unit/logicaffeine_kernel/interface/lean/package/native_contract_controls.rs"]
3009mod native_contract_controls;