pub struct BuildConfig {
pub command: Option<String>,
pub out_dir: PathBuf,
pub render_command: Option<String>,
}Expand description
Build step that produces the static artifact published by a
mesofact-static workload.
deny_unknown_fields is load-bearing (R658-B1). TOML scopes every key
written after a table header into that table, so a manifest that puts a
top-level MesofactStaticWorkload field — routes was the one that
actually happened — below [build] silently produces build.routes
instead. Without this attribute serde discards the stray key, the
top-level field falls back to its default (or fails with a missing field
error pointing at the wrong place), and the manifest deploys with a
declaration nobody honours. Every real workload.toml in the camp and the
CLI’s own yah cloud site init scaffold carried exactly that shape for
months without a single reader noticing.
The cost is forward-compat: a manifest carrying a [build] key this binary
doesn’t know is a hard parse error, not an ignored key. That is deliberate.
A build config is a small, slow-moving, load-bearing table — a key that
silently does nothing is worse here than one that refuses to load, because
the failure surfaces as a wrong artifact rather than an error.
Note deny_unknown_fields is inert for the postcard kamaji wire, which is
non-self-describing and positional — this only constrains TOML/JSON.
@yah:relay(R905, “Per-environment build override — a mirror cannot change what its component builds with”)
@yah:status(review)
@yah:at(2026-09-14T20:25:20Z)
@yah:assignee(agent:bundle-anthropic-ashguard)
@yah:next(“THE GAP, stated structurally. A [build] block lives on the COMPONENT’s workload.toml (BuildConfig here: command, out_dir, render_command — nothing else), and a component is declared once in .yah/services/<svc>/service.toml. The MIRROR is the only per-environment surface, and .yah/schema/mirror.toml.schema.json’s top-level keys are exactly asset_aliases, drivers, ingress, ingress_machines, providers, schema_version, shape — no build, no env. So a service that deploys the SAME component to two environments has no way to build it differently for each, and mesofact_static’s build step passes no environment either (run_build -> ExecContext::default().with_cwd()).”)
@yah:next(“FIRST CONSUMER, CONFIRMED LIVE, NOT HYPOTHETICAL — noisetable’s staging origin (noisetable camp R704-T4). web/landing/workload.toml:39 hardcodes command = \"bun run build:cloud\", and build:cloud is NOISETABLE_API_ORIGIN=https://api.noisetable.com bun run build (web/landing/package.json:17). A sibling build:staging pointing at https://api-staging.noisetable.com EXISTS at package.json:18 and rg build:staging over the whole repo returns exactly that one definition line — nothing can reference it, because there is nowhere to put the reference. Measured consequence: curl -sS https://staging.noisetable.com/account serves \"api_base\":\"https://api.noisetable.com/api/v1\", production CORS allows only https://noisetable.com, so the staging account page’s every API call is blocked and the page renders "unreachable".”)
@yah:next(“TWO SHAPES, both plausible; the ticket does not pick one. (a) An env table on BuildConfig plus a mirror-level override of it — most direct, but puts per-env data on a per-component struct. (b) A [build] override block on the mirror that replaces command for that environment — keeps the environment axis where every other environment fact already lives (providers, ingress), at the cost of a second place a build command can come from. (b) is the one this filer leans toward, precisely because the mirror is ALREADY the per-environment surface and (a) invents a second one.”)
@yah:next(“DO NOT ‘SOLVE’ THIS CONSUMER-SIDE WITH HOSTNAME SNIFFING. The tempting workaround is deriving the API origin from location.hostname in the browser (staging.noisetable.com -> api-staging.noisetable.com). It was considered and rejected by the noisetable operator: it is one-off string-match control flow, and it converts a deploy-time fact into client-bundle logic that no build can validate. The absence of that workaround is why this ticket exists — do not close it by suggesting one.”)
@yah:verify(“rg -n \"build:staging\" --glob '!node_modules' inside the noisetable checkout returns MORE than the single package.json definition line — i.e. something now references it. Today it returns exactly one hit, which is the whole defect.”)
@yah:verify(“curl -sS https://staging.noisetable.com/account | grep -o '\"api_base\":\"[^\"]*\"' prints https://api-staging.noisetable.com/api/v1. It printed https://api.noisetable.com/api/v1 on 2026-09-13 when this was filed.”)
@yah:gotcha(“THE NOISETABLE SIDE IS NOT ALL OF THIS TICKET’S BLAST RADIUS, and shipping the override does not by itself fix that page. api-staging.noisetable.com currently resolves and serves 200 on /health from the PRODUCTION passway edge, whose upstreams_by_host lists api.noisetable.com and * with no api-staging entry — so a staging bundle pointed at it would 404 until noisetable R704-T4’s own door (a cloudflare-tunnel on the NAT’d dev node us-west-011) is up. The two are independent and both are required; neither is a reason to defer the other.”)
@yah:handoff(“SHAPE (b) SHIPPED, widened by an env table. MirrorConfig.build: BTreeMap<component id, MirrorBuildOverride{command, render_command, env}> (oss/yubaba/crates/cloud/src/config.rs). Field-wise override of the workload’s [build]; out_dir deliberately not overridable. Wired into BOTH build paths: MesofactStaticReconciler::rebuild_static/revalidate_static (env via ExecContext::with_env) and the bundle tier in app/yah/cli/src/cloud.rs (effective_build_command + spawn_component_build shared by single- and multi-component assembly; deploy_mesofact_bundle’s provenance/refusal resolve through it too). noisetable staging uses providers.bundle, so the CLI path is the one its first consumer hits. cross_ref_validate refuses [build.[build.<component id>] with command, render_command and env (MirrorBuildOverride, oss/yubaba/crates/cloud/src/config.rs). Each field overrides the matching field of the workload’s [build]; out_dir cannot be overridden. Honoured by BOTH build paths: MesofactStaticReconciler::rebuild_static/revalidate_static (env via ExecContext::with_env) and the bundle tier (app/yah/cli/src/cloud.rs effective_build_command + spawn_component_build, shared by single- and multi-component assembly and by deploy_mesofact_bundle’s provenance/refusal). cross_ref_validate refuses a key naming an undeclared component. Schema regenerated; it was swept into peer commit 30c2c02c.”)
@yah:verify(“cd oss/yubaba && cargo test -p yah-cloud –lib – reconciler::mesofact_static build_override → 56 + 5 pass, including rebuild_static_runs_the_mirrors_overridden_command_with_its_env, revalidate_static_honours_the_mirrors_render_override_and_env, and cloud_config_cross_ref_fails_on_a_build_override_for_an_unknown_component”)
@yah:gotcha(“An OLD yah binary silently ignores [build.<id>], because MirrorConfig has no deny_unknown_fields. Install a yah that includes R905 before relying on the override, or staging keeps building with build:cloud and nothing errors.”)
@yah:assumes(“The noisetable consumer edit is noisetable-camp work (R704-T4), not done here. Add [build.site] command = \"bun run build:staging\" (or [build.site.env] NOISETABLE_API_ORIGIN = ...) to .yah/services/noisetable-marketing/mirrors/staging.toml, then apply. The two filing-time verify lines (rg build:staging, curl api_base) are only satisfiable after that step and after R704-T4’s api-staging door is up.”)
@yah:handoff(“NOISETABLE LOCKSTEP (operator-authorized 2026-09-14): (1) noisetable .yah/services/noisetable-marketing/mirrors/staging.toml gains [build.site] command = \"bun run build:staging\" with the why + old-binary warning inline. (2) web/landing/workload.toml’s comment claiming the reconciler passes no build env replaced — it now says build:cloud is production’s default and staging overrides it from its mirror. (3) noisetable .yah/schema/mirror.toml.schema.json refreshed from yah’s regenerated copy (+35 lines). external/yah there is a symlink to this monorepo, so the mesofact-build side needs no separate bump.”)
@yah:gotcha(“~/.local/bin/yah (0.8.39+e0530813-dirty) does NOT carry R905 (strings check for the new cross_ref_validate message: 0 hits). Until an R905 yah is installed, noisetable’s [build.site] is silently ignored and staging still builds with build:cloud. Also: the filing-time verify rg build:staging skips hidden dirs, so it never sees .yah/ — its hits come from web/landing/workload.toml’s comment, not from the mirror itself.”)
@yah:verify(“Against the REAL noisetable tree with an R905 build (target/debug/yah, 2026-09-14 13:33): yah cloud validate --path ~/ss/noisetable → ok. Negative: the same .yah copied to /tmp with the key changed to [build.sight] → Error: loading workspace declarations: services/noisetable-marketing/mirrors/staging.toml: [build.sight] — no component \"sight\" in services/noisetable-marketing/service.toml (declared: [\"site\", \"app\"]). CLI: cargo test -p yah –lib (6 bundle_assembly_tests incl. a_mirror_build_override_decides_what_a_component_builds_with) pass.”)
Fields§
§command: Option<String>Shell command run from the manifest’s directory, e.g. "bun run build".
Absent means “this project has no external bundler step” (R838-B1),
not “run nothing by accident”. mesofact new’s scaffold deliberately
omits it — the in-process pipeline (mesofact-dev / mesofact-build)
produces out_dir with no third binary, no package manager and no Node
— so requiring it here made every scaffolded project’s manifest fail to
load through this envelope. Setting it opts back out to a shell command,
which is what a project with its own bundler wants.
Consumers were already written for this: read_workload_build
(app/yah/cli/src/cloud.rs) has always typed it Option<String> and
yah cloud bundle build only needs it under --run-build; the bundle
sync arm refuses None by name. MesofactStaticReconciler:: rebuild_static skips the build step for None — the same thing it
already did for a workload with no workload.toml at all.
WIRE NOTE: this is Option<String> on the postcard kamaji wire, so it
costs a leading 0x00/0x01 tag byte that the bare String did not
have. A pre-R838 node decoding a new frame fails loudly (the string’s
length byte is not a valid Option tag) rather than silently reading a
shifted field — which is why this is Option and not a #[serde(default)]
empty String sentinel. Not a cluster_epochs surface: those hash the
raft modules and the openraft pin, not workload_spec.
out_dir: PathBufOutput directory (relative to the manifest) the reconciler uploads.
render_command: Option<String>Data-only re-render command (W225 §3 “revalidate”), run from the
manifest’s directory against the already-built out_dir — no
bundler. {route} is substituted with the invalidated route pattern,
e.g. "../../../../scripts/mesofact-build.sh render . --route {route} --all" (R746-F9 — resolves a prebuilt binary rather than shelling to
cargo, which cannot even find the package from a site’s own dir).
Absent → a revalidate dispatch republishes out_dir as-is.
Trait Implementations§
Source§impl Clone for BuildConfig
impl Clone for BuildConfig
Source§impl Debug for BuildConfig
impl Debug for BuildConfig
Source§impl<'de> Deserialize<'de> for BuildConfig
impl<'de> Deserialize<'de> for BuildConfig
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
impl Eq for BuildConfig
Source§impl PartialEq for BuildConfig
impl PartialEq for BuildConfig
Source§impl Serialize for BuildConfig
impl Serialize for BuildConfig
impl StructuralPartialEq for BuildConfig
Source§impl TS for BuildConfig
impl TS for BuildConfig
Source§type WithoutGenerics = BuildConfig
type WithoutGenerics = BuildConfig
WithoutGenerics should just be Self.
If the type does have generic parameters, then all generic parameters must be replaced with
a dummy type, e.g ts_rs::Dummy or (). The only requirement for these dummy types is that
EXPORT_TO must be None. Read moreSource§type OptionInnerType = BuildConfig
type OptionInnerType = BuildConfig
std::option::Option<T>, then this associated type is set to T.
All other implementations of TS should set this type to Self instead.Source§fn docs() -> Option<String>
fn docs() -> Option<String>
TS is derived, docs are
automatically read from your doc comments or #[doc = ".."] attributesSource§fn decl_concrete(cfg: &Config) -> String
fn decl_concrete(cfg: &Config) -> String
TS::decl().
If this type is not generic, then this function is equivalent to TS::decl().Source§fn decl(cfg: &Config) -> String
fn decl(cfg: &Config) -> String
type User = { user_id: number, ... }.
This function will panic if the type has no declaration. Read moreSource§fn inline(cfg: &Config) -> String
fn inline(cfg: &Config) -> String
{ user_id: number }.
This function will panic if the type cannot be inlined.Source§fn inline_flattened(cfg: &Config) -> String
fn inline_flattened(cfg: &Config) -> String
Source§fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_generics(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn output_path() -> Option<PathBuf>
fn output_path() -> Option<PathBuf>
T should be exported, relative to the output directory.
The returned path does not include any base directory. Read moreSource§fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
fn visit_dependencies(v: &mut impl TypeVisitor)where
Self: 'static,
Source§fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
fn dependencies(cfg: &Config) -> Vec<Dependency>where
Self: 'static,
Source§fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export_all. Read moreSource§fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
fn export_all(cfg: &Config) -> Result<(), ExportError>where
Self: 'static,
TS::export. Read moreSource§fn export_to_string(cfg: &Config) -> Result<String, ExportError>where
Self: 'static,
fn export_to_string(cfg: &Config) -> Result<String, ExportError>where
Self: 'static,
Auto Trait Implementations§
impl Freeze for BuildConfig
impl RefUnwindSafe for BuildConfig
impl Send for BuildConfig
impl Sync for BuildConfig
impl Unpin for BuildConfig
impl UnsafeUnpin for BuildConfig
impl UnwindSafe for BuildConfig
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§impl<Q, K> Equivalent<K> for Q
impl<Q, K> Equivalent<K> for Q
Source§fn equivalent(&self, key: &K) -> bool
fn equivalent(&self, key: &K) -> bool
key and return true if they are equal.