Skip to content

Expose authoritative watch inputs from contract configs#356

Open
jkomyno wants to merge 29 commits intomainfrom
tml-2276-authoritative-watch-inputs
Open

Expose authoritative watch inputs from contract configs#356
jkomyno wants to merge 29 commits intomainfrom
tml-2276-authoritative-watch-inputs

Conversation

@jkomyno
Copy link
Copy Markdown
Contributor

@jkomyno jkomyno commented Apr 19, 2026

Why

The Vite auto-emit flow was relying on the config module graph as the source of truth for invalidation.

That breaks in two supported authoring paths:

  • PSL-first configs pass schema paths like ./prisma/schema.prisma as strings to provider helpers, so those files never appear in the config module graph.
  • The built-in extension facades for contract: './prisma/contract.ts' load that file later inside contract.source.load() via dynamic import, so the config module graph still does not see the actual contract file.

Without explicit source declarations from providers, those edits do not reliably trigger re-emission during vite dev.

What this PR does

This PR moves authoritative source declarations onto the contract source provider and keeps watcher-specific behavior in the Vite integration.

It does that by:

  • changing contract.source from a bare async function into a provider object with optional inputs plus load(context, environment)
  • splitting file-system concerns out of ContractSourceContext into a new ContractSourceEnvironment that carries configDir
  • populating inputs for built-in SQL and Mongo PSL providers with their schema paths
  • populating inputs for the postgres and mongo .ts facades with the user-declared contract path they dynamic-import
  • extracting typescriptContractFromPath() so the .ts path-loading logic is shared and resolves relative to configDir
  • keeping the CLI/control path source-agnostic on a single loadConfig() API
  • making the Vite plugin own watcher behavior: it merges the config module graph with provider-declared inputs, treats JS/TS inputs as extra module-graph roots, refreshes the watch set before emit, normalizes hot-update paths, filters emitted artifacts out of the watch set, and re-emits when config changes swap the authoritative inputs

Config shape change

// before
contract: {
  source: async (context) => /* Result<Contract, Diagnostics> */,
}

// after
contract: {
  source: {
    inputs: ['./prisma/schema.prisma'],
    load: async (context, environment) => /* Result<Contract, Diagnostics> */,
  },
}

load() now receives:

  • context: the existing domain inputs (codec lookup, authoring contributions, mutation defaults, ...)
  • environment: file-system execution context, currently configDir

Declare source.inputs only for source files that are not already covered by the config module graph, such as PSL schema paths and TypeScript contract paths passed as strings.

Omitted inputs semantics

For providers that omit source.inputs, the current behavior is intentional: the Vite plugin falls back to watching the config file plus the config module graph.

This PR does not emit a partial-coverage warning in that case. That is sufficient for TS-authored providers whose real sources are already reachable from the module graph, but any custom provider that reads extra non-module-graph files must declare those files in source.inputs.

Summary

  • add tool-agnostic contract.source.inputs?: readonly string[]
  • replace contract.source(context) with contract.source.load(context, environment)
  • add ContractSourceEnvironment with configDir
  • resolve PSL and TS-path contract sources from configDir instead of process.cwd()
  • populate inputs for built-in SQL and Mongo PSL providers and for postgres/mongo .ts facades
  • extract typescriptContractFromPath() to share .ts dynamic-import logic
  • keep a single loadConfig() surface; no metadata-returning loader variant
  • move watch policy into the Vite plugin instead of shared config
  • keep a runtime ignore set for emitted artifacts so Vite file-change events on contract.json / contract.d.ts do not loop back into auto-emit
  • add Vite unit coverage plus Vite HMR e2e coverage for PSL edits and config-authored input swaps

Breaking change

Every user config migrates from:

contract: {
  source: async (context) => ...
}

to:

contract: {
  source: {
    inputs: [...],
    load: async (context, environment) => ...
  }
}

configDir no longer lives on ContractSourceContext; providers receive it through the second environment argument.

Known limitation

Default contract.output fallback still relies on defineConfig(). Raw object configs that omit contract.output are not normalized by loadConfig() in this PR.

Testing

  • pnpm --filter ./packages/1-framework/1-core/config test
  • pnpm --filter ./packages/1-framework/3-tooling/cli test -- test/config-loader.test.ts test/config-types.test.ts test/control-api/contract-emit.test.ts
  • pnpm --filter ./packages/1-framework/3-tooling/vite-plugin-contract-emit test -- test/plugin.test.ts
  • pnpm --filter ./packages/2-sql/2-authoring/contract-psl test -- test/provider.test.ts
  • pnpm --filter ./packages/2-mongo-family/2-authoring/contract-psl test
  • pnpm --filter ./packages/2-mongo-family/2-authoring/contract-ts test
  • pnpm --filter ./packages/3-extensions/postgres test -- test/config/define-config.test.ts
  • pnpm --filter ./packages/3-extensions/mongo test -- test/config/define-config.test.ts
  • pnpm --filter ./test/integration exec vitest run test/vite-plugin.hmr.e2e.test.ts
  • pnpm --filter ./test/integration exec vitest run test/cli.emit-command.test.ts

Closes

TML-2276

Summary by CodeRabbit

Release Notes

  • New Features

    • Contract source providers now support declaring authoritative input files via inputs metadata for better tooling integration.
    • Vite plugin watches declared input files in addition to the config module graph.
  • Documentation

    • Updated configuration and architecture documentation to reflect contract source provider API changes.
  • Chores

    • Contract source provider API refactored to use structured object interface with explicit load method for improved extensibility.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR refactors the contract source provider API from a directly-callable async function to a structured object with load and optional inputs properties. This enables declaring authoritative input files for build/tooling integrations and provides configuration directory context during contract loading. The change cascades across configuration types, validation logic, CLI/Vite tooling, and test fixtures.

Changes

Cohort / File(s) Summary
Core Contract Source Types
packages/1-framework/1-core/config/src/contract-source-types.ts, packages/1-framework/1-core/config/src/exports/config-types.ts
Updated ContractSourceProvider from callable function type to interface with optional inputs?: readonly string[] and required load(context, environment) method. Added new exported ContractSourceEnvironment interface.
Config Type System & Validation
packages/1-framework/1-core/config/src/config-types.ts, packages/1-framework/1-core/config/src/config-validation.ts, packages/1-framework/1-core/config/README.md
Updated ContractConfigSchema to validate against structured schema instead of 'unknown'. Extended validateConfig with new validateContractConfig helper that validates provider object shape, optional inputs array, required load function, and cross-field validation preventing artifact paths in inputs.
Config Type Tests
packages/1-framework/1-core/config/test/config-types.test-d.ts, packages/1-framework/1-core/config/test/config-validation.test.ts
Added type-level tests for provider object structure and input metadata. Updated validation tests to use createSourceProvider() helper and verify nested field errors (contract.source.inputs[], contract.source.load).
CLI Config Loading & Tooling
packages/1-framework/3-tooling/cli/src/config-loader.ts, packages/1-framework/3-tooling/cli/src/commands/contract-emit.ts
Refactored loadConfig to delegate validation to new internal loadValidatedConfig() helper. Updated contract emit request to use contractConfig.source (instead of sourceProvider) and added contractConfig.configDir.
CLI Control API
packages/1-framework/3-tooling/cli/src/control-api/types.ts, packages/1-framework/3-tooling/cli/src/control-api/client.ts, packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts, packages/1-framework/3-tooling/cli/src/exports/config-types.ts
Renamed EmitContractConfig.sourceProvider to source and added configDir field. Updated contract source invocations to call source.load(context, { configDir }) instead of source(context). Re-exported ContractSourceEnvironment type.
SQL Contract Providers
packages/2-sql/2-authoring/contract-psl/src/provider.ts, packages/2-sql/2-authoring/contract-ts/src/config-types.ts, packages/2-sql/2-authoring/contract-ts/package.json
Updated prismaContract to use provider object with inputs: [schemaPath] and load method. Added new factory typescriptContractFromPath(contractPath, output?) with dynamic imports and path resolution via environment.configDir. Added pathe dependency.
Mongo Contract Providers
packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts, packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts, packages/2-mongo-family/2-authoring/contract-ts/package.json
Updated mongoContract to use provider object with inputs and load. Added typescriptContractFromPath factory with pathe dependency.
Extension Implementations
packages/3-extensions/postgres/src/config/define-config.ts, packages/3-extensions/mongo/src/config/define-config.ts
Simplified .ts contract handling by delegating to new typescriptContractFromPath() helper instead of inline dynamic import logic. Removed pathToFileURL and related imports.
CLI Testing Infrastructure
packages/1-framework/3-tooling/cli/test/.../*, packages/1-framework/3-tooling/cli/README.md, packages/1-framework/3-tooling/cli/recordings/fixtures/prisma-next.config.ts
Updated test helpers (createSourceProvider, createSource) to return provider objects. Updated config fixture and tests to use source: { load: async () => ... } shape. Updated README to document .load(context, environment) invocation.
Vite Plugin Watch Resolution
packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts, packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md, packages/1-framework/3-tooling/vite-plugin-contract-emit/package.json
Major refactor: new resolveWatchedFiles() builds watch set from config file + contract.source.inputs + module graph roots, filtering out output artifacts. New handleTrackedFileChange() manages debounced emits. Watcher events now registered on configureServer. Added pathe dependency. Updated README to document authoritative inputs behavior.
Vite Plugin Testing
packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts
Mocked @prisma-next/cli/config-loader.loadConfig alongside executeContractEmit. Expanded test coverage for watch file scenarios: omitted inputs, merged inputs with module graph, artifact filtering. Added watcher event handler setup via beforeEach.
SQL Module Test Fixtures
packages/2-sql/2-authoring/contract-psl/test/fixtures.ts, packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
Added createPostgresTestEnvironment() helper returning ContractSourceEnvironment with configDir: process.cwd(). Added provider test validating inputs and load behavior with relative path resolution.
Integration Tests - Config & Emission
test/integration/test/cli.emit-contract.test.ts, test/integration/test/cli.emit-command.test.ts, test/integration/test/authoring/cli.emit-parity-fixtures.test.ts, test/integration/test/authoring/psl.pgvector-dbinit.test.ts
Updated all contract source invocations to call source.load(context, environment) instead of source(context). Added sourceEnvironmentFromCwd() / buildSourceEnvironment() helpers. Updated config resolution to pass configDir based on loaded config path.
Integration Tests - Provider Behavior
test/integration/test/authoring/side-by-side-contracts.test.ts, test/integration/test/sql-builder/fixtures/prisma-next.config.ts
Updated provider calls to use new load signature with environment. Updated test config fixtures to provider object shape.
Integration Tests - PSL Vite Plugin
test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/*, test/integration/test/vite-plugin.hmr.e2e.test.ts
Added new PSL fixture variant with contract.prisma, contract-alt.prisma, prisma-next.config.ts, and vite.config.ts. Added e2e HMR tests validating schema changes and authoritative input switching with watch file updates.
Config Fixtures (Multiple Directories)
test/integration/test/fixtures/.../prisma-next.config.*.ts, packages/1-framework/3-tooling/cli/recordings/fixtures/prisma-next.config.ts, packages/3-extensions/sql-orm-client/test/fixtures/prisma-next.config.ts
Updated all config fixtures across CLI and extension test suites from source: async () => ... to source: { load: async () => ... } shape (30+ files).
ADR & Documentation
docs/architecture docs/adrs/ADR 163 - Provider-invoked source interpretation packages.md
Updated ADR description to call config.contract.source.load() instead of config.contract.source().
Contract Test Fixtures
test/integration/test/authoring/parity/callback-mode-scalars/contract.ts, test/integration/test/fixtures/prisma-next.config.ts, test/integration/test/mongo/fixtures/prisma-next.config.ts
Updated contract field definitions to use explicit Postgres column-type bindings. Updated config to provider object shape.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI/Vite Plugin
    participant ConfigLoader as Config Loader
    participant Validator as Validator
    participant Provider as Contract Source Provider
    participant FS as File System

    CLI->>ConfigLoader: loadConfig(configPath)
    ConfigLoader->>ConfigLoader: resolve config path + load via c12
    ConfigLoader->>Validator: validateConfig(config)
    Validator->>Validator: validate contract.source.load (function)
    Validator->>Validator: validate contract.source.inputs (array)
    Validator->>Validator: validate no inputs match output artifacts
    Validator-->>ConfigLoader: validated config
    ConfigLoader-->>CLI: PrismaNextConfig

    CLI->>CLI: resolveWatchedFiles()
    CLI->>CLI: collect config path + contract.source.inputs
    CLI->>CLI: build module graph from config + input paths
    CLI->>CLI: filter out contract output artifacts
    CLI-->>CLI: watched file set

    CLI->>Provider: source.load(context, { configDir })
    FS->>Provider: resolve contractPath relative to configDir
    Provider->>FS: read/parse schema or import module
    Provider-->>CLI: Result<Contract, Diagnostics>

    CLI->>CLI: emit contract + update watched files
    FS-->>CLI: file system watches for changes
    CLI->>CLI: handleTrackedFileChange(file)
    alt file in ignoredOutputFiles
        CLI-->>CLI: ignore (self-triggered)
    else file in watchedFiles
        CLI->>Provider: source.load(...) again
        Provider-->>CLI: Result<Contract>
        CLI->>CLI: emit contract
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • TML-1817: Vite plugin #109: Introduced the original prismaVitePlugin implementation; this PR's Vite plugin refactor for watch file resolution and the new loadConfig integration directly extend that work.

Suggested reviewers

  • wmadden
  • igalklebanov
  • saevarb

Poem

🐰 Hops with glee,
Structured sources now decree,
Inputs watched with care,
configDir is shared,
Load methods guide the spree!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Expose authoritative watch inputs from contract configs' clearly and concisely describes the main change: introducing provider-declared source inputs for tooling watch purposes.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tml-2276-authoritative-watch-inputs

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 and usage tips.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 19, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-runtime@356

@prisma-next/family-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-mongo@356

@prisma-next/sql-runtime

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-runtime@356

@prisma-next/family-sql

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/family-sql@356

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@356

@prisma-next/mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo@356

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-paradedb@356

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-pgvector@356

@prisma-next/postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/postgres@356

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-orm-client@356

@prisma-next/sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sqlite@356

@prisma-next/target-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-mongo@356

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-mongo@356

@prisma-next/driver-mongo

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-mongo@356

@prisma-next/contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract@356

@prisma-next/utils

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/utils@356

@prisma-next/config

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/config@356

@prisma-next/errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/errors@356

@prisma-next/framework-components

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/framework-components@356

@prisma-next/operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/operations@356

@prisma-next/contract-authoring

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/contract-authoring@356

@prisma-next/ids

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/ids@356

@prisma-next/psl-parser

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-parser@356

@prisma-next/psl-printer

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/psl-printer@356

@prisma-next/cli

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/cli@356

@prisma-next/emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/emitter@356

@prisma-next/migration-tools

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/migration-tools@356

prisma-next

npm i https://pkg.pr.new/prisma/prisma-next@356

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/vite-plugin-contract-emit@356

@prisma-next/runtime-executor

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/runtime-executor@356

@prisma-next/mongo-codec

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-codec@356

@prisma-next/mongo-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract@356

@prisma-next/mongo-value

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-value@356

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-psl@356

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-contract-ts@356

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-emitter@356

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-schema-ir@356

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-query-ast@356

@prisma-next/mongo-orm

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-orm@356

@prisma-next/mongo-pipeline-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-pipeline-builder@356

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-lowering@356

@prisma-next/mongo-wire

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/mongo-wire@356

@prisma-next/sql-contract

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract@356

@prisma-next/sql-errors

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-errors@356

@prisma-next/sql-operations

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-operations@356

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-schema-ir@356

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-psl@356

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-ts@356

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-contract-emitter@356

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-lane-query-builder@356

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-relational-core@356

@prisma-next/sql-builder

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/sql-builder@356

@prisma-next/target-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-postgres@356

@prisma-next/target-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/target-sqlite@356

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-postgres@356

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/adapter-sqlite@356

@prisma-next/driver-postgres

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-postgres@356

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/driver-sqlite@356

commit: 2c3a0d6

Comment thread packages/1-framework/1-core/config/src/config-types.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
packages/1-framework/3-tooling/cli/test/config-loader.test.ts (1)

68-167: Good coverage of the new metadata paths.

The four new cases exercise the authoritative-inputs, moduleGraph, default-artifact filtering, and fallback-warning branches. Use of toEqual on the full contractWatch object is aligned with the "prefer object matchers" guideline.

One small suggestion: the last case uses result.metadata.contractWatch?.inputs with optional chaining; since contract is present in the config, contractWatch is guaranteed non-null — prefer a non-null assertion or assert on the full object via toEqual/toMatchObject to keep style consistent with the other three cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/config-loader.test.ts` around lines
68 - 167, The test uses optional chaining when asserting contract watch metadata
(result.metadata.contractWatch?.inputs) but contract is always present here;
replace the optional assertion with a non-null assertion or assert the entire
object like the other tests (e.g.,
expect(result.metadata.contractWatch).toEqual(...) or toMatchObject(...)) so the
style is consistent with the other cases and avoids the unnecessary optional
chaining (refer to result.metadata.contractWatch and the surrounding it block).
packages/1-framework/3-tooling/cli/src/config-loader.ts (1)

47-50: Unnecessary optional chaining on outputJsonPath.

resolve(...) always returns a non-nullable string, so outputJsonPath?.endsWith('.json') can be simplified to outputJsonPath.endsWith('.json').

Proposed nit
-  const outputDtsPath = outputJsonPath?.endsWith('.json')
+  const outputDtsPath = outputJsonPath.endsWith('.json')
     ? `${outputJsonPath.slice(0, -5)}.d.ts`
     : undefined;

Also worth noting: when contract.output uses a non-.json extension, the colocated .d.ts artifact won't be filtered out of watchInputs. That's probably fine given the canonical output is .json, but worth a comment if that's intentional.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/src/config-loader.ts` around lines 47 -
50, The optional chaining on outputJsonPath is unnecessary because resolve(...)
always returns a string; update the conditional that sets outputDtsPath to use
outputJsonPath.endsWith('.json') instead of outputJsonPath?.endsWith('.json')
and remove the redundant null-safety. Locate the code that computes
outputJsonPath (uses resolve(configDir, contract.output ??
DEFAULT_CONTRACT_OUTPUT)) and outputDtsPath and make the change; optionally add
a brief comment near outputDtsPath noting that non-".json" contract.output
values will not produce a colocated ".d.ts" (so watchInputs may include it) if
you want to document that behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/1-framework/3-tooling/cli/src/config-loader.ts`:
- Around line 47-50: The optional chaining on outputJsonPath is unnecessary
because resolve(...) always returns a string; update the conditional that sets
outputDtsPath to use outputJsonPath.endsWith('.json') instead of
outputJsonPath?.endsWith('.json') and remove the redundant null-safety. Locate
the code that computes outputJsonPath (uses resolve(configDir, contract.output
?? DEFAULT_CONTRACT_OUTPUT)) and outputDtsPath and make the change; optionally
add a brief comment near outputDtsPath noting that non-".json" contract.output
values will not produce a colocated ".d.ts" (so watchInputs may include it) if
you want to document that behavior.

In `@packages/1-framework/3-tooling/cli/test/config-loader.test.ts`:
- Around line 68-167: The test uses optional chaining when asserting contract
watch metadata (result.metadata.contractWatch?.inputs) but contract is always
present here; replace the optional assertion with a non-null assertion or assert
the entire object like the other tests (e.g.,
expect(result.metadata.contractWatch).toEqual(...) or toMatchObject(...)) so the
style is consistent with the other cases and avoids the unnecessary optional
chaining (refer to result.metadata.contractWatch and the surrounding it block).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 75bf42d9-cb80-4ee9-b957-e1dbac73ee33

📥 Commits

Reviewing files that changed from the base of the PR and between 21a24dd and ba2cc16.

📒 Files selected for processing (20)
  • packages/1-framework/1-core/config/README.md
  • packages/1-framework/1-core/config/src/config-types.ts
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • packages/1-framework/1-core/config/test/config-validation.test.ts
  • packages/1-framework/3-tooling/cli/src/config-loader.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/config-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/mongo/src/config/define-config.ts
  • packages/3-extensions/mongo/test/config/define-config.test.ts
  • packages/3-extensions/postgres/src/config/define-config.ts
  • packages/3-extensions/postgres/test/config/define-config.test.ts

Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

I like the direction to make a ContractSourceProvider explicitly return its sources. I don't think the framework should be aware of dev watcher integrations though. Also watchStrategy appears to have no consumer and it's unclear from its doc comment what its future purpose will be. In any case, this doesn't belong in Prisma Next config, it belongs in the watcher integration's config

So I recommend:

  • rename the watcher-coupled properties of the contractProvider to be consumer agnostic, eg watcherInputs could as easily be sources or sourceFiles etc
  • move watcher-specific config (watcherStrategy) to the watcher integration (ie the vite or NextJS integratio)

Comment thread packages/1-framework/1-core/config/src/config-types.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-types.ts Outdated
Comment thread packages/1-framework/3-tooling/cli/src/config-loader.ts Outdated
Comment thread packages/1-framework/3-tooling/cli/src/config-loader.ts Outdated
Comment thread packages/1-framework/3-tooling/cli/src/config-loader.ts Outdated
@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented Apr 20, 2026

I agree with this review.

The current change solves the Vite invalidation problem, but it does it at the wrong boundary: it teaches ContractConfig about dev watcher behavior, and then adds a parallel loadConfigWithMetadata() API to recover that information later.

I think the cleaner model is:

  • keep contract focused on emission concerns (source, output)
  • move authoritative source declaration to the source-provider side
  • make that declaration tool-agnostic, not watcher-specific

So instead of contract.watchInputs / contract.watchStrategy, I’d rather have providers declare something like:

  • moduleGraph
  • paths
  • configPathOnly

That also addresses the “why is this optional?” point. Every provider should declare its authoritative source model explicitly. For built-ins, that means:

  • PSL providers declare paths
  • direct TS helpers declare moduleGraph
  • extension facades that dynamically import contract.ts declare paths, because the config module graph is not authoritative in that flow

Then the Vite plugin can resolve that provider metadata into watched files, filter out emitted artifacts, and handle the partial-coverage warning there. That keeps the fallback behavior we want for custom providers, but avoids encoding it as implicit back-compat logic in the generic config loader.

I'll get rid of the retro-compatibility that snuck in.

Separately, if any validation remains for this shape, it should live in the Arktype schema rather than in ad hoc manual checks.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
test/integration/test/cli.emit-command.test.ts (1)

568-570: ⚠️ Potential issue | 🟡 Minor

Stale type annotation for sourceResult.

typeof providerConfig.contract['source'] is now an object (with authoritativeInputs + load), not a callable, so ReturnType<...> on it is invalid. The annotation happens to still resolve (likely to never/broken inference) but no longer describes the load result. Use ReturnType<NonNullable<typeof providerConfig.contract>['source']['load']> instead.

Proposed fix
-        let sourceResult: Awaited<
-          ReturnType<NonNullable<typeof providerConfig.contract>['source']>
-        >;
+        let sourceResult: Awaited<
+          ReturnType<NonNullable<typeof providerConfig.contract>['source']['load']>
+        >;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/cli.emit-command.test.ts` around lines 568 - 570, The
type annotation for sourceResult is stale because
providerConfig.contract['source'] is now an object with a load method; update
the declaration to use the load method's return type by replacing
ReturnType<NonNullable<typeof providerConfig.contract>['source']> with
ReturnType<NonNullable<typeof providerConfig.contract>['source']['load']> (keep
the NonNullable wrapper) so sourceResult correctly reflects the load result from
providerConfig.contract.source.load.
test/integration/test/authoring/cli.emit-parity-fixtures.test.ts (1)

95-98: ⚠️ Potential issue | 🟡 Minor

Update ReturnType<typeof …source> annotations to target .source.load.

Same issue as in cli.emit-command.test.ts: contract.source is now an object, not a function, so ReturnType<typeof tsConfig.contract.source> (and the matching psl/diagnostics ones) no longer reflects the load() result type. Point these at .source.load instead.

Proposed fix
-          let tsProviderResultFirst: Awaited<ReturnType<typeof tsConfig.contract.source>>;
-          let tsProviderResultSecond: Awaited<ReturnType<typeof tsConfig.contract.source>>;
-          let pslProviderResultFirst: Awaited<ReturnType<typeof pslConfig.contract.source>>;
-          let pslProviderResultSecond: Awaited<ReturnType<typeof pslConfig.contract.source>>;
+          let tsProviderResultFirst: Awaited<ReturnType<typeof tsConfig.contract.source.load>>;
+          let tsProviderResultSecond: Awaited<ReturnType<typeof tsConfig.contract.source.load>>;
+          let pslProviderResultFirst: Awaited<ReturnType<typeof pslConfig.contract.source.load>>;
+          let pslProviderResultSecond: Awaited<ReturnType<typeof pslConfig.contract.source.load>>;
...
-          let sourceResult: Awaited<ReturnType<typeof pslConfig.contract.source>>;
+          let sourceResult: Awaited<ReturnType<typeof pslConfig.contract.source.load>>;

Also applies to: 241-241

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/authoring/cli.emit-parity-fixtures.test.ts` around
lines 95 - 98, The type aliases for provider result variables use
ReturnType<typeof ...contract.source> but contract.source is now an object;
change the annotations to target the load method return type instead (e.g., use
Awaited<ReturnType<typeof tsConfig.contract.source.load>> for
tsProviderResultFirst/Second and similarly use Awaited<ReturnType<typeof
pslConfig.contract.source.load>> for pslProviderResultFirst/Second and update
any diagnostics-related ReturnType references to point at .source.load as well)
so the types reflect the result of calling source.load().
🧹 Nitpick comments (2)
packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts (1)

81-86: Redundant manual validation — already enforced by ContractSourceProviderSchema.

loadConfig() runs ContractConfigSchema via validateContractConfig, which requires contract.source to be an object with authoritativeInputs and load: Function. By the time this code runs, contractConfig.source.load is guaranteed to be a function. Per the PR direction to move provider-shape validation into the Arktype schema, and per guideline to prefer assertions over defensive checks once validated, this block can be dropped.

♻️ Proposed cleanup
-  // Validate source exists and is callable
-  if (typeof contractConfig.source?.load !== 'function') {
-    throw errorContractConfigMissing({
-      why: 'Contract config must include a valid source provider object',
-    });
-  }
-

As per coding guidelines: "Use schema validators (Arktype) for structural validation of required fields, types, and shapes rather than redundant manual checks for these properties."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts`
around lines 81 - 86, Remove the redundant runtime check for
contractConfig.source.load in loadConfig(): since validateContractConfig runs
ContractConfigSchema/ContractSourceProviderSchema and guarantees
contractConfig.source.load is a Function, delete the if (typeof
contractConfig.source?.load !== 'function') { throw ... } block; rely on
validateContractConfig/ContractConfigSchema for enforcement and keep the rest of
loadConfig() logic intact (referencing symbols: loadConfig,
validateContractConfig, ContractConfigSchema, ContractSourceProviderSchema,
contractConfig).
packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts (1)

152-179: Assert the watcher registration in the moduleGraph case.

This test says it registers watchers, but it only verifies ssrLoadModule(). A regression that loads the module graph but never calls watcher.add() would still pass.

🧪 Proposed test assertion
       await configureServer(mockServer);
 
       expect(mockServer.ssrLoadModule).toHaveBeenCalled();
+      expect(mockServer.watcher.add).toHaveBeenCalledWith('/project/prisma-next.config.ts');
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts`
around lines 152 - 179, The test currently only asserts ssrLoadModule was called
but must also assert the plugin registered file watchers from the module graph;
after calling configureServer(mockServer) add an assertion that
mockServer.watcher.add was invoked for the module file returned by
mockServer.moduleGraph.getModuleById (e.g. '/project/prisma-next.config.ts') —
use prismaVitePlugin, configureServer, and the mocked moduleGraph.getModuleById
to locate where watcher.add should be called and assert it was called (and
optionally called once).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/3-tooling/cli/test/config-types.test.ts`:
- Around line 9-12: The default parameter of createSourceProvider is too
narrowly inferred; update its signature to type the authoritativeInputs
parameter as ContractAuthoritativeInputs (imported from
`@prisma-next/config/config-types`) so it accepts all three kinds (e.g.,
'moduleGraph' and 'paths'); change the parameter annotation on
createSourceProvider to use ContractAuthoritativeInputs and add the
corresponding import statement so callers like the one at the failing call
accept the wider union type.

In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts`:
- Around line 1-2: The file imports dirname and resolve from 'node:path' —
replace that import to use 'pathe' instead to ensure
cross-platform/cross-runtime compatibility; update the import that currently
reads the symbols dirname and resolve so they are imported from 'pathe' and
leave all usages of dirname and resolve in this module (plugin.ts) unchanged.

In `@packages/2-sql/2-authoring/contract-psl/src/provider.ts`:
- Around line 35-58: The loader's load function resolves the schemaPath with
resolve(schemaPath) (in load) which is cwd-relative and can differ from the
watcher that resolved authoritativeInputs.paths against the config directory;
change the resolution in load to use the same base (e.g.,
resolve(context.configDir, schemaPath) or equivalent from ContractSourceContext)
before calling readFile so the watcher and loader target the same file, and
propagate the adjusted absolute path into the notOk payload fields
(sourceId/meta.absoluteSchemaPath) so diagnostics and metadata remain
consistent; update references around load, resolve(schemaPath), readFile, and
the notOk return accordingly.

---

Outside diff comments:
In `@test/integration/test/authoring/cli.emit-parity-fixtures.test.ts`:
- Around line 95-98: The type aliases for provider result variables use
ReturnType<typeof ...contract.source> but contract.source is now an object;
change the annotations to target the load method return type instead (e.g., use
Awaited<ReturnType<typeof tsConfig.contract.source.load>> for
tsProviderResultFirst/Second and similarly use Awaited<ReturnType<typeof
pslConfig.contract.source.load>> for pslProviderResultFirst/Second and update
any diagnostics-related ReturnType references to point at .source.load as well)
so the types reflect the result of calling source.load().

In `@test/integration/test/cli.emit-command.test.ts`:
- Around line 568-570: The type annotation for sourceResult is stale because
providerConfig.contract['source'] is now an object with a load method; update
the declaration to use the load method's return type by replacing
ReturnType<NonNullable<typeof providerConfig.contract>['source']> with
ReturnType<NonNullable<typeof providerConfig.contract>['source']['load']> (keep
the NonNullable wrapper) so sourceResult correctly reflects the load result from
providerConfig.contract.source.load.

---

Nitpick comments:
In
`@packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts`:
- Around line 81-86: Remove the redundant runtime check for
contractConfig.source.load in loadConfig(): since validateContractConfig runs
ContractConfigSchema/ContractSourceProviderSchema and guarantees
contractConfig.source.load is a Function, delete the if (typeof
contractConfig.source?.load !== 'function') { throw ... } block; rely on
validateContractConfig/ContractConfigSchema for enforcement and keep the rest of
loadConfig() logic intact (referencing symbols: loadConfig,
validateContractConfig, ContractConfigSchema, ContractSourceProviderSchema,
contractConfig).

In
`@packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts`:
- Around line 152-179: The test currently only asserts ssrLoadModule was called
but must also assert the plugin registered file watchers from the module graph;
after calling configureServer(mockServer) add an assertion that
mockServer.watcher.add was invoked for the module file returned by
mockServer.moduleGraph.getModuleById (e.g. '/project/prisma-next.config.ts') —
use prismaVitePlugin, configureServer, and the mocked moduleGraph.getModuleById
to locate where watcher.add should be called and assert it was called (and
optionally called once).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: bcabb0ac-ebd7-4a51-ba2a-0bf1b27bfc21

📥 Commits

Reviewing files that changed from the base of the PR and between ba2cc16 and 9adefba.

📒 Files selected for processing (57)
  • docs/architecture docs/adrs/ADR 163 - Provider-invoked source interpretation packages.md
  • packages/1-framework/1-core/config/README.md
  • packages/1-framework/1-core/config/src/config-types.ts
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/1-framework/1-core/config/src/contract-source-types.ts
  • packages/1-framework/1-core/config/src/exports/config-types.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • packages/1-framework/1-core/config/test/config-validation.test.ts
  • packages/1-framework/3-tooling/cli/README.md
  • packages/1-framework/3-tooling/cli/recordings/fixtures/prisma-next.config.ts
  • packages/1-framework/3-tooling/cli/src/config-loader.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/contract-emit.test.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/config-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/mongo/src/config/define-config.ts
  • packages/3-extensions/mongo/test/config/define-config.test.ts
  • packages/3-extensions/postgres/src/config/define-config.ts
  • packages/3-extensions/postgres/test/config/define-config.test.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/prisma-next.config.ts
  • test/integration/test/authoring/cli.emit-parity-fixtures.test.ts
  • test/integration/test/authoring/psl.pgvector-dbinit.test.ts
  • test/integration/test/authoring/side-by-side-contracts.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-init/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-driver.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-sign/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-driver.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-verify.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/emit/prisma-next.config.emit.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-apply/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-plan/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/mongo-db-commands/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.async-source.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.mongo-contract-ts.ts
  • test/integration/test/fixtures/prisma-next.config.ts
  • test/integration/test/mongo/fixtures/prisma-next.config.ts
  • test/integration/test/sql-builder/fixtures/prisma-next.config.ts
✅ Files skipped from review due to trivial changes (8)
  • docs/architecture docs/adrs/ADR 163 - Provider-invoked source interpretation packages.md
  • packages/1-framework/3-tooling/cli/README.md
  • packages/1-framework/1-core/config/src/exports/config-types.ts
  • packages/3-extensions/postgres/test/config/define-config.test.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md
  • packages/1-framework/3-tooling/cli/src/config-loader.ts
  • packages/3-extensions/mongo/test/config/define-config.test.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/mongo/src/config/define-config.ts
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • packages/1-framework/1-core/config/test/config-validation.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts
  • packages/1-framework/1-core/config/README.md
  • packages/2-sql/2-authoring/contract-ts/src/config-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/postgres/src/config/define-config.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts

Comment thread packages/1-framework/3-tooling/cli/test/config-types.test.ts Outdated
Comment thread packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-psl/src/provider.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/1-framework/3-tooling/cli/test/control-api/client.test.ts (1)

467-499: ⚠️ Potential issue | 🟡 Minor

Duplicate of earlier test.

This test is effectively identical to the 'emits resolveSource and emit spans' case at lines 421-465 — both use createSourceProvider() with default load (returning ok(...)) and assert the same resolveSource span outcome. Since the provider-object shape is already the only supported shape (callable sources were removed), the "when source is a provider object" wording is redundant. Consider removing this test or, if the intent was to exercise a distinct scenario (e.g., different authoritativeInputs kind), adjust it to cover something not already covered by the first case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/cli/test/control-api/client.test.ts` around
lines 467 - 499, The test "emits resolveSource and emit spans when source is a
provider object" duplicates the earlier "emits resolveSource and emit spans"
case; remove this redundant test block (the it(...) that uses
createSourceProvider() and asserts resolveSource spans) to avoid duplicate
coverage, or if you prefer retaining a distinct case replace it with a test that
exercises a different provider behavior (e.g., pass a createSourceProvider()
that returns authoritativeInputs or a failing load) and assert the corresponding
span outcome; locate the duplicate by the test name and the use of
createSourceProvider(), client.emit(), and the resolveSource span assertions.
🧹 Nitpick comments (3)
packages/1-framework/1-core/config/src/config-types.ts (1)

89-114: Doc comment placement is now misleading.

The JSDoc block at lines 89-92 describes ContractConfigSchema, but two additional schemas (ContractAuthoritativeInputsSchema, ContractSourceProviderSchema) now sit between the comment and ContractConfigSchema. Either move the comment down to ContractConfigSchema (line 111) or add a short JSDoc per schema so the mapping is unambiguous.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/src/config-types.ts` around lines 89 -
114, The JSDoc currently placed above ContractAuthoritativeInputsSchema is meant
for ContractConfigSchema; either move that comment so it immediately precedes
ContractConfigSchema or add short JSDoc comments above each schema to clarify
intent — specifically update the block so ContractAuthoritativeInputsSchema,
ContractSourceProviderSchema and ContractConfigSchema each have unambiguous
documentation (or relocate the existing JSDoc directly above the
ContractConfigSchema declaration) to resolve the misleading placement.
packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts (1)

152-165: Redundant inner Set in filterOutputArtifacts.

new Set([...new Set(values)].filter(...)) wraps values in a Set, spreads to an array for filtering, then re-wraps. The outer Set already dedupes, so the inner one is pointless.

-    return new Set(
-      [...new Set(values)].filter((value) => value !== outputJsonPath && value !== outputDtsPath),
-    );
+    return new Set(
+      values.filter((value) => value !== outputJsonPath && value !== outputDtsPath),
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts`
around lines 152 - 165, filterOutputArtifacts currently wraps values in an
unnecessary inner Set before filtering; replace the inner dedupe with a direct
array filter and then dedupe once when constructing the returned Set.
Specifically, inside filterOutputArtifacts (which uses values, contractOutput,
DEFAULT_CONTRACT_OUTPUT, absoluteConfigPath, outputJsonPath, outputDtsPath)
remove new Set([...new Set(values)].filter(...)) and instead filter values
directly (values.filter(...)) and then wrap that filtered array in a single new
Set to deduplicate the results.
packages/1-framework/1-core/config/test/config-types.test-d.ts (1)

102-123: Type assertions largely restate the interface.

Because result.contract!.source is already typed as ContractSourceProvider, expectTypeOf(...source.load).toEqualTypeOf<ContractSourceProvider['load']>() and the kind union assertion are close to tautological: they check each field's declared type against itself rather than behavior of defineConfig. Consider instead asserting that the literal narrowing is preserved (e.g., constructing config with as const and asserting result.contract!.source.authoritativeInputs narrows to { kind: 'paths'; paths: ... }), or else asserting the input-shape is assignable to ContractSourceProvider via toExtend — those catch regressions the current assertions cannot.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/test/config-types.test-d.ts` around lines
102 - 123, The test's current type assertions merely restate the declared
ContractSourceProvider shape and don't verify that defineConfig preserves
literal narrowing; update the test to assert narrowing or assignability instead:
construct the config.authoritativeInputs with an explicit const assertion (e.g.,
use `as const` on the authoritativeInputs object) or build the entire config as
const, then assert that result.contract!.source.authoritativeInputs has the
narrowed literal type (exact `{ kind: 'paths'; paths: string[] }`) or use
expectTypeOf(result.contract!.source).toExtend<ContractSourceProvider>() to
confirm assignability; modify the assertions around defineConfig and result to
use these stricter checks so regressions in defineConfig's type inference are
caught.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts`:
- Around line 217-265: resolveWatchedFiles currently omits the config file when
authoritativeInputs.kind === 'paths' and also swallows the fact that loadConfig
failed in the catch; update the 'paths' branch so the returned files Set
includes both the resolved authoritativeInputs.paths (via filterOutputArtifacts)
AND absoluteConfigPath (so edits to the config trigger a re-emit), and change
the catch block to return the PARTIAL_COVERAGE_WARNING (or otherwise a non-null
warning) instead of null while still calling logError; refer to
resolveWatchedFiles, authoritativeInputs.kind === 'paths', absoluteConfigPath,
filterOutputArtifacts, loadConfig, logError, and PARTIAL_COVERAGE_WARNING to
locate where to apply these fixes.

---

Outside diff comments:
In `@packages/1-framework/3-tooling/cli/test/control-api/client.test.ts`:
- Around line 467-499: The test "emits resolveSource and emit spans when source
is a provider object" duplicates the earlier "emits resolveSource and emit
spans" case; remove this redundant test block (the it(...) that uses
createSourceProvider() and asserts resolveSource spans) to avoid duplicate
coverage, or if you prefer retaining a distinct case replace it with a test that
exercises a different provider behavior (e.g., pass a createSourceProvider()
that returns authoritativeInputs or a failing load) and assert the corresponding
span outcome; locate the duplicate by the test name and the use of
createSourceProvider(), client.emit(), and the resolveSource span assertions.

---

Nitpick comments:
In `@packages/1-framework/1-core/config/src/config-types.ts`:
- Around line 89-114: The JSDoc currently placed above
ContractAuthoritativeInputsSchema is meant for ContractConfigSchema; either move
that comment so it immediately precedes ContractConfigSchema or add short JSDoc
comments above each schema to clarify intent — specifically update the block so
ContractAuthoritativeInputsSchema, ContractSourceProviderSchema and
ContractConfigSchema each have unambiguous documentation (or relocate the
existing JSDoc directly above the ContractConfigSchema declaration) to resolve
the misleading placement.

In `@packages/1-framework/1-core/config/test/config-types.test-d.ts`:
- Around line 102-123: The test's current type assertions merely restate the
declared ContractSourceProvider shape and don't verify that defineConfig
preserves literal narrowing; update the test to assert narrowing or
assignability instead: construct the config.authoritativeInputs with an explicit
const assertion (e.g., use `as const` on the authoritativeInputs object) or
build the entire config as const, then assert that
result.contract!.source.authoritativeInputs has the narrowed literal type (exact
`{ kind: 'paths'; paths: string[] }`) or use
expectTypeOf(result.contract!.source).toExtend<ContractSourceProvider>() to
confirm assignability; modify the assertions around defineConfig and result to
use these stricter checks so regressions in defineConfig's type inference are
caught.

In `@packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts`:
- Around line 152-165: filterOutputArtifacts currently wraps values in an
unnecessary inner Set before filtering; replace the inner dedupe with a direct
array filter and then dedupe once when constructing the returned Set.
Specifically, inside filterOutputArtifacts (which uses values, contractOutput,
DEFAULT_CONTRACT_OUTPUT, absoluteConfigPath, outputJsonPath, outputDtsPath)
remove new Set([...new Set(values)].filter(...)) and instead filter values
directly (values.filter(...)) and then wrap that filtered array in a single new
Set to deduplicate the results.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: eccac6d8-050c-4bc2-8ab7-d7837ce7b8ea

📥 Commits

Reviewing files that changed from the base of the PR and between 9adefba and aa556f7.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • packages/1-framework/1-core/config/src/config-types.ts
  • packages/1-framework/1-core/config/src/contract-source-types.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • packages/1-framework/3-tooling/cli/src/commands/contract-emit.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/package.json
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/postgres/test/config/define-config.test.ts
  • test/integration/test/authoring/cli.emit-parity-fixtures.test.ts
  • test/integration/test/authoring/psl.pgvector-dbinit.test.ts
  • test/integration/test/authoring/side-by-side-contracts.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/cli.emit-contract.test.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/package.json
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • test/integration/test/authoring/side-by-side-contracts.test.ts
  • test/integration/test/authoring/psl.pgvector-dbinit.test.ts
  • test/integration/test/authoring/cli.emit-parity-fixtures.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • test/integration/test/cli.emit-command.test.ts

Comment thread packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts Outdated
jkomyno added 3 commits April 20, 2026 13:03
The SQL provider was updated to use context.configDir but the mongo
provider was missed, causing watcher/loader path divergence when cwd
differs from the config directory.
…onfigDir

- Switch config-loader from node:path to pathe for consistency with
  the rest of the tooling layer.
- Resolve .ts contract paths inside load() via context.configDir instead
  of capturing process.cwd() at config-definition time, so the watcher
  and loader always reference the same file.
The paths branch omitted the config file from the watch set, so edits
to prisma-next.config.ts (e.g. changing paths or swapping provider)
would not trigger re-emit. Also surface a partial-coverage warning when
loadConfig fails instead of silently falling back.
Comment thread packages/1-framework/3-tooling/cli/src/config-loader.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-types.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-types.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/1-framework/1-core/config/src/contract-source-types.ts Outdated
Comment thread packages/1-framework/1-core/config/src/contract-source-types.ts Outdated
Comment thread packages/3-extensions/postgres/src/config/define-config.ts Outdated
@wmadden
Copy link
Copy Markdown
Contributor

wmadden commented Apr 20, 2026

Local review artifact — System / Solution Design Review

Generated locally against branch tml-2276-authoritative-watch-inputs at 93007201. Paired with a separate "Code Review" comment. Links resolve to the PR branch at that SHA.

System / Solution Design Review — PR #356

Scope: origin/main...HEAD on branch tml-2276-authoritative-watch-inputs — PR #356 "Expose authoritative watch inputs from contract configs".

Spec: spec.md (inferred from Linear TML-2276, PR body, and docs landed on this branch).

1. Problem & new guarantees

The pre-change Vite plugin resolved "what to watch" by SSR-loading the user config and crawling Vite's module graph from the config entrypoint. That is authoritative only when the contract source is imported directly by the config file. Two first-class authoring flows break that assumption:

  1. PSL providers (prismaContract('./schema.prisma', …)) accept the schema as a string; the schema is never imported by the config.
  2. The @prisma-next/postgres / @prisma-next/mongo facade-style defineConfig({ contract: './prisma/contract.ts' }) loads the contract via a dynamic import(pathToFileURL(absolutePath).href) inside source.load(...). That dynamic import is evaluated lazily and never flows through Vite's config module graph.

The PR establishes a new invariant: the contract source provider owns its watch metadata. Tooling must not guess, it must read contract.source.authoritativeInputs and react accordingly. The three explicit strategies (moduleGraph, paths, configPathOnly) make the failure mode ("I can't tell you what to watch") an explicit, observable signal rather than a silent regression.

Additionally, emitted artifacts (contract.json + colocated contract.d.ts) are explicitly filtered out of the watch set by the plugin. The intent is to close any self-triggering feedback loop — but as discussed below and in F18, no currently-valid provider or config can actually route output paths into the watch set, so this filter is defensive-in-depth rather than load-bearing.

Protocol-level concern: static inputs vs. dynamic load

The new protocol has a structural asymmetry worth naming at the design level. source.authoritativeInputs is declared once at defineConfig() time (static data, captured in a closure), while source.load() is a dynamic function that can read any file it wants at runtime. Nothing in the protocol ties the two together, and load has no feedback channel — its success value returns a Contract, not the set of files it read.

This does not invalidate the design — for the simple common cases (single PSL schema, TS-first config whose contract is imported at the top of the config module), the static declaration is complete and accurate. It does, however, turn a class of cases into silent coverage gaps:

  • Facade .ts contracts that dynamic-import transitive TS files (today).
  • PSL with multi-file schemas, if the parser ever supports import directives (latent).
  • Any contract load that consults files based on runtime inputs (hypothetical today; plausible as providers evolve).

A clean resolution would be to let load report its observed reads back to the consumer — either by extending the Result success value with loadedFiles: readonly string[], or by passing a recordLoadedPath callback on the context. Consumers then treat authoritativeInputs as the initial watch set and reconcile against the observed set after the first successful load. This subsumes the facade-transitive-imports problem (described as a local bug in F03) as a protocol-level fix, and makes the framework neutral to however providers choose to implement load.

See code review F11 for the concrete proposal.

Protocol-level concern: the "self-loop guard" is a band-aid for API misuse

The plugin's filterOutputArtifacts silently removes contract.json and its colocated .d.ts from the watched set on every resolve. Tracing the two inputs the plugin actually receives:

  • moduleGraph mode crawls Vite's graph starting at the config file. Configs import authored source modules; they do not import generated output artifacts. The generated files are not in the config's subgraph under any supported authoring shape.
  • paths mode lets providers declare source inputs. No in-repo provider declares its output path. A third-party provider that did so would be misusing the protocol.

So the filter exists to rescue a provider that violates the "declare source files" contract, and the positive test for AC5 has to fabricate exactly that misuse to exercise the filter. A cleaner shape is to enforce the invariant at defineConfig / ContractConfigSchema validation time — reject any paths entry that resolves onto the declared contract.output (or its .d.ts sibling). The AC becomes a protocol property instead of a consumer-side filter, and the contrived test is replaced by a structural one ("this provider config fails to validate"). See code review F18.

2. Subsystem fit

This PR sits at the intersection of three subsystems:

  • Config authoring protocol (@prisma-next/config) — owns the types and schema for ContractConfig, ContractSourceProvider, and ContractAuthoritativeInputs. The new discriminated union is the tool-agnostic contract between authoring and tooling.
  • Contract source providers (@prisma-next/sql-contract-psl, @prisma-next/mongo-contract-psl, @prisma-next/sql-contract-ts, @prisma-next/mongo-contract-ts) — produce the provider objects and set their authoritativeInputs. Provider-invoked source interpretation (ADR 163) is preserved: the CLI remains source-agnostic.
  • Dev tooling (@prisma-next/vite-plugin-contract-emit) — consumes authoritativeInputs to build the watch set. The plugin still delegates emit to @prisma-next/cli's executeContractEmit.

Extensions (@prisma-next/postgres, @prisma-next/mongo) sit as thin facades over defineConfig + a family-specific provider. They produce the provider object inline for their .ts branch (because the facade itself owns the dynamic import).

Dependency direction is preserved: tooling → CLI → config → contract types. No back-edges are introduced.

3. Boundary correctness

Dependency graph

  • @prisma-next/config remains pure: no I/O, no tooling awareness. The provider protocol is expressed as types + arktype schemas, with validation delegated to ContractConfigSchema.
  • The Vite plugin imports @prisma-next/cli/config-loader and @prisma-next/cli/control-api — unchanged. It adds pathe as a direct dependency to resolve configDir consistently with the CLI.
  • Providers (sql-contract-psl, mongo-contract-psl) gained a dependency on context.configDir for path resolution. They no longer implicitly depend on process.cwd(). This is correct: providers are invoked from multiple sites (CLI emit, vite plugin, integration tests) with different working directories, and relying on cwd was a latent correctness bug.

Plane/layer discipline

  • The ContractAuthoritativeInputs schema lives in config-types.ts (core plane), not in the Vite plugin. Good — the plugin is just one consumer of the metadata.
  • Extension-facade .ts branches in packages/3-extensions/postgres/src/config/define-config.ts and packages/3-extensions/mongo/src/config/define-config.ts construct a provider object inline rather than calling typescriptContract(...). That's because the facade owns the dynamic-import step, so it has to declare paths instead of moduleGraph. This is a correct boundary — the facade is the only site that knows the contract file is loaded via dynamic import rather than a static top-of-file import.

Determinism

  • configDir propagation is the critical correctness hook: providers resolve ./schema.prisma the same way whether they run from CLI, Vite, or a test-monorepo chdir. Every test and integration call site that builds a ContractSourceContext was updated to pass configDir.
  • Artifact path filtering uses resolve(configDir, contract.output) in the plugin, mirroring the same logic in executeContractEmit. If the filter is kept (see F18 for the alternative), this mirroring is what makes the two sites agree on which files are outputs.

4. ADRs

No new ADR is added. One existing ADR is touched:

Gap: the PR arguably warrants a new ADR (or an extension of ADR 032 "Dev Auto-Emit Integration") documenting the moduleGraph / paths / configPathOnly split as the canonical contract between provider authors and dev tooling. Today that contract is documented only in README prose. See code review finding F01.

5. Test strategy at the architectural level

What must be proven

For each acceptance criterion:

  • AC1 (built-in PSL providers expose paths): unit tests at the provider package level prove authoritativeInputs === { kind: 'paths', paths: [schemaPath] }.
  • AC2 (config loading surfaces metadata for both moduleGraph and paths): CLI-loader tests that round-trip a config file and inspect authoritativeInputs on the loaded result.
  • AC3 (custom providers can declare additional watch inputs): type-level + validation proof that authoritativeInputs.paths: string[] is accepted by ContractConfigSchema.
  • AC4 (fallback + warning for configPathOnly): Vite-plugin test that mocks a loaded config with authoritativeInputs: { kind: 'configPathOnly' } and asserts the watcher adds only the config path and console.warn receives the partial-coverage message.
  • AC5 (artifacts excluded from watch set): Vite-plugin test that declares paths including contract.json / contract.d.ts and asserts watcher.add is not called with those.

What is covered

Gaps

  • No E2E test for dev watch retriggering. Every verification of the core user-visible behavior ("editing ./prisma/schema.prisma during vite dev triggers re-emit") is done via unit mocks of loadConfig and executeContractEmit. There is no end-to-end test that spins up a real Vite dev server, modifies a real file, and asserts the emit side effect. For a feature whose raison d'être is the April VP3 stop condition, that gap is significant.
  • No integration test for the extension-facade .ts branch. The @prisma-next/postgres / @prisma-next/mongo facade .ts branches construct the provider inline with a dynamic-import inside load. The existing unit test covers only authoritativeInputs shape — it does not prove that the inline provider actually loads and returns the contract when load is invoked.
  • No negative test for malformed provider metadata. Because c12 silently round-trips whatever the config module exports, a custom-provider exporting source: { authoritativeInputs: {...}, load: notAFunction } is caught only at emit time, not at watch-setup time. The partial-coverage fallback hides the root cause (see finding F06).
  • No proof that the configDir change reaches every call site. The PR updated every test fixture, but a static check that ContractSourceContext is always constructed with configDir is not present. A new context constructor (or a required factory) would be more robust than an interface field; see finding F03.

6. Risk & rollout

  • Breaking change magnitude: every user config file must migrate from contract.source: async () => … to contract.source: { authoritativeInputs, load }. The PR modernizes every in-repo fixture, so no in-repo caller is left behind. External users will see Error: Config validation failed: source must be an object … at defineConfig() time — a clean, structured failure.
  • Partial coverage: configPathOnly is a valid deliberate choice (the custom provider opts into "just watch the config"), but it is also the fallback path when loadConfig throws inside the plugin (plugin.ts L261–267 — lines 261–267). Collapsing those two distinct conditions into a single warning message that blames the provider is confusing (F06).
  • Security: no new attack surface. Providers still execute arbitrary code inside load; the new metadata is pure data.
  • Performance: updateWatchedFiles re-invokes loadConfig after every successful emit to rediscover the watched set. For PSL-backed configs with kind: 'paths', the set is static — re-running c12 on every emit is unnecessary work (F07).

7. Overall verdict

Architecturally sound. The PR converts a leaky heuristic (module-graph crawling) into an explicit, tool-agnostic metadata protocol, with clean provider-owned ownership. The discriminated-union design handles the three cases that actually occur in practice, and the "partial coverage" escape hatch is honest about what dev tooling can and cannot do.

The principal architectural concern is test-strategy depth: the critical user-visible behavior (PSL edit → re-emit during vite dev) is verified entirely via unit mocks. At least one integration test that actually exercises the Vite dev server lifecycle would close the largest remaining confidence gap.

@wmadden
Copy link
Copy Markdown
Contributor

wmadden commented Apr 20, 2026

Local review artifact — Code Review

Generated locally against branch tml-2276-authoritative-watch-inputs at 93007201. Paired with a separate "System Design Review" comment. Links resolve to the PR branch at that SHA. Findings are numbered F01F18; deferred items are D1D2.

Code Review — PR #356

Scope: origin/main...HEAD on branch tml-2276-authoritative-watch-inputs — PR #356 "Expose authoritative watch inputs from contract configs".

Spec: spec.md (inferred from Linear TML-2276 and the PR body; see that document for the full acceptance-criteria list).

Summary

The PR converts ContractConfig.source from a bare async callable into a provider object with authoritativeInputs (discriminated-union metadata tooling can consume) and load (the existing resolve step). The Vite plugin now reads that metadata and branches between moduleGraph, paths, and configPathOnly strategies, filters emitted artifacts out of the watch set, and warns on partial coverage. ContractSourceContext gains a configDir so providers resolve relative paths deterministically.

What looks solid

  • Clean protocol split. Pulling the watch strategy out of the load call and into sibling metadata cleanly separates "how do I build the contract?" from "what do I watch to know when to rebuild?". That decoupling is what makes the PSL-first and facade cases both representable.
  • Arktype-driven validation. ContractAuthoritativeInputsSchema uses type.unit for the kind discriminants, so a custom provider exporting { kind: 'paths' } without paths or { kind: 'foo' } fails at defineConfig() time with a structured, field-accurate message — validated by the new negative tests in packages/1-framework/1-core/config/test/config-validation.test.ts (L346–367) — lines 346–367.
  • Artifact filter has a focused test. filterOutputArtifacts has a dedicated unit test (plugin.test.ts L181–217 — lines 181–217). Whether the filter is necessary at all is a separate question — see F18.
  • configDir propagation. ContractSourceContext.configDir plus the uniform use of pathe.resolve(context.configDir, schemaPath) in both PSL providers removes an implicit dependency on process.cwd() that was a latent correctness hazard under integration tests that chdir away from the config. The new "resolves relative schema paths from configDir when cwd differs" test proves it (provider.test.ts L69–98 — lines 69–98).
  • Fixture migration is complete. A repo-wide grep for the old contract.source( callable pattern returns no matches; every fixture (internal packages, CLI recordings, integration tests) was updated in this PR.
  • Last-change-wins semantics preserved. The existing AbortController + emitRequestId dance in emitContract was not disturbed; only the watch-resolution step was refactored.

Findings

F01 — Watch-strategy protocol is only documented in README prose

packages/1-framework/1-core/config/README.md — lines 55–58

Issue. The three-way split (moduleGraph / paths / configPathOnly) is the new public contract between provider authors and dev tooling. It deserves an ADR so future provider authors and tooling (e.g. a webpack or Turbopack plugin) have an authoritative reference. The README paragraph is easy to miss and gives no guidance on when to pick which. ADR 032 already covers dev auto-emit at a high level; an extension there (or a new ADR) would capture the metadata protocol, its invariants, and the partial-coverage contract.

Suggestion. Add an ADR covering:

  • the three discriminants and their intended semantics;
  • that outputs must be excluded from watch inputs (AC5 as a system-level invariant, not a plugin implementation detail);
  • the fallback rule + warning when a provider declares configPathOnly;
  • expected tooling behavior when load fails.

F02 — configPathOnly warning wording misfires on loadConfig failure

packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 12–14, 261–267

Issue. resolveWatchedFiles has a try/catch that wraps the entire flow — including loadConfig(absoluteConfigPath). When loading the config throws (missing file, syntax error, validation error), the catch branch returns the same PARTIAL_COVERAGE_WARNING string used for configPathOnly, which literally says:

"Contract source provider declared configPathOnly. Watching only the config file; dev watch coverage is partial until authoritative paths or moduleGraph are declared."

When the real failure is "your config file couldn't be loaded", this message misleads the user into looking at their provider metadata. The actual error is logged via logError('Failed to resolve watched files:', error), but the visible warning banner blames the wrong thing.

Suggestion. Distinguish the two cases. E.g.:

const CONFIG_LOAD_FALLBACK_WARNING =
  'Failed to load prisma-next config; watching only the config file until it can be loaded. Dev watch coverage is partial.';

// in catch:
return { files: new Set([absoluteConfigPath]), warning: CONFIG_LOAD_FALLBACK_WARNING };

Alternatively, surface a structured warning object ({ reason: 'config-load-failed' | 'config-path-only', detail?: string }) so tests can assert the cause rather than grep substrings.

F03 — Provider-authored .ts facade only watches the contract file itself, not its imports

packages/3-extensions/postgres/src/config/define-config.ts — lines 36–57
packages/3-extensions/mongo/src/config/define-config.ts — lines 29–50

Issue. When a user writes defineConfig({ contract: './prisma/contract.ts' }), the facade declares authoritativeInputs: { kind: 'paths', paths: [options.contract] }. That watches the contract file itself — but the contract file typically has transitive TS imports (shared type helpers, enum definitions, contract fragments colocated in the repo). A developer editing ./prisma/types/user.ts that contract.ts imports will not see a re-emit.

This is a direct regression from the moduleGraph path, which does crawl transitive imports. The regression is inherent to the fact that the facade loads the contract via dynamic import(...) inside load, which is why kind: 'paths' is the only option that surfaces anything useful at all. But the user who wrote the facade call has no idea their edits to adjacent .ts files are not watched.

Suggestion. Either

  • document this clearly in the postgres / mongo extension README ("the .ts facade only watches the contract entry file; for transitive TS imports use a TS-first config that imports the contract module directly"), or
  • let the facade emit a configPathOnly-style dev warning when it detects a facade-authored .ts contract, or
  • (future) introduce a kind: 'moduleGraph+paths' hybrid where tooling crawls Vite's module graph starting from a declared entry file after its first load call.

Pick one explicitly. As landed, the behavior is a silent silent coverage regression for facade-style .ts users, directly at odds with the spirit of AC4 ("emits a dev warning that coverage is partial").

F04 — ContractSourceContext context construction is a field-by-field contract

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 39–46

Issue. Making configDir a required field means every call site that constructs a ContractSourceContext had to be updated by hand. The PR touches 10+ such sites. Any future site that forgets a field silently falls back to process.cwd() or breaks a provider.

Suggestion. Expose a single factory (e.g. in @prisma-next/cli/control-api or @prisma-next/config):

export function createContractSourceContext(args: {
  configDir: string;
  stack: ControlStack;
}): ContractSourceContext;

The CLI executeContractEmit, the ControlClientImpl.emit, and test helpers would all use this; new call sites can't forget fields because they exist in one place. The 10+ inline constructions duplicated today become one source of truth.

F05 — prismaContract diagnostic sourceId inconsistency with mongoContract

packages/2-sql/2-authoring/contract-psl/src/provider.ts — lines 47–63
packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts — lines 27–43

Issue. In the read-failure diagnostic:

  • SQL provider uses sourceId: absoluteSchemaPath (absolute).
  • Mongo provider uses sourceId: schemaPath (relative, exactly as passed by the user).

In the parse step, both use the relative schemaPath. This mismatch means downstream diagnostic consumers (IDE extensions, agents) see different path formats for the same conceptual event across families. Tests encode the current behavior (SQL matches a trailing-regex, Mongo matches the exact string), so this is not a test failure — but it's a protocol inconsistency.

Suggestion. Pick one convention (I recommend the relative schemaPath as the user wrote it; use meta.absoluteSchemaPath for the resolved form, which both providers already do) and align both families.

F06 — updateWatchedFiles re-invokes loadConfig on every successful emit

packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 217–307

Issue. After every successful emit, updateWatchedFilesresolveWatchedFiles calls loadConfig(absoluteConfigPath) again. c12 re-compiles TypeScript on each call. For kind: 'paths' providers the watched set is fully static (it's just [configPath, ...paths]); there is nothing transitive to rediscover after a schema edit. For kind: 'moduleGraph' the module graph crawl is the only thing that can change, and that does not require re-reading the config file (Vite already has the graph in memory).

In a fast-edit loop this means every keystroke's worth of debounced emit runs a c12 compile that can take hundreds of milliseconds — potentially queueing behind the next debounced emit and extending the observed "time to contract.json" latency.

Suggestion. Cache the authoritativeInputs at server start and only re-run loadConfig when the config file itself changes. For kind: 'moduleGraph', just re-crawl Vite's module graph after each emit without re-loading the user config.

F07 — handleHotUpdate uses strict equality, missing alias/symlink hits

packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 381–387

Issue. watchedFiles.has(ctx.file) requires exact string equality. If Vite normalizes the hot-update file differently from how pathe.resolve produced the watched-set entries (e.g. case folding on case-insensitive filesystems, pnpm symlinked workspaces, .. segments), legitimate edits won't match. chokidar/Vite paths and pathe.resolve output both use forward slashes, but realpath vs aliased path differences are possible.

Suggestion. At minimum, normalize both sides through pathe.resolve before comparison:

handleHotUpdate(ctx) {
  const normalized = resolve(ctx.file);
  if (watchedFiles.has(normalized)) {
    ...
  }
}

Consider also adding a debug log for "hot update received for untracked file" to make future failures easier to diagnose.

F08 — contract-emit.ts uses join where filterOutputArtifacts uses resolve

packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts — lines 88–95
packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 152–165

Issue. The CLI emit operation uses join(configDir, contractConfig.output) while the Vite plugin's artifact filter uses resolve(configDir, contractOutput). configDir is always absolute at both sites, so the two calls produce the same path today. But the two sites must agree exactly on the artifact path (that's what makes the self-loop guard work), and using two different path-joining primitives invites future drift. A future configDir that is ever relative (say, from a refactor) would silently diverge.

Suggestion. Extract a shared helper (e.g. deriveContractArtifactPaths(configDir, contractOutput)) in @prisma-next/cli that both sites call. Keeps the invariant "CLI emit writes here ⇔ plugin filters out here" in one location.

F09 — Default contractOutput fallback silently uses the generic default path

packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 152–165

Issue. filterOutputArtifacts uses resolve(configDir, contractOutput ?? DEFAULT_CONTRACT_OUTPUT). After defineConfig() normalization, contract.output is always set, so the fallback is only reached for raw object configs that bypass defineConfig(). If such a config has no output, CLI emit will throw (Contract config output path must end with .json), but the plugin's filter silently assumes src/prisma/contract.json — so the watched-set filter may exclude a path that the CLI never writes.

Suggestion. Read contract.output from the already-normalized config (same loadConfig call) without a default; if absent, skip filtering rather than fabricating a path. Or document that the plugin assumes a normalized config and let the ?? branch go away.

F10 — prismaContract options validation is loose for composedExtensionPackRefs

packages/2-sql/2-authoring/contract-psl/src/provider.ts — lines 74–86

Issue. options.composedExtensionPackRefs?.length ? options.composedExtensionPackRefs : undefined uses truthy-ness on length. A length-0 array and an undefined argument are treated the same (both resolved to undefined), which is intentional — but the expression conflates "not provided" with "provided as empty", which may hide author errors (passing an empty array when they meant to pass a populated one).

Suggestion. Stylistic — prefer Array.isArray(options.composedExtensionPackRefs) && options.composedExtensionPackRefs.length > 0 to make intent explicit. Low impact.

F11 — Static authoritativeInputs can silently diverge from what load() actually reads

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 48–65
packages/3-extensions/postgres/src/config/define-config.ts — lines 44–54

Issue. The protocol has a structural asymmetry: source.authoritativeInputs is declared once (static data captured at defineConfig() time), while source.load() is a dynamic function that can read any files it chooses at runtime. Nothing in the protocol ties the two together, and there is no feedback channel from load back to the consumer.

This is a protocol-level concern, not a local bug — every provider is exposed to it. Concrete manifestations:

  • Facade .ts transitive imports (current, not hypothetical). The postgres / mongo facade's load does import(pathToFileURL(absolutePath).href), and Node resolves every transitive TS import. Only the entry file is declared in paths; the transitive set is silently invisible to the watcher. Finding F03 describes this as a facade-local bug, but it is really this protocol gap hitting that code path.
  • PSL multi-file schemas (latent). Today prismaContract reads exactly one file (provider.ts L40–58 — lines 40–58). If the parser ever gains multi-file semantics (PSL import directives, schema splits, shared fragments), the declared paths: [schemaPath] will become a lie with no affordance in the protocol for the parser to report "I also read X".
  • New files added during dev. A user who adds a new import to ./prisma/contract.ts has to restart vite dev for the watcher to pick up the new file, even though load would see it on the next invocation.
  • No post-load reconciliation. load returns Result<Contract, Diagnostics> — there is no loadedFiles: readonly string[] in the success value, so even in principle the watcher cannot self-correct after the first load.

Suggestion. Add a feedback channel so the actual set observed during load can refine the declared set. Two shapes worth considering:

  • Extend the success value of load: Result<{ contract: Contract; loadedFiles: readonly string[] }, ContractSourceDiagnostics>. The plugin merges loadedFiles into the watch set after the first successful load and warns if it diverges meaningfully from authoritativeInputs.
  • Or pass a recordLoadedPath(path: string): void callback on ContractSourceContext. Minimally invasive; load calls it for each file it reads; the plugin updates the watcher.

Either approach keeps authoritativeInputs as the initial watch set (what you can declare up front) and treats the observed set as a runtime refinement. This is the right answer for the PSL-imports-in-future and facade-transitive-imports cases; it would subsume F03 as a protocol-level fix rather than per-facade documentation.

F12 — Discriminated union mixes source declaration with tooling strategy

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 48–58

Issue. ContractAuthoritativeInputs has three variants: moduleGraph, paths, configPathOnly. But only paths is an actual source declaration. The other two are instructions to the consumer:

  • moduleGraph tells a Vite-shaped consumer "crawl from the config entrypoint" — but Vite already watches its own module graph without being told to. For non-Vite consumers the instruction is uninterpretable.
  • configPathOnly is the "I have nothing to give you, please fall back" sentinel. That is semantically identical to paths: [] plus an obligation on the consumer to warn.

Collapsing the union to paths: readonly string[] would express the same information without baking Vite-centric strategies into the framework config:

  • TS-first config exports paths: [] (the config's module graph is whatever the build tool chooses to watch; the framework does not prescribe).
  • PSL: paths: ['./schema.prisma'].
  • Custom provider with no knowledge: paths: []. The consumer chooses whether to warn (Vite plugin does, a future webpack plugin may not).

The current shape leaks tooling semantics (moduleGraph, configPathOnly) into @prisma-next/config, which is otherwise tooling-agnostic. See Will's review comment 7 for the original observation.

Suggestion. Collapse to sources: readonly string[] (see also F13 on naming) at the config layer. Leave moduleGraph as an implicit behavior of the Vite plugin (which already crawls the module graph when the plugin is loaded) rather than a declarative flag. Move any "warn if nothing was declared" policy into the plugin.

F13 — "authoritativeInputs" naming is still watcher-coupled

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 48–65

Issue. An earlier iteration of the PR called this field watchInputs / watchStrategy; Will's review summary pushed back that the framework should not be aware of watcher integrations and suggested consumer-agnostic naming like sources or sourceFiles. The rename to authoritativeInputs softens the coupling but keeps it — "authoritative" implies "authoritative for something", which in practice means "for the watcher to consume". See also comment 8: "What does 'authoritative inputs' mean? How is it different from just 'inputs' or 'paths'?"

Suggestion. Rename to sources: readonly string[] (paired with F12's collapse), or at minimum sourceFiles. The documentation would read: "the files this provider reads to compute the contract". Consumers (watchers, build tools, bundlers, CI cache-invalidation) then interpret that declaration as they see fit.

F14 — ContractSourceContext.configDir leaks file I/O into a previously I/O-agnostic interface

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 39–46
packages/3-extensions/postgres/src/config/define-config.ts — lines 44–54

Issue. Before this PR, ContractSourceContext carried only domain concerns: composedExtensionPacks, scalarTypeDescriptors, authoringContributions, codecLookup, controlMutationDefaults. Adding configDir: string is the first file-system concept in the interface. Its only consumers are PSL providers' and facade's load functions, which use it to resolve closure-captured relative paths (resolve(context.configDir, schemaPath)). See Will's comment 13 ("this interface was completely agnostic of file I/O") and comment 15 ("configDir should be available in the calling context, it doesn't need to be exposed via ContractSourceContext").

Alternative placements worth considering:

  1. Pre-resolve at the CLI boundary. The CLI / plugin could absolutize authoritativeInputs.paths before handing them to the watcher and pass absolute paths into a refactored provider closure. This works for authoritativeInputs but not for load's own I/O — load still needs a base directory to resolve its internally-captured paths, so configDir (or absolute paths) still has to arrive there somehow.
  2. Provider factory pattern. The user's config calls a factory prismaContract(schemaPath, options) that the CLI re-invokes with configDir at load time, returning a closed-over provider with absolute paths baked in. This is a bigger refactor and makes every provider a two-phase construct.
  3. Keep configDir in the context but name the leak. Accept that contract resolution is file-I/O-coupled by nature and document that ContractSourceContext is a mixed-concern bag.

None of these is obviously right. The current design works; it just invites the architectural question Will is raising.

Suggestion. Either (a) split ContractSourceContext into ContractSourceDomainContext (the old fields) + a thin ContractSourceFileSystemContext ({ configDir }) and have load take both, making the file-I/O dependency explicit; or (b) accept option 3 and add a short comment on configDir stating why it lives here rather than in the calling site.

F15 — ContractAuthoritativeInputs type and schema live in different files

packages/1-framework/1-core/config/src/contract-source-types.ts — lines 48–58
packages/1-framework/1-core/config/src/config-types.ts — lines 93–104

Issue. The TypeScript type ContractAuthoritativeInputs is hand-written in contract-source-types.ts. The arktype schema ContractAuthoritativeInputsSchema is hand-written in config-types.ts. They encode the same discriminated union by hand in two places, in two files. See Will's comment 14 ("why is this schema redeclared?") — in the current layout it isn't strictly redeclared, but the two declarations are a maintenance hazard.

Arktype's idiomatic pattern is type.infer<typeof Schema>:

export const ContractAuthoritativeInputsSchema = type.or(
  { kind: type.unit('moduleGraph') },
  { kind: type.unit('configPathOnly') },
  { kind: type.unit('paths'), paths: 'string[]' },
);
export type ContractAuthoritativeInputs = typeof ContractAuthoritativeInputsSchema.infer;

Suggestion. Co-locate the type and schema (one file), derive the type from the schema, delete the hand-written union. This also fixes the type-vs-schema drift risk: the union currently orders variants moduleGraph | paths | configPathOnly in the type but moduleGraph | configPathOnly | paths in the schema — cosmetic today, easy to forget to keep aligned later.

F16 — normalizeContractForValidation is conditional on a required field

packages/1-framework/1-core/config/src/config-validation.ts — lines 15–25

Issue. The schema (ContractConfigSchema) declares source as required. normalizeContractForValidation nevertheless handles the case where source is absent or not an object:

const source = Object.hasOwn(contract, 'source') ? contract['source'] : undefined;
return {
  ...contract,
  ...(source && typeof source === 'object'
    ? { source: { ...(source as Record<string, unknown>) } }
    : {}),
};

Two concerns (both raised by Will — comment 10 and comment 9):

  1. Purpose is undocumented. The normalization appears to exist solely to shallow-clone source (possibly to defeat Proxy wrappers, readonly views, or non-enumerable keys from defineConfig). That intent is not stated anywhere; a reader has to infer it from the shape of the code.
  2. The conditional is dead-ish code. If source is missing or non-object, arktype will produce a structured error for the caller (contract.source required / must be an object) — we don't need to pre-process around it. The branch only exists because the helper tries to be safe against shapes that the schema will immediately reject.

Suggestion. Either (a) drop the helper entirely and pass contract straight to ContractConfigSchema, letting arktype own the error paths; or (b) keep the clone but make it unconditional, and add a short comment explaining why the shallow-clone is needed (so future readers don't strip it). If ifDefined or a dedicated "clone if present" helper exists in @prisma-next/utils, use it for the minor residual case.

F17 — ValidationProblemLike hand-rolls arktype's error type

packages/1-framework/1-core/config/src/config-validation.ts — lines 9–13, 32–37

Issue. The local interface ValidationProblemLike is a subset of arktype's own validation-error type (accessible as type.errors[number] or via ArkErrors). Raised by Will in comment 11. The consumer code then treats every field as optional:

const firstProblem = validated[0] as ValidationProblemLike | undefined;
const fieldSuffix = firstProblem?.propString ?? '';
const field = fieldSuffix.length > 0 ? `contract.${fieldSuffix}` : 'contract';
const problem = firstProblem?.problem ?? firstProblem?.message;
throwValidation(field, problem ? `Config.${field} ${problem}` : undefined);

Two sub-issues (see also Will's comment 12 "why is so much of this conditional?"):

  • If arktype does export the validation-error type (in recent versions it does — type.errors, ArkErrors), the hand-rolled interface risks drift and hides a direct dependency on arktype's shape.
  • Even if the hand-rolled shape is correct, the defensive ?. chains handle cases that arktype does not actually produce for object-schema errors in practice (arktype always populates message; propString is empty for root-level errors, not undefined). A lot of the ?? '' / ?? message defensiveness is for shapes we never see.

Suggestion. Import and reuse the arktype-exported error type (whichever name is current for the pinned version). Simplify the downstream code: propString is always a string; message is always present. A single branch on propString === '' (root vs. nested) is enough. If arktype's type is not exported in the pinned version, add a comment saying so and pin the hand-rolled shape to the arktype error shape of that version explicitly.

F18 — "Self-loop guard" is defensive against a case that cannot occur in valid configurations

packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts — lines 152–165, 235–252
packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts — lines 181–217

Issue. filterOutputArtifacts removes contract.json and colocated .d.ts from the watched set on every call to resolveWatchedFiles. Tracing the actual code paths, neither file can legitimately arrive in the pre-filter set:

  • moduleGraph mode (collectModuleGraphFiles — lines 177–215) crawls Vite's module graph starting at absoluteConfigPath. The config transitively imports authored source modules (the user's ./prisma/contract.ts, @prisma-next/postgres, etc.). The generated artifacts (./prisma/contract.json, ./prisma/contract.d.ts) are output files the user's application code may consume — they are never imported by the config itself. Unless the user writes a pathological import './prisma/contract.json' with { type: 'json' } inside prisma-next.config.ts, the generated files cannot appear in the config's subgraph.
  • paths mode exists for providers to declare their source inputs. PSL: paths: [schemaPath]. Facade: paths: [options.contract]. A well-behaved provider has no reason to list its own output. The only way contract.json / contract.d.ts enters the set is if a provider declares them — which is misuse of the API.

The unit test (plugin.test.ts L181–217 — lines 181–217) fabricates a provider that explicitly declares ./src/prisma/contract.json and ./src/prisma/contract.d.ts alongside ./prisma/schema.prisma in order to prove the filter works. No real-world provider produces that shape.

So the filter is a defensive band-aid. It silently rescues a provider that violates the "declare source files" contract rather than surfacing the violation.

Suggestion. Two ways to eliminate the band-aid:

  1. Enforce at validation. In defineConfig() / ContractConfigSchema, reject provider configs where authoritativeInputs.paths (post-resolve against configDir) intersects the derived output paths. A clean Error: contract.source.paths includes the contract output path … is much more useful than silent filtering.
  2. Leave the guard but name its purpose. Keep the filter as defense-in-depth against buggy custom providers, but rename it (e.g. guardAgainstOutputLoop) and add a comment explaining why it exists — so future readers do not interpret it as load-bearing.

Either is preferable to the current shape: silent filtering plus a contrived unit test that documents misuse rather than real behavior. This also affects how AC5 reads — see the AC5 verification row below.

Relatedly, filterOutputArtifacts uses resolve(configDir, contractOutput ?? DEFAULT_CONTRACT_OUTPUT). The fallback path (F09) becomes moot under option 1 and is still live under option 2; either way the default-output fabrication should be removed.

Deferred (out of scope)

D1 — No end-to-end test for the VP3 stop condition

This is the most important functional question the PR answers ("editing PSL during vite dev triggers a re-emit that the framework picks up"), and the entire verification chain is unit-level with mocks. Writing a real Vite dev-server integration test is expensive (process lifecycle, temp-dir scaffolding, chokidar timing) and arguably belongs to the umbrella VP3 milestone testing strategy rather than this PR. Flagging it as a milestone-level follow-up rather than a PR blocker.

D2 — Provider authoring docs outside this repo

The feature introduces a public API that ecosystem provider authors will consume. A dedicated "writing a custom provider" doc is worth having, but sits outside the scope of "expose authoritative watch inputs" — defer to a follow-up docs PR.

Acceptance-criteria verification

Acceptance criteria are from the Linear ticket TML-2276, reproduced in spec.md.

AC Verdict Detail
AC1: Built-in PSL-first providers expose their authoritative contract source paths as watch inputs PASS SQL: provider.ts L36–39 sets { kind: 'paths', paths: [schemaPath] } and provider.test.ts L50–53 asserts that exact shape. Mongo: same pattern in provider.ts L16–19; new dedicated test provider.test.ts asserts the full authoritativeInputs shape.
AC2: Config loading returns enough metadata for the Vite plugin to watch both config-driven inputs and Vite-imported TS inputs PASS config-loader.test.ts L72–148 round-trips paths, moduleGraph, and configPathOnly metadata through loadConfig and asserts the values. The Vite plugin reads these via plugin.ts L231–255 and branches on kind. Plugin tests (plugin.test.ts L152–217) verify both the moduleGraph crawl path and the explicit paths path add the right files to the watcher.
AC3: Custom providers can declare additional watch inputs explicitly PASS ContractConfigSchema in config-types.ts L93–114 accepts any provider object with a paths: string[] array. config-validation.test.ts L346–388 asserts that malformed shapes (missing paths, unknown kind, wrong types) throw structured errors. The type-level config-types.test-d.ts L102–123 proves the public type accepts a user-authored { kind: 'paths', paths: [...] } provider.
AC4: If custom providers omit watch inputs, the system falls back to watching the config path and emits a dev warning that coverage is partial PASS plugin.ts L257–260 returns files: new Set([absoluteConfigPath]) with PARTIAL_COVERAGE_WARNING when authoritativeInputs.kind === 'configPathOnly'. plugin.test.ts L271–295 asserts both that only the config path is added to the watcher and that console.warn is invoked with a message containing 'configPathOnly'. Note: finding F02 points out that the same warning text is also emitted when loadConfig itself fails, which confuses the signal but does not contradict the AC.
AC5: Output artifact paths are not reported as source watch inputs PASS (consumer-side only) Satisfied by filterOutputArtifacts (plugin.ts L152–165) silently dropping the output paths from the watched set. However, the AC is stated as a system property (outputs never flow in as inputs). In current code this is enforced only at the consumer — no provider today emits output paths, no validation rejects them if one did. The positive test (plugin.test.ts L181–217) documents that the filter fires but also demonstrates that the protocol permits a provider to declare outputs as inputs without error. See F18 for the recommendation to enforce at validation time.

Summary

Result Count ACs
PASS 5 AC1, AC2, AC3, AC4, AC5
FAIL 0
NOT VERIFIED 0
WEAK 0

All acceptance criteria are verified by tests that actually assert the required behavior. The architectural gap is the absence of an end-to-end test that exercises a real Vite dev server lifecycle (covered as D1). Unit-mock verification is sufficient for AC pass, but not for high confidence against the broader VP3 stop condition.

jkomyno added 8 commits April 20, 2026 16:03
Close D1 from the PR #356 review by driving a real Vite dev server
through both scenarios that were previously only unit-mocked:
modifying `contract.prisma` re-emits the contract, and editing the
config to swap the declared PSL input re-emits against the new file.
Align with the Mongo PSL provider and with the parser step's sourceId.
The absolute form stays accessible via meta.absoluteSchemaPath for
downstream consumers that need it.
Collapse the ~20-line inline provider that postgres and mongo facades
each build around a dynamic import of the user's `contract.ts`. The
logic (path resolution, pathToFileURL, default-export extraction) is
family-agnostic, so each contract-ts package exposes it as a sibling
to typescriptContract.
- handleTrackedFileChange now runs ctx.file through pathe.resolve
  before comparing against watchedFiles / ignoredOutputFiles, so
  symlinked or case-folded paths line up with the resolved entries.
- resolveContractOutputFiles no longer falls back to the hardcoded
  'src/prisma/contract.json' when contract.output is missing. The
  filter simply skips; defineConfig() guarantees output is present
  in the happy path, and raw-object configs get a clean no-op instead
  of a path the CLI never actually writes.
…ceContext

Left over from the split that moved configDir out of ContractSourceContext
into ContractSourceEnvironment — the context builder no longer needs it.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
test/integration/test/vite-plugin.hmr.e2e.test.ts (2)

108-113: Duplicated module-graph invalidation pattern — consider extracting a helper.

The getModulesByFile(...) + for-loop invalidateModule(...) block is repeated verbatim in two tests (and the change emit follows each). A small helper like invalidateAndEmitChange(server, filePath) would reduce duplication and clarify intent. Optional; not blocking.

Also applies to: 252-257

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/vite-plugin.hmr.e2e.test.ts` around lines 108 - 113,
Extract the repeated pattern that calls
server.moduleGraph.getModulesByFile(filePath) and then loops calling
server.moduleGraph.invalidateModule(module) followed by emitting the change into
a small helper (e.g., invalidateAndEmitChange(server, filePath)); implement the
helper so it retrieves modules for the given filePath, invalidates each via
invalidateModule, and then performs the same "change" emit used in the tests,
then replace both duplicated blocks (the one around contractPath and the one at
the other occurrence) with calls to invalidateAndEmitChange(server,
contractPath) (or the appropriate filePath) to remove duplication and clarify
intent.

210-310: Consider splitting the "config changes the authoritative inputs" test into two cases.

This single it exercises two distinct behaviors back‑to‑back: (1) switching the referenced schema via a vite.config.ts edit triggers a re‑emit using the new inputs, and (2) subsequent edits to contract-alt.prisma are now watched. Splitting them into nested describe blocks (e.g. given config switched to contract-alt.prisma → when config edited → then re-emits / …and when alt schema edited → then re-emits) would make each assertion read as a standalone behavior and produce more actionable failure messages if only one phase breaks.

As per coding guidelines: "Group test assertions by the conditions under which they're true using nested describe() blocks to model state and transitions (given/when/then structure)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration/test/vite-plugin.hmr.e2e.test.ts` around lines 210 - 310,
The long test "re-emits contract when config changes the authoritative inputs"
exercises two behaviors and should be split: extract the first phase (editing
vite.config.ts to switch './contract.prisma' → './contract-alt.prisma' and
asserting the contract reflects 'name' nullable) into its own test (use the same
setupTestDirectoryFromFixtures, createServer, waitForFileChange logic and
assertions) and create a second test that either repeats the setup or begins by
performing the config switch (you can reuse the helper calls:
replaceInFileOrThrow(configPath, './contract.prisma', './contract-alt.prisma'),
invalidate modules via server.moduleGraph.getModulesByFile(configPath) and
server.watcher.emit('change', configPath)) and then edits altSchemaPath
(replaceInFileOrThrow(altSchemaPath, '  name  String?\n', '  name  String?\n 
nickname String?\n')) and asserts the subsequent re-emit contains 'nickname';
keep the same waitForFileChange/expect flows and timeouts
(timeouts.typeScriptCompilation, timeouts.spinUpPpgDev) and reference the
existing symbols like replaceInFileOrThrow, waitForFileChange,
server.moduleGraph.getModulesByFile, server.watcher.emit to locate where to
split and duplicate required setup/teardown.
packages/1-framework/1-core/config/src/config-types.ts (1)

89-96: Use Schema.array() for the Arktype inputs array.

Line 90 uses the string shorthand; the repo convention is to define arrays through .array().

♻️ Proposed refactor
+const ContractSourceInputSchema = type('string');
+
 export const ContractSourceProviderSchema = type({
-  'inputs?': 'string[]',
+  'inputs?': ContractSourceInputSchema.array(),
   load: 'Function',
 });

As per coding guidelines, use Schema.array() for array definitions in Arktype schemas.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/src/config-types.ts` around lines 89 - 96,
The ContractSourceProviderSchema currently uses the string shorthand for the
optional inputs array ('inputs?': 'string[]'); update it to use Arktype's
Schema.array() form instead so it follows repo convention: replace the 'inputs?'
shorthand in the ContractSourceProviderSchema definition with
Schema.array(Schema.string()). Also ensure the import/usage matches other
schemas and that the optional marker remains (i.e., keep inputs optional) in the
ContractSourceProviderSchema and does not change ContractConfigSchema.
packages/1-framework/1-core/config/test/config-validation.test.ts (1)

464-476: Split this test file before adding more coverage.

This test file now reaches Line 519, exceeding the 500-line limit. Since it already has distinct top-level groups (validateConfig, ConfigValidationError, defineConfig), consider moving the defineConfig coverage into a focused sibling test file.

As per coding guidelines, **/*.test.ts: Keep test files under 500 lines to maintain readability and navigability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/test/config-validation.test.ts` around
lines 464 - 476, This test file exceeds the 500-line limit; move the tests that
target defineConfig (including the "preserves source provider metadata" case and
any other defineConfig-specific tests) into a new sibling test file so each
top-level group (validateConfig, ConfigValidationError, defineConfig) lives in
its own file; locate tests referencing defineConfig, create a new test file
containing the describe/it blocks that reference defineConfig (and helper calls
like createValidConfig and createSourceProvider), update imports so
defineConfig, createValidConfig, createSourceProvider remain resolved, and
remove those tests from the original file to keep both files under 500 lines.
packages/1-framework/1-core/config/src/contract-source-types.ts (1)

47-56: Document the provider path contract at the public interface.

inputs is now a tooling protocol, but the type does not state whether relative paths are resolved from configDir, whether entries are direct provider reads vs transitive imports, or that generated outputs must not be listed.

Suggested TSDoc
 export interface ContractSourceEnvironment {
+  /**
+   * Directory containing the loaded Prisma Next config file. Providers should
+   * resolve relative source paths from this directory.
+   */
   readonly configDir: string;
 }
 
 export interface ContractSourceProvider {
+  /**
+   * Authoritative files read directly by this provider. Relative entries are
+   * resolved from `ContractSourceEnvironment.configDir`; generated output
+   * artifacts must not be listed here.
+   */
   readonly inputs?: readonly string[];
+  /**
+   * Loads the contract using domain context plus environment-specific I/O data.
+   */
   readonly load: (

As per coding guidelines, **/*.{md,ts,tsx}: Keep docs current.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/src/contract-source-types.ts` around lines
47 - 56, Update the public interface docs for ContractSourceProvider and
ContractSourceEnvironment: add TSDoc on ContractSourceEnvironment.configDir to
state it is the base directory for resolving relative provider input paths; add
TSDoc on ContractSourceProvider.inputs to note this is a tooling protocol (not
user-facing), that each entry is a direct provider input (not transitive
imports), that relative paths are resolved against configDir, and explicitly
prohibit listing generated/derived outputs in inputs; also document the
load(context, environment) contract expectations regarding inputs resolution and
diagnostics reporting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/1-framework/1-core/config/src/config-validation.ts`:
- Around line 23-47: The current validation allows sourceConfig['inputs'] to
include the same path as contract['output'], which can create self-trigger
loops; update the inputs validation in the block that iterates over
sourceConfig['inputs'] to check for and reject any entry equal to
contract['output'] (or equal after normalizing paths if your project uses
relative paths) by calling throwValidation (e.g.,
throwValidation('contract.source.inputs[]', 'Config.contract.source.inputs must
not include the emitted artifact path specified by contract.output')); use the
existing sourceConfig, contract and throwValidation symbols to locate and
implement this guard.

---

Nitpick comments:
In `@packages/1-framework/1-core/config/src/config-types.ts`:
- Around line 89-96: The ContractSourceProviderSchema currently uses the string
shorthand for the optional inputs array ('inputs?': 'string[]'); update it to
use Arktype's Schema.array() form instead so it follows repo convention: replace
the 'inputs?' shorthand in the ContractSourceProviderSchema definition with
Schema.array(Schema.string()). Also ensure the import/usage matches other
schemas and that the optional marker remains (i.e., keep inputs optional) in the
ContractSourceProviderSchema and does not change ContractConfigSchema.

In `@packages/1-framework/1-core/config/src/contract-source-types.ts`:
- Around line 47-56: Update the public interface docs for ContractSourceProvider
and ContractSourceEnvironment: add TSDoc on ContractSourceEnvironment.configDir
to state it is the base directory for resolving relative provider input paths;
add TSDoc on ContractSourceProvider.inputs to note this is a tooling protocol
(not user-facing), that each entry is a direct provider input (not transitive
imports), that relative paths are resolved against configDir, and explicitly
prohibit listing generated/derived outputs in inputs; also document the
load(context, environment) contract expectations regarding inputs resolution and
diagnostics reporting.

In `@packages/1-framework/1-core/config/test/config-validation.test.ts`:
- Around line 464-476: This test file exceeds the 500-line limit; move the tests
that target defineConfig (including the "preserves source provider metadata"
case and any other defineConfig-specific tests) into a new sibling test file so
each top-level group (validateConfig, ConfigValidationError, defineConfig) lives
in its own file; locate tests referencing defineConfig, create a new test file
containing the describe/it blocks that reference defineConfig (and helper calls
like createValidConfig and createSourceProvider), update imports so
defineConfig, createValidConfig, createSourceProvider remain resolved, and
remove those tests from the original file to keep both files under 500 lines.

In `@test/integration/test/vite-plugin.hmr.e2e.test.ts`:
- Around line 108-113: Extract the repeated pattern that calls
server.moduleGraph.getModulesByFile(filePath) and then loops calling
server.moduleGraph.invalidateModule(module) followed by emitting the change into
a small helper (e.g., invalidateAndEmitChange(server, filePath)); implement the
helper so it retrieves modules for the given filePath, invalidates each via
invalidateModule, and then performs the same "change" emit used in the tests,
then replace both duplicated blocks (the one around contractPath and the one at
the other occurrence) with calls to invalidateAndEmitChange(server,
contractPath) (or the appropriate filePath) to remove duplication and clarify
intent.
- Around line 210-310: The long test "re-emits contract when config changes the
authoritative inputs" exercises two behaviors and should be split: extract the
first phase (editing vite.config.ts to switch './contract.prisma' →
'./contract-alt.prisma' and asserting the contract reflects 'name' nullable)
into its own test (use the same setupTestDirectoryFromFixtures, createServer,
waitForFileChange logic and assertions) and create a second test that either
repeats the setup or begins by performing the config switch (you can reuse the
helper calls: replaceInFileOrThrow(configPath, './contract.prisma',
'./contract-alt.prisma'), invalidate modules via
server.moduleGraph.getModulesByFile(configPath) and
server.watcher.emit('change', configPath)) and then edits altSchemaPath
(replaceInFileOrThrow(altSchemaPath, '  name  String?\n', '  name  String?\n 
nickname String?\n')) and asserts the subsequent re-emit contains 'nickname';
keep the same waitForFileChange/expect flows and timeouts
(timeouts.typeScriptCompilation, timeouts.spinUpPpgDev) and reference the
existing symbols like replaceInFileOrThrow, waitForFileChange,
server.moduleGraph.getModulesByFile, server.watcher.emit to locate where to
split and duplicate required setup/teardown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: a084229c-376c-4c8e-b83c-11dd50ea2fc5

📥 Commits

Reviewing files that changed from the base of the PR and between aa556f7 and 27f0e71.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (72)
  • packages/1-framework/1-core/config/README.md
  • packages/1-framework/1-core/config/src/config-types.ts
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/1-framework/1-core/config/src/contract-source-types.ts
  • packages/1-framework/1-core/config/src/exports/config-types.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • packages/1-framework/1-core/config/test/config-validation.test.ts
  • packages/1-framework/3-tooling/cli/README.md
  • packages/1-framework/3-tooling/cli/recordings/fixtures/prisma-next.config.ts
  • packages/1-framework/3-tooling/cli/src/config-loader.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/operations/contract-emit.ts
  • packages/1-framework/3-tooling/cli/src/exports/config-types.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/contract-emit.test.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/package.json
  • packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/exports/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/package.json
  • packages/2-sql/2-authoring/contract-ts/src/config-types.ts
  • packages/2-sql/2-authoring/contract-ts/src/exports/config-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/3-extensions/mongo/src/config/define-config.ts
  • packages/3-extensions/mongo/test/config/define-config.test.ts
  • packages/3-extensions/postgres/src/config/define-config.ts
  • packages/3-extensions/postgres/test/config/define-config.test.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/prisma-next.config.ts
  • test/integration/test/authoring/cli.emit-parity-fixtures.test.ts
  • test/integration/test/authoring/parity/callback-mode-scalars/contract.ts
  • test/integration/test/authoring/psl.pgvector-dbinit.test.ts
  • test/integration/test/authoring/side-by-side-contracts.test.ts
  • test/integration/test/cli.emit-command.test.ts
  • test/integration/test/cli.emit-contract.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-init/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-driver.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-sign/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-driver.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-verify.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/emit/prisma-next.config.emit.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-apply/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-plan/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/mongo-db-commands/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/contract-alt.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/contract.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/vite.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.async-source.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.mongo-contract-ts.ts
  • test/integration/test/fixtures/prisma-next.config.ts
  • test/integration/test/mongo/fixtures/prisma-next.config.ts
  • test/integration/test/sql-builder/fixtures/prisma-next.config.ts
  • test/integration/test/vite-plugin.hmr.e2e.test.ts
  • test/utils/package.json
✅ Files skipped from review due to trivial changes (20)
  • packages/2-sql/2-authoring/contract-ts/src/exports/config-types.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/emit/prisma-next.config.emit.ts
  • test/integration/test/mongo/fixtures/prisma-next.config.ts
  • packages/2-sql/2-authoring/contract-ts/package.json
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/contract.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin-psl/contract-alt.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/1-framework/1-core/config/test/config-types.test-d.ts
  • test/integration/test/authoring/psl.pgvector-dbinit.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/contract-emit.test.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.with-db.ts
  • packages/2-sql/2-authoring/contract-psl/test/provider.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.with-db.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/README.md
  • packages/1-framework/3-tooling/cli/src/exports/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-ts/package.json
🚧 Files skipped from review as they are similar to previous changes (34)
  • packages/1-framework/3-tooling/cli/recordings/fixtures/prisma-next.config.ts
  • packages/1-framework/1-core/config/src/exports/config-types.ts
  • packages/3-extensions/sql-orm-client/test/fixtures/prisma-next.config.ts
  • test/integration/test/authoring/side-by-side-contracts.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/vite-plugin/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/mongo-db-commands/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.async-source.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-init/prisma-next.config.with-db.ts
  • packages/3-extensions/mongo/src/config/define-config.ts
  • test/integration/test/fixtures/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-sign/prisma-next.config.with-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/prisma-next.config.ts
  • packages/3-extensions/postgres/src/config/define-config.ts
  • packages/1-framework/3-tooling/cli/README.md
  • packages/3-extensions/postgres/test/config/define-config.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-plan/prisma-next.config.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-db.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.with-db.ts
  • test/integration/test/cli.emit-contract.test.ts
  • packages/1-framework/3-tooling/cli/test/control-api/client.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-verify.ts
  • packages/1-framework/3-tooling/cli/src/config-loader.ts
  • test/integration/test/authoring/cli.emit-parity-fixtures.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/src/config-types.ts
  • packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts
  • packages/2-sql/2-authoring/contract-ts/src/config-types.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/prisma-next.config.mongo-contract-ts.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-introspect/prisma-next.config.no-driver.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-verify/prisma-next.config.no-driver.ts
  • packages/3-extensions/mongo/test/config/define-config.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/migration-apply/prisma-next.config.with-db.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts
  • packages/1-framework/3-tooling/vite-plugin-contract-emit/test/plugin.test.ts

Comment thread packages/1-framework/1-core/config/src/config-validation.ts Outdated
Comment thread packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts Outdated
Comment thread packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts Outdated
Comment thread packages/2-mongo-family/2-authoring/contract-psl/src/provider.ts Outdated
Comment thread packages/2-sql/2-authoring/contract-psl/src/provider.ts Outdated
Comment thread packages/3-extensions/postgres/src/config/define-config.ts Outdated
Comment thread test/utils/package.json Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/1-framework/1-core/config/test/config-validation.test.ts (1)

335-397: Reuse createSourceProvider helper to reduce duplication.

The inline source: { inputs: ..., load: async () => ok({ targetFamily: 'sql' } as Contract) } pattern is repeated four times. Since createSourceProvider already accepts arbitrary overrides, these cases can use it to eliminate the duplicated load literal and match the helper-based style used elsewhere in the file.

♻️ Example refactor
-        contract: {
-          source: {
-            inputs: 123,
-            load: async () => ok({ targetFamily: 'sql' } as Contract),
-          },
-        },
+        contract: {
+          source: createSourceProvider({ inputs: 123 }),
+        },

Apply the same pattern to the ['valid', 123], ['./generated/contract.json'], and ['./generated/contract.d.ts'] cases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/1-framework/1-core/config/test/config-validation.test.ts` around
lines 335 - 397, Replace the repeated inline source objects in the tests with
the existing helper by using createSourceProvider to build the contract.source
value: for each expectFieldError call that currently passes contract: { source:
{ inputs: <...>, load: async () => ok({ targetFamily: 'sql' } as Contract) } }
(and the two cases that add output), call createValidRawConfig({ contract: {
source: createSourceProvider({ inputs: <...> }), output: ...? } }) instead; this
reuses the load implementation from createSourceProvider and removes the
duplicated load literal while keeping expectFieldError and createValidRawConfig
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/1-framework/1-core/config/test/config-validation.test.ts`:
- Around line 335-397: Replace the repeated inline source objects in the tests
with the existing helper by using createSourceProvider to build the
contract.source value: for each expectFieldError call that currently passes
contract: { source: { inputs: <...>, load: async () => ok({ targetFamily: 'sql'
} as Contract) } } (and the two cases that add output), call
createValidRawConfig({ contract: { source: createSourceProvider({ inputs: <...>
}), output: ...? } }) instead; this reuses the load implementation from
createSourceProvider and removes the duplicated load literal while keeping
expectFieldError and createValidRawConfig unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 68cb01b4-4682-421a-add3-7dbd0c8942a2

📥 Commits

Reviewing files that changed from the base of the PR and between 27f0e71 and 0f1f15d.

📒 Files selected for processing (5)
  • packages/1-framework/1-core/config/README.md
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/1-framework/1-core/config/test/config-validation.test.ts
  • packages/1-framework/3-tooling/cli/test/config-loader.test.ts
  • test/integration/test/cli.emit-contract.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/integration/test/cli.emit-contract.test.ts
  • packages/1-framework/1-core/config/src/config-validation.ts
  • packages/1-framework/1-core/config/README.md

@wmadden
Copy link
Copy Markdown
Contributor

wmadden commented Apr 20, 2026

Re-review — most of the feedback is addressed

Thanks for the substantial rework. The protocol is in a much better shape now.

Biggest wins since the last round

  • Discriminated union collapsed to a simple inputs?: readonly string[]. The plugin now always unions the config path + declared inputs + module-graph crawl from the config and each .ts-family input. This is strictly more powerful than the earlier three-way moduleGraph / paths / configPathOnly branching — the TS facade's dynamic-import transitive files are now watched automatically, which was the concrete motivating failure in the earlier F03 / F11 discussion. [1a653a81f]
  • configDir moved out of ContractSourceContext into a new ContractSourceEnvironment. context is I/O-agnostic again; environment is where file-system concerns live. This is exactly the split I was hoping for (F14). [1a653a81f]
  • Outputs-as-inputs rejected at validation time. validateConfig throws ConfigValidationError('contract.source.inputs[]', …) if any declared input matches contract.output or its colocated .d.ts after normalization. This is the option-1 resolution I suggested for F18 — much cleaner than a consumer-side filter. [329e17d6f]
  • Real end-to-end HMR test. test/integration/test/vite-plugin.hmr.e2e.test.ts spins up a real Vite dev server for three flows: TS contract edit, PSL schema edit, and a config-swap that points at an alternate schema. The third case is particularly valuable — it proves the "config change → re-resolve inputs → new inputs watched in time for next edit" loop actually works end-to-end. This closes D1 from the previous review. [2bed54e8d]
  • Facade .ts helper extracted. typescriptContractFromPath lives in both sql-contract-ts and mongo-contract-ts; the postgres/mongo extensions now delegate to it. [d1563f348]
  • SQL PSL sourceId now uses the relative schema path, matching Mongo PSL. (F05) [e94a791ef]
  • resolveContractOutputFiles no longer fabricates a default path when contract.output is undefined. (F09) [27f0e716a]
  • handleTrackedFileChange normalizes resolve(file) on both sides so relative vs. ./ vs. absolute forms agree. (F07, partial — symlinks still not canonicalized) [27f0e716a]

What's still live

Two new items surfaced by the re-review:

  • R01 — AC4 semantics drift without a doc trail. The Linear AC4 reads: "If custom providers omit watch inputs, the system falls back to watching the config path and emits a dev warning that coverage is partial." The new implementation has no such warning — it silently falls back to "config + module-graph crawl". For TS-authored configs this is usually fine (the module graph catches the contract module). For a custom provider that omits inputs and reads files outside the module graph (e.g. a future PSL-like format), edits will silently fail to retrigger emit with no warning. This may be the correct call given the simpler protocol, but it isn't mentioned in the PR body. Either update the Linear AC to describe the new semantics, or reintroduce a narrower sanity warning at startup.
  • R02 — Runtime ignoredOutputFiles guard is load-bearing but under-documented. With the validation-time rule in place, the runtime filter in handleTrackedFileChange now serves a different purpose: Vite's watcher by default watches the entire project root, so chokidar fires change events on contract.json whenever the plugin writes it — regardless of what's in watchedFiles. The guard stops the plugin from looping on its own emits. Worth a one-line comment on ignoredOutputFiles making that purpose explicit so a future refactor doesn't drop it.

Still-live from the previous round (downgraded where appropriate):

  • F01 — ADR for the new protocol. The simpler inputs? + module-graph-union + validation-time output rejection contract deserves an ADR (or an extension of ADR 032) so future dev-tooling plugins have an authoritative reference.
  • F04 — ContractSourceContext still hand-constructed at every call site. Low priority now that configDir is out of it, but a small createContractSourceContext(stack) factory would still DRY up the handful of sites.
  • F06 — loadConfig still runs on every emit. The timing was moved from after-emit to before-emit (more justified now), but in steady-state PSL editing the c12 compile is unnecessary. Minor.
  • F07 — symlink/realpath still not canonicalized. resolve-based normalization is in place; realpath would fully close this. Defer until someone reports a real failure in pnpm symlinked workspaces.
  • F08 — three path-resolution primitives for the same concept. join in contract-emit.ts, resolve in plugin.ts, normalize + replaceAll('\\\\','/') in config-validation.ts. A shared deriveContractArtifactPaths helper would keep them in lockstep.
  • F10 — composedExtensionPackRefs truthy-length check. Unchanged, low impact.
  • F11-residual — static inputs vs. dynamic load for non-module-graph input formats. Module-graph crawling reconciles the TS case; PSL multi-file (if that ever lands) would re-open this. Not a blocker.

Nothing in the still-live list is a merge blocker. AC4 / R01 is the only item I'd prefer to see a thread on before landing — a one-liner in the PR body explaining the intentional drift, or a short startup sanity warning, would be enough.

@jkomyno
Copy link
Copy Markdown
Contributor Author

jkomyno commented Apr 20, 2026

Addressed the two remaining re-review items:

  • R01: updated the PR description to spell out the omitted-source.inputs fallback semantics explicitly. It now states that the plugin falls back to the config file plus config module graph, and that this PR does not emit a partial-coverage warning in that case.
  • R02: added a comment on ignoredOutputFiles in packages/1-framework/3-tooling/vite-plugin-contract-emit/src/plugin.ts clarifying that Vite still surfaces writes to emitted artifacts under the project root, so the runtime guard is what prevents self-trigger loops. This landed in 2c3a0d60d.

I also resolved the stale outdated review threads that were already addressed by the current branch.

@wmadden can you take another look?

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