Skip to content

TML-2804: extensions can contribute top-level PSL block keywords#718

Draft
wmadden wants to merge 15 commits into
mainfrom
tml-2804-slice-1-parsing-printing-extensibility-substrate
Draft

TML-2804: extensions can contribute top-level PSL block keywords#718
wmadden wants to merge 15 commits into
mainfrom
tml-2804-slice-1-parsing-printing-extensibility-substrate

Conversation

@wmadden
Copy link
Copy Markdown
Contributor

@wmadden wmadden commented Jun 4, 2026

At a glance

An extension can now register a new top-level keyword for the PSL parser. Here's what authoring looks like once an extension ships such a contribution — concretely, what RLS will let you write once the RLS project lands on top of this:

namespace public {
  model Profile {
    id     String @id
    userId String @unique
  }

  policy profiles_select_anon {
    target = Profile
    using  = "true"
  }
}

The framework's PSL parser doesn't know about policy. The Postgres extension does, because it ships a contribution that registers the keyword along with everything needed to make it round-trip through parse → IR → JSON → IR → print. This PR is the mechanism that makes that possible. RLS itself ships in a separate project.

What this PR decides

An extension registers a new top-level PSL keyword with one descriptor that carries both a parser and a printer, plus a matching lowering factory in the existing entityTypes registry — the two tied together by a shared discriminator string:

const policyContributions: AuthoringContributions = {
  pslBlocks: {
    policy: {
      discriminator: 'postgres-policy',
      // method syntax (not arrow properties) so a contribution's concrete node
      // type assigns across the printer's parameter — see the descriptor JSDoc.
      parser(ctx): PostgresPolicyAst { /* read source lines, return the AST node */ },
      printer(node: PostgresPolicyAst, ctx): string { /* render back to PSL source */ },
    },
  },
  entityTypes: {
    policy: {
      kind: 'entity',
      discriminator: 'postgres-policy',
      output: { factory: (input) => new PostgresPolicyIr(input) },
    },
  },
};

Parser and printer live on one descriptor because they cannot exist independently — a parser with no printer breaks contract infer, a printer with no parser parses nothing. The lowering factory stays in entityTypes because a factory can stand alone (the TS-builder path reaches it without PSL). Load-time validation enforces the one direction that matters: every pslBlocks descriptor must have a matching-discriminator entityTypes factory.

Why this had to ship

The framework's PSL parser had a fixed set of top-level keywords (model, enum, type, types, namespace) and no way for an extension to add new ones. Three downstream projects were blocked by that gap and would otherwise re-litigate "where should this keyword live" on every first PR:

  • Postgres RLS wants policy { … } blocks. Without an extension surface, RLS would either edit the framework parser directly (so SQLite + Mongo carry parser surface they can never use), or be authorable only through the TypeScript builder (breaking the framework's PSL/TS structural-parity promise).
  • Postgres roles / privileges wants role { … } for the same reasons.
  • Postgres-specific entity types (custom domains, etc.) — same shape.

Shipping the mechanism now means each downstream feature lands as a focused PR.

How it works

AuthoringContributions had three namespaces before this PR — type (type constructors), field (field presets), entityTypes (entity factories). This PR adds one: pslBlocks, whose descriptors each carry a parser + a printer.

Parsing. The framework parser's top-level dispatch already handled the five built-in keywords; everything else surfaced an Unsupported top-level block diagnostic. Now, on an unknown identifier, the parser consults the pslBlocks registry: if a descriptor claims the keyword, its parser runs and produces an AST node that lands in a new generic slot on PslNamespace:

interface PslNamespace {
  // ...existing per-kind slots (models, enums, compositeTypes, ...)
  readonly extensionBlocks: readonly PslExtensionBlock[];
}

interface PslExtensionBlock {
  readonly kind: string;   // matches the contribution's discriminator
  readonly name: string;   // every block kind we've shipped or planned has one
  readonly span: PslSpan;
}

The slot is generic deliberately — a typed slot per contributed kind would force a framework PR for every new kind, defeating the point. (Note: this is the PSL-AST layer. ADR 224's entries coordinate-addressing is a distinct, IR-layer concept on the lowered namespace concretions; the PSL AST keeps its per-kind-slot shape for now. Converging it onto entries is a decided, planned follow-up — see "Alternatives considered.")

The contributed-parser contract is enforced at the parser boundary. A contributed parser is foreign code the framework invokes, so the dispatch site holds it to a contract rather than trusting it:

  • A parser that throws is caught and converted to a PSL_EXTENSION_BLOCK_PARSE_FAILED diagnostic against the block span — the rest of the document keeps parsing, instead of one bad extension crashing the whole parse (and contract infer with it).
  • A parser that returns no node produces the same diagnostic rather than silently dropping the block.
  • Immediately after the parser returns, the dispatch asserts node.kind === descriptor.discriminator. A discriminator typo (postgres-polcy) now fails at parse time naming the descriptor, instead of surfacing far away as "no contribution registered" at print time. This makes the routing invariant — which the type system can't express, since the framework holds the descriptor at its base shape — a checked one, documented on both AuthoringPslBlockDescriptor.discriminator and PslExtensionBlock.kind.

Printing. The PSL printer is two-phase (AST → PrintDocument → string), and both need to handle extension-contributed blocks for contract infer to keep working. Phase 1 carries them into a PrintNamespaceSection.extensionBlocks slot; phase 2 builds a discriminator-keyed map from pslBlocks and renders each block by calling the owning descriptor's printer. If a block's discriminator has no matching descriptor, the serializer throws (silent-drop would lose user-authored content during contract infer). The CLI's contract infer threads the assembled pslBlocks namespace through to printPsl(ast, { pslBlocks }).

Validation at load time. Within-namespace duplicates throw, naming both extensions. A pslBlocks descriptor with no matching entityTypes factory throws. Malformed descriptors — objects carrying kind/discriminator but not satisfying the descriptor shape — are rejected rather than silently treated as sub-namespaces.

What's in the diff

The substrate, plus a test-only fixture extension (fake_policy { target = T; using = "..." }) that ships the descriptor + factory and exercises the full round-trip:

PSL text → parse → AST → lower (entityTypes factory) → IR class instance
                       → JSON serialize → JSON hydrate → IR class instance
                                                       → print → PSL text → re-parse → equivalent AST

Tests pin the substrate end-to-end:

  • Round-trip (fake-extension.round-trip.test.ts): parse correctness, round-trip equivalence, factory output, JSON serialize/hydrate, mixed framework + fixture round-trip, and the unknown-keyword diagnostic.
  • Parser failure isolation (parser.extension-blocks.test.ts): a throwing contributed parser → diagnostic with the document still parsing; an undefined return → diagnostic; kinddiscriminator → diagnostic naming both.
  • contract infer end-to-end (contract-infer.command.test.ts): a real fake_policy block driven through the actual command path (control-stack namespace → command → printPsl → written file), asserting the rendered block survives to disk.
  • Type narrowing (framework-authoring-psl.types.test-d.ts): parser-return = printer-input = factory-input on the contribution literal, plus a negative test (@ts-expect-error) pinning that the descriptor's method form — not arrow properties — is what lets a narrower contributed node assign to the base descriptor.

The fixture is the regression test for the substrate going forward; downstream consumers (RLS first) follow its pattern. No production contribution ships from this PR.

The branch is rebased onto current main (incorporating ADR 224 / #715). The slice's spec + plan ride along as project artifacts (deleted at close-out by the follow-up docs slice). The commit history reflects the build → review-restructure → review-remediation path; it squash-merges to a single commit.

Out of scope

  • No real RLS / roles / custom-Postgres-type implementations. Downstream projects that consume this substrate.
  • No keyword migration. All existing keywords stay framework-parsed. In particular, enum is not moved to an extension — see "Alternatives considered."
  • No PslNamespaceentries migration in this slice. The substrate ships the flat extensionBlocks slot as a deliberate interim; converging the PSL AST onto ADR 224's entries[kind][name] shape is the project's closing slice, TML-2849 — see "Alternatives considered."
  • No custom-attribute extensibility (@policy(…)). Different SPI shape; tracked separately.
  • No pluggable expression grammar.
  • No three-layer extensibility ADR / subsystem-doc updates / AGENTS.md entitiesentityTypes doc-bug fix / project-directory deletion. Those land in TML-2806 (the docs + close-out slice), stacked after this PR so the ADR codifies the substrate as actually shipped.

Verification

Gate Result
pnpm typecheck (the four touched packages + dependents) pass (135 tasks)
pnpm test:packages for framework-components / psl-parser / psl-printer / cli pass (344 / 99 / 38 / 1212)
pnpm lint:deps clean (1026 modules)
pnpm lint:casts current=1282 merge-base=1289 delta=-7 (no regression; net cast reduction)

A workspace-wide typecheck has one pre-existing failure unrelated to this PR: @prisma-next/integration-tests can't resolve @prisma-next/adapter-sqlite/control (the ./control export lacks a types condition — an artifact of the #715 / ADR-224 merge). @prisma-next/adapter-postgres has the same missing-condition shape. Tracked separately in TML-2844; out of this PR's scope.

Alternatives considered

Two descriptors for parser and printer (the first cut of this PR: separate pslBlocks + pslPrinters namespaces tied by a discriminator cross-check). Rejected on review: they cannot exist independently, so expressing them as two registrations validated against each other is ceremony around one thing. Collapsed to a single descriptor carrying both — which also removes a type guard and the parser↔printer cross-check.

Folding the entityTypes factory into the same descriptor too. Considered (if a PSL block always needs a factory, why not bundle all three?). Kept separate because the reverse isn't true today — entityTypes can stand alone (the TS-builder path; e.g. the existing enum factory has no extension-contributed PSL block, since enum is framework-parsed). The deeper question this surfaces — should every entity with a PSL representation contribute its own parser/printer rather than relying on framework-parsing? — is real but belongs to the enum-as-domain-plane work in TML-2815.

Migrating enum to an extension contribution as the load-bearing proof-of-concept (the original project framing). Rejected: enum is an application-level (domain-plane, ADR 221) concept that happens to have target-specific storage (Postgres native enum, SQLite TEXT, Mongo string), not a Postgres feature. Migrating it would remove enum from SQLite + Mongo contracts — a regression vs Prisma 1.x/2.x. Cross-target enum support is TML-2815. The integration-test fixture substitutes for the missing real migration; RLS is the first real consumer.

Shipping parser extensibility without the printer. Smaller diff, but contract infer would silently break for every extension-contributed block kind. They ship together.

The flat extensionBlocks slot vs. ADR 224's entries shape. This was the open question at review. The PSL AST and the contract IR are two near-identical trees, and ADR 224 gave only the IR a uniform entity coordinate (storage.namespaces[id].entries[kind][name]). A flat extensionBlocks array next to the built-in per-kind slots is a less-general second answer to the same "address namespace entities by kind and name" problem — and "no generic consumer needs the coordinate shape yet" is circular, since the coordinate shape is what would let such consumers exist (the bet ADR 224 already made at the IR layer). The decision: the PSL AST converges on entries. Because that migration is framework-wide (parser, both printer phases, sqlSchemaIrToPslAst, every PSL-AST consumer and test), it does not ride in this substrate slice — the flat slot ships as a deliberate interim, and the full migration is the project's closing slice, TML-2849, sequenced before the ADR so the ADR records the converged shape. The interim is safe because extensions contribute AST nodes through the descriptor SPI and never touch PslNamespace directly, so the storage-shape migration is framework-internal.

A typed PslNamespace slot per contributed kind (policies: PslPolicy[], …). Rejected: every new kind would need a framework PR. The generic extensionBlocks slot keeps the framework AST stable as extensions add kinds (and is the interim the entries migration subsumes).

Silent-drop on a missing printer, or a crashing contributed parser vs. diagnose. Diagnose — silent-drop loses user-authored content during contract infer, and a thrown error from a contributed parser would take down the whole parse rather than the one block. Both degrade to a PSL_EXTENSION_BLOCK_PARSE_FAILED diagnostic instead.


Refs: TML-2804 (this slice), TML-2803 (planning), TML-2849 (PSL-AST → entries migration, closing slice), TML-2806 (docs + close-out slice), TML-2844 (adapter types-condition fix), TML-2815 (enum-as-application-concept, sibling project).

Generated with Devin

Summary by CodeRabbit

Release Notes

  • New Features
    • PSL now supports extension-contributed top-level blocks, allowing custom block types to be defined and used in schema files.
    • Enhanced the parser and printer to handle these custom blocks with proper validation, diagnostics, and round-trip support (parse → print → parse).
    • Cross-registry validation ensures extension blocks are properly linked to their corresponding entity type definitions.

@wmadden wmadden requested a review from a team as a code owner June 4, 2026 13:49
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 84f850f8-18ef-4f69-9c5c-f2bc606ed17d

📥 Commits

Reviewing files that changed from the base of the PR and between 93be405 and 9e93e0f.

📒 Files selected for processing (4)
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/2-authoring/psl-printer/src/print-psl.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts

📝 Walkthrough

Walkthrough

Adds an authoring registry for PSL top-level extension blocks: shared SPI types, descriptor namespace and validation, control-stack assembly, parser routing and diagnostics, printer dispatch with options, CLI wiring, and comprehensive tests and fixtures.

Changes

PSL Extension-Block Authoring and Rendering

Layer / File(s) Summary
Extension-block SPI types and PSL AST contracts
packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts, packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
Adds position/span/diagnostic primitives and extension-block descriptor base shapes, makes AST carry extensionBlocks per namespace and parse input accept optional pslBlocks namespace.
Authoring descriptor types and validation rules
packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
Adds AuthoringPslBlockDescriptor with required parser/printer functions, type-guard predicate, discriminator-leaf collection, and cross-registry validation that enforces matching entityTypes factories.
Control-stack assembly and authoring exports
packages/1-framework/1-core/framework-components/src/control/control-stack.ts, packages/1-framework/1-core/framework-components/src/exports/authoring.ts
Extends assembled contributions to merge and validate pslBlocks descriptors, includes cross-registry collision checks, and re-exports new authoring types and guards.
PSL parser extension-block support
packages/1-framework/2-authoring/psl-parser/src/parser.ts, packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
Recognizes keyword BlockName { ... } patterns, resolves contributed descriptors, invokes parser functions with bounds/diagnostics context, accumulates blocks per namespace, and emits parse failures as diagnostics.
Printer options, types, and exports
packages/1-framework/2-authoring/psl-printer/src/print-psl.ts, packages/1-framework/2-authoring/psl-printer/src/print-document.ts, packages/1-framework/2-authoring/psl-printer/src/exports/index.ts
Adds PrintPslOptions with optional pslBlocks, carries extension blocks through print-document structures, and exports printer context contracts.
Print serialization with extension-block dispatch
packages/1-framework/2-authoring/psl-printer/src/serialize-print-document.ts
Builds discriminator-keyed printer dispatch maps from authoring namespace, constructs shared printer context, and renders extension blocks by invoking matched printer functions.
Control API and CLI integration
packages/1-framework/3-tooling/cli/src/control-api/..., packages/1-framework/3-tooling/cli/src/commands/...
Adds getPslBlocksNamespace() method to control client, includes pslBlocksNamespace in inspect-live-schema results, and passes it into contract-infer PSL printing.
Framework authoring assembly tests
packages/1-framework/1-core/framework-components/test/control-stack.test.ts
Tests control-stack assembly for empty state and pslBlocks merge/validation scenarios including duplicate detection and factory linkage.
Type-safety tests for PSL authoring
packages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.ts
Compile-time type tests assert parser/printer output compatibility, entity factory matching, discriminator literal preservation, and printer function variance constraints.
Parser behavior tests for extension blocks
packages/1-framework/2-authoring/psl-parser/test/parser.extension-blocks.test.ts
Tests recognized/unrecognized keywords, diagnostics propagation, source order, mixed built-in/extension blocks, and failure isolation when contributed parsers malfunction.
Fake-policy extension fixture and printer tests
packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-extension.ts, packages/1-framework/2-authoring/psl-printer/test/fake-extension.round-trip.test.ts, packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.extension-blocks.test.ts
Defines test-only fake_policy extension with parser/printer/IR hydration; tests round-trip parsing/printing, mixing with built-in blocks, indentation, and error cases.
CLI tests and cross-package fixture updates
packages/1-framework/3-tooling/cli/test/..., packages/2-mongo-family/.../test/..., packages/2-sql/.../test/...
Updates test mocks and fixtures across CLI, mongo, and SQL packages to include pslBlocks and pslPrinters namespaces in authoring context shapes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested reviewers

  • aqrln

Poem

🐰 With whiskers twitching, I parse and write,

I stitch new blocks from day to night,
Keywords dance in schema rows,
Printers hum and parsing grows,
Hooray — extensions jump to light!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'TML-2804: extensions can contribute top-level PSL block keywords' clearly and concisely summarizes the main change: enabling extensions to register new top-level PSL block keywords. It is specific, descriptive, and directly reflects the primary objective documented in the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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-2804-slice-1-parsing-printing-extensibility-substrate

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 Jun 4, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@718

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@718

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@718

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@718

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@718

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@718

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 9e93e0f

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 4, 2026

size-limit report 📦

Path Size
postgres / no-emit 145.62 KB (+0.39% 🔺)
postgres / emit 117.08 KB (+0.01% 🔺)
mongo / no-emit 76.59 KB (+0.65% 🔺)
mongo / emit 70.95 KB (0%)
cf-worker / no-emit 175.17 KB (+0.32% 🔺)
cf-worker / emit 143.48 KB (0%)

Copy link
Copy Markdown
Contributor

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts`:
- Around line 456-489: collectAuthoringLeafDiscriminators currently descends
into objects that look like malformed descriptors (e.g., have a "kind" or
"discriminator" property) because isLeaf(value) returns false, letting invalid
descriptor-like objects bypass validation; update
collectAuthoringLeafDiscriminators so that before recursively descending it
checks for descriptor-like keys (at minimum "discriminator" and the alias
"kind") and, if present but the object does not satisfy isLeaf, treat it as a
leaf (i.e., record the path or surface it to validation) instead of recursing;
reference the collectAuthoringLeafDiscriminators function and the isLeaf
predicate when implementing this check so malformed descriptor objects are
discovered and rejected during load-time validation.
🪄 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: cdb3a9d9-edbf-4982-946e-3a96f57e2da1

📥 Commits

Reviewing files that changed from the base of the PR and between c159cd4 and 631ff01.

⛔ Files ignored due to path filters (4)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • projects/target-contributed-psl-blocks/plan.md is excluded by !projects/**
  • projects/target-contributed-psl-blocks/slices/substrate/plan.md is excluded by !projects/**
  • projects/target-contributed-psl-blocks/spec.md is excluded by !projects/**
📒 Files selected for processing (33)
  • packages/1-framework/1-core/framework-components/src/control/control-stack.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-substrate.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.pack-blocks.test.ts
  • packages/1-framework/2-authoring/psl-printer/package.json
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-printer/src/print-document.ts
  • packages/1-framework/2-authoring/psl-printer/src/print-psl.ts
  • packages/1-framework/2-authoring/psl-printer/src/serialize-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/test/fake-target-pack.round-trip.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.pack-blocks.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/contract-infer.ts
  • packages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/db-schema.command.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/inspect-live-schema.test.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/provider.test.ts
  • packages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/fixtures.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/9-family/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts

@wmadden wmadden changed the title TML-2804: parsing + printing extensibility substrate for pack-contributed PSL blocks TML-2804: target packs can contribute top-level PSL block keywords Jun 4, 2026
Copy link
Copy Markdown
Contributor Author

@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.

There's incorrect vocabulary all over the place. What is a "pack block"? And don't use "pack", use "extension". There's more - see my comments.

Also we shouldn't separate psl printing from psl blocks. If you have one you must have the other.

Positively: I love the round trip test with the fake RLS policy block.

Comment thread packages/1-framework/1-core/framework-components/src/control/psl-ast.ts Outdated
Comment thread packages/1-framework/3-tooling/cli/src/control-api/client.ts Outdated
Comment thread packages/1-framework/1-core/framework-components/src/shared/psl-substrate.ts Outdated
Comment thread packages/1-framework/1-core/framework-components/src/shared/psl-substrate.ts Outdated
Comment thread packages/1-framework/2-authoring/psl-parser/src/parser.ts Outdated
wmadden and others added 10 commits June 5, 2026 12:58
The original spec migrated `enum` from the framework parser to a
Postgres-pack contribution as the load-bearing proof-of-concept for
parser extensibility, on the theory that `enum` is a Postgres-flavoured
feature. Subsequent design review established that `enum` is an
application-level (domain-plane per ADR 221) concept that happens to
have target-specific storage representations, not a Postgres feature.
The migration is dropped; cross-target `enum` support is tracked
separately at TML-2815.

The rewrite tightens the project scope to substrate work:

- Pack-contributed top-level PSL block parsers via
  AuthoringContributions.pslBlocks.
- Pack-contributed PSL printers via AuthoringContributions.pslPrinters,
  paired with parsers so `contract infer` continues to work for all
  pack-contributed block kinds.
- Pack-owned AST types via a generic PslNamespace.packBlocks slot, so
  future block kinds do not require framework PRs to type.
- Pack-load-time validation enforcing the triple-bundle (pslBlocks +
  pslPrinters + entityTypes with matching discriminators).
- Integration-test fixture target pack (RLS-shaped) as the regression
  test; RLS becomes the first real downstream consumer once it lands.

Slice count drops from three to two: substrate (TML-2804) and
ADR + close-out (TML-2806), stacked so the ADR reflects what the
substrate actually ships.

Refs: TML-2803, TML-2804, TML-2805, TML-2806, TML-2815

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: Will Madden <madden@prisma.io>
Decomposes Slice 1 (TML-2804) into four sequential dispatches:

- D1: Substrate types + triple-bundle validation (framework-components only).
- D2: AST packBlocks slot + parser dispatch + parser SPI extraction.
- D3: Printer dispatch (both phases) + PrintNamespaceSection extension.
- D4: Integration-test fixture target pack + end-to-end round-trip test.

Each dispatch passes dispatch-INVEST. The hand-off chain is linear; no
non-linear dependencies. Final dispatch closes the slice-DoD by
demonstrating end-to-end round-trip parse → lower → IR → serialize →
hydrate → IR → print → re-parse with a real fixture target pack
exercising all three contributions (pslBlocks + pslPrinters + entityTypes)
on an RLS-shaped block kind.

Refs: TML-2804

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ibuted PSL blocks

Pack-contributed top-level PSL block keywords need three contributions
to round-trip through parse → IR → print: a parser (`pslBlocks`), a
printer (`pslPrinters`), and a lowering factory (`entityTypes`).
Without all three, contract inference silently breaks: a parser
without a printer renders unknown blocks as garbage; a printer
without a parser is unreachable; either without a factory has no
IR class to interpret. Surfacing those gaps at descriptor-build
time turns a runtime data-loss bug into a pack-author diagnostic.

Mirrors the existing `entityTypes` shape:
- `AuthoringPslBlockDescriptor` and `AuthoringPslPrinterDescriptor`
  carry a discriminator string plus a generic that narrows to the
  pack's AST node shape; the parser's context/bounds and the
  printer's context are typed `unknown` placeholders refined when
  the parser SPI (D2) and printer SPI (D3) land.
- The cross-registry path-collision check now refuses path overlap
  across all five namespaces.
- A new triple-bundle check rejects `pslBlocks`/`pslPrinters`
  contributions that lack a discriminator-matched sibling. The
  check is asymmetric on `entityTypes` because the TS-builder
  reaches `entityTypes` directly (`helpers.enum({...})`) without
  any PSL participation — a factory-only contribution stays valid.

Signed-off-by: Will Madden <madden@prisma.io>
…1 follow-up)

Cross-registry path-collision check forced pack authors into ugly
distinct-paths-per-registry naming (e.g. `pslBlocks.policyBlock` /
`pslPrinters.policyPrinter` / `entityTypes.policy`) for what is a
single triple-bundle contribution. The natural authoring shape is
the same path key across all three namespaces, with the discriminator
string tying the bundle together.

The cross-registry rule's purpose was disambiguating user-facing
`helpers.<path>(...)` resolution among `type` / `field` /
`entityTypes`. `pslBlocks` and `pslPrinters` are internal
framework indices consumed by parser and printer dispatch, not
user-facing helper paths — they have no resolution-ambiguity
problem with the other three or with each other.

Path collisions among `type` / `field` / `entityTypes` still
throw. Triple-bundle discriminator consistency still throws.
Within-namespace duplicate detection (the merge walker) is
unchanged. Only the cross-registry-against-pslBlocks/pslPrinters
arm is removed.

Test 4 inverts: the case that previously asserted a thrown error
now asserts that `assembleAuthoringContributions` resolves cleanly
when a single descriptor registers `entityTypes.policy` +
`pslBlocks.policy` + `pslPrinters.policy` with matching
discriminators.

Signed-off-by: Will Madden <madden@prisma.io>
…-contributed PSL blocks

The framework parser now consults AuthoringContributions.pslBlocks when
its top-level dispatch sees an unrecognised `<keyword> <name> { … }`
opener line. A registered contribution gets called with a
PslPackBlockParserContext handle (block name, keyword, brace-bounds,
source lines, span helpers, diagnostic sink), and its return value
lands in the enclosing namespace's new `packBlocks: readonly
PslPackBlock[]` slot. Unregistered keywords still surface
PSL_UNSUPPORTED_TOP_LEVEL_BLOCK with the keyword named at the offending
span.

The descriptor's parser signature is no longer a placeholder: it now
takes `PslPackBlockParserContext` and returns an `Output extends
PslPackBlock`. Type narrowing flows end-to-end across the triple bundle
— a pack literal declaring `parser: (ctx) => SomeAst` constrains the
matching `pslPrinters` and `entityTypes` factory inputs to
`SomeAst`, with a compile-time check that `SomeAst extends
PslPackBlock` so the framework AST slot can hold it.

The substrate types — PslPackBlock, PslPackBlockBounds,
PslPackBlockDiagnostic, PslPackBlockParserContext — live in a new
shared/psl-substrate.ts alongside the foundational PslPosition / PslSpan
/ PslDiagnosticCode (relocated from control/psl-ast.ts). The
relocation is needed because the descriptor is in the shared plane and
the AST types it now references can't sit in the migration plane;
psl-ast.ts re-exports the moved names so existing import paths keep
working. The PSL AST node types and parser remain in the migration
plane.

Parser SPI types are re-exported from `@prisma-next/psl-parser` so
pack authors have a single import location matching the surface they
implement against.

Signed-off-by: Will Madden <madden@prisma.io>
…er SPI substrate

The framework PSL printer now consults AuthoringContributions.pslPrinters
when serializing pack-contributed top-level blocks. PslNamespace.packBlocks
flows through phase 1 (astDocumentToPrintDocument) verbatim into the new
PrintNamespaceSection.packBlocks slot. Phase 2 (serializePrintDocument)
builds a discriminator-keyed dispatch map from the pslPrinters namespace
and renders each block by looking it up by its AST node's kind. The
result text appears after framework models in the same namespace, keeping
parser source order; namespace wrappers indent the rendered block as they
do for built-in declarations.

The printer's entry point (printPslFromAst, exported as printPsl) now
takes an optional options bag with pslPrinters; existing callers that
print framework-only ASTs work unchanged. ASTs containing pack-contributed
blocks throw a clear error when the matching pslPrinter contribution is
missing — silently dropping would lose user-authored content without
diagnostic, so the printer surfaces the misuse loudly.

The descriptor's printer signature is no longer a placeholder: it's
(node: Input extends PslPackBlock, ctx: PslPackBlockPrinterContext) =>
string. Input defaults to never (the contravariance idiom from
AuthoringEntityTypeFactoryOutput) so a pack literal declaring printer:
(node: SomeAst, ctx) => string assigns to the base shape across the
contravariant function-parameter position. PslPackBlockPrinterContext
joins PslPackBlockParserContext in shared/psl-substrate.ts as the
mirror SPI; it exposes indent (the body-line indent unit) and
escapeStringLiteral (PSL string escape) — calibrated to the minimum a
policy-shaped RLS-style printer needs.

The CLI's contract infer command threads the assembled pslPrinters
namespace from its control stack into printPsl, exposed via a new
ControlClient.getPslPrintersNamespace() method. The command's test
mocks update accordingly.

Signed-off-by: Will Madden <madden@prisma.io>
Pins the substrate's round-trip property going forward. A test-only
fixture target pack registers all three contributions (pslBlocks
parser, pslPrinters printer, entityTypes factory) for one made-up
RLS-shaped keyword fake_policy. The integration test exercises the
full path:

  text → parse (pslBlocks dispatch) → AST with packBlocks
       → entityTypes factory → FakePolicyIr instance
       → JSON.stringify → JSON.parse → hydrated FakePolicyIr
       → print (pslPrinters dispatch) → text
       → parse → AST with packBlocks (equivalent to first)

The fixture lives at test/fixtures/fake-target-pack.ts alongside the
integration test in psl-printer/test/. psl-parser is already
available as a devDep there, and the fixture only touches
framework-components + psl-parser + psl-printer surfaces — no new
package boundaries crossed.

FakePolicyIr extends IRNodeBase directly, freezes itself in the
constructor, and serializes JSON-cleanly via JSON.stringify. The
hydrateFakePolicyIrFromJson helper validates the JSON envelope at
the boundary and reconstructs an instance with the same fields,
matching the JSON-canonical / class-in-memory pattern documented at
docs/architecture docs/patterns/json-canonical-class-in-memory.md.

Future projects (RLS, roles, custom Postgres types) follow this
fixture's shape as the canonical example of how to ship a
pack-contributed top-level block.

Signed-off-by: Will Madden <madden@prisma.io>
…view

Review surfaced a vocabulary problem ("pack"/"substrate"/"pack block")
and an architectural one: parser and printer were expressed as two
descriptors (pslBlocks + pslPrinters) tied by a discriminator cross-check,
when they are one inseparable unit. The spec is rewritten to:

- Use "extension" (per the glossary; "extension pack" is being retired)
  and drop "substrate" throughout.
- Collapse parser + printer onto one PSL-block descriptor; keep entityTypes
  a separate registry (a factory may stand alone today via the TS builder;
  the "every PSL entity contributes its own parser/printer" question is
  routed to TML-2815).
- Correct the entries-pattern framing: ADR 224's entries is an IR-layer
  concept; the PSL AST keeps its per-kind-slot shape (a generic
  extensionBlocks slot), no entries migration here.
- Require malformed-descriptor rejection at load time.

The slice plan records D1-D4 as as-built and adds two restructure
dispatches: R5 (collapse the descriptor + fix validation) and R6
(vocabulary sweep), sequenced judgment-before-rename.

Refs: TML-2804
Signed-off-by: Will Madden <madden@prisma.io>
…tion

Parser and printer are one inseparable unit — a parser with no printer
breaks `contract infer`, a printer with no parser parses nothing.
Expressing them as two namespaces (`pslBlocks` + `pslPrinters`) tied by
a discriminator cross-check was ceremony around one thing. Collapse them
onto a single `AuthoringPslBlockDescriptor` carrying both `parser` and
`printer`.

Structural changes:
- `AuthoringPslBlockDescriptor` now declares `parser` and `printer` as
  methods (bivariant params) so a contribution literal's concrete node
  type assigns to the base shape across the otherwise-contravariant
  printer parameter — the same node the parser produces. This also
  removes the dispatch-site `blindCast` the old `Input = never` arrow
  shape forced in the printer.
- Delete `AuthoringPslPrinterDescriptor`, `AuthoringPslPrinterNamespace`,
  `isAuthoringPslPrinterDescriptor`, and the `pslPrinters` namespace on
  `AuthoringContributions` / `AssembledAuthoringContributions`.
- Printer dispatch builds its discriminator→descriptor map from
  `pslBlocks`, reading each descriptor's `printer`. `printPsl` /
  `serializePrintDocument` take `{ pslBlocks }`; the CLI exposes
  `getPslBlocksNamespace()` and `contract infer` threads it through.

Validation changes:
- Drop the parser↔printer cross-check (structurally impossible to
  violate with one descriptor). Keep the one-directional check that a
  `pslBlocks` descriptor has a matching `entityTypes` factory; an
  `entityTypes` factory may still stand alone (the TS-builder path).
- Reject malformed descriptors: a value carrying `kind`/`discriminator`
  that fails the leaf guard is no longer silently descended into as a
  sub-namespace — it throws a clear load-time error naming the path.

Audited `ControlClient.getPslBlocksNamespace`'s `stack?.`: `init()`
unconditionally assigns `this.stack`, so the optional chaining and
`?? {}` fallback were unnecessary — replaced with `this.stack!`,
matching the `emit` path's existing access.

Vocabulary ("pack" / `packBlocks` / `PslPackBlock*`) is unchanged here;
a separate dispatch does that rename.

Signed-off-by: Will Madden <madden@prisma.io>
…ock slice

Pure mechanical rename on the settled R5 descriptor shape — no behaviour
change. Retires "pack" (use "extension" per the glossary) and "substrate"
(meaningless — everything is substrate for something) from this slice's
surface.

- PslPackBlock -> PslExtensionBlock (and PslPackBlockBounds / Diagnostic /
  ParserContext / PrinterContext).
- packBlocks slot on PslNamespace + PrintNamespaceSection -> extensionBlocks.
- Internal helpers lookupPackBlockDescriptor / invokePackBlockParser /
  createPackBlockParserContext / serializePackBlock and locals
  (packBlockMatch, packBounds) -> *ExtensionBlock*.
- File psl-substrate.ts -> psl-extension-block.ts (git mv; the PSL
  position/span/diagnostic primitives stay co-located — they were
  relocated here so the shared-plane descriptor can reach them). Dropped
  the Ref: TML-2804 breadcrumb from its header.
- Prose: "pack-contributed" -> "extension-contributed", "pack block" ->
  "extension block", etc., in this slice's added comments/JSDoc/test names.

Unchanged: the pslBlocks namespace name (it contains no "pack"); the
entityTypes namespace; the method syntax on parser/printer (and its
contravariance JSDoc) — preserved verbatim except vocabulary. Pre-existing
"extension pack" / "migration package" terminology and the entity-type
JSDoc's broader "pack" mentions are left untouched.

Same test counts as R5 (framework-components 342, psl-parser 96,
psl-printer 38, cli 1211); identifiers renamed only.

Signed-off-by: Will Madden <madden@prisma.io>
@wmadden wmadden force-pushed the tml-2804-slice-1-parsing-printing-extensibility-substrate branch from 631ff01 to 64b7e74 Compare June 5, 2026 13:06
@wmadden wmadden changed the title TML-2804: target packs can contribute top-level PSL block keywords TML-2804: extensions can contribute top-level PSL block keywords Jun 5, 2026
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Jun 5, 2026

Review addressed — restructure landed (force-pushed)

Thanks for the review. The feedback split into a vocabulary problem, an architectural one, and a few code-quality findings; all addressed across two restructure dispatches on top of a rebase onto current main (which now includes #715 / ADR 224). The branch was force-pushed; it squash-merges to a single commit.

Two commits carry the work:

  • 3d7339487 — collapse the descriptor + fix validation (the architectural + code-quality changes)
  • 64b7e746d — vocabulary sweep (pack → extension, drop "substrate")

Vocabulary

Thread Resolution
psl-ast.ts:203 — avoid "pack", use "extension" Done (64b7e746d). packBlocks slot → extensionBlocks.
psl-substrate.ts:66 — don't use "pack" Done (64b7e746d).
psl-substrate.ts (file) — don't use "substrate" Done (64b7e746d). File renamed psl-substrate.tspsl-extension-block.ts (via git mv, history preserved).
parser.ts:220 — "Wtf is a pack block?" Done (64b7e746d). PslPackBlockPslExtensionBlock, helpers renamed; "extension block" is the term throughout.
psl-substrate.ts:14 — no transient IDs in source Done (64b7e746d). The Ref: TML-2804 breadcrumb is gone; the surrounding comment already carries the rationale.

Scope note: I left legitimate pre-existing "pack" terminology untouched (the "extension pack" concept, pack manifest, the fakeTargetPackContributions test fixture name). rg over the four touched packages confirms zero PslPackBlock / packBlocks / psl-substrate / "pack block" remain.

Architecture — parser + printer are now one descriptor

Thread Resolution
framework-authoring.ts:181 — "These should be one interface. If you provide a psl block you must also provide its printer." Done (3d7339487). AuthoringPslBlockDescriptor now carries both parser and printer. AuthoringPslPrinterDescriptor, the pslPrinters namespace, and the parser↔printer cross-check are deleted.
framework-authoring.ts:557 — "the only reason to have the type predicates is to distinguish two objects which ought to be inseparable?" Done (3d7339487). isAuthoringPslPrinterDescriptor is gone. The one remaining guard (isAuthoringPslBlockDescriptor) now distinguishes a block descriptor from an entityTypes descriptor / sub-namespace for the merge walker during assembly — a different job than splitting parser from printer. If you'd still like that one reworked, say so.
framework-authoring.ts:297 — "Why do we need this type guard?" The guard count dropped 3 → 2 (entity + block). The survivor is load-bearing for the namespace merge walker (mergeAuthoringNamespaces needs a leaf predicate to tell descriptors from sub-namespaces). Not removable without reworking the walker; flagging in case you want that as a follow-up.
framework-authoring.ts:543 — "Should this include entity as well? … Why does helpers.enum() provide itself only to the TS builder? PSL has an enum representation too." Decision (please confirm): entityTypes kept as a separate registry for this slice. Rationale: a factory can stand alone today (the TS-builder path — the existing enum factory has no extension-contributed PSL block because enum is framework-parsed), so folding it in would force every TS-builder entity to ship parser/printer. The deeper question you're pointing at — should every PSL-representable entity contribute its own parser/printer instead of relying on framework-parsing? — is real, and it's exactly the enum-dual-representation tension; I routed it to TML-2815 (enum-as-application-concept) rather than resolve it here. Validation now enforces the one direction that holds: a pslBlocks descriptor requires a matching entityTypes factory; a factory may stand alone. Spec records this as an explicit non-goal with the rationale.

entries pattern (psl-ast.ts:188)

You asked whether we can use #715's namespace.entries pattern here. Having dug into the merged code: entries is an IR-layer concept (ADR 224 — it addresses the lowered namespace concretions, storage.namespaces[id].entries[kind][name]). #715 deliberately left the PSL-AST PslNamespace on its per-kind slots (models/enums/compositeTypes). So adopting entries here would mean inventing a PSL-AST shape #715 chose not to build — and doing it for only extension blocks (while models/enums stay slots) would create exactly the inconsistency you'd dislike. Migrating the whole PSL AST to entries is a framework-wide change worth its own project. For this slice I kept the per-kind-slot shape with a generic extensionBlocks slot for contributed kinds. Flagged in the spec's non-goals + the PR description's "Alternatives considered." Open to a follow-up ticket for the full PSL-AST entries migration if you want it.

Code-quality findings

Thread Resolution
framework-authoring.ts:456 (CodeRabbit) — malformed descriptor objects bypass validation Done (3d7339487). collectAuthoringLeafDiscriminators now rejects values carrying kind/discriminator that fail the descriptor guard, instead of silently descending into them. New test pins the rejection.
client.ts:551 — "Why is stack optional?" Done (3d7339487). Audited init(): it unconditionally assigns this.stack before setting initialized, and sibling methods already rely on that (this.stack! in emit). Dropped the ?./?? {} for this.stack! + a one-line comment recording the invariant. (The method also renamed getPslPrintersNamespacegetPslBlocksNamespace, since it returns the block namespace now.)

Validation after the restructure: pnpm typecheck clean on the four touched packages, their tests pass (framework-components 342 / psl-parser 96 / psl-printer 38 / cli 1211), lint:deps clean, lint:casts delta=-7 (net reduction). One pre-existing workspace-wide typecheck failure unrelated to this PR — @prisma-next/integration-tests can't resolve @prisma-next/adapter-sqlite/control (missing types export condition, an artifact of the #715/ADR-224 merge; adapter-postgres has the same shape). Worth a separate ticket.

Two threads have a decision I'd like you to confirm rather than just resolve — the entityTypes-separation (543) and the entries-layer call (188). The rest are straightforward done.

Copy link
Copy Markdown
Contributor

@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: 2

🧹 Nitpick comments (2)
packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts (1)

135-157: ⚡ Quick win

Consider extracting shared unescape logic.

The decodePslEscapes function duplicates the unescapePslString logic from ast-to-print-document.ts (lines 155-177). Both iterate character-by-character and handle the same escape sequences (\\, \", \', \n, \r) with identical logic.

Since unescapePslString is not currently exported, this duplication keeps the test fixture self-contained. However, extracting this to a shared utility (or exporting it from the printer) would reduce maintenance burden and ensure consistent escape handling across parser and printer paths.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts`
around lines 135 - 157, The test fixture duplicates the unescaping logic found
in ast-to-print-document.ts (function unescapePslString) via its local
decodePslEscapes; remove the duplication by moving the shared logic into a
single utility and reusing it: either export unescapePslString from
ast-to-print-document.ts and import it into fake-target-pack.ts (replacing
decodePslEscapes), or create a new shared helper (e.g., psl-string-utils with
unescapePslString) and update both callers (decodePslEscapes usage site and the
original location) to import that helper so escape handling for \\ , \", \', \n
and \r is centralized.
packages/1-framework/2-authoring/psl-printer/src/print-psl.ts (1)

28-31: ⚡ Quick win

Consider using ifDefined for conditional object spread.

The ternary-based conditional spread can be simplified using the ifDefined helper from @prisma-next/utils/defined, matching the established pattern in this codebase.

♻️ Refactor to use ifDefined
+import { ifDefined } from '`@prisma-next/utils/defined`';
+
 export function printPslFromAst(ast: PslDocumentAst, options: PrintPslOptions = {}): string {
   const doc = astDocumentToPrintDocument(ast);
   return serializePrintDocument(
     doc,
-    options.pslBlocks !== undefined ? { pslBlocks: options.pslBlocks } : {},
+    { ...ifDefined('pslBlocks', options.pslBlocks) },
   );
 }

Based on learnings from this repo: prefer ifDefined from @prisma-next/utils/defined for conditional object spreads instead of inline ternary-based spreads.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/1-framework/2-authoring/psl-printer/src/print-psl.ts` around lines
28 - 31, Replace the inline ternary used when calling serializePrintDocument
with the repository helper: import ifDefined from '`@prisma-next/utils/defined`'
and pass a conditional object produced by ifDefined for options.pslBlocks
instead of options.pslBlocks !== undefined ? { pslBlocks: options.pslBlocks } :
{}; specifically update the call site of serializePrintDocument and ensure the
import for ifDefined is added near other imports so the second argument only
contains the pslBlocks property when options.pslBlocks is defined.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts`:
- Around line 451-460: The current malformed-descriptor check in
framework-authoring.ts (inside the loader that walks namespaces) fires whenever
a non-leaf object has a kind/discriminator key; update the condition so it only
throws when the object actually looks like a broken descriptor: require that
kind/discriminator is present AND that the object does NOT contain any
descriptor-required fields (e.g. parser/printer or other descriptor properties
used by your descriptors). In short, replace the broad condition (record['kind']
!== undefined || record['discriminator'] !== undefined) with a tighter one that
also checks for the absence of descriptor fields (for example: (record.kind ||
record.discriminator) && !record.parser && !record.printer), so child namespaces
that legitimately use "kind"/"discriminator" segments are not rejected (and keep
mergeAuthoringNamespaces() behavior unchanged).
- Around line 530-546: The code currently collapses duplicate discriminators via
new Set and therefore misses duplicate-checks; update the validation in the
block/entity collection step (use the results of
collectAuthoringLeafDiscriminators for blockEntries and entityEntries) to detect
and reject duplicate discriminators within each group before creating
entityDiscriminators: scan blockEntries for repeated entry.discriminator and
throw a clear Error referencing the offending pslBlock paths and discriminator,
do the same for entityEntries (entityType) so duplicates are rejected rather
than silently last-wins, then proceed to build entityDiscriminators and keep the
existing cross-check between block.discriminator and entityDiscriminators.

---

Nitpick comments:
In `@packages/1-framework/2-authoring/psl-printer/src/print-psl.ts`:
- Around line 28-31: Replace the inline ternary used when calling
serializePrintDocument with the repository helper: import ifDefined from
'`@prisma-next/utils/defined`' and pass a conditional object produced by ifDefined
for options.pslBlocks instead of options.pslBlocks !== undefined ? { pslBlocks:
options.pslBlocks } : {}; specifically update the call site of
serializePrintDocument and ensure the import for ifDefined is added near other
imports so the second argument only contains the pslBlocks property when
options.pslBlocks is defined.

In
`@packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts`:
- Around line 135-157: The test fixture duplicates the unescaping logic found in
ast-to-print-document.ts (function unescapePslString) via its local
decodePslEscapes; remove the duplication by moving the shared logic into a
single utility and reusing it: either export unescapePslString from
ast-to-print-document.ts and import it into fake-target-pack.ts (replacing
decodePslEscapes), or create a new shared helper (e.g., psl-string-utils with
unescapePslString) and update both callers (decodePslEscapes usage site and the
original location) to import that helper so escape handling for \\ , \", \', \n
and \r is centralized.
🪄 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: 65f14c7b-a87f-4e76-8284-e0bb00be3847

📥 Commits

Reviewing files that changed from the base of the PR and between 631ff01 and 64b7e74.

📒 Files selected for processing (28)
  • packages/1-framework/1-core/framework-components/src/control/control-stack.ts
  • packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.ts
  • packages/1-framework/2-authoring/psl-parser/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.pack-blocks.test.ts
  • packages/1-framework/2-authoring/psl-printer/package.json
  • packages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/src/exports/index.ts
  • packages/1-framework/2-authoring/psl-printer/src/print-document.ts
  • packages/1-framework/2-authoring/psl-printer/src/print-psl.ts
  • packages/1-framework/2-authoring/psl-printer/src/serialize-print-document.ts
  • packages/1-framework/2-authoring/psl-printer/test/fake-target-pack.round-trip.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.pack-blocks.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/contract-infer.ts
  • packages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/db-schema.command.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/inspect-live-schema.test.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
💤 Files with no reviewable changes (10)
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.ts
  • packages/1-framework/3-tooling/cli/test/commands/inspect-live-schema.test.ts
  • packages/1-framework/3-tooling/cli/test/commands/db-schema.command.test.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/contract-infer.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.pack-blocks.test.ts
  • packages/1-framework/3-tooling/cli/src/control-api/client.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/2-authoring/psl-printer/package.json
  • packages/1-framework/2-authoring/psl-parser/test/parser.pack-blocks.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-printer/test/fake-target-pack.round-trip.test.ts

wmadden and others added 2 commits June 6, 2026 21:05
…+ finish vocabulary sweep

Parser contract (R7): a contributed parser that throws, or returns no node, now produces a PSL_EXTENSION_BLOCK_PARSE_FAILED diagnostic against the block span instead of crashing the parse or silently dropping the block. The dispatch asserts node.kind === descriptor.discriminator immediately after parsing, so a discriminator typo fails at parse time naming the descriptor rather than far away at contract-infer print time. The invariant is documented on both AuthoringPslBlockDescriptor.discriminator and PslExtensionBlock.kind.

Test gaps (R7): a command-level contract-infer test drives a real fake_policy block through the actual command path (AC8 now genuinely covered); a negative type test pins that the descriptor methods, not arrow properties, are what let a narrower contributed node assign to the base descriptor.

Vocabulary (R8): completed the pack -> extension rename on this slice surface — renamed the fixture/test files and the contributions export, reworded the CLI comments and the entity-type factory docstring, changed the test discriminator pack-foo -> fake-foo, and removed the Ref: TML-2804 breadcrumbs. Pre-existing pack references in unrelated code are left for the separate repo-wide rename.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…s closing slice (TML-2849)

PR-718 review surfaced that the PSL AST carries extension-contributed blocks in a flat extensionBlocks slot, a less-general second answer to the coordinate problem ADR 224 already solved on the contract IR. The decision is to converge PslNamespace onto ADR 224 entries[kind][name]; the substrate ships the flat slot as a deliberate interim. Reframe the spec section + non-goal + add project-DoD item 12, and add the entries-migration slice (TML-2849) to the project and slice plans, sequenced before the ADR + close-out.

Also records the two PR-review remediation dispatches (R7 contract hardening + test gaps, R8 vocabulary sweep) in the slice plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Copy link
Copy Markdown
Contributor

@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/3-tooling/cli/test/commands/contract-infer.command.test.ts (1)

377-377: ⚡ Quick win

Strengthen assertion to verify rendered block structure.

The current assertion only checks for the presence of 'fake_policy ReadOnlyUser', but does not verify that the target and using fields are correctly rendered. The printer at lines 326-332 explicitly formats these fields, and the test constructs a node with target: 'User' and using: 'auth.uid = user_id' (lines 344-345).

Add assertions to confirm the full block structure is written, ensuring the printer receives and renders all node fields correctly.

🔍 Suggested additional assertions
 const outputPath = join(testDir, 'output/contract.prisma');
 expect(existsSync(outputPath)).toBe(true);
 const content = readFileSync(outputPath, 'utf-8');
 expect(content).toContain('fake_policy ReadOnlyUser');
+expect(content).toContain('target = User');
+expect(content).toContain('using = "auth.uid = user_id"');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts`
at line 377, The test only asserts 'fake_policy ReadOnlyUser' but must also
verify the printer rendered the node's target and using fields; update the test
(contract-infer.command.test.ts) to assert that the output contains the 'target:
User' and 'using: auth.uid = user_id' lines (or assert the full block sequence
with a single regex like /fake_policy ReadOnlyUser\s*{\s*target: User\s*using:
auth.uid = user_id\s*}/) so the printer code that formats target/using (the
rendering around lines 326-332) is exercised and validated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In
`@packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts`:
- Line 377: The test only asserts 'fake_policy ReadOnlyUser' but must also
verify the printer rendered the node's target and using fields; update the test
(contract-infer.command.test.ts) to assert that the output contains the 'target:
User' and 'using: auth.uid = user_id' lines (or assert the full block sequence
with a single regex like /fake_policy ReadOnlyUser\s*{\s*target: User\s*using:
auth.uid = user_id\s*}/) so the printer code that formats target/using (the
rendering around lines 326-332) is exercised and validated.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 9fb55024-9e8e-487f-ad7d-33e3b8b0c6b6

📥 Commits

Reviewing files that changed from the base of the PR and between 64b7e74 and 93be405.

⛔ Files ignored due to path filters (3)
  • projects/target-contributed-psl-blocks/plan.md is excluded by !projects/**
  • projects/target-contributed-psl-blocks/slices/substrate/plan.md is excluded by !projects/**
  • projects/target-contributed-psl-blocks/spec.md is excluded by !projects/**
📒 Files selected for processing (12)
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.extension-blocks.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/fake-extension.round-trip.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-extension.ts
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.extension-blocks.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.ts
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.extension-blocks.test.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • packages/1-framework/3-tooling/cli/src/control-api/types.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/1-core/framework-components/test/control-stack.test.ts
  • packages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts

wmadden and others added 3 commits June 7, 2026 09:25
…iminators

A01: `collectAuthoringLeafDiscriminators` now only treats a non-leaf object as
a malformed descriptor when it carries descriptor-shaped keys (kind/discriminator)
AND lacks the descriptor body (parser/printer). Previously any object with a `kind`
or `discriminator` key triggered the rejection, which would have incorrectly
rejected valid sub-namespaces keyed "kind" or "discriminator". New test pins
that such a sub-namespace descends normally.

A02: Add `assertUniqueDiscriminators` helper called in `assertPslBlocksHaveFactories`
before building the entity discriminator match set. If two `pslBlocks` entries or two
`entityTypes` entries share a discriminator, the helper throws naming both offending
paths and the discriminated value. New tests pin duplicate-within-pslBlocks and
duplicate-within-entityTypes rejection.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Use `ifDefined('pslBlocks', options.pslBlocks)` instead of the inline
ternary, following the repo's `use-if-defined` rule for conditional object
spreads.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
The AC8 test previously asserted only that the written PSL file contains
'fake_policy ReadOnlyUser'. Add assertions for the contributed block's field
lines: `target = User` and `using = "auth.uid = user_id"`, matching the
inline fixture printer's `=`-and-quoted format (not the reviewer's `:` form).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Jun 7, 2026

Already addressed in commit 3d73394. collectAuthoringLeafDiscriminators now rejects objects that carry descriptor-shaped keys (kind/discriminator) but fail the leaf guard at load time, instead of silently recursing into them. A01 (thread PRRT_kwDOQM0QJc6HXxFJ) further tightens this in commit 4e1233c so valid sub-namespaces keyed 'kind'/'discriminator' still descend normally.

@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Jun 7, 2026

Both findings in this review are already addressed. A04a (vocabulary): the pack→extension rename is done in commit 64b7e74 and reinforced in 046b45fPslPackBlockPslExtensionBlock, packBlocksextensionBlocks, psl-substrate.tspsl-extension-block.ts, with no remaining occurrences of the old names in src. A04b (parser+printer on one descriptor): done in commit 3d73394AuthoringPslBlockDescriptor now declares both parser and printer; the separate pslPrinters slot was deleted so they cannot diverge.

@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Jun 7, 2026

A05a (ifDefined): done in commit c345054printPslFromAst now uses ifDefined('pslBlocks', options.pslBlocks) per the use-if-defined rule. A05b (fixture dedup): not addressed — unescapePslString is intentionally not exported from the printer, and the local copy in the test fixture keeps it self-contained. Exporting a printer internal purely to de-duplicate ~20 lines of test fixture would couple the test to printer internals without clear benefit.

@wmadden
Copy link
Copy Markdown
Contributor Author

wmadden commented Jun 7, 2026

Done — commit 9e93e0f adds assertions for target = User and using = "auth.uid = user_id" (the actual =-and-quoted format the inline printer emits, matching the fixture printer in fake-extension.ts lines 127-128).

@wmadden-electric
Copy link
Copy Markdown
Contributor

Converting to draft — pivoting the SPI shape

A design discussion concluded that the extension-contributed PSL block SPI should be declarative, not function-based. An extension should describe a top-level block — its keyword, name, and typed parameters — as data, and the framework interprets any declared block with one generic parser / validator / printer. It should not ship imperative parser / printer functions.

Why. The PSL grammar is closed and uniform (a block is a name + field declarations + x = y assignments + double-quoted values). So a block's structure can be described as data and validated/analysed without executing extension code. The function SPI on this branch re-implements parsing the framework already does, can't be validated from its data alone, and is the reason this PR needed the failure-isolation hardening (catching thrown parsers, undefined returns, kind/discriminator drift) — all of which exists only because arbitrary code runs at parse time. Describing blocks as data deletes that whole class of risk.

Parameter value-kinds (the vocabulary that replaces parser/printer):

Nothing here is discarded. The mechanism this branch built is cherry-picked into the reshaped project: the generic extensionBlocks slot, the unknown-keyword parser dispatch + diagnostic, the load-time validation (within-namespace duplicates, block↔factory matching, duplicate-discriminator + malformed-descriptor checks), the two-phase printer plumbing, the contract infer threading, and the round-trip test harness. Only the descriptor's parser/printer functions are replaced by the declarative descriptor + generic interpreter. The branch and its commits stay intact for cherry-picking.

Where the design + replan live: projects/target-contributed-psl-blocks/ (revised spec + plan) and a revision to ADR 126 (PSL top-level block SPI). This PR reopens once the reshaped substrate slice lands.

@wmadden-electric wmadden-electric marked this pull request as draft June 7, 2026 10:04
wmadden added a commit that referenced this pull request Jun 7, 2026
Reshapes the project from a function-based PSL-block SPI to a declarative one: an extension describes a block as data (keyword, name, typed parameters ref/value/option/list) and the framework owns one generic parser/validator/printer. Rewrites spec.md + plan.md (four-slice composition), banners ADR 126 (superseded function SPI; full rewrite is the close-out slice), and banners the superseded #718 dispatch plan. Docs only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
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