/**
* Building a module's published payload — the one computation both gates run.
*
* `telo publish` pushes it and `telo release` digests it, and they have to be
* the same bytes or the ledger's number and the registry's number are answers to
* different questions. So there is one builder, and the publish command consumes
* it rather than carrying its own copy of the transform.
*
* ## What makes the bytes a pure function of the commit
*
* Three inputs used to leak in, and each is closed here:
*
* 1. **Pins were discovered, not authored.** Publish fetched each remote
* dependency's published `telo.yaml` to derive its hash, best-effort — so one
* commit produced different manifest bytes depending on network reachability.
* An author's pin is now the input: `telo install` / `telo upgrade` write it,
* and an unpinned remote import is refused here rather than resolved.
* 2. **Re-serialization was conditional** on at least one pin having been
* written, so the same manifest serialized two ways depending on its own
* content. It is unconditional now.
* 3. **A sibling's pin came from the registry.** An in-repo sibling is on disk,
* so its published manifest is *derived* — recursively, through this same
* builder — which is what lets a whole release batch be planned offline, with
* post-bump versions the registry has never seen.
* 4. **The derived manifest was not the published one.** The `layers:` index was
* injected during the push, so what this builder returned was a document no
* registry ever holds — and a sibling pin, being a hash of it, named bytes
* that do not exist. The index is written here now, which is what makes
* `publishedManifest()` true to its name and requires layer framing to be a
* pure function of the files it covers.
*
* The publish destination stays an input, because canonicalization writes it
* into the manifest. It is not derivable, so the ledger records which base its
* digests were taken against.
*/
import { type ArtifactSelector, type LayerRole } from "@telorun/analyzer";
import { type PayloadFile } from "@telorun/kernel";
import { type Partition } from "./partition-layers.js";
/** A layer built from a working copy: what publish pushes and what the release
* ledger digests. Mutable-shaped because the transport's own `PayloadLayer` is,
* and the two are handed straight across. */
export interface BuiltLayer {
role: LayerRole;
selector?: ArtifactSelector;
files: PayloadFile[];
}
export interface ModulePayload {
/** The `telo.yaml` that ships, byte for byte: canonicalized, pinned, includes
* inlined, `layers:` index written. A dependent's import pin is a hash of
* exactly this. */
readonly manifest: string;
readonly layers: BuiltLayer[];
readonly partition: Partition;
/**
* Absolute paths of every file the controller builds read — the module's own
* sources, the shared libraries they inline, their dependency tree. The
* release edge graph is built from these; nothing else can see what a bundle
* actually contains.
*/
readonly buildInputs: readonly string[];
/**
* In-repo siblings this module imports relatively: the sibling's absolute
* manifest path (the release edge) and the ref the import canonicalized to
* (which publish checks resolves at its published location before pushing).
*/
readonly relativeImports: readonly {
readonly manifestPath: string;
readonly ref: string;
}[];
/**
* Remote imports carrying an author-written pin, already split into the ref
* and the hash it claims.
*
* Split HERE because this is the only place both spellings are in hand: a
* scalar-form pin carries its hash as a `#sha256-…` fragment on the source,
* and the object form carries it as an `integrity:` sibling. A consumer handed
* only the ref can recover the first by re-splitting and the second not at all
* — which is exactly how the verification below came to be a silent no-op for
* every pin in this repo.
*
* Publish verifies these against the registry — fetch, compare, hard-fail on
* mismatch. A sibling-derived pin is deliberately not here: it was computed
* from local bytes the registry has not seen yet, and the batch pushes
* dependencies first precisely so it does not have to have.
*/
readonly authoredPins: readonly {
readonly alias: string;
/** The ref with no integrity fragment — what the registry is asked for. */
readonly ref: string;
/** The hash the author committed. */
readonly integrity: string;
}[];
}
export interface PayloadBuilderOptions {
/** The read-only registry origin `registry://` refs resolve against. Not the
* publish destination, which is per module and passed to `payload()`. */
readonly registryOrigin?: string;
/** Where the controller build cache lives. */
readonly cacheRoot: string;
}
/**
* One module's payload, memoized across a batch.
*
* Memoized because a release digests every module in a workspace and the
* standard library's import graph is dense — `modules/sql`'s published manifest
* is needed by five dependents to derive their pins, and re-deriving it per
* dependent would rebuild its controller each time. One memo, not two: a
* module's published manifest carries its `layers:` index, so producing the
* text and producing the payload are the same computation.
*/
export declare class ModulePayloadBuilder {
private readonly options;
private readonly payloads;
/** Manifest paths whose payload is in flight, so an import cycle is reported
* rather than deadlocking on a memo entry that is awaiting itself. */
private readonly deriving;
/**
* Where each manifest publishes.
*
* A ROOT's destination is an input — which repo a module publishes to is a
* policy no graph can answer, so the caller states it (the release derives it
* as `/`, the rule the OCI mirror has always used).
*
* A SIBLING's is *derived*: the parent's destination with the relative path
* applied, which is the transport's own rule and exactly what canonicalization
* writes into the manifest. That one is not a free choice — deriving it
* independently, from the directory name say, would work for this repo's
* layout and quietly disagree with the ref the artifact actually carries the
* moment a layout differs.
*
* When a module is reached both ways and the two answers differ,
* `claimDestination` refuses rather than picking one, because the manifest can
* only carry a single ref.
*/
private readonly destinations;
constructor(options: PayloadBuilderOptions);
payload(manifestPath: string, destination: string): Promise;
/**
* The in-repo siblings this module imports relatively, without building a
* single controller.
*
* Publish ORDER needs only this — a dependency must be pushed before its
* dependents, because canonicalization writes the sibling's ref into the
* manifest and publish then hard-fails when it does not resolve — and running
* esbuild across the whole standard library to answer it would be minutes of
* work for a question the manifests already contain.
*/
relativeImportsOf(manifestPath: string, destination: string): Promise;
/**
* The published `telo.yaml` text — byte for byte what the transport pushes,
* which is what a dependent hashes to derive its pin.
*
* It builds the module's layers, because the `layers:` index is inside that
* text and each entry's `blob` covers framed bytes. Deriving the manifest
* alone was cheaper and wrong: the number it produced was of a document that
* is never published.
*/
publishedManifest(manifestPath: string): Promise;
private assertNotDeriving;
private claimDestination;
private destinationOf;
/** The transport that owns where this manifest publishes. It decides both the
* ref a sibling import canonicalizes to and the framing a layer's `blob`
* digest covers, so both halves of the published manifest come from one. */
private transportFor;
/**
* Canonicalize, pin and inline — every rewrite that stands between the
* author's `telo.yaml` and the published one.
*/
private transformManifest;
private siblingHash;
private buildPayload;
}
//# sourceMappingURL=module-payload.d.ts.map