TML-2804: extensions can contribute top-level PSL block keywords#718
TML-2804: extensions can contribute top-level PSL block keywords#718wmadden wants to merge 15 commits into
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
🚧 Files skipped from review as they are similar to previous changes (3)
📝 WalkthroughWalkthroughAdds 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. ChangesPSL Extension-Block Authoring and Rendering
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/emitter
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (4)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlprojects/target-contributed-psl-blocks/plan.mdis excluded by!projects/**projects/target-contributed-psl-blocks/slices/substrate/plan.mdis excluded by!projects/**projects/target-contributed-psl-blocks/spec.mdis excluded by!projects/**
📒 Files selected for processing (33)
packages/1-framework/1-core/framework-components/src/control/control-stack.tspackages/1-framework/1-core/framework-components/src/control/psl-ast.tspackages/1-framework/1-core/framework-components/src/exports/authoring.tspackages/1-framework/1-core/framework-components/src/shared/framework-authoring.tspackages/1-framework/1-core/framework-components/src/shared/psl-substrate.tspackages/1-framework/1-core/framework-components/test/control-stack.test.tspackages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.tspackages/1-framework/2-authoring/psl-parser/src/exports/index.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser.pack-blocks.test.tspackages/1-framework/2-authoring/psl-printer/package.jsonpackages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.tspackages/1-framework/2-authoring/psl-printer/src/exports/index.tspackages/1-framework/2-authoring/psl-printer/src/print-document.tspackages/1-framework/2-authoring/psl-printer/src/print-psl.tspackages/1-framework/2-authoring/psl-printer/src/serialize-print-document.tspackages/1-framework/2-authoring/psl-printer/test/fake-target-pack.round-trip.test.tspackages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.tspackages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.pack-blocks.test.tspackages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.tspackages/1-framework/3-tooling/cli/src/commands/contract-infer.tspackages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.tspackages/1-framework/3-tooling/cli/src/control-api/client.tspackages/1-framework/3-tooling/cli/src/control-api/types.tspackages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.tspackages/1-framework/3-tooling/cli/test/commands/db-schema.command.test.tspackages/1-framework/3-tooling/cli/test/commands/inspect-live-schema.test.tspackages/1-framework/3-tooling/cli/test/config-types.test.tspackages/2-mongo-family/2-authoring/contract-psl/test/provider.test.tspackages/2-mongo-family/2-authoring/contract-ts/test/config-types.test.tspackages/2-sql/2-authoring/contract-psl/test/fixtures.tspackages/2-sql/2-authoring/contract-ts/test/config-types.test.tspackages/2-sql/9-family/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts
wmadden
left a comment
There was a problem hiding this comment.
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.
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>
631ff01 to
64b7e74
Compare
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:
Vocabulary
Scope note: I left legitimate pre-existing "pack" terminology untouched (the "extension pack" concept, Architecture — parser + printer are now one descriptor
|
| 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 getPslPrintersNamespace → getPslBlocksNamespace, 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
packages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.ts (1)
135-157: ⚡ Quick winConsider extracting shared unescape logic.
The
decodePslEscapesfunction duplicates theunescapePslStringlogic fromast-to-print-document.ts(lines 155-177). Both iterate character-by-character and handle the same escape sequences (\\,\",\',\n,\r) with identical logic.Since
unescapePslStringis 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 winConsider using
ifDefinedfor conditional object spread.The ternary-based conditional spread can be simplified using the
ifDefinedhelper 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
ifDefinedfrom@prisma-next/utils/definedfor 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
📒 Files selected for processing (28)
packages/1-framework/1-core/framework-components/src/control/control-stack.tspackages/1-framework/1-core/framework-components/src/control/psl-ast.tspackages/1-framework/1-core/framework-components/src/exports/authoring.tspackages/1-framework/1-core/framework-components/src/shared/framework-authoring.tspackages/1-framework/1-core/framework-components/src/shared/psl-extension-block.tspackages/1-framework/1-core/framework-components/test/control-stack.test.tspackages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.tspackages/1-framework/2-authoring/psl-parser/src/exports/index.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser.pack-blocks.test.tspackages/1-framework/2-authoring/psl-printer/package.jsonpackages/1-framework/2-authoring/psl-printer/src/ast-to-print-document.tspackages/1-framework/2-authoring/psl-printer/src/exports/index.tspackages/1-framework/2-authoring/psl-printer/src/print-document.tspackages/1-framework/2-authoring/psl-printer/src/print-psl.tspackages/1-framework/2-authoring/psl-printer/src/serialize-print-document.tspackages/1-framework/2-authoring/psl-printer/test/fake-target-pack.round-trip.test.tspackages/1-framework/2-authoring/psl-printer/test/fixtures/fake-target-pack.tspackages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.pack-blocks.test.tspackages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.test.tspackages/1-framework/3-tooling/cli/src/commands/contract-infer.tspackages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.tspackages/1-framework/3-tooling/cli/src/control-api/client.tspackages/1-framework/3-tooling/cli/src/control-api/types.tspackages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.tspackages/1-framework/3-tooling/cli/test/commands/db-schema.command.test.tspackages/1-framework/3-tooling/cli/test/commands/inspect-live-schema.test.tspackages/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
…+ 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>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
packages/1-framework/3-tooling/cli/test/commands/contract-infer.command.test.ts (1)
377-377: ⚡ Quick winStrengthen assertion to verify rendered block structure.
The current assertion only checks for the presence of
'fake_policy ReadOnlyUser', but does not verify that thetargetandusingfields are correctly rendered. The printer at lines 326-332 explicitly formats these fields, and the test constructs a node withtarget: 'User'andusing: '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
⛔ Files ignored due to path filters (3)
projects/target-contributed-psl-blocks/plan.mdis excluded by!projects/**projects/target-contributed-psl-blocks/slices/substrate/plan.mdis excluded by!projects/**projects/target-contributed-psl-blocks/spec.mdis excluded by!projects/**
📒 Files selected for processing (12)
packages/1-framework/1-core/framework-components/src/shared/framework-authoring.tspackages/1-framework/1-core/framework-components/src/shared/psl-extension-block.tspackages/1-framework/1-core/framework-components/test/control-stack.test.tspackages/1-framework/1-core/framework-components/test/framework-authoring-psl.types.test-d.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser.extension-blocks.test.tspackages/1-framework/2-authoring/psl-printer/test/fake-extension.round-trip.test.tspackages/1-framework/2-authoring/psl-printer/test/fixtures/fake-extension.tspackages/1-framework/2-authoring/psl-printer/test/print-psl-from-ast.extension-blocks.test.tspackages/1-framework/3-tooling/cli/src/commands/inspect-live-schema.tspackages/1-framework/3-tooling/cli/src/control-api/types.tspackages/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
…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>
|
Already addressed in commit 3d73394. |
|
Both findings in this review are already addressed. A04a (vocabulary): the pack→extension rename is done in commit 64b7e74 and reinforced in 046b45f — |
|
A05a (ifDefined): done in commit c345054 — |
|
Done — commit 9e93e0f adds assertions for |
Converting to draft — pivoting the SPI shapeA 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 Why. The PSL grammar is closed and uniform (a block is a name + field declarations + Parameter value-kinds (the vocabulary that replaces
Nothing here is discarded. The mechanism this branch built is cherry-picked into the reshaped project: the generic Where the design + replan live: |
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>
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
entityTypesregistry — the two tied together by a shareddiscriminatorstring: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 inentityTypesbecause a factory can stand alone (the TS-builder path reaches it without PSL). Load-time validation enforces the one direction that matters: everypslBlocksdescriptor must have a matching-discriminatorentityTypesfactory.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: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).role { … }for the same reasons.Shipping the mechanism now means each downstream feature lands as a focused PR.
How it works
AuthoringContributionshad three namespaces before this PR —type(type constructors),field(field presets),entityTypes(entity factories). This PR adds one:pslBlocks, whose descriptors each carry aparser+ aprinter.Parsing. The framework parser's top-level dispatch already handled the five built-in keywords; everything else surfaced an
Unsupported top-level blockdiagnostic. Now, on an unknown identifier, the parser consults thepslBlocksregistry: if a descriptor claims the keyword, itsparserruns and produces an AST node that lands in a new generic slot onPslNamespace: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
entriescoordinate-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 ontoentriesis 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:
PSL_EXTENSION_BLOCK_PARSE_FAILEDdiagnostic against the block span — the rest of the document keeps parsing, instead of one bad extension crashing the whole parse (andcontract inferwith it).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 bothAuthoringPslBlockDescriptor.discriminatorandPslExtensionBlock.kind.Printing. The PSL printer is two-phase (AST →
PrintDocument→ string), and both need to handle extension-contributed blocks forcontract inferto keep working. Phase 1 carries them into aPrintNamespaceSection.extensionBlocksslot; phase 2 builds a discriminator-keyed map frompslBlocksand renders each block by calling the owning descriptor'sprinter. If a block's discriminator has no matching descriptor, the serializer throws (silent-drop would lose user-authored content duringcontract infer). The CLI'scontract inferthreads the assembledpslBlocksnamespace through toprintPsl(ast, { pslBlocks }).Validation at load time. Within-namespace duplicates throw, naming both extensions. A
pslBlocksdescriptor with no matchingentityTypesfactory throws. Malformed descriptors — objects carryingkind/discriminatorbut 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:Tests pin the substrate end-to-end:
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.extension-blocks.test.ts): a throwing contributed parser → diagnostic with the document still parsing; anundefinedreturn → diagnostic;kind≠discriminator→ diagnostic naming both.contract inferend-to-end (contract-infer.command.test.ts): a realfake_policyblock driven through the actual command path (control-stack namespace → command →printPsl→ written file), asserting the rendered block survives to disk.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
enumis not moved to an extension — see "Alternatives considered."PslNamespace→entriesmigration in this slice. The substrate ships the flatextensionBlocksslot as a deliberate interim; converging the PSL AST onto ADR 224'sentries[kind][name]shape is the project's closing slice, TML-2849 — see "Alternatives considered."@policy(…)). Different SPI shape; tracked separately.AGENTS.mdentities→entityTypesdoc-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
pnpm typecheck(the four touched packages + dependents)pnpm test:packagesforframework-components/psl-parser/psl-printer/clipnpm lint:depspnpm lint:castscurrent=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-testscan't resolve@prisma-next/adapter-sqlite/control(the./controlexport lacks atypescondition — an artifact of the #715 / ADR-224 merge).@prisma-next/adapter-postgreshas 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+pslPrintersnamespaces 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
entityTypesfactory 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 —entityTypescan stand alone (the TS-builder path; e.g. the existingenumfactory has no extension-contributed PSL block, sinceenumis 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
enumto an extension contribution as the load-bearing proof-of-concept (the original project framing). Rejected:enumis 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 removeenumfrom SQLite + Mongo contracts — a regression vs Prisma 1.x/2.x. Cross-targetenumsupport 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 inferwould silently break for every extension-contributed block kind. They ship together.The flat
extensionBlocksslot vs. ADR 224'sentriesshape. 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 flatextensionBlocksarray 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 onentries. 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 touchPslNamespacedirectly, so the storage-shape migration is framework-internal.A typed
PslNamespaceslot per contributed kind (policies: PslPolicy[], …). Rejected: every new kind would need a framework PR. The genericextensionBlocksslot keeps the framework AST stable as extensions add kinds (and is the interim theentriesmigration 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 aPSL_EXTENSION_BLOCK_PARSE_FAILEDdiagnostic instead.Refs: TML-2804 (this slice), TML-2803 (planning), TML-2849 (PSL-AST →
entriesmigration, closing slice), TML-2806 (docs + close-out slice), TML-2844 (adaptertypes-condition fix), TML-2815 (enum-as-application-concept, sibling project).Generated with Devin
Summary by CodeRabbit
Release Notes