Feature design: multi-project dbt releases¶
- Status: APPROVED
- Owner: repository maintainer; Codex is the delegated implementation integrator
- Target release: next certified release; no version reserved by this document
- Last verified: 2026-08-28
- Implementation baseline:
5a2e6c06272805f5dcb3069d23a2be181bb8e17b
This is a design, not a statement that multi-project publishing is available. See the dbt integration hub for currently supported behavior.
Executive summary¶
A repository can already contain several dbt projects, but current dpone publishing assigns every project the same release-relative project and manifest paths. Consumer CI therefore rejects more than one publishing project. Removing that check would overwrite sources and would leave promotion/evidence readers bound to the wrong project.
Publish all participating projects as one immutable release, with one deployment binding and a complete source inventory. SQL, resolved packages, manifests and selection locks are release artifacts, not runtime-image contents. Model-only changes must not rebuild a toolchain image. Existing single-project wire behavior remains supported; new workspace publishing uses an explicit new dbt wire.
Personas and customer journey¶
| Persona | Goal and first-success path | Observable result |
|---|---|---|
| Analyst | Add a standard dbt project and project-local publishing declarations; run workspace check | Project is discovered without editing central domain/CI maps |
| Data engineer | Compile two independent projects; inspect the plan before publishing | One release lists both projects and each workflow's exact source trio |
| Platform engineer | Promote the accepted release without rebuilding SQL | Same release digest, new environment-bound deployment digest |
| Operator | Retry an old attempt after a new release; roll back deployment | Original attempt uses its pinned bytes; rollback restores the retained deployment |
Discovery is offline. Authors first run the certified dbt toolchain's deps and
canonical manifest preparation where required, then check and compile. The CLI
explains the selected projects, excluded non-publishing projects, collisions and
missing artifacts. Missing dependencies are not installed implicitly. A dbt-only
project remains in the consumer's dbt-quality lane without becoming a dpone
delivery project. Project-local publishing configuration is the opt-in.
Scope and constraints¶
In scope: discovery, typed aggregate compile inputs, project-local diagnostics, content-addressed payloads, one source inventory/release, readers, complete DEV evidence, promotion, transactional audit mirror, and consumer integration.
Non-goals: cross-project ref/dbt Mesh/dbt-loom, distributed database transactions,
changing existing DAG/workflow IDs or schedules, new connector certification,
root-to-non-root migration, or weakening exact producer/provider compatibility.
Independent projects may use different admitted profiles, but this feature does
not certify a new toolchain/route. Current publishing remains constrained by its
exact certified SQL Server policies. dbt-only ClickHouse projects are not silently
converted into dpone publishing projects.
The release is a complete desired inventory, not only the Git diff. Diff selects quality work; it must not delete unchanged projects from a release. If a project is intentionally removed, the complete inventory makes removal explicit in the plan and existing deployment reconciliation handles it. No implicit tombstones are inferred from an absent/failed manifest.
Public contract¶
CLI and Python API¶
Add dpone dbt workspace discover|check|compile without changing the existing
single-project commands. Each accepts --root (default current directory) and
--format text|json. Discovery returns sorted repository-relative project paths,
project names and publishing capability/reason. It never runs dbt or reads secrets.
Check validates every publishing project and workspace-wide identities. Compile
accepts --output-dir and emits one complete release tree, never a partial set.
The workspace CLI uses success 0 and validation failure 2; the existing
singleton check's blocker exit 1 remains unchanged. Machine-readable output goes to stdout and diagnostics
to stderr. Existing publishing error objects carry code/path/remediation. New
workspace error codes must be added to the canonical catalog and reference.
The discovery report is dpone.dbt-workspace-discovery.v1, with exactly
schema, passed, projects, and blockers. Each project has project_path,
project_name (null when invalid), publishing, profiles_path (project-relative
or null), manifest_path (project-relative or null), and reason:
policy_present, not_configured, or invalid. A regular project-local file
at exactly one of DEFAULT_PROFILE_PATHS opts the project into publishing;
model-level intents are subsequently validated by the canonical compiler, not
guessed by scanning SQL. Absence of that file is explicitly not_configured,
not a claim that models contain no publishing intent. Discovery does not read
policy contents, credential profiles or manifests. Invalid project metadata or
ambiguous policy files fail discovery rather than silently excluding a project.
Standard packages-install-path and target-path values must be literal,
confined relative paths; their directories are excluded from discovery.
Non-ignored symlinks fail closed, including in-root aliases; this prevents both
escape and duplicate project ownership. Directory traversal is descriptor-based.
The check report is dpone.dbt-workspace-check.v1, with exactly schema,
passed, discovery, projects, and blockers. Project rows have
project_path and the existing dpone.dbt-publish-compile.v2 report.
Check preserves all publishing-project results even when one fails; global
workflow, DAG and workload collisions name both owners without renaming them.
No manifest preparation, dependency installation, output publication or database
execution is implicit. Neither a successful discovery nor check report certifies
physical-target disjointness or authorizes deployment.
Workspace compiler acquisition uses the existing canonical decoders through
confined reads; YAML inputs retain a 1 MiB bound and graph manifests retain the
existing 16 MiB bound. Both regular and semantic graph checks consume the
successfully acquired manifest snapshot and reject a changed file before graph
evaluation. Ordinary malformed-input types become project validation failures,
not internal failures that discard other project results.
Python callers use DbtWorkspaceService.discover(root: Path),
check(root: Path), and compile(root: Path, *, output_dir: Path),
returning immutable discovery/check/compile report models. Construct the service
with injected project discovery, compiler, bundle/selection operations and
tree publisher. CLI and Python use the same planner and writer. No CLI scraping,
private _build_tree calls, or runtime SDK imports in discovery/help paths.
The compile report is dpone.dbt-workspace-compile.v1, with exactly schema,
passed, check, output_dir, release_id, source_snapshot_sha256,
subject_sha256 and blockers. The complete check report is retained, including
all completed project diagnostics. The output directory is an absolute local
path in the report only; it never enters release identity. Success requires a
nonempty passed check, no blockers and three canonical non-null digests.
subject_sha256 hashes the exact published release-subjects.sha256 bytes.
All three digests are null on failure. The writer is acquired only for compile
after a successful nonempty check; discover/check/help do not construct execution
dependencies. Compile may run authoritative offline dbt parse/selection, but
never installs dependencies, executes model SQL, signs or activates a deployment.
--output-dir is required for compile. Empty publishing inventory, typed
validation failures and output conflicts return exit 2. A durable-publication
failure returns exit 5, retaining the check report and null identities:
output may already be visible, so retry identical inputs or verify it; never
infer permission to delete it. Unexpected writer failures are redacted into
the same compile-report shape with exit 5. The existing generic internal-error
envelope is used only when a complete check report could not be obtained.
Compile accepts optional --dbt-profiles-dir: one standard dbt parse-profile
directory for all projects, resolved once against the invocation working
directory. Without it, each project's own profiles.yml is used explicitly.
This is not a publishing-policy override. Existing policy
runtime.dbt_profile / dbt_target names select entries; no new domain map.
Missing, unsafe, oversized (1 MiB), malformed or missing named profile/target
inputs fail with DPONE_DBT_WORKSPACE_PARSE_PROFILE_INVALID and the owning
project path. Read each selected file once with confinement; keep the exact
bytes in the existing private temporary profile store (directory 0700/file 0600)
through parse and selection. A shared file is captured once for the workspace.
Cleanup runs on success and exceptions. Profiles never enter release artifacts
or reports. Use non-secret parse-only credentials: isolated_v1 remains
unchanged, without ambient profile-directory or secret-variable fallback.
Canonical manifest comparison proves graph/logical-target compatibility,
not physical-server or credential identity; deployment binding remains separate.
Default discovery ignores dependency, generated, VCS and environment directories;
configured package-install paths are also excluded. Use the existing
DEFAULT_PROFILE_PATHS project-local defaults. An inherited
DPONE_DBT_PUBLISH_PROFILES or a workspace-wide profile override is an error:
do not silently apply one project's publishing policy to every project.
Symlinks escaping the root,
nested publishing roots and duplicate project names/paths fail with actionable
diagnostics. Discovery is bounded to 100,000 visited entries, 64 project roots
and depth 32, checked while traversing rather than after collecting everything.
Skip .git, .worktrees, .venv, venv, node_modules, target,
logs, dbt_packages, .dpone-cache and .ci directories by default.
Release payload limits can reduce the number of publishable projects further;
64 discovered projects is not a promise of 64-project runtime capacity.
A project located directly at the workspace root is represented by project_path:
"." and is supported only as the sole project. It must not be silently excluded
by discovery. Root-plus-descendant publishing projects are an overlap error.
Paths are NFC-normalized POSIX relative paths (at most 1024 UTF-8 bytes and 32
components); reserved VCS/worktree names and overlap checks are case-insensitive.
Wire, payloads and identity¶
Keep generic dpone.release-set.v2 and its descriptor structure. Add the dbt
producer wire dpone.dbt-airflow-self-service.v2; preserve the v1 value and
semantics, including existing single-project output and identifier ordering.
Do not simply redefine the old constant to mean v2. Unknown versions fail closed.
V2 runtime descriptors use canonical lowercase content hashes:
| Kind | ID | Release-relative path |
|---|---|---|
| Project | dbt_project_sha256_<hex> |
runtime/dbt/objects/<hex>/project.tar.gz |
| Manifest | dbt_manifest_sha256_<hex> |
runtime/dbt/objects/<hex>/manifest.json |
| Selection | dbt_selection_sha256_<hex> |
runtime/dbt/objects/<hex>/selection-lock.json |
<hex> is exactly 64 lowercase hexadecimal digits for the bytes of that object.
Selection IDs hash the exact serialized lock bytes, not the semantic
selection_sha256 fingerprint. ID, kind, path, media type, digest and length
must agree. Identical same-kind
objects may be deduplicated; equal IDs with different descriptors/bytes fail.
Every workflow references exactly one ordered project/manifest/selection trio.
Mixing v1/v2 IDs in a trio, reordering it, foreign selections, missing/extra
objects and a correct hash under the wrong kind are rejected before execution.
Keep task-local dbt-project and target paths unchanged.
Runtime validates payload IDs against the actual project/manifest/selection bytes,
then checks the execution pack's existing project digest, manifest digest,
selection lock and workflow ownership. Content-addressing is not a substitute
for these semantic checks.
Invocation target versus materialized relations¶
Use explicit dpone.dbt-execution-pack.v2 for workspace wire v2.
Keep shipped execution-pack v1 bytes and singleton defaults unchanged.
In both versions, profile.database/schema remains the effective logical target
of the admitted model relations. V2 adds mandatory invocation_target with
exactly database and schema: the base target used by dbt to parse the
captured source. Both fields are nonempty validated tokens of at most 256
characters, matching the published schema, and participate in
the execution-pack fingerprint. V1 rejects this field; v2 rejects its absence,
unknown fields and downgrade/mixed wire-pack versions.
The builder derives the base target from the exact installed dbt toolchain's rendered profile, using the same captured profile bytes, profile/target names, canonical vars and isolated environment as parse/selection. Do not infer it by stripping suffixes or reading unrendered YAML strings. A bounded isolated build-plane adapter returns only non-secret database/schema identity; it must not emit credential objects, raw profile contents or unsanitized SDK exceptions. Discovery and check remain SDK-free; only explicit compilation acquires this capability. No extra field is required from analysts.
Runtime obtains the invocation profile through one canonical pack method: v1 returns the existing profile, while v2 returns an immutable copy with the base target. The existing profile renderer still verifies resolved connection identity exactly. Before SQL mutation, runtime parse/selection must reproduce the effective target and graph locked by the original pack. Evidence continues to identify the effective target; its required workload-pack digest binds the base target too. Neither successful offline parsing nor a matching credential alias proves a physical server identity.
This distinction follows the documented dbt behavior: by default, model
+schema is appended to the profile's target schema. For example, base
alpha and custom alpha produce alpha_alpha; feeding that result back as
the base would produce the wrong relation on the next parse.
dbt custom schemas and
standard profiles, checked
2026-08-28. Preserve supported custom schemas; do not remove them from user
projects or relax preflight to make tests pass.
Required regression evidence: actual runtime profile renderer followed by actual certified dbt parse/ls preflight with dummy credentials and no SQL/build; base-only and effective-only tampering; missing/extra target fields; unknown/mixed/downgraded versions; identical v1 bytes. Keep semantic-refresh template readers on their existing v1 path. Reader support must be rolled out before v2 emission or consumer activation.
Source inventory and ownership¶
Retain the release sidecar path _dbt/dbt-source-snapshot.json. Add
dpone.dbt-source-snapshot.v2 containing schema, projects, and
snapshot_sha256. projects is nonempty and sorted by project_path;
workflow entries are sorted by workflow_id. Each project entry has
project_path, project_name, project_bundle_sha256, manifest_sha256,
toolchain_sha256, and sorted workflows. Each workflow entry has workflow_id,
dag_id, workload_id, ordered runtime_payload_ids, and
selection_lock_sha256 (exact byte digest, equal to the selection descriptor's
sha256, not the lock's semantic selection_sha256). Exact key sets are
validated; duplicate JSON keys,
nonfinite values, duplicate entries, unsafe paths and inconsistent membership
are rejected. The snapshot digest is the canonical fingerprint excluding itself.
Publication, promotion and DEV-evidence readers load the confined sidecar,
recompute its canonical fingerprint and
compare it with the release's provenance.source_snapshot_sha256. Every
referenced project/manifest/selection descriptor and byte digest must match.
project_name must equal both bundled dbt_project.yml name and manifest
metadata name. Each workflow's DAG/workload/trio/toolchain must match the
release DAG and workload descriptors and its execution pack, not just a name
in the source inventory.
Missing sidecar or wire/snapshot version mismatch fails; readers may not
reconstruct a missing snapshot by trusting current checkout contents.
Per-task runtime does not fetch this whole-workspace sidecar or other projects'
SQL. Its authority is the signature-verified release and deployment, validated
release membership, and the selected trio bound to the execution pack. Full
workspace source closure is enforced before release signing/publication and
again by promotion and campaign verification; runtime verifies the selected
workload and bytes without pretending it inspected the other projects.
The release's existing provenance.source_snapshot_sha256 binds this complete
inventory. Its selection fingerprint continues to bind all selections and route
certifications. The source snapshot's membership must equal the release's dbt
workload membership, not merely be a subset. Volatile local paths/timestamps do
not enter identity. Database credentials never enter the source inventory.
Workflow, DAG and workload IDs remain globally unique without auto-renaming.
dbt node unique_id is project-scoped: repeated node IDs across projects are not
by themselves collisions. Logical target conflicts are checked at compilation;
environment-bound physical target conflicts are checked after connection/binding
resolution. Do not infer distinct databases solely from different alias strings.
The collision set includes all selected materialized dbt relations and every
transfer destination. A deployment preflight service receives an injected target
identity resolver, producing (connector_kind, server_authority,
database, schema, relation) under that connector's certified identifier
comparison policy. Server authority identifies the resolved physical service,
not its connection alias or credential version. SQL Server comparison requires
the admitted server/database collation policy; generic string lowercasing is
not a substitute. Missing physical authority, unsupported comparison semantics
or inability to prove disjointness blocks activation before any data writes.
Do not add a new config field every analyst must populate: platform binding
resolution owns this evidence and retains only non-secret identity digests.
At compilation, reject duplicate literal (connector family, connection_ref,
database-or-default, schema, relation) coordinates across all selected model
writes and transfer destinations, including within a single workflow. Compare
exact identifier strings; do not simulate SQL collation with case folding.
Different aliases, a default versus named database, or differently spelled
identifiers remain unresolved for physical preflight, not certified disjoint.
Names come from the locked manifest and generated transfer manifest, never from
domain directory names. A conflict reports both project/workflow/resource owners
as DPONE_DBT_WORKSPACE_TARGET_COLLISION; incomplete coordinates report
DPONE_DBT_WORKSPACE_TARGET_INVALID. Both return compile validation exit 2 with
all check rows retained, no published identities and no output replacement.
Discovery/check remain offline and do not claim authoritative selected-write
closure. No new wire fields, central registry or analyst configuration is added.
The declared write footprint includes fixed destructive adapter slots, not only
final targets. For the pinned SQL Server macro authority, let A be the resolved
model alias and U the unit-test name plus _v{tested_model.version} when its
version is not null (including version 0):
| Selected resource | Reserved relation names in its resolved database/schema |
|---|---|
| table | A, A__dbt_tmp, A__dbt_backup, A__dbt_tmp__dbt_tmp_vw |
| view | A, A__dbt_tmp, A__dbt_backup |
| incremental | A, A__dbt_tmp, A__dbt_backup, A__dbt_tmp_vw, A__dbt_tmp__dbt_tmp_vw |
| unit test | U__dbt_tmp, U__dbt_tmp__dbt_tmp_vw, in the attached model's database/schema |
| data test | No relation writes under the admitted no-stored-failures policy |
The temp and intermediate incremental slot is the same coordinate: count it
once. Do not add speculative CTAS slots to views. Reject another selected
resource or transfer occupying any slot, including within one workflow. This
projection is valid only under the existing exact graph/macro authority; it must
not infer another adapter's lifecycle. Explicitly include the Python-dispatched
drop_relation root and its transitive get_drop_sql closure, plus the reachable
rename_relation, get_columns_in_relation, and list_relations_without_caching
roots, in the generated macro authority: the materialization's manifest
dependencies alone omit these execution paths. This tightens the existing exact-body contract, changes policy
and graph digests on both release wires, and requires recompilation; it does not
establish live dependency completeness. These slots participate in environment
binding too. The pinned adapter can recursively delete dependent views when
dropping a view; that dynamic dependency set requires physical preflight and is
not certified by the fixed-name inventory. Missing dependency authority blocks
activation. Offline checking does not certify unrelated existing database objects.
Whole-source verification re-derives this inventory from locked manifests and embedded transfer YAML. A transfer archive has exactly its canonical regular manifest member, at most 1 MiB, and at most 8 MiB compressed; execution-pack archives retain their 2 MiB compressed/1 MiB member bounds. Enforce canonical base64 and cap the complete expanded TAR stream at the member limit plus two 10 KiB TAR records before parsing extension metadata. Reject extra members, links, PAX metadata and malformed compression as validation errors, not internal failures. These checks do not alter artifact wire fields or accept foreign runtime commands as proof of execution.
Evidence, promotion and compatibility¶
Environment-bound SQL Server catalog observation¶
Approved implementation detail (2026-08-29): add a read-only runtime observer
before implementing the protected admission decision. Its input is the complete
source-derived write subset for one resolved connection/database, the exact
release and loader-verified runtime-context subject digests, an existing registry-authored
MssqlDatabaseAuthorityPin, the resolved default database, the complete bounded
set of invocation databases, and bounded limits.
It must not discover projects, resolve secrets a second time, or invent a new
analyst-maintained registry. The caller owns signature/context validation;
passing a typed request is not evidence of that validation.
The observer returns immutable internal observations, not a new signed wire,
admission token, certified flag, or activation permission:
- exact input write rows and input subject digests;
- observed database pin, server/topology fingerprint, effective/original login SID fingerprints and database-principal SID fingerprint;
- observed engine version and server/database/catalog collations;
- every input slot's SQL-defined equivalence class and existing object identity, including absent schema/object slots;
- bounded incoming potential view-drop dependencies with raw referenced-name coordinates and resolved local view identities. They are potential effects, not a claim that every runtime branch actually drops every listed view.
Algorithm and failure contract:
- Validate every input and limit before connecting, including nonempty tuples, supported MSSQL writes, exact current macro-authority digest, canonical digests and identifiers of at most 128 UTF-16 code units. Resolve defaults from the already resolved runtime connection, never the authoring checkout.
- Open a dedicated connection in the pinned database using existing bounded connection and query-timeout capabilities. Check the authored pin against the live catalog; never learn or rotate a pin from that observation. Verify the resolved default database, every invocation database, and every named/default write database resolve to this same database through SQL Server. Cross-database batch execution is explicitly unsupported by this first observer, not silently reinterpreted.
- Read one metadata header. Require visible database
VIEW DEFINITION, dependency-catalogSELECT, valid original/effective/database principal SIDs, exact database continuity, and a non-contained SQL Server database. Record actual catalog semantics; do not hardcode a particular CI/CS collation. Reject unavailable metadata and unsupported impersonation, not empty-success. - Send bounded JSON parameters; classify all slots with SQL
DENSE_RANKover schema/relation underCOLLATE CATALOG_DEFAULT. Include absent slots and retain their original owners. Class numbers are local observations, not durable physical resource IDs. Never classify endpoints as independent using different aliases, server names, principal SIDs or database GUIDs alone. - For dbt slots, observe the incoming recursive-view predicate pinned by
sqlserver__get_drop_sql: database/schema/entity equality and referencing object typeV. Do not replace it with outgoingreferenced_idtraversal, filter out cross-server references, or include NULL database-name rows. Recurse with the returned local view names and original database argument. Check original and returned names for lossless database-codepage roundtrip, because the pinned macro emits non-Unicode literals. Reject literal/identifier forms the pinned macro cannot represent safely. Transfer slots participate in slot observations, not in an invented dbt lifecycle for transfer code. - Bound slots, distinct view nodes, raw dependency rows, traversal depth, input
JSON bytes and elapsed time. Fetch
remaining + 1sentinel rows and fail on overflow; cycles and incomplete traversal are failures, never truncation. Roll back the dedicated read transaction and close the connection on every path. Failed cleanup or exceeded deadline cannot yield an observation.
Defaults are 8,192 slots, 8,192 view nodes, 32,768 raw dependency rows, depth 32,
8 MiB JSON and 60 seconds elapsed, with connection/query caps at 10 seconds.
The slot budget includes intermediate/backup/helper relations: a 197-model
project must not be mistaken for only 197 writes or exceed a 256-slot toy limit.
These are internal platform observation budgets, not new user config fields.
Errors are detached/content-free; credentials and raw driver exceptions never
enter reports. No live catalog query runs during module import, offline compile,
or AirflowDeploymentProjectionService.materialize.
Validation must run the actual observer against a disposable SQL Server: CI/CS and absent slots; explicit/default DB; fully qualified incoming chain/diamond and two-part-reference control; permission denial; Unicode roundtrip; exact and exceeded budgets; read-only state and cleanup. Retain unit tests for malformed rows, injected timeouts/cleanup errors and cycles that SQL Server refuses to construct. A skipped live case is not proof. Extend the existing candidate certification runner rather than dispatching a parallel certification workflow.
This observation does not solve cross-endpoint physical service identity, preflight-to-write races, concurrent DDL, runtime reservations, post-task SQL quiescence, or ClickHouse destinations. Those remain mandatory parts of the same admission implementation; workspace activation remains denied until they are complete. This is not a narrower replacement for the final contract.
Protected activation occurrence and reservation protocol¶
Approved implementation detail (2026-08-29): workspace activation uses an
injected platform authority, never an optional success callback. The immutable
request binds one canonical UUID activation_id, environment, release and
deployment IDs, previous deployment ID, complete source/write inventory,
loader-verified runtime-context subject, the complete per-binding catalog
observation set and a canonical physical-resource closure. Every physical
resource has a platform-issued service authority, connector, target authority,
observation subject and collision-namespace guard ID. Caller aliases, host text,
database GUIDs, SIDs and local equivalence classes are observations, not global
service identity.
The runtime-context authority subject is workload-neutral: it binds the exact environment, release/deployment and release, deployment, binding-set, connection-registry and credential-runtime descriptor digests. The loader also retains the separately verified init-fetch-plan digest for each workload attempt. A plan digest is not mislabeled as the shared binding snapshot because the plan includes a workload-specific pack and payload selection; conversely, the shared context subject is never evidence that a particular workload plan was loaded.
The authority persists and reads back an exact occurrence through this saga:
- Under the existing promotion lock, after immutable projection validation and
local/remote CAS, reserve the sorted complete guard set in one protected
SERIALIZABLE transaction. Persist
PREPARED, exact request/closure subjects and monotonically advanced fencing epochs. Commit-unknown remains PREPARED or unknown and blocks pointer mutation; it is reconciled by exact readback on a new non-pooled session, never inferred as rollback. - Read back the exact durable PREPARED occurrence before filesystem pointer commit. A stale, foreign, incomplete or differently fenced receipt fails.
- Commit the pointer with the same
activation_id. Then transition that exact occurrence toACTIVEand read it back. If acknowledgement is unknown, the pointer may already have changed, but runtime remains fail-closed until exact durable ACTIVE readback succeeds. Recovery reconciles this state; it never manufactures a success receipt from pointer bytes. - Ordinary recovery creates a fresh activation UUID and reservation after its reviewed CAS. Audit-only repair does not reserve again: it requires the pointer's existing occurrence to be ACTIVE with identical release, deployment, environment and guard closure before appending audit evidence.
- Each dbt or transfer attempt obtains occurrence-bound fencing authority before opening a writer session. The execution path rechecks ACTIVE state and guard epochs at its mutation boundary. Mapped, inline and transfer paths use the same authority; no best-effort cleanup branch is accepted as coverage.
- Retirement/finalization moves ACTIVE to RETIRING, waits for durable terminal closure of every admitted attempt and connector-specific quiescence, then releases only the exact owned epochs and records RETIRED. TTL, current-tip change, missing pod or task-state observation alone never releases a guard.
The guard namespace must collide with the existing semantic-refresh physical
resource namespace. A second unrelated workspace-only guard table is forbidden:
it would allow both systems to own the same target. All connectors in the source
write closure need service authority, reservation, attempt fencing and terminal
quiescence support. Until SQL Server and every emitted transfer destination meet
this contract, release_activation_failure() keeps workspace V2 denied.
Required regressions cover: authority absent; stale occurrence; partial guard closure; PREPARED/ACTIVE commit-unknown; pointer failure after reservation; runtime pointer without ACTIVE occurrence; fresh recovery UUID; audit exact readback without new reservation; concurrent semantic-refresh collision; mapped/inline/transfer attempt fencing; finalizer crash/replay; and exact-epoch release only after complete terminal/quiescence proof.
Primary semantics checked 2026-08-29: catalog/database collations, dependency metadata and permissions, SQL literal types. The market comparison above remains applicable; these internal observations do not establish an unmeasured performance or safety advantage over another tool.
Reader-first cache installation and activation boundary¶
Local release installation explicitly dispatches the validated dbt producer wire; the historical default producer check remains V1 for callers that have not opted into workspace reading. Unknown/malformed producers never fall back. The exact installed dpone version is required for both wires. V2 installation preserves the complete canonical tree, including the source inventory and existing checksum subject, rather than copying only descriptor-referenced files. It verifies canonical input closure and the captured bytes in a private stage through the complete-source reader and integrity verifier before publication. Missing verification capability is a failure, not a reduced validation mode. Legacy singleton installation keeps its existing layout and behavior.
The cache integrity validator carries the producer wire from its already
validated release read into the projection observation. It must not infer the
wire from environment variables, deployment/index versions or caller hints.
Reading/materializing a V2 release is not physical admission: until the protected
admission provider and runtime reservation/finalizer are implemented and certified,
workspace activation fails closed with
DPONE_DBT_WORKSPACE_ADMISSION_UNAVAILABLE. No opt-out flag or synthetic success
provider is exposed. Ordinary promote, recovery and audit restoration cannot
turn successful offline validation into permission to execute V2 writes.
Denial preserves current/pointer/audit bytes; snapshot preparation may still
leave a sealed, inactive cache tree. Legacy activation remains unchanged.
This migration boundary implements the existing missing-authority rule; it is not a replacement for physical-target admission. The occurrence-specific signed transport, persistent resource reservation, protected post-task finalizer and complete transfer-destination coverage remain required before enabling V2 activation. No new public evidence wire is introduced by reader-first support.
Reuse existing immutable release/deployment IDs, dbt execution evidence and DEV
campaign completeness contracts. Evidence must resolve every workflow to its own
project/manifest/selection; one successful project cannot stand in for the other.
Existing v1 evidence and promotion continue to read through explicit v1 dispatch.
Any changed report shape receives a distinct version, never optional fields that
silently reinterpret old evidence. Existing dpone.dbt-prod-promotion.v2
already means campaign-bound singleton promotion; workspace promotion uses a
new v3 descriptor, not a second interpretation of v2.
V3 has exactly schema, promotion_id, release_id, mirror_root,
source_snapshot_path, source_snapshot_sha256, dev_deployment_id,
dev_evidence_ref, dev_evidence_subject_sha256,
dev_evidence_artifact_name, dev_evidence_producer_workflow,
dev_evidence_source_commit, dev_evidence_set_id, and
dev_evidence_campaign_request_sha256. Both campaign identity fields are
mandatory. Preserve all existing trust validation/signature requirements and
compute promotion_id as the canonical fingerprint excluding itself.
Install each project at <mirror_root>/<project_path>; paths must be confined,
nonoverlapping and repository-relative. Snapshot and descriptor remain outside
the mirror subtree. Removed projects disappear only inside this bot-owned tree.
Add dpone dbt workspace verify-promotion --root <checkout>
--descriptor <path> --release-set <path> --format json. The descriptor pins
the mirror root and snapshot. Require v3 descriptor, dbt wire v2 and source
snapshot v2; reject mixed modes. Existing singleton verification stays unchanged.
The new dpone.dbt-workspace-promotion-verification.v1 report has exactly
schema, status, passed, code, release_id,
source_snapshot_sha256, and projects. Each project row has
project_path, expected_project_bundle_sha256,
observed_project_bundle_sha256, passed, and code.
Rows cover the entire pinned inventory in project-path order. Unavailable
observations are null and failed, never omitted or passed. This source-verification
report does not replace the complete cryptographic promotion gate.
Promotion validates all source projects against the pinned aggregate snapshot and release. The audit mirror installs a complete workspace under one bot-owned root; no sequential per-project mutation of the active mirror. Stage all projects and metadata, verify hashes and confinement, then use one recoverable transaction. The transaction has three possible destinations and at most three replacements: one entirely bot-owned workspace subtree, snapshot and descriptor. It does not add one journal entry per project: existing journal recovery admits at most three replacements. Crash recovery and concurrent-owner conflicts use the existing lock/journal boundary, with no writes outside owned paths. This means rollback-safe, serialized installation, not simultaneous visibility to arbitrary unlocked filesystem readers. Verifiers hold the same lock; the bot commits the Git mirror only after the transaction succeeds. Airflow never executes SQL from this mutable audit mirror. No author-owned tree may become the aggregate mirror destination. Recovery restores the three output destinations, not exact parent-directory topology. Empty metadata parents may remain after interrupted bootstrap; recovery must not guess which pre-existing empty directories it is allowed to delete.
Workspace preparation is an explicit new boundary, not a change to singleton
prepare-prod-mirror: dpone dbt workspace prepare-prod-mirror accepts
--root, --compiled-root, --mirror-root, --source-snapshot-path,
--descriptor-path, --expected-release-id, the existing mandatory DEV
deployment/reference/trust fields, both mandatory campaign identity fields,
and --format text|json. Its Python service receives the same inputs and
injected source reader, bundle operations and transaction. The
dpone.dbt-workspace-mirror-prepare.v1 report has exactly schema,
release_id, promotion_id, mirror_root, source_snapshot_path,
descriptor_path, projects, and no_op; projects is the sorted list of
project paths. It prepares a local audit mirror only; neither its report nor
a self-consistent descriptor authenticates the DEV campaign. Existing signature
verification and independent comparison of every expected DEV identity are
required by the promotion gate before activation.
Bootstrap requires all three destinations to be absent. An update requires an existing valid v3 descriptor at the requested descriptor path, with the same mirror root and snapshot path, and a matching v2 snapshot fingerprint. These establish the prior bot-owned layout, not cryptographic authorization. Missing or malformed ownership metadata, partial bootstrap or an existing unowned tree fails without replacement; adoption/migration is a separate explicit operation. Drift inside an established bot-owned mirror may be replaced. All three paths must be pairwise disjoint (including equality, ancestry and portable case collisions), outside VCS/worktree and transaction control paths, with no symlink components. A missing mirror may be restored only from valid ownership metadata.
The shared lock-only primitive has no repository recovery side effects. Workspace verification holds that lock and rejects a pending transaction journal with a recovery instruction, without recovering or deleting it. Installation retains the existing lock-and-recover wrapper. The lock file may be created in the operating-system temporary directory; read-only means no repository writes. The entire workspace tree is checked, including unexpected files/directories outside project roots and ignored/generated files inside them; these cannot be hidden by the project bundle builder's ignore rules.
Roll out readers before emitting v2. Old readers explicitly reject v2. Exact wire dispatch is authoritative: dbt wire v2 requires source snapshot v2 and exclusively content-addressed referenced payloads; v1 retains the historical inventory and IDs. Runtime obtains the wire from the already verified release artifact in its fetched plan, not an untrusted environment variable or an ID guess. The existing plan carries that release artifact, so no new plan field is needed. Preserve historical order healing only for v1. V2 reordered references are rejected, even if their set is correct. Exact producer/provider pin checks are retained; a reader's ability to parse v1 does not waive an exact runtime version requirement. Retain the old runtime image and deployment for rollback. The legacy compile command remains v1 unless a separate approved migration changes its default.
Detailed algorithm and failure semantics¶
- Confine and discover project roots; classify publishing capability from each project's existing declarations. Fail duplicate/nested/ambiguous roots.
- Freeze the complete input inventory. Acquire each canonical manifest and resolved source bundle using existing validation and bundle ports. Verify exact toolchain/route policy and missing package errors before publication.
- Compile every project once into a typed intermediate representation. Validate project/workflow/DAG/workload/target ownership across the whole workspace.
- Derive each content-addressed trio. Construct the aggregate snapshot and one release from all validated pack, DAG, runtime and schema bytes. No generated pack mutation or post-hoc fingerprint patching is permitted.
- Validate the complete tree with schema, integrity, membership and reader checks. Atomically publish it. Identical output is an idempotent no-op; different existing output is a conflict, not permission to delete it.
- Bind the release to the environment once, resolve physical target identities, sign through existing deployment-scoped authority, publish and reconcile.
- Execute each workflow from its pinned release trios. Validate complete dbt results before transfers under the existing workflow contract. Record actual attempts and results; never label skipped/unavailable checks as passed.
- Finalize the DEV campaign only when every required workflow passes. Promote unchanged release bytes; validate and transactionally install the audit mirror.
discover -> validate complete inventory -> compile all -> validate identities
-> build one release -> verify complete tree -> atomic publish
-> environment binding -> execution/evidence -> promote same release
any pre-publication failure -> no active output change
any execution failure -> failed/incomplete campaign, no automatic promotion
Compile cancellation, timeout or process crash leaves at most owned staging files, never a partly active release. Concurrent output writers are fenced by the publisher; conflicting content is an error. Retry identical compilation is idempotent. Empty publishing discovery is a reported no-op for discovery/check; explicit compile fails instead of publishing an empty release that could remove live workloads. Project removal requires a reviewed nonempty desired inventory or the existing explicit retirement procedure.
Data execution is not an all-project database transaction: if A committed and B failed, A stays committed and campaign status is failed. Retry follows existing workflow idempotency and commit-unknown rules; do not replay A blindly. Preserve run/deployment/interval/attempt binding when retrying after a new release. Deployment rollback restores code/bindings, not already committed data. Data repair is a separate explicit operator action.
Keep current compact producer limits: 64 unique runtime payloads, 256 MiB per payload, 512 MiB aggregate. Limits apply to the whole release after safe deduplication, not independently per project; do not multiply the effective limit. Staged extraction must retain existing archive confinement and expansion bounds.
Workspace v2 emits only canonical descriptor paths, release-set.json, the
fixed source snapshot and release-subjects.sha256. Singleton aliases,
intermediate transfer YAML and its compile-evidence report are not copied into
the workspace release. Transfer packs already embed their runtime manifests;
they do not fetch the omitted intermediates. Compile diagnostics belong to the
workspace report, outside the immutable release. Validate this exact file set
before creating the checksum subject, so a checksum cannot bless an orphan.
V1 retains its existing complete output layout. The legacy semantic-refresh
template branch has no executable DAG and is not admitted by workspace v2;
reject it before source capture rather than emitting an incomplete inventory.
Architecture, alternatives and quality¶
| Component | Responsibility |
|---|---|
| Pure dbt payload/source-inventory contracts | Version dispatch, IDs, membership and canonical fingerprints; no I/O/framework imports |
| Workspace discovery adapter and application planner | Confined local discovery and complete typed input plan |
| Existing compiler/bundle/selection ports | Project compilation and authoritative source/selection validation |
| Workspace release writer | One aggregate tree and atomic publication through injected publisher |
| Compact/runtime/evidence/promotion readers | Consume the same canonical contracts, no copied regex/identity algorithms |
| Mirror transaction | Stage, validate, commit/recover the complete workspace |
| Consumer CI | Diff quality selection, canonical manifests, one public compile call and unchanged-artifact promotion |
Contracts cannot import services, runtime, CLI or optional SDKs. Composition roots
construct adapters. New modules respect docs/benchmarks/quality_budgets.yml;
existing module-size/coupling debt must not grow. Split the writer by coherent
project projection versus aggregate publication, not mechanical file fragments.
An ADR is required for the versioned wire and source-inventory semantics.
Rejected alternatives: concatenate independent release JSON (invalid identity and last-writer wins); per-domain hardcoded payload directories (manual central registration); SQL in runtime image (model changes require image delivery); remove the one-project guard first (false support); one project per deployment (does not meet the required coherent release boundary).
Market comparison and measurable acceptance¶
Sources below were checked on 2026-08-28. These are capability comparisons, not claims that dpone outperforms the products overall.
| System/version context | Observation and decision |
|---|---|
| Astronomer Cosmos, current OSS docs | Multi-project guide separates project configurations and supports external references through dbt-loom. Adopt project isolation; cross-project references remain outside this release feature. Pre-generated manifests avoid parsing-time dbt execution. |
| Astro Remote Execution Agents, current hosted docs | Deployment guide documents separate dbt-code delivery approaches. Use source artifacts independent of the toolchain; do not claim identical execution or trust guarantees. |
| Apache Airflow 3.3.1 | Versioned DAG bundles preserve a run's code version. Adopt pinned retry semantics; local/S3 bundle delivery alone is not version-pinning evidence. |
| dbt, current artifact docs | Manifest describes project resources; state caveats warn against overwriting comparison artifacts. Preserve distinct project manifests and immutable state. |
| dlt, Informatica, Airbyte, Fivetran, Pentaho, SSIS, Apache Beam | N/A for this narrow dpone artifact-wire migration; no connector/managed-ELT/stream processing capability is being replaced or compared. |
| gusty | N/A for the runtime artifact and promotion boundary; this change does not replace YAML DAG authoring. |
Measured axis: compared with current dpone 0.74.28's rejected two-project compile,
two independently named publishing projects must yield exactly one release and
one deployment, zero missing/cross-wired payloads, zero central CI domain-map
edits and zero runtime image builds for SQL-only changes. Repeat with reversed
discovery order, same node names, one project changed, one failed workflow,
restart, retry of the old version and rollback. Preserve machine-readable
dbt-workspace-certification.json with exact commits, image digests, attempts,
test results and links. Unit/fixture results do not establish live readiness or
the separate >=30-root CI SLO.
Security, tests, documentation and rollout¶
Secrets remain deployment connection references. Reject path traversal,
symlink/archive attacks, source mutation during snapshotting, digest spoofing,
wrong-kind substitution, duplicate JSON keys and incomplete inventories. Retain
existing trust verification, exact pins and output redaction. No permissive
require_certified_routes=False default for publishable production artifacts.
| Layer | Required proof |
|---|---|
| Unit | v1/v2 dispatch, ordered trios, canonical paths, hash/length/kind mismatch, duplicate/missing/extra memberships, resource limits |
| Contract | v1 outputs unchanged; old readers reject v2; schemas generated from producers; SDK-free base import/help |
| Integration | Two projects, same local node IDs, global workflow collisions, physical alias collisions, stable reversed order, atomic output/no-op/conflict |
| Failure/recovery | Project B compile failure leaves no partial output; execution B failure blocks campaign; mirror crash at each commit boundary; concurrent writer fencing |
| Runtime | Both projects fetched into isolated task roots, complete result/transfer evidence, exact provider/image/source identities |
| Promotion/rollback | Complete mirror verified; tampered/missing project rejected; retained deployment and old-attempt retry validated |
| Performance | Compile once per selected source snapshot; no second private writer invocation; SQL-only change produces no runtime image build |
Extend existing publish atomicity, compact payload limits/order, runtime identity, init-fetch, DEV evidence, promotion verification and mirror tests. Add CLI discovery/check/compile tests. Run change-aware check selection, full mandatory architecture/type/unit gates and documentation checks. Live validation must use the already-authorized scoped environment and cannot run business workloads outside the explicit certification fixtures.
Documentation deliverables: dbt hub links, first-two-project tutorial, CLI/Python/schema reference, architecture ADR, operator retry/rollback runbook, compatibility matrix, generated schemas and changelog. Consumer docs show that authors configure only standard project-local files, not CI routing registries.
Rollout slices: canonical contracts and readers with v1 preserved; workspace producer and schema/docs; scoped two-project certification; exact provider/runtime upgrade; consumer public-API integration and guard removal; DEV acceptance then PROD promotion and rollback. A failed identity/evidence/recovery check halts promotion, not weakens the gate. No default v2 emission until required readers are deployed. The complete feature is not done after the reader-only slice.
Agent execution and approval¶
Codex is the sole writer/integrator in this worktree. Shared schemas, changelog, navigation and composition roots remain integrator-owned. Read-only architecture, test and UX review may inspect all related paths and must not change files, dispatch workflows or certify live behavior. A fresh-context review is required before integration.