TML-2804: declarative SPI for extension-contributed PSL blocks (Slice 1 — read side)#753
Conversation
…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>
📝 WalkthroughWalkthroughThis PR introduces a comprehensive extension-block mechanism for PSL, enabling declarative registration and parsing of custom top-level blocks like ChangesPSL Extension Blocks Feature
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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)
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. 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/extension-supabase
@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: 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
📒 Files selected for processing (39)
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/control/psl-extension-block-validator.tspackages/1-framework/1-core/framework-components/src/exports/authoring.tspackages/1-framework/1-core/framework-components/src/exports/codec.tspackages/1-framework/1-core/framework-components/src/exports/psl-ast.tspackages/1-framework/1-core/framework-components/src/shared/codec-descriptor.tspackages/1-framework/1-core/framework-components/src/shared/codec-types.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/psl-block-descriptor.types.test.tspackages/1-framework/1-core/framework-components/test/psl-extension-block-validator.test.tspackages/1-framework/1-core/framework-components/test/psl-literal-parse.test.tspackages/1-framework/2-authoring/psl-parser/src/parser.tspackages/1-framework/2-authoring/psl-parser/test/parser.test.tspackages/1-framework/2-authoring/psl-printer/test/declarative-policy-select.round-trip.test.tspackages/1-framework/2-authoring/psl-printer/test/fixtures/declarative-policy-select-extension.tspackages/1-framework/3-tooling/cli/test/config-types.test.tspackages/1-framework/3-tooling/emitter/test/domain-type-generation.test.tspackages/2-mongo-family/2-authoring/contract-psl/test/derive-json-schema.test.tspackages/2-mongo-family/2-authoring/contract-psl/test/interpreter.polymorphism.test.tspackages/2-mongo-family/2-authoring/contract-psl/test/interpreter.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-psl/test/provider.test.tspackages/2-sql/2-authoring/contract-ts/test/config-types.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.value-objects.test.tspackages/2-sql/3-tooling/emitter/test/emitter-hook.typeref-resolver.test.tspackages/2-sql/4-lanes/relational-core/src/ast/sql-codecs.tspackages/2-sql/4-lanes/relational-core/test/ast/sql-codecs.test.tspackages/3-mongo-target/1-mongo-target/test/mongo-runner.polymorphism.integration.test.tspackages/3-targets/3-targets/postgres/src/core/codecs.tspackages/3-targets/3-targets/sqlite/src/core/codecs.tspackages/3-targets/6-adapters/postgres/test/raw-expr-lowering.test.tspackages/3-targets/6-adapters/postgres/test/sql-renderer-namespace-qualification.test.tspackages/3-targets/6-adapters/postgres/test/sql-renderer.cast-policy.test.ts
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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(); |
There was a problem hiding this comment.
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.
| const segments = splitTopLevelSegments(inner, ','); | ||
| for (const segment of segments) { | ||
| const itemRhs = segment.value.trim(); | ||
| if (itemRhs.length === 0) { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
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).
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/printerfunctions. The mechanism is cherry-picked here; the functions are replaced by the descriptor + generic interpreter. Rationale + design are inprojects/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_selectkeyword directly — the descriptor is data.What's in this PR
The parameter value-kind vocabulary and the generic read pipeline, built across six dispatches:
framework-components) —AuthoringPslBlockDescriptoris data: keyword, discriminator,name, and aparametersmap of the four-kind unionPslBlockParam: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). Noparser/printerfields. Plus the uniform parsed-node base ({ kind, name, parameters, span }) and thePslNamespace.extensionBlocksslot.encodeJson; string codecs (sql/pg/sqlite) cover whatvalueneeds. Sovaluerides the same type system field types and@defaultalready use.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.refresolution — reports (with spans) unknown / missing-required parameters,option-not-in-set, codec-rejectedvalue, andrefunresolved within its scope (same-namespace/same-spaceresolved against the parsed namespaces;cross-spaceis 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 smallPSL_EXTENSION_*diagnostic taxonomy.assembleAuthoringContributionsrejects duplicate keyword, duplicate discriminator (withinpslBlocksand withinentityTypes), apslBlocksdescriptor with no matchingentityTypesfactory, and a malformed descriptor. StandaloneentityTypesfactories stay allowed.entityTypesfactory; a test-only declarativepolicy_selectfixture round-trips parse → validate → lower → IR.A design decision worth a look
Validation runs inside
parsePslDocumentwheneverpslBlocksare registered (joining the parser's existing diagnostics), withcodecLookupdefaulting to a fail-closedemptyCodecLookup(so a caller that registers blocks but doesn't thread codecs gets loudvaluerejections rather than silent skips). No production caller passespslBlocksyet, 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)
contract infer— Slice 2, TML-2854. This PR's round-trip stops at the lowered IR.PslNamespace→ ADR 224entriesmigration — Slice 3, TML-2849.numbervalue-kind / field-declaration block bodies (deferred to first consumer); real RLS (downstream).Relationships
refcross-spacescope builds on its coordinate model. Not a blocker.optionis not an enum,valueuses ordinary scalar codecs.Verification
pnpm typecheck(the four touched packages)pnpm test:packages(psl-parser / psl-printer / framework-components / cli, in isolation)pnpm lint:depspnpm lint:castsdelta=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-kindvsdiscriminatormismatch for a non-cross-spaceref to an extension-contributed entity (the first RLS-roles-style consumer must maprefKind→ discriminator).Refs TML-2804.
🤖 Generated with Claude Code
Summary by CodeRabbit