Skip to content

feat(dev): run the deploy pipeline against local providers — the dev extension seam#162

Merged
wmadden-electric merged 35 commits into
mainfrom
claude/local-dev-s4-dev-target
Jul 23, 2026
Merged

feat(dev): run the deploy pipeline against local providers — the dev extension seam#162
wmadden-electric merged 35 commits into
mainfrom
claude/local-dev-s4-dev-target

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 23, 2026

Copy link
Copy Markdown
Contributor
// The same lower() a deploy uses — one flag, local state:
export default lower(app, config, {
  name: 'store',
  bundles: { ... },
  dev: true,              // ← infrastructure providers come from each extension's `dev` field
  state: localState(),    // ← Alchemy's own file store; no cloud, no token
});
$ alchemy deploy .prisma-composer/dev/alchemy.run.ts --stage dev
Done: 40 succeeded            # postgres provisioned, bucket registered, services deployed…
$ curl localhost:3000/health  # …to the Compute emulator, which is now serving them:
{"ok":true,"version":"v1","db":true,"store":true}

The decision: local dev is the deploy pipeline with its infrastructure providers swapped — nothing else changes.

Context in one breath: prisma-composer dev (ADR-0041, in the base branch) runs a whole Composer app locally with no cloud credentials. This is the third layer of the PR stack, on top of #160 (the emulator daemons). Stacked PR: the diff here is only this layer.

How a deploy works, and what dev swaps

Deploying a Composer app runs a pipeline: load the app's graph from its code, assemble each service's built output into a bundle, lower the graph into infrastructure resources (services, databases, buckets, environment variables), and hand those to Alchemy — the provisioning engine — which converges them by calling provider plugins that talk to the platform's API. Everything above the providers is pure logic: addresses, config serialization, secret wiring, minted service keys, migrations.

This PR adds a dev field to the extension descriptor (the object a platform package registers with the framework). It carries local counterparts for exactly the operational pieces: providers that talk to the emulators instead of the cloud, a local container identity, a dev preflight, and hooks the dev command will use to start emulators and attach to the running app. Deliberately absent from the field: anything that could change what gets provisioned. The lowering is shared code, so local dev cannot diverge from a real deploy by construction. Deploy state is also untouched — dev uses Alchemy's built-in file store, as the snippet shows.

The local providers are thin emulator clients. Registering a compute service returns its stable local port, which is how a service's own origin URL resolves with zero special-casing. Deploying unpacks the real packaged artifact (the same tar a deploy ships), prepares the child's full environment from the lowered configuration rows, and PUTs it at the Compute emulator. The Postgres provider shells the ORM's prisma dev with a probe-then-recover sequence (its port stability across stop/start was verified experimentally first). Where dev deliberately differs from deploy, it's pinned and reasoned: a secret missing from your shell becomes a persisted placeholder plus one warning, so the app boots and only the paths touching that credential fail, at the real external service; a missing environment-sourced param is a hard error naming it, because params feed schema validation where a placeholder produces a confusing crash. Captured subprocess output is credential-masked before it can appear in any error.

One practical seam: an extension that only supplies build adapters has nothing to emulate and is exempt from the dev requirement (one shared predicate, used everywhere).

Tested

An integration proof drives the snippet above for real: a fixture app (two services, Postgres, a bucket) converges credential-free with the actual alchemy binary and serves — HTTP round-trip asserted. A second converge with an unchanged build restarts nothing (asserted via pid stability). Changing one service's artifact restarts exactly that service. Environment rows, the poison DATABASE_URL rows, and secret pointer wiring are asserted against the store. Plus focused suites: the tar extractor round-trips the packager's output, placeholder minting is stable across runs, the missing-param error lists names, and the extension factory constructs with a fully scrubbed environment. Workspace gates green, no new type casts.

Alternatives considered

  • A local Management API (emulate the platform's HTTP surface, point the real providers at it) — reimplements another team's server-side semantics across ~30 endpoints and drifts silently; the provider interface is a contract this repo owns, where drift is a compile error.
  • A bespoke dev harness with per-kind dev descriptors — an open-ended parallel seam every extension must implement and keep from drifting; swapping providers keeps one pipeline.
  • Direct-injection boot for dev (hand services their config, skip the env wire protocol) — would make local verification prove less than it appears to; dev boots services exactly as deployed.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@wmadden-electric, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 59 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 7936070b-25da-4a61-a481-29214e09fd14

📥 Commits

Reviewing files that changed from the base of the PR and between b90eb61 and 6f6fafb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (74)
  • .drive/projects/local-dev/spec.md
  • architecture.config.json
  • dependency-cruiser.config.mjs
  • examples/storefront-auth/scripts/e2e-verify.sh
  • package.json
  • packages/0-framework/1-core/core/package.json
  • packages/0-framework/1-core/core/src/__tests__/invariants.test.ts
  • packages/0-framework/1-core/core/src/__tests__/local-target.test.ts
  • packages/0-framework/1-core/core/src/__tests__/lowering.test.ts
  • packages/0-framework/1-core/core/src/control/app-config.ts
  • packages/0-framework/1-core/core/src/control/deploy.ts
  • packages/0-framework/1-core/core/src/control/local-target.ts
  • packages/0-framework/1-core/core/src/exports/local-target.ts
  • packages/0-framework/1-core/core/tsdown.config.ts
  • packages/1-prisma-cloud/0-lowering/dev-emulators/src/exports/index.ts
  • packages/1-prisma-cloud/0-lowering/local-target/package.json
  • packages/1-prisma-cloud/0-lowering/local-target/src/__tests__/artifact-extract.test.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/__tests__/postgres-instance-name-drift.test.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/__tests__/postgres-module-resolution.test.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/app-name.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/artifact-extract.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/bucket.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/compute.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/dev-store.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/exports/index.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/postgres.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/providers.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/resolve-package-entry.ts
  • packages/1-prisma-cloud/0-lowering/local-target/src/teardown.ts
  • packages/1-prisma-cloud/0-lowering/local-target/tsconfig.json
  • packages/1-prisma-cloud/0-lowering/local-target/tsdown.config.ts
  • packages/1-prisma-cloud/0-lowering/lowering/src/__tests__/artifact.test.ts
  • packages/1-prisma-cloud/0-lowering/lowering/src/compute/artifact.ts
  • packages/1-prisma-cloud/1-extensions/target/package.json
  • packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-env.test.ts
  • packages/1-prisma-cloud/1-extensions/target/src/__tests__/control-lowering.test.ts
  • packages/1-prisma-cloud/1-extensions/target/src/__tests__/invariants.test.ts
  • packages/1-prisma-cloud/1-extensions/target/src/__tests__/local-target-preflight.test.ts
  • packages/1-prisma-cloud/1-extensions/target/src/container.ts
  • packages/1-prisma-cloud/1-extensions/target/src/control/extension.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/bucket.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/compute.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/postgres.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/prisma-next.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/s3-credentials.ts
  • packages/1-prisma-cloud/1-extensions/target/src/descriptors/s3-store.ts
  • packages/1-prisma-cloud/1-extensions/target/src/exports/local-target.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target-entry.d.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/attach.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/container.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/descriptor.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/emulators.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/preflight.ts
  • packages/1-prisma-cloud/1-extensions/target/src/local-target/teardown.ts
  • packages/1-prisma-cloud/1-extensions/target/src/preflight-names.ts
  • packages/1-prisma-cloud/1-extensions/target/src/preflight.ts
  • packages/1-prisma-cloud/1-extensions/target/tsdown.config.ts
  • packages/9-public/composer-prisma-cloud/package.json
  • packages/9-public/composer-prisma-cloud/src/exports/local-target.ts
  • packages/9-public/composer-prisma-cloud/tsdown.config.ts
  • packages/9-public/composer/package.json
  • packages/9-public/composer/src/exports/local-target.ts
  • packages/9-public/composer/tsdown.config.ts
  • test/integration/package.json
  • test/integration/test/cli.extension-config.test.ts
  • test/integration/test/fixtures/local-dev/bg-service.ts
  • test/integration/test/fixtures/local-dev/built/bg-server.mjs
  • test/integration/test/fixtures/local-dev/built/web-server.mjs
  • test/integration/test/fixtures/local-dev/dev-config.ts
  • test/integration/test/fixtures/local-dev/module.ts
  • test/integration/test/fixtures/local-dev/web-service.ts
  • test/integration/test/local-dev.integration.ts
  • tsconfig.depcruise.json
  • turbo.json
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/local-dev-s4-dev-target
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/local-dev-s4-dev-target

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@prisma/composer@162
npm i https://pkg.pr.new/@prisma/composer-prisma-cloud@162

commit: 6f6fafb

@wmadden-electric
wmadden-electric force-pushed the claude/local-dev-s4-dev-target branch from 8088bfa to ff117ac Compare July 23, 2026 11:05
@wmadden-electric wmadden-electric changed the title feat(dev): the dev target — DevDescriptor seam + local providers over the emulators feat(dev): run the deploy pipeline against local providers — the dev extension seam Jul 23, 2026
Comment thread packages/0-framework/1-core/core/src/control/app-config.ts Outdated
Comment thread packages/0-framework/1-core/core/src/control/deploy.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/compute/artifact-extract.ts Outdated

@wmadden wmadden left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

THIS IS A MESS

Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/compute/Deployment.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/compute/Deployment.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/app-name.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/local-target/src/app-name.ts
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/resolve-bin.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/spawn-utils.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/teardown.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/teardown.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/lowering/src/dev/teardown.ts Outdated
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
DevExtensionDescriptor (full qualifier); lower() learns nothing about
dev (providers become an explicit LowerOptions override resolved by the
generated dev stack module — one orchestration point, everything
downstream provenance-ignorant); DeploymentProps.serviceAddress reverted
(the address rides the artifact own manifest, v2); artifact extraction
via node-tar per the dependency razor; the local provider suite moves to
a new @internal/local-target package (a lowering dev subtree conflates
Composer vocabulary); the @prisma/dev programmatic adoption is pulled
into this wave — postgres-main daemon, CLI shell-out deleted.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
…endency razor pins)

Takes .drive/projects/local-dev/spec.md wholesale from design-branch
commit 904d23a, which pins two things this round implements: the
POST /apps/<app>/start session-resume endpoint (70c0ef1) and the
dependency-razor decision retiring the no-new-deps contract in favor
of proper-lockfile and get-port for the daemon lock and free-port
probing (904d23a itself).

Verified empty diff against 904d23a exactly. The design branch has
since moved to d407c1f ("the #162 rework wave — operator review
reversals"), which is out of scope for this round and not chased here.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
…surface pin)

Takes .drive/projects/local-dev/spec.md wholesale from design-branch
commit 62d1a6a, which revises the Postgres providers section (operator
review of #162, S7 pulled forward): the CLI shell-out is replaced by a
third emulator daemon, postgres-main, hosting @prisma/dev's
startPrismaDevServer() programmatically.

Verified empty diff against 62d1a6a exactly. The design branch has
since moved to 5c6eb71 ("the dev seam is a lazy reference — no dev
code in production bundles"), out of scope for this round and not
chased here.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wmadden-electric
wmadden-electric force-pushed the claude/local-dev-s4-dev-target branch from ff117ac to b7971f6 Compare July 23, 2026 14:24
@wmadden-electric

Copy link
Copy Markdown
Contributor Author

All review findings addressed — the branch was rebuilt (force-pushed, tip b7971f6) and independently delta-reviewed against each finding:

  1. DevDescriptor namingDevExtensionDescriptor (it is the dev variant of ExtensionDescriptor; the qualifier is now in the name and the doc comment).
  2. dev/real branching in lower()LowerOptions.dev is deleted. LowerOptions gained an explicit providers override (same precedence pattern as state); the generated dev stack module is the single orchestration point (it resolves containers and passes providers + state explicitly). The only dev token left in the deploy layer is one doc comment — everything downstream is ignorant of provider provenance.
  3. serviceAddress on DeploymentProps / transient ids → reverted entirely. The address rides the artifact's own compute.manifest.json (it was already baked into bootstrap.js; the manifest now carries it as a field — manifestVersion stays "1", no consumers to protect).
  4. "lowering dev is not a Composer primitive" → new @internal/local-target package holds the local provider suite; @internal/lowering has no dev subtree, no extractor, no tar dependency.
  5. bin walk-up / CLI shell-out / glob teardown ("trash") → deleted wholesale. Postgres runs through a third emulator daemon (postgres-main) hosting @prisma/dev's programmatic startPrismaDevServer — named persistent servers, stable ports, app-owned versioning (the module resolves from the app's node_modules), teardown via the daemon's admin API.
  6. Physical separationExtensionDescriptor.dev is a lazy async thunk; the production control entry carries exactly one line (a bare-specifier dynamic import of @prisma/composer-prisma-cloud/dev). Verified by walking the built production bundle's full chunk graph: zero dev-implementation code, enforced by a new firewall invariant test. Core's dev machinery likewise ships only behind @prisma/composer/dev.
  7. Floating doc comments → fixed in the moves.

Two open items before re-review: a slug-derivation drift between local-target and the daemon (found by the delta review; fix in flight — one shared derivation) and a behavioral note for your eye: prismaCloud() no longer throws eagerly on a missing PRISMA_WORKSPACE_ID (it must construct env-free for dev); the check now lives in the container lifecycle, where a real deploy still fails loudly.

@wmadden-electric

Copy link
Copy Markdown
Contributor Author

The last open item from the delta review is closed (954ebec): the duplicate slug in @internal/local-target is deleted — postgres.ts now imports the daemon's own instanceNameFor (the single derivation, freshly linear-time after the CodeQL fix), and a new drift test runs the pathological shape end-to-end against the real daemon (app and database names ending in -; previously produced a doubled-dash name the listing lookup could not resolve). All gates green including the live three-daemon integration proof. Ready for re-review.

@wmadden-electric
wmadden-electric force-pushed the claude/local-dev-s4-dev-target branch from b7971f6 to 954ebec Compare July 23, 2026 14:53
wmadden-electric and others added 16 commits July 23, 2026 17:11
DevDescriptor + DevProvidersInput/DevEmulatorsInput/DevAttachInput/
DevAttachment, DEV_DIR, and ExtensionDescriptor.dev in app-config.ts;
LowerOptions.dev and mergedDevProviders (an extension with no dev
descriptor throws, unlike mergedProviders skipping) in deploy.ts,
wired into lower() so opts.dev routes the stack at the dev providers
layer instead of the hosted one. No nodes/provisions/state on the new
type by design -- the lowering stays byte-identical between dev and
deploy; only the Alchemy provider boundary is substituted.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DeploymentProps gains an optional serviceAddress, ignored by the
hosted provider. descriptors/compute.ts populates ComputeSerialized.address
from ctx.address at serialize and threads it into the Deployment call
at deploy -- the local Deployment provider (added next) requires it to
key the Compute emulators env materialization.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New @internal/lowering/dev subpath: dev-store.ts (env.json/secrets.json/
postgres.json, temp-then-rename, one in-process queue per file), the
eight local providers (compute cluster: ComputeService/Deployment/
EnvironmentVariable/Project; postgres cluster: Database/Connection over
the ORM CLIs prisma dev, with the stop-then-start port-stability
sequence verified live against prisma dev v0.16.22 -- the port holds,
so no --db-port fallback is wired; bucket cluster: Bucket/BucketKey as
bucket-emulator clients), devProviders() collecting them under the same
Providers tag providers() uses, resolve-bin.ts (a generalized
resolveAlchemyBin), and compute/artifact-extract.ts, a minimal ustar
reader matching packageComputeArtifact byte for byte.

Local providers read the app name off the generic ContainerInstance
directly (container.input.appName) rather than through a targets
narrowing helper: @internal/lowering sits below the extensions layer
and cannot import one (ADR-0028s layer order), and the field is
already on the base interface, so no narrowing is needed for it.
Likewise, the Deployment providers env-materialization step needs the
exact env-var key the deployed artifacts serializer.ts computes for
the port param; the formula is duplicated inline (documented as a
protocol match, not a convenience) because the real configKey() lives
in the extensions layer and cannot be imported from here either.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ec S5)

preflight-names.ts pulls the provisionManifest/paramManifest walk out
of preflight.ts as collectPreflightNames(), returning secrets and
envParams as two separate lists rather than one merged map -- deploy
preflight still merges them (identical policy for both), but the
upcoming dev preflight needs to tell them apart (a placeholder for a
missing secret, a hard error for a missing env-sourced param). Sharing
this walk is what keeps the two preflights from drifting on which
names they check. No behavior change to runPreflight.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lazily

prismaCloud() now builds with no environment present: resolveOptions()
no longer requires PRISMA_WORKSPACE_ID (nothing downstream ever read
it, and container.ts already independently requires it where the value
actually matters, at ensure/locate), and region resolution is wrapped
in a memoized thunk threaded into every node descriptor instead of
called eagerly at construction. A descriptor calls the thunk from
inside provision/serialize, at lowering time rather than construction
time, which is what makes dev (which runs the same node descriptors)
safe: an absent PRISMA_REGION resolves to undefined without throwing,
same as today; a garbage one still fails loudly, just at first use.

container.ts exports its existing deserialize() so the new dev
container descriptor can reuse it verbatim; dev and deploy containers
produce and consume the identical wire shape.

New src/dev/*: container.ts (a purely local identity, projectId
literal "local", no platform call), preflight.ts (dev value-sourcing
policy, shell env else a minted placeholder plus warning for secrets,
shell env else a hard error for env-sourced params, over the shared
preflight-names.ts walk), emulators.ts (ensures the compute daemon
always, buckets only when the graph uses an s3-kinded resource),
attach.ts (a Compute-emulator client: endpoints, merged logs,
stopServices), teardown.ts (--fresh: removes this app's prisma dev
instances and emulator records, delegating every actual fs/
child-process operation to @internal/lowering/dev so this package's
own source stays free of node:/bun: imports, invariant 5). All wired
onto prismaCloud()'s new dev field per spec.

Test fallout: control-env.test.ts rewritten for the new construction-
time behavior (succeeds in a scrubbed env; a bad PRISMA_REGION only
fails at first lowering use, driven directly through the compute
descriptor's provision()); invariants.test.ts allowlists dev/
preflight.ts's two process.env reads (its own shell-token read, dev's
value-sourcing policy); one direct computeDescriptor(o) call site in
control-lowering.test.ts updated for the thunk signature.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
artifact-extract.test.ts: extractComputeArtifact round-trips
packageComputeArtifact output byte for byte, and rejects a
non-regular-file tar entry (typeflag != "0") that the real writer
never produces.

dev-preflight.test.ts: a missing secret mints a placeholder, persists
it, and warns exactly once; the placeholder is stable across two
preflight runs (the pinned policy warns only on the mint branch, so a
second run finding the persisted value stays silent); a shell-set
secret is used directly with no placeholder or warning; a missing
env-sourced param is a hard error listing the name/service and
pointing at the dev shell; a shell-set env-sourced param is written
with no error.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e workspace-id CLI proof

lower()'s dev devDir join used node:path, which core's own invariant
test forbids (core stays runtime-agnostic, no Node/Bun coupling in its
shipped surface). Local dev is POSIX-only already (the spec's own
Windows note), so a plain `/`-join is exact, not an approximation --
same fix already applied to the target extension's dev/preflight.ts.

test/integration/test/cli.extension-config.test.ts's second test
asserted the OLD eager-throw behavior removed in the prior commit
(prismaCloud() failing at config evaluation without
PRISMA_WORKSPACE_ID). Updated to assert the new, correct behavior:
config evaluation now succeeds regardless, and deploy fails at the
same missing-built-entry point either way -- a stronger proof of the
restructure, since it goes through the real CLI/config-loading path
rather than a unit test of prismaCloud() alone.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…error

Mid-flight spec update (cherry-picked b1d7324): the could-not-read-URL
error embeds captured `prisma dev --detach` output verbatim, which can
contain a connection URL with a real password when the CLI's stdout
happens to echo one back on failure. The no-value-logging behavior
contract applies to embedded diagnostics too, not just stashed config
values, so the output is masked
(`://user:***@`) before it ever reaches the thrown message.

Test drives LocalDatabaseProvider's reconcile against a fake failing
`prisma` bin that echoes a password-bearing URL, asserting the thrown
message keeps the URL legible but never contains the raw password.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixture at test/integration/test/fixtures/local-dev/: compute x2
(web depends on postgres+bucket, bkg has no deps), a bare postgres()
resource, and a bucket() resource, so a dev: true lowering exercises
all eight local providers. Built entries live under built/ (dist/ is
gitignored repo-wide) and are hand-written self-contained files (no
bundler): each declares its own compute() node inline rather than
importing the sibling authoring .ts file, because a relative import to
a file outside the packaged artifact's own bundle breaks once the
artifact is unpacked elsewhere -- confirmed live, not theoretical.

dev-config.ts is the dev stack's own narrower config (prismaCloud()
only, no nodeBuild()) -- see its doc comment and the spec's recorded
Open Questions entry: mergedDevProviders throws for any configured
extension with no dev descriptor, including a build-only one, which
structurally can't implement one. lower() itself never needs the build
registry (only assembleServices does, a CLI-tooling-level concern), so
omitting nodeBuild() from the dev stack's own config is safe, not a
behavior change.

local-dev.integration.test.ts hand-writes the dev stack file per spec
S6's template (S5 owns generate-dev-stack.ts) and drives it through the
real alchemy binary and the real prisma dev CLI, importing only
9-public per ADR-0028 -- the emulator daemons and dev-store files are
verified through their documented wire protocol and on-disk contracts
(plain fetch + fs.readFileSync), never by importing @internal/lowering
or @internal/dev-emulators. Covers: scrubbed-env construction, SERVING
via HTTP round-trip, emulator listings, env/secrets/postgres store
contents, a real running prisma dev instance, second-converge no-op
(pid stability), and changed-artifact restart of exactly one service.

Also fixes composer-prisma-cloud's own package.json: daemon.ts's
import.meta.resolve('@internal/dev-emulators/...') is a genuine runtime
resolution once bundled into this package's dist, so it needs to be a
real "dependencies" entry, not bundled away like its devDependencies
siblings (confirmed: without it, the published control.mjs throws
Cannot find module at runtime). Flagged in the S4 report: this makes a
private, unpublished workspace package a real runtime dependency of a
published one -- a real concern for whenever local dev needs to work
for actual npm consumers, not resolved here.

KNOWN LIMITATION, recorded in the test file's own doc comment and the
S4 report rather than worked around: under bun test specifically, the
postgres local provider's `prisma dev --detach` subprocess (spawned
from inside the alchemy child's own Effect runtime) runs and genuinely
creates the instance, but its stdout is not reliably captured by the
calling spawnSync, so the test's first converge can fail with an empty
captured-output error. Confirmed NOT a defect in the local providers:
the identical stack file converges successfully via a plain shell, via
`bun run <script>` (not bun test), and via plain node, every time,
with the fixture's /health endpoint then returning exactly the
expected SERVING payload. The failure is specific to being a
process-tree descendant of bun test's own test runner, independent of
nesting depth, stdio mode, or the detached flag.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… cleanup

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…uperseded Open Questions entry

The cherry-pick of 90f4383 three-way-merged onto my diverged spec.md
and produced a version that neither matched the design branch nor
made sense on its own. Overwrite spec.md and plan.md with the design
branch's exact content so the S4 Open Questions entry (superseded by
the pinned build-only exemption) is gone and the files match
bot/claude/local-dev-feature-design-0b49fd byte for byte.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-0041)

Export isBuildOnlyExtension(extension) from app-config.ts: true when
every nodes entry is kind: 'build' and none of
providers/application/provisions/container are declared. A build-only
extension (e.g. nodeBuild()) owns no resources or services, so it has
nothing to emulate.

mergedDevProviders now skips an exempt extension the way
mergedProviders skips an extension with no providers, instead of
throwing the no-dev-support error unconditionally for every configured
extension.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eBuild() in the dev config

bun test was silently losing a deeply-nested grandchild's stdout
(prisma dev --detach, spawned from inside alchemy's own Effect
runtime) specifically when the process tree's root was bun test —
confirmed by rigorous isolation across plain shell, bun run, and
plain node, none of which lost it. Rather than work around an
unrelated subprocess this test doesn't control, the test is now a
standalone script (test/local-dev.integration.ts) driven with plain
asserts and process.exitCode, invoked by package.json's test script
as `bun test && bun run test/local-dev.integration.ts` so it stays a
required gate. The header comment documents the root cause.

dev-config.ts now includes nodeBuild() again, exercising the
build-only exemption (mergedDevProviders no longer needs the
fixture-side workaround that omitted it).

Also fixed two assertion bugs only reachable once the test could run
to completion: postgres.json is keyed by the full instance name
(pcdev-<app>-<node>), not the bare node id; and the SERVING/pid
stability checks now poll with a bounded deadline instead of
asserting on the first attempt, since a freshly-deployed child needs
a moment to start listening — the same tolerance a real client
(browser, curl) would need.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d a real port race

@prisma/integration-tests' local-dev proof now runs its real
machine-global compute/buckets daemons for its full duration (the
bun-test fix in the previous commit means it actually gets that far).
@internal/dev-emulators' own test suite spins up isolated daemon
instances that scan for a free port starting at the same MIN_PORT
(4300). Run concurrently by turbo, the two raced for the same OS-level
port, and one test suite's fetches would occasionally land on the
other's process.

Adding a package-scoped test-task dependency
(@prisma/integration-tests#test depends on
@internal/dev-emulators#test) makes turbo run them one after another
within a single `pnpm test` invocation, eliminating the race — nothing
in @internal/dev-emulators itself changes.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nce derivation does

removeLocalPostgresInstances globbed pcdev-<app>-* with the raw app
name, but postgres.ts's instanceName derives pcdev-<slug(app)>-<slug(id)>.
An app name containing characters slug() transforms (spaces, dots,
uppercase, ...) would orphan its instances at teardown. Export
postgres.ts's slug and share it in the glob.

Add dev-teardown.test.ts: a fake prisma bin records its argv, asserted
against both a slug-transformed app name (My.App) and an already-safe
one.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…child

local-dev spec § 4 pins that the Deployment provider's port override
(COMPOSER_<ADDRESS>_PORT = the emulator-assigned port, JSON-encoded)
is materialized fresh into each deployment's own env and never
persisted to env.json ("ports live nowhere here") — so reading
env.json for that key finds nothing. The fixture's /health handler now
echoes process.env.COMPOSER_WEB_PORT back in its response, and the
test asserts it equals JSON.stringify(the emulator-assigned port)
reported by the compute emulator's own service listing — proving the
override reaches the deployed process through the documented HTTP
contract, not by reading a file that was never meant to carry it.

Bucket-credential assertions in this test are already single-app only
(no cross-app PUT or read), so S3's newly-enforced per-app bucket
scoping needs no changes here.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric and others added 12 commits July 23, 2026 17:11
…v-ignorant

DevDescriptor is renamed to DevExtensionDescriptor everywhere in
app-config.ts, since it's the dev variant OF ExtensionDescriptor.

deploy.ts loses LowerOptions.dev and every dev branch; it gains
LowerOptions.providers, following the same precedence pattern as
state. The dev provider aggregation moves out of deploy.ts into a new
control/dev.ts, so lower() no longer knows anything about dev.

ExtensionDescriptor.dev becomes a lazy async thunk
(() => Promise<DevExtensionDescriptor>) rather than the descriptor
itself. This keeps any production control entry's static import graph
free of dev implementation code: the thunk is a one-line dynamic
import of the extension's own dev entry by bare specifier.

control/dev.ts adds resolveDevDescriptors(config), which resolves
every non-build-only extension's thunk once into a map, and
devProviders(resolved, containers, devDir), which merges the
resolved descriptors' provider layers.

Registers a new /dev control-plane subpath for the core package
(src/exports/dev.ts, published as @prisma/composer/dev) in
tsconfig.depcruise.json and architecture.config.json, re-exporting
devProviders, resolveDevDescriptors, DEV_DIR, and the seam types.
Nothing dev-flavored is exported from /deploy or the root.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… it, extract via node-tar

Emulation code is physically separated from production source: the
dev-only implementation that used to live under @internal/lowering's
src/dev/ (dev-store, app-name, compute/postgres/bucket providers,
providers.ts, teardown, resolve-bin, spawn-utils) moves wholesale into
a new package, @internal/local-target, registered as its own plane
(control), layer (lowering), domain (prisma-cloud) entry.

Import specifiers inside the moved files are fixed from relative
../compute/*, ../postgres/* paths to package imports against
@internal/lowering's own public subpaths (compute, postgres, buckets).

app-name.ts's floating module doc comment is reattached directly above
the appNameOf function it actually describes, per repo style; its
module header is now a single line.

compute.ts's local Deployment provider reads the artifact's address
from compute.manifest.json after extraction, instead of threading it
through DeploymentProps/ComputeSerialized (see the next commit for the
revert on the lowering side). Missing address throws a pinned error:
"artifact manifest carries no address — repackage with a current
@prisma/composer."

artifact-extract.ts also moves here (not left in @internal/lowering)
and is rewritten to extract via the tar package instead of a
hand-rolled ustar reader, keeping the same pinned filtering: only
regular files, reject links/devices/path-escaping entries, and a
temp-dir-then-rename swap for atomicity. A round-trip test imports
packageComputeArtifact from @internal/lowering/compute (downward) and
asserts extraction fidelity against format drift.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…in the manifest

DeploymentProps.serviceAddress and the serialize/deploy threading that
carried it are reverted. The compute artifact's address is now
intrinsic artifact metadata: packageComputeArtifact writes it into
compute.manifest.json as an address field, so any deployment target
can recover it after unpacking without a platform-primitive field
dedicated to dev.

manifestVersion stays "1" — there are no consumers to protect against,
so this is not a breaking manifest change, just an added field.

The lowering package no longer has a /dev export or dev-only
dependencies (@internal/dev-emulators, @internal/s3-protocol); that
code moved to @internal/local-target in the previous commit.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… in extension.ts

control/extension.ts's dev field is now the one-line lazy reference
required by ADR-0041: dev: () => import('@prisma/composer-prisma-cloud/dev')
.then((m) => m.devDescriptor()). No dev implementation is imported
statically by this file anymore.

The actual devDescriptor() implementation moves to src/dev/descriptor.ts
(self-contained, no deploy-factory options since dev is credential-free),
assembling the container descriptor, provider layer, preflight,
emulators, attach, and teardown from the existing src/dev/* files plus
@internal/local-target's devProviders. src/exports/dev.ts is a thin
re-export over it, published as @prisma/composer-prisma-cloud/dev.

src/dev-entry.d.ts is an ambient module declaration for the bare
specifier @prisma/composer-prisma-cloud/dev, so tsc can typecheck the
dynamic import without creating either a real package.json dependency
(that would be an actual build-order cycle, since composer-prisma-cloud
already depends on this package's dist) or a bundler-visible resolvable
path (which would get the import inlined/rewritten instead of left as
a genuine external reference). Precedent: streams-server.d.ts.

invariants.test.ts gets a new invariant asserting the built ./control
bundle's import graph carries the bare-specifier dynamic import to
./dev but no dev-implementation token — proof the production control
bundle stays free of dev code.

Import specifiers referencing the old @internal/lowering/dev subpath
are updated to @internal/local-target.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oser-prisma-cloud

@prisma/composer/dev re-exports @internal/core/dev, and
@prisma/composer-prisma-cloud/dev re-exports @internal/prisma-cloud/dev,
mapped exactly like the existing /control and /deploy subpaths.

composer-prisma-cloud also gains @internal/local-target as a
devDependency (its /dev entry pulls in devProviders through the
target extension's dev descriptor).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…dev subpath

The public-is-a-sink rule has no built-in exceptions mechanism (unlike
the general cross-domain rules), so this adds a narrow pathNot directly
on that rule's to matcher, scoped to exactly
packages/9-public/composer-prisma-cloud/src/exports/dev.ts.

This edge exists only because architecture-coverage requires every
workspace specifier to be aliased in tsconfig.depcruise.json, which
then makes dependency-cruiser able to see, and by default reject, the
target's dynamic import of its own published dev entry
(ADR-0041's lazy dev reference). The specifier resolves at a consuming
app's runtime, never as a real build-time dependency between these two
packages — verified by the target's invariant test that the built
control.mjs keeps it as a genuine external dynamic import, never
inlined.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oviders layer

The hand-written dev stack module renderDevStackFile() generates now
imports devProviders, resolveDevDescriptors, and DEV_DIR from
@prisma/composer/dev instead of building providers inline; it
top-level-awaits resolveDevDescriptors(config), then passes the
resolved map into devProviders(resolved, containers, devDir) as
LowerOptions.providers.

The outer test script resolves the extension's lazy dev thunk
(descriptor.dev()) before reading its teardown, matching the
async-thunk API.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wholesale sync from the design branch (bot/claude/local-dev-feature-design-0b49fd)
at its current tip, covering the manifest-v1+address correction, the
artifact-extract/local-target package placement, core's /dev subpath,
and the lazy dev reference pivot. Diff against the design branch's
copies is empty.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…c @prisma/dev

The CLI shell-out is deleted wholesale: no bin walk-up, no stdout URL
parsing, no prisma dev stop/rm glob teardown. Database and Connection
become clients of the postgres-main emulator daemon (S3's
`bot/claude/local-dev-s3-dev-emulators`, hosting @prisma/dev's
programmatic startPrismaDevServer — one named, persistent server per
Database resource):

- LocalDatabaseProvider resolves the app's own @prisma/dev module path
  (two-step: resolve @prisma/dev directly from the app's node_modules;
  on failure, resolve prisma and resolve @prisma/dev from its own
  dependency tree; both failing throws the pinned
  "local dev needs @prisma/dev..." error), PUTs the daemon's ensure,
  and stores the returned url on its own attributes.
- LocalConnectionProvider resolves its url from the daemon's live
  listing (GET databases), matched by instance name.

dev-store.ts loses postgres.json — the daemon owns instance state now.
resolve-bin.ts and spawn-utils.ts are deleted with the shell-out; their
tests are replaced by module-resolution tests for the two-step
@prisma/dev resolution. teardown.ts loses removeLocalPostgresInstances
(the CLI glob) — postgres teardown is now a plain
postgresClient().deleteApp(app) call, made directly by the target
extension's own teardown.ts alongside compute/buckets, needing no
local-target delegate of its own.

resolve-package-entry.ts is new: import.meta.resolve + fileURLToPath,
for resolving a daemon program's published entrypoint. Lives here (not
in the target extension's own src/dev/emulators.ts) because that
extension's shipped source must stay free of node:/bun: imports
(invariant 5).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lient, re-emit its program

emulators.ts now ensures postgres-main whenever the graph has a
postgres- or prisma-next-kinded resource node (Postgres is a
first-class daemon since the programmatic @prisma/dev adoption).
Entry resolution for all three daemons goes through
@internal/local-target's resolvePackageEntry, resolved against the
PUBLISHED @prisma/composer-prisma-cloud/dev/*-main subpaths — never
the private @internal/dev-emulators ones — so a published install's
dev command finds its daemon programs in its own dependency tree.

teardown.ts calls postgresClient().deleteApp(app) directly, alongside
the existing compute/buckets DELETEs, replacing the CLI-glob
removeLocalPostgresInstances call.

composer-prisma-cloud's tsdown config re-emits compute-main.mjs,
buckets-main.mjs, and postgres-main.mjs from @internal/dev-emulators'
own dist, at the SAME depth as its other top-level entries (not a
subdirectory) — the daemon programs' readOwnVersion() resolves
../package.json relative to themselves, and the CALLING side
(ensureDaemon, inside dev.mjs) resolves the same relative path from
its own location; both sides must sit at the same depth under dist/ or
they read different package.json files and disagree on "own version",
breaking ensureDaemon's staleness check. Confirmed live: a fresh spawn
under the wrong depth failed with an ENOENT reading dist/package.json;
moving to the top level fixed it.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es.json

The postgres-related assertions move from reading dev-store's
postgres.json to the postgres-main daemon's own documented wire
protocol (GET /apps/<app>/databases, plain fetch — same convention the
compute/buckets assertions already use). Instance names are unchanged.
The registry/baseline tracking that already existed for compute and
buckets extends to postgres (registered, only stopped at the end if
this run caused it to start). The data-persistence assertion (the
no-op second converge keeping the same pids) is unaffected — it never
depended on how Postgres itself is hosted.

Also updates the file's own stale doc comments: the "why a standalone
script, not bun:test" rationale no longer blames
LocalDatabaseProvider's own prisma dev --name --detach subprocess
(that shell-out is gone) — the same class of stdout-loss-under-bun-test
issue applies more generally to any daemon a converge causes
ensureDaemon to spawn.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… derivation

Delta review finding A (#160): LocalDatabaseProvider re-derived the
pcdev-<app>-<database-id> instance name with its own slug, which
dropped the leading/trailing-dash trim postgres-main's own derivation
(now dev-emulators' instance-name.ts, extracted in the CodeQL
linear-regex fix) applies. An app name or database id ending in a
hyphen — a shape the wire protocol's own segment rule already allows
through — produced a DIFFERENT concatenated name here than the one the
daemon actually created the server under (a doubled dash at the
boundary), so LocalConnectionProvider's listing-based lookup threw
noRecordedInstanceError even though the server existed.

postgres.ts drops its own slug/instanceName and imports instanceNameFor
directly from @internal/dev-emulators instead — one implementation, so
this can no longer drift. instance-name.ts's slug/instanceNameFor are
exported through dev-emulators' main entry (a pure function module, no
daemon main() to run on import — verified importing it is side-effect
free).

Adds a cross-boundary test against the real postgres-main daemon: ensure
through LocalDatabaseProvider, Connection-resolve through
LocalConnectionProvider, for an app name and database id each ending in
a hyphen — the exact pathological pair that threw
noRecordedInstanceError before this fix.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wmadden-electric
wmadden-electric force-pushed the claude/local-dev-s4-dev-target branch from 954ebec to 57e7374 Compare July 23, 2026 15:15
Base automatically changed from claude/local-dev-s3-dev-emulators to main July 23, 2026 15:36
… name only

Operator naming decision (spec pin, design tip 12956d9): "dev" names
the user-facing feature only — the `prisma-composer dev` command, the
`[dev]` log prefix, the `.prisma-composer/dev/` state dir (DEV_DIR
keeps its name and value). The architectural seam takes the concept's
real noun, already established by ADR-0041 and @internal/local-target:

- ExtensionDescriptor.dev -> localTarget (still the lazy async thunk);
  DevExtensionDescriptor -> LocalTargetDescriptor;
  DevProvidersInput/DevEmulatorsInput/DevAttachInput/DevAttachment ->
  LocalTargetProvidersInput/LocalTargetEmulatorsInput/
  LocalTargetAttachInput/LocalTargetAttachment.
- Core subpath @prisma/composer/dev -> @prisma/composer/local-target;
  resolveDevDescriptors -> resolveLocalTargets; devProviders ->
  localTargetProviders; control/dev.ts -> control/local-target.ts,
  exports/dev.ts -> exports/local-target.ts likewise. The pinned
  no-local-target-support error's field-name reference updates to
  match: "declares no `localTarget` descriptor" (was "`dev`
  descriptor").
- Extension subpath @prisma/composer-prisma-cloud/dev ->
  /local-target, exporting localTargetDescriptor() (was
  devDescriptor()); src/exports/dev.ts -> src/exports/local-target.ts;
  src/dev-entry.d.ts -> src/local-target-entry.d.ts; the one-line
  thunk in extension.ts updates accordingly.
- The re-emitted daemon program subpaths (compute-main/buckets-main/
  postgres-main) move from ./dev/*-main to ./local-target/*-main —
  their subpath literally contained /dev/; the physical dist files
  (already at the top level, not a subdirectory) are unaffected. The
  entry resolver in the target extension's emulators.ts updates to
  match.
- @internal/local-target's own internal `devProviders` aggregator (a
  DIFFERENT function from core's, per-extension rather than
  cross-extension) renames to localTargetProviders for the same
  reason, even though it isn't part of the public seam.
- Ripple: 9-public mappings, tsdown configs, tsconfig.depcruise.json
  path aliases, architecture.config.json globs and the
  crossDomainExceptions entry, dependency-cruiser.config.mjs's
  sanctioned-dynamic-import pathNot, the target's firewall invariant
  7 test's token list and entry-map test, the S4 integration test's
  generated stack module and descriptor.localTarget usage.
- Internal descriptor hook names (providers/container/preflight/
  emulators/attach/teardown) unchanged, per the spec's own pin.
  @internal/local-target's package name was already correct.

spec.md/plan.md synced wholesale from design tip 12956d9; empty diff
confirmed against it.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
…n point

The generated module now deserializes the containers, top-level-awaits
resolveDevDescriptors(config), and passes providers: devProviders(...) +
state: localState() explicitly (spec § 3 REVISED — operator review of
#162). The dev: true flag is gone — LowerOptions no longer has it; lower()
learns nothing about dev. Rendering test updated to the new source.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
…n point

The generated module now deserializes the containers, top-level-awaits
resolveDevDescriptors(config), and passes providers: devProviders(...) +
state: localState() explicitly (spec § 3 REVISED — operator review of
#162). The dev: true flag is gone — LowerOptions no longer has it; lower()
learns nothing about dev. Rendering test updated to the new source.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The platform removed GET /v1/projects/{id}/compute-services today; the
resource now lists at GET /v1/apps?projectId= with appEndpointDomain.
The deploy providers and the TypeScript verify scripts already use
/v1/apps — only this bash script still hit the deleted route, which
turned every 'Deploy, verify, destroy' run red across all branches.
Verified live: the new query returns the domain for a deployed app and
correctly reports empty for a destroyed project.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread packages/0-framework/1-core/core/src/control/app-config.ts Outdated
Comment thread packages/1-prisma-cloud/0-lowering/local-target/src/bucket.ts
wmadden-electric and others added 2 commits July 23, 2026 18:58
…and finish the localTarget rename in its error text

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The naming rule reserves 'dev' for user-facing surfaces (the command, the
[dev] log prefix, .prisma-composer/dev). Internal paths now say what the
code is: @internal/local-target's implementation flattens to src/ (the
package name already scopes it), and the target extension's local-target
implementation moves from src/dev/ to src/local-target/, matching its
export subpath. dev-store.ts keeps its name because it is the store OF
the user-facing .prisma-composer/dev directory. Tests, the env-touch
invariant allow-list, and stale comment paths updated; no behavior
change.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric added a commit that referenced this pull request Jul 23, 2026
…review)

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
wmadden-electric and others added 2 commits July 23, 2026 19:04
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@wmadden-electric
wmadden-electric merged commit 248827b into main Jul 23, 2026
18 checks passed
@wmadden-electric
wmadden-electric deleted the claude/local-dev-s4-dev-target branch July 23, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants