Virtual cells bind to warehouse objects; stop the context document lying - #19
Merged
Conversation
Resolves the two blocking issues MNTN APE filed against 0.0.14, plus six document divergences and one deploy defect found while fixing them. The model change: `materialize: never` is retired. A virtual export now declares `bind: <source>` naming an existing warehouse object. SQL that nothing executes was a promise nothing kept — a derived column was unreachable, a rename was unreachable, and a WHERE was unenforced row filtering. Transforms now require materialization. Document correctness: - `served_here` derives from the mounted route list, computed once and fed to both the interface digest and the document, so the two cannot disagree (#17). The proposed fix used `discoverable_routes`, which includes bound exports by design and would have been a no-op. - Hosted `/context` surfaces `.cell/source_check.json`, gated on digest AND profile and fail-closed on a missing profile, so a staging attestation cannot validate a prod server (#16). - `grain_verified` is false when any discoverable export declares no grain; `verify` skips the uniqueness check there, so it was true for a check that never ran. - The `query` block is unconditional. It is interface grammar, not mount state; a digest that moved on a serving flag violated its own contract. Reverses ADR 0012 s4, amended in this change. - A two-doors regression test asserts the portable and hosted documents agree modulo an explicit, justified allowlist, so a ninth divergence fails the build. Types and metadata: - A bound export's declared types are checked against the warehouse's own types, not DuckDB's rendering. Wide BigQuery NUMERIC renders as VARCHAR, so verify was forcing authors to declare `string` for a column the warehouse returns as a number. - Warehouse column descriptions surface under `observed.source_descriptions`, scoped to sources an export actually binds. A machine fact, outside the interface digest and outside `description_digest`, so an upstream comment edit cannot move our release gate. Deploy: - A bound cell can deploy a Server. Its `/context` is the integration surface for agent consumers, which a static file cannot authenticate. No init Job is rendered for it, since `datamk run` refuses the cell. - `schedule:` on a bound cell is refused; the relax would otherwise have opened a CronJob that fails forever. - The anonymous-access refusal names the document itself as the payload for a bound cell. Behavior changes callers should know about: a zero-transform cell now refuses `run`; `materialize: never` is rejected with a migration error naming both exits; the interface digest moves once for `served_here` and the `query` block. `datamk interface import` (#18) is deliberately not here. Its selector depends on the binding model, and shipping a generator before the model settled risked mass-producing contracts we would have had to migrate. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #16 and #17. Does not close #18 — see "What is not here".
Filed by the MNTN APE team against
0.0.14while building their first virtual cell. Fixing them surfaced six more document divergences, one deploy defect that made the whole cell class undeployable, and a model problem underneath all of it.The model change
materialize: neveris retired. A virtual export declaresbind: <source>, naming an existing warehouse object.The reasoning: a
nevertransform's SQL was executed by nothing. It ran insideverify's DuckDB session for a few milliseconds and was discarded, so every semantic in it was a promise nobody kept — a derived column was unreachable, a rename was unreachable, and aWHEREwas unenforced row filtering, which on a PII surface is silent over-disclosure. A contract must describe a relation something actually produces. Transforms now require materialization.This was found by building a non-passthrough
nevertransform and following the document's own instructions:verifypassed,contextpublished all three columns, and there was no path by which an agent could obtain the derived one. Everyneverfixture in the tree wasSELECT *, which is why it had never been caught.Document correctness
served_herederives from the mounted route list, computed once and fed to both the interface digest and the document, so they cannot disagree. Worth noting: the fix proposed inserved_hereis derived from --no-data, not from whether any route is mounted #17 useddiscoverable_routes, which deliberately includes bound exports, and would have been a no-op./contextsurfaces.cell/source_check.json, gated on digest and profile and fail-closed when the profile is absent, so a staging attestation cannot validate a prod server. The record also now ships in the deploy artifact and folds intocontent_hash— without that half the fix reached a laptop and not a pod.grain_verifiedis false when any discoverable export declares no grain.verifyskips the uniqueness check in that case, so the field wastruefor a check that never ran.queryblock is unconditional. It is interface grammar, not mount state. Reverses ADR 0012 §4, amended here.source_checkwork with no new entries./contextETag now covers the startup-fixedobservedinputs. Without it a client honoringIf-None-Matchwould 304 straight past theverified_at_sourceupgrade this PR delivers.Types and metadata
A bound export's declared types are checked against the warehouse's own types rather than DuckDB's rendering. Wide BigQuery
NUMERICrenders asVARCHAR, soverifywas rejectingdecimaland forcing authors to declarestringfor a column the warehouse returns as a number — publishing a false type to a machine.Warehouse column descriptions surface under
observed.source_descriptions, scoped to sources an export actually binds. They are a machine fact: outside the interface digest, and outsidedescription_digest, so an upstream comment edit cannot move our release gate. Contract prose stays authored and reviewed indeclared.Only BigQuery populates this today; Postgres and Snowflake run no metadata job, and the absence is explicit rather than silent.
Deploy
A bound cell can deploy a Server — its
/contextis the integration surface for agent consumers, which a static file cannot authenticate. No init Job is rendered, sincedatamk runrefuses the cell; both share one predicate.schedule:on a bound cell is refused, closing a hole the relax would otherwise have opened. The anonymous-access refusal now names the document itself as the payload.Behavior changes
run.materialize: neveris rejected, with a migration error naming both exits and hintingbind:only when the transform is a genuine passthrough.served_hereand thequeryblock. Nodatamk_contextbump — nothing is renamed or re-meant.What is not here
datamk interface import(#18). Its selector depends on the binding model, and shipping a generator before that settled risked mass-producing contracts we would then have had to migrate by hand. #18 stays open.The thin-interface lint is also deferred:
schema:is optional, an empty one makesserveemitSELECT *, andverifythen checks nothing. Real, but a new refusal surface with no spec, and it should not land before import makes the honest path cheap.Verification
624 tests, 0 failures.
cargo fmt --checkandcargo clippy --all-targets --all-features -- -D warningsclean.Two review passes ran before this was committed. They found nine issues, all fixed here — including one blocker where the deploy relax was a no-op because the init Job was still rendered unconditionally, and one case where a deleted test had genuinely lost coverage rather than being converted. The BigQuery metadata query is covered by a credential-gated test that skips without
DATAMK_TEST_BQ_PROJECT, per the existing convention; it does not run in default CI.🤖 Generated with Claude Code