Skip to content

TML-2804: declarative SPI for extension-contributed PSL blocks (Slice 1 — read side)#753

Open
wmadden-electric wants to merge 6 commits into
mainfrom
tml-2804-slice-1-declarative-block-descriptor-generic
Open

TML-2804: declarative SPI for extension-contributed PSL blocks (Slice 1 — read side)#753
wmadden-electric wants to merge 6 commits into
mainfrom
tml-2804-slice-1-declarative-block-descriptor-generic

Conversation

@wmadden-electric
Copy link
Copy Markdown
Contributor

@wmadden-electric wmadden-electric commented Jun 7, 2026

At a glance

Slice 1 of 4 of Target-contributed top-level PSL blocks — the read side of the declarative SPI. After this, an extension describes a new top-level PSL block as data (a descriptor of its keyword, name, and typed parameters), and the framework parses, validates, and lowers it to contract IR with one generic interpreter. No extension-supplied parse/print code runs.

This supersedes the function-based SPI built on #718 (now draft): that cut had each extension ship imperative parser/printer functions. The mechanism is cherry-picked here; the functions are replaced by the descriptor + generic interpreter. Rationale + design are in projects/target-contributed-psl-blocks/spec.md (landed via #749).

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

  policy_select profiles_select_anon {
    target = Profile               // ref (same-namespace)
    as     = permissive            // option: permissive | restrictive
    roles  = [anon, authenticated] // list of ref (cross-space)
    using  = "auth.uid() = user_id"// value (codec-typed)
  }
}

The framework never learns the policy_select keyword directly — the descriptor is data.

What's in this PR

The parameter value-kind vocabulary and the generic read pipeline, built across six dispatches:

  • Declarative descriptor + vocabulary (framework-components) — AuthoringPslBlockDescriptor is data: keyword, discriminator, name, and a parameters map of the four-kind union PslBlockParam: ref (resolves to a declared entity; refKind + scope), value (codec-typed), option (a closed set of allowed literal tokens — an authoring constraint, not a domain enum), list (combinator). No parser/printer fields. Plus the uniform parsed-node base ({ kind, name, parameters, span }) and the PslNamespace.extensionBlocks slot.
  • Codec PSL-literal parse hook — codecs gain a PSL-text → literal parse/validate direction pairing with the existing encodeJson; string codecs (sql/pg/sqlite) cover what value needs. So value rides the same type system field types and @default already use.
  • Generic parser (psl-parser) — on an unknown top-level keyword, the parser reads any declared block into the uniform node, capturing each parameter's RHS by its declared kind. Built-in keywords stay framework-parsed; an unregistered keyword still gets the clean unknown-keyword diagnostic.
  • Generic validator + ref resolution — reports (with spans) unknown / missing-required parameters, option-not-in-set, codec-rejected value, and ref unresolved within its scope (same-namespace/same-space resolved against the parsed namespaces; cross-space is a documented pass-through per the spec's first-consumer allowance, leaning on the merged TML-2500 (M1): cross-contract FK carrier + aggregate-load checks #745 coordinate model). A small PSL_EXTENSION_* diagnostic taxonomy.
  • Load-time validationassembleAuthoringContributions rejects duplicate keyword, duplicate discriminator (within pslBlocks and within entityTypes), a pslBlocks descriptor with no matching entityTypes factory, and a malformed descriptor. Standalone entityTypes factories stay allowed.
  • Lowering + fixture + round-trip — the uniform node lowers to IR via the matching entityTypes factory; a test-only declarative policy_select fixture round-trips parse → validate → lower → IR.

A design decision worth a look

Validation runs inside parsePslDocument whenever pslBlocks are registered (joining the parser's existing diagnostics), with codecLookup defaulting to a fail-closed emptyCodecLookup (so a caller that registers blocks but doesn't thread codecs gets loud value rejections rather than silent skips). No production caller passes pslBlocks yet, and fail-closed is the right default for a security-adjacent SPI — but it's a seam choice, flagged for visibility.

Out of scope (later slices)

  • Generic printer + contract infer — Slice 2, TML-2854. This PR's round-trip stops at the lowered IR.
  • PslNamespace → ADR 224 entries migration — Slice 3, TML-2849.
  • ADR 126 revision + three-layer ADR + close-out — Slice 4, TML-2806.
  • A number value-kind / field-declaration block bodies (deferred to first consumer); real RLS (downstream).

Relationships

  • #718 — superseded function-SPI cut; draft; mechanism cherry-picked; branch preserved.
  • #745 / TML-2500 — merged; ref cross-space scope builds on its coordinate model. Not a blocker.
  • #748 / enums-as-domain — independent; option is not an enum, value uses ordinary scalar codecs.

Verification

Gate Result
pnpm typecheck (the four touched packages) clean
pnpm test:packages (psl-parser / psl-printer / framework-components / cli, in isolation) pass — 100 / 34 / 395 / 1316
pnpm lint:deps clean
pnpm lint:casts delta=0 (no regression)

A monorepo-wide typecheck has pre-existing, unrelated worktree dist-not-built failures (migration-tools / mongo-contract / sql-schema-ir / emitter / mongo-lowering) — none introduced here; per-package typechecks for the touched packages are clean, and the changes don't alter a public export signature those packages consume.

Follow-ups recorded for downstream (not this slice)

  • ref-kind vs discriminator mismatch for a non-cross-space ref to an extension-contributed entity (the first RLS-roles-style consumer must map refKind → discriminator).
  • Duplicate-keyword-string across different namespace keys isn't caught at assembly (duplicate-path is); harden in the keyword-dispatch index if it matters.

Refs TML-2804.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for PSL extension blocks—custom declarative blocks that can be registered and used within namespaces.
    • Extended parameter validation for extension blocks, including reference resolution, literal value parsing, and scope-aware validation.
    • Added PSL literal parsing support to codec descriptors for enhanced value validation.

wmadden and others added 6 commits June 7, 2026 12:57
…pes (D1)

AuthoringPslBlockDescriptor is data — keyword, discriminator, name, parameters map of the four-kind union PslBlockParam (ref/value/option/list). No parser/printer functions. Adds the uniform parsed-block node base (name + parameter map), the PslNamespace.extensionBlocks slot, AuthoringContributions.pslBlocks, and isAuthoringPslBlockDescriptor. PslPosition/PslSpan/PslDiagnosticCode move to the shared plane (re-exported from psl-ast) so the shared-plane descriptor can reference spans. Types + type tests only; runtime is D2-D6.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
Adds a PSL-text -> literal parse/validate direction pairing with encodeJson: an optional parsePslLiteral(raw) on CodecDescriptor (rejecting default on CodecDescriptorImpl), surfaced via CodecLookup.parsePslLiteralFor(id, raw) (mirrors renderOutputTypeFor). String codecs (sql/pg/sqlite text/char/varchar) override it to accept double-quoted strings; shared parseStringPslLiteral, pg/sqlite delegate. Result {ok,value|error} feeds the D4 validator. Parse direction only — print-back is Slice 2 (TML-2854); Int deferred.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
On an unknown top-level keyword, the parser consults the pslBlocks registry and reads the block body GENERICALLY into a uniform PslExtensionBlock node (name + parameter map), capturing each parameter RHS by its declared kind: ref->identifier, value->raw text, option->token, list->bracketed items (recursive per element kind). Reuses #718 block-bounds/comment-strip/diagnostic; DROPS the contributed-parser failure isolation + per-extension SPI context (no contributed code runs). Unknown keys captured faithfully as raw values for D4 to report by key-set diff; built-in parsing unchanged. Validation, codec calls, ref resolution, printing, lowering are all out of scope (D4/D6).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
validateExtensionBlock(node, descriptor, sourceId, codecLookup, refCtx?) reports, with spans: unknown parameter (key-set diff, not by captured kind), missing required, option-not-in-set, value rejected by its codec (parsePslLiteralFor), and ref unresolved within scope (same-namespace/same-space resolved against the parsed namespaces; cross-space a documented pass-through per the spec first-consumer allowance); list recurses. Adds the PSL_EXTENSION_* diagnostic taxonomy incl. PSL_INVALID_EXTENSION_BLOCK_MEMBER, and re-points D3 two malformed-body-line parser sites to it. char/varchar length intentionally not enforced (unbounded text). Validator is exported; D6 wires it into the round-trip.

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

assembleAuthoringContributions rejects, naming the extension: within-namespace duplicate path (merge walker), duplicate discriminator within pslBlocks and within entityTypes (assertUniqueDiscriminators), a pslBlock with no matching entityTypes factory (assertPslBlocksHaveFactories), and a malformed declarative descriptor (re-keyed: isAuthoringPslBlockDescriptor rejects AND it looks descriptor-shaped). Standalone entityTypes factories stay allowed (one-directional check). Cherry-picked from #718 review-final; pslBlocks now a required field on AssembledAuthoringContributions (+ empty-namespace fixture updates).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
…er round-trip (D6)

Slice-closing dispatch. A test-only declarative policy_select extension (descriptor + entityTypes factory + IR class, no parser/printer functions) round-trips parse -> validate -> lower -> IR. The factory reads the uniform node.parameters declaratively. validateExtensionBlock is wired into parsePslDocument: when pslBlocks are registered, each block is validated against its descriptor (resolved by discriminator) with refCtx={ownerNamespace,allNamespaces}; codecLookup defaults to emptyCodecLookup (fail-closed for value params). Adds optional codecLookup to ParsePslDocumentInput. Fixes the D4 stale JSDoc code. Print leg is Slice 2 (TML-2854).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric wmadden-electric requested a review from a team as a code owner June 7, 2026 14:26
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 7, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive extension-block mechanism for PSL, enabling declarative registration and parsing of custom top-level blocks like policy_select. The implementation spans type contracts, codec literal parsing, authoring framework integration, parser support, validation logic, and production SQL codec implementations with extensive test coverage.

Changes

PSL Extension Blocks Feature

Layer / File(s) Summary
Extension block type vocabulary
packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts, test/psl-block-descriptor.types.test.ts
Introduces PslPosition, PslSpan, diagnostic codes, descriptor-level PslBlockParam* types, parsed parameter shapes PslExtensionBlockParam*, and the PslExtensionBlock AST node; includes compile-time discriminated union and runtime narrowing tests.
Codec descriptor PSL literal parsing
packages/1-framework/1-core/framework-components/src/shared/codec-descriptor.ts, codec-types.ts, src/exports/codec.ts, test/psl-literal-parse.test.ts
Adds optional parsePslLiteral hook to codec descriptors with default rejection; extends CodecLookup with parsePslLiteralFor(id, raw) lookup; tests override behavior, default fallback, and "not registered" results.
Extension block authoring contributions
packages/1-framework/1-core/framework-components/src/shared/framework-authoring.ts, src/exports/authoring.ts, test/control-stack.test.ts
Extends AuthoringContributions with optional pslBlocks namespace; introduces AuthoringPslBlockDescriptor type and isAuthoringPslBlockDescriptor runtime guard; enforces discriminator uniqueness and validates entityType factory consistency; includes namespace merging and collision detection tests.
Control stack extension block assembly
packages/1-framework/1-core/framework-components/src/control/control-stack.ts, src/control/psl-ast.ts, src/exports/psl-ast.ts
Updates AssembledAuthoringContributions with pslBlocks; merges descriptors via predicate; wires parsePslLiteralFor into extractCodecLookup; extends PslNamespace with optional extensionBlocks field and ParsePslDocumentInput with pslBlocks/codecLookup parameters.
D4 extension block validator
packages/1-framework/1-core/framework-components/src/control/psl-extension-block-validator.ts, test/psl-extension-block-validator.test.ts
Implements validateExtensionBlock with comprehensive diagnostics: unknown/missing parameters, option value set membership, codec literal validation via CodecLookup, and ref scope resolution (same-namespace, same-space, cross-space); includes entity resolution across built-in and extension blocks.
D3 parser support for extension blocks
packages/1-framework/2-authoring/psl-parser/src/parser.ts, test/parser.test.ts
Extends parser to parse D3 extension blocks within namespaces using registered descriptor lookup; collects into extensionBlocks; triggers D4 validation post-AST; captures unknown parameters as raw values; updates unspecified namespace dropping logic; includes keyword handling and ordering tests.
SQL codec PSL literal parsing
packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts, test/ast/sql-codecs.test.ts, packages/3-targets/3-targets/{postgres,sqlite}/src/core/codecs.ts
Implements parseStringPslLiteral helper for double-quoted strings; overrides parsePslLiteral in SqlTextDescriptor, SqlCharDescriptor, SqlVarcharDescriptor, and target descriptors; tests quote stripping and rejection of invalid formats.
Test infrastructure codec lookup stubs
packages/{1-framework/3-tooling/cli,2-mongo-family,2-sql,3-targets}/*/test/*.ts
Updates test fixtures across domains to implement parsePslLiteralFor on codec lookup stubs and add pslBlocks: {} to authoring contribution contexts.
Declarative policy_select fixture and round-trip
packages/1-framework/2-authoring/psl-printer/test/fixtures/declarative-policy-select-extension.ts, test/declarative-policy-select.round-trip.test.ts
Implements policy_select IR class with hydration; registers authoring contributions with pslBlocks schema; validates complete parse/validate/lower/serialize flow with optional as, missing required fields, unresolvable refs, and codecLookup omission scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • aqrln

Poem

🐰 Extension blocks bloom in nested spaces,
Where codecs parse literals in graceful traces,
Validators whisper scope-bound rules so fine,
And parsers dance through policy_select lines.
A uniform AST, round-tripped with care,
Custom top-level blocks now flourish there! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 clearly and specifically describes the main change: implementing a declarative SPI for extension-contributed PSL blocks, with 'Slice 1 — read side' indicating this is part of a larger feature.
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-declarative-block-descriptor-generic

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

❤️ Share

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 7, 2026

Open in StackBlitz

@prisma-next/extension-author-tools

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

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-cache

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

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/extension-postgis

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/extension-supabase

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/extension-supabase@753

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/cli-telemetry

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 7bce958

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 7, 2026

size-limit report 📦

Path Size
postgres / no-emit 146.89 KB (+0.52% 🔺)
postgres / emit 117.5 KB (+0.15% 🔺)
mongo / no-emit 76.65 KB (+0.73% 🔺)
mongo / emit 70.95 KB (0%)
cf-worker / no-emit 176.74 KB (+0.48% 🔺)
cf-worker / emit 144.12 KB (+0.15% 🔺)

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: 3

🤖 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 267-301: The current isAuthoringPslBlockDescriptor function only
checks that parameters is a non-null object, letting malformed parameter
descriptors slip through; update isAuthoringPslBlockDescriptor to fully validate
parameters by: confirming parameters is a plain object (not an array), iterating
Object.entries(parameters) and for each entry ensuring the key is a non-empty
string and the value is an object (not null/array) with the expected properties
(at minimum a boolean 'required' and a non-empty string 'type' or similar
parameter descriptor fields used elsewhere), returning false if any parameter
entry fails these checks; keep the changes inside isAuthoringPslBlockDescriptor
to locate via that function name.

In `@packages/1-framework/1-core/framework-components/test/control-stack.test.ts`:
- Around line 383-409: The test description claims to exercise pslBlocks but the
fixture populates authoring.entityTypes instead, so update the test to actually
exercise pslBlocks: change the fixture passed to
assembleAuthoringContributions/createDescriptor to place the nested namespaces
under authoring.pslBlocks (mirroring the existing kind/discriminator entries) or
alternatively adjust the test description to reference authoring.entityTypes;
ensure the symbols assembleAuthoringContributions, createDescriptor and the
nested keys kind/discriminator are used under authoring.pslBlocks so the
malformed-check path for pslBlocks is truly tested.

In `@packages/1-framework/2-authoring/psl-parser/src/parser.ts`:
- Around line 1656-1661: The loop that parses list segments currently skips
empty items (if itemRhs.length === 0) instead of reporting an error; change this
to emit a PSL_INVALID_EXTENSION_BLOCK_MEMBER diagnostic for empty list elements
(e.g., when encountering `[a,,b]` or `[a,]`) using the parser's existing
error/diagnostic API and the segment's location information (use the segment
token/span like segment.start/segment.end or whatever location fields exist)
rather than continuing; keep normal parsing for non-empty items (the code that
handles itemRhs should remain unchanged).
🪄 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: cf839e2a-9f5d-4bb3-ab37-436d1b02bd25

📥 Commits

Reviewing files that changed from the base of the PR and between b661ee1 and 7bce958.

📒 Files selected for processing (39)
  • 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/control/psl-extension-block-validator.ts
  • packages/1-framework/1-core/framework-components/src/exports/authoring.ts
  • packages/1-framework/1-core/framework-components/src/exports/codec.ts
  • packages/1-framework/1-core/framework-components/src/exports/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/shared/codec-descriptor.ts
  • packages/1-framework/1-core/framework-components/src/shared/codec-types.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/psl-block-descriptor.types.test.ts
  • packages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.ts
  • packages/1-framework/1-core/framework-components/test/psl-literal-parse.test.ts
  • packages/1-framework/2-authoring/psl-parser/src/parser.ts
  • packages/1-framework/2-authoring/psl-parser/test/parser.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.ts
  • packages/1-framework/2-authoring/psl-printer/test/fixtures/declarative-policy-select-extension.ts
  • packages/1-framework/3-tooling/cli/test/config-types.test.ts
  • packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/derive-json-schema.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.ts
  • packages/2-mongo-family/2-authoring/contract-psl/test/interpreter.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-psl/test/provider.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/config-types.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.ts
  • packages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.ts
  • packages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.ts
  • packages/3-mongo-target/1-mongo-target/test/mongo-runner.polymorphism.integration.test.ts
  • packages/3-targets/3-targets/postgres/src/core/codecs.ts
  • packages/3-targets/3-targets/sqlite/src/core/codecs.ts
  • packages/3-targets/6-adapters/postgres/test/raw-expr-lowering.test.ts
  • packages/3-targets/6-adapters/postgres/test/sql-renderer-namespace-qualification.test.ts
  • packages/3-targets/6-adapters/postgres/test/sql-renderer.cast-policy.test.ts

Comment on lines +267 to +301
export function isAuthoringPslBlockDescriptor(
value: unknown,
): value is AuthoringPslBlockDescriptor {
if (typeof value !== 'object' || value === null) {
return false;
}
const record = blindCast<
Record<string, unknown>,
'type-guard probing an unknown candidate-descriptor object for known property names'
>(value);
if (record['kind'] !== 'pslBlock') {
return false;
}
const keyword = record['keyword'];
if (typeof keyword !== 'string' || keyword.length === 0) {
return false;
}
const discriminator = record['discriminator'];
if (typeof discriminator !== 'string' || discriminator.length === 0) {
return false;
}
const name = record['name'];
if (typeof name !== 'object' || name === null) {
return false;
}
const nameRecord = blindCast<
Record<string, unknown>,
'type-guard probing the name property of a candidate pslBlock descriptor'
>(name);
if (typeof nameRecord['required'] !== 'boolean') {
return false;
}
const parameters = record['parameters'];
return typeof parameters === 'object' && parameters !== null && !Array.isArray(parameters);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Tighten isAuthoringPslBlockDescriptor to reject malformed parameter descriptors.

At Line 299, the guard only verifies that parameters is an object. That lets malformed parameter entries pass as “valid” descriptors, which defeats load-time malformed-descriptor rejection and pushes failures to later parser/validator paths.

Proposed fix
+function isPslBlockParamDescriptor(value: unknown): value is PslBlockParam {
+  if (typeof value !== 'object' || value === null) {
+    return false;
+  }
+  const record = blindCast<
+    Record<string, unknown>,
+    'type-guard probing pslBlock parameter descriptor candidate'
+  >(value);
+  const required = record['required'];
+  if (required !== undefined && typeof required !== 'boolean') {
+    return false;
+  }
+  switch (record['kind']) {
+    case 'ref':
+      return (
+        typeof record['refKind'] === 'string' &&
+        typeof record['scope'] === 'string' &&
+        ['same-namespace', 'same-space', 'cross-space'].includes(
+          blindCast<string, 'scope string check'>(record['scope']),
+        )
+      );
+    case 'value':
+      return typeof record['codecId'] === 'string';
+    case 'option':
+      return (
+        Array.isArray(record['values']) &&
+        record['values'].every((entry) => typeof entry === 'string')
+      );
+    case 'list':
+      return isPslBlockParamDescriptor(record['of']);
+    default:
+      return false;
+  }
+}
+
 export function isAuthoringPslBlockDescriptor(
   value: unknown,
 ): value is AuthoringPslBlockDescriptor {
@@
   const parameters = record['parameters'];
-  return typeof parameters === 'object' && parameters !== null && !Array.isArray(parameters);
+  if (typeof parameters !== 'object' || parameters === null || Array.isArray(parameters)) {
+    return false;
+  }
+  return Object.values(parameters).every(isPslBlockParamDescriptor);
 }
🤖 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/1-core/framework-components/src/shared/framework-authoring.ts`
around lines 267 - 301, The current isAuthoringPslBlockDescriptor function only
checks that parameters is a non-null object, letting malformed parameter
descriptors slip through; update isAuthoringPslBlockDescriptor to fully validate
parameters by: confirming parameters is a plain object (not an array), iterating
Object.entries(parameters) and for each entry ensuring the key is a non-empty
string and the value is an object (not null/array) with the expected properties
(at minimum a boolean 'required' and a non-empty string 'type' or similar
parameter descriptor fields used elsewhere), returning false if any parameter
entry fails these checks; keep the changes inside isAuthoringPslBlockDescriptor
to locate via that function name.

Comment on lines +383 to +409
it('descends into a pslBlocks sub-namespace whose key is "kind" or "discriminator" without triggering malformed check', () => {
// A sub-namespace keyed "kind" or "discriminator" that does not itself
// look like a descriptor must descend normally.
expect(() =>
assembleAuthoringContributions([
createDescriptor({
authoring: {
entityTypes: {
kind: {
nested: {
kind: 'entity',
discriminator: 'test-entity-in-kind-ns',
output: { factory: () => ({}) },
},
},
discriminator: {
nested: {
kind: 'entity',
discriminator: 'test-entity-in-discriminator-ns',
output: { factory: () => ({}) },
},
},
},
},
}),
]),
).not.toThrow();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test intent and exercised namespace are mismatched.

Line 383 says this verifies pslBlocks traversal for keys "kind"/"discriminator", but the fixture only populates authoring.entityTypes. That means the pslBlocks path isn’t actually tested here.

Suggested test fix
-  it('descends into a pslBlocks sub-namespace whose key is "kind" or "discriminator" without triggering malformed check', () => {
+  it('descends into a pslBlocks sub-namespace whose key is "kind" or "discriminator" without triggering malformed check', () => {
     // A sub-namespace keyed "kind" or "discriminator" that does not itself
     // look like a descriptor must descend normally.
     expect(() =>
       assembleAuthoringContributions([
         createDescriptor({
           authoring: {
             entityTypes: {
-              kind: {
-                nested: {
-                  kind: 'entity',
-                  discriminator: 'test-entity-in-kind-ns',
-                  output: { factory: () => ({}) },
-                },
-              },
-              discriminator: {
-                nested: {
-                  kind: 'entity',
-                  discriminator: 'test-entity-in-discriminator-ns',
-                  output: { factory: () => ({}) },
-                },
-              },
+              kindEntity: {
+                kind: 'entity',
+                discriminator: 'test-entity-in-kind-ns',
+                output: { factory: () => ({}) },
+              },
+              discriminatorEntity: {
+                kind: 'entity',
+                discriminator: 'test-entity-in-discriminator-ns',
+                output: { factory: () => ({}) },
+              },
+            },
+            pslBlocks: {
+              kind: {
+                nested: makeDeclarativePslBlockDescriptor('test-entity-in-kind-ns'),
+              },
+              discriminator: {
+                nested: makeDeclarativePslBlockDescriptor('test-entity-in-discriminator-ns'),
+              },
             },
           },
         }),
       ]),
     ).not.toThrow();
   });
🤖 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/1-core/framework-components/test/control-stack.test.ts`
around lines 383 - 409, The test description claims to exercise pslBlocks but
the fixture populates authoring.entityTypes instead, so update the test to
actually exercise pslBlocks: change the fixture passed to
assembleAuthoringContributions/createDescriptor to place the nested namespaces
under authoring.pslBlocks (mirroring the existing kind/discriminator entries) or
alternatively adjust the test description to reference authoring.entityTypes;
ensure the symbols assembleAuthoringContributions, createDescriptor and the
nested keys kind/discriminator are used under authoring.pslBlocks so the
malformed-check path for pslBlocks is truly tested.

Comment on lines +1656 to +1661
const segments = splitTopLevelSegments(inner, ',');
for (const segment of segments) {
const itemRhs = segment.value.trim();
if (itemRhs.length === 0) {
continue;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Report empty list elements instead of silently dropping them.

Line 1659 currently continues on empty list items, so malformed input like [a,,b] or [a,] is accepted without PSL_INVALID_EXTENSION_BLOCK_MEMBER.

Proposed fix
       if (inner.length > 0) {
         const segments = splitTopLevelSegments(inner, ',');
         for (const segment of segments) {
           const itemRhs = segment.value.trim();
           if (itemRhs.length === 0) {
-            continue;
+            pushDiagnostic(context, {
+              code: 'PSL_INVALID_EXTENSION_BLOCK_MEMBER',
+              message: `Invalid empty list element in "${rhs}"`,
+              span,
+            });
+            continue;
           }
           const item = captureParamRhs(context, lineIndex, itemRhs, param.of);
           if (item !== undefined) {
             items.push(item);
           }
🤖 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-parser/src/parser.ts` around lines 1656
- 1661, The loop that parses list segments currently skips empty items (if
itemRhs.length === 0) instead of reporting an error; change this to emit a
PSL_INVALID_EXTENSION_BLOCK_MEMBER diagnostic for empty list elements (e.g.,
when encountering `[a,,b]` or `[a,]`) using the parser's existing
error/diagnostic API and the segment's location information (use the segment
token/span like segment.start/segment.end or whatever location fields exist)
rather than continuing; keep normal parsing for non-empty items (the code that
handles itemRhs should remain unchanged).

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