From 890e1956d2b7b1104abb7f5cbb643f8e1d4788b5 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Thu, 27 Aug 2026 14:25:11 +0000 Subject: [PATCH 1/2] docs(psl): reconcile typed attribute parser architecture Signed-off-by: Steven McClankerton --- ... - Declarative attribute specifications.md | 366 ++++++++++-------- projects/typed-attribute-parsers/close-out.md | 43 ++ projects/typed-attribute-parsers/retros.md | 50 +++ 3 files changed, 306 insertions(+), 153 deletions(-) create mode 100644 projects/typed-attribute-parsers/close-out.md create mode 100644 projects/typed-attribute-parsers/retros.md diff --git a/docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md b/docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md index e76dc39d6a7c..c5c3cca51f55 100644 --- a/docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md +++ b/docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md @@ -1,275 +1,335 @@ # ADR 231 — Declarative attribute specifications: composable argument combinators with typed inference -**Status:** Proposed +**Status:** Accepted **Date:** 2026-06-29 +**Accepted:** 2026-08-27 --- ## At a glance -A PSL attribute carries arguments — positional, named, or both. Two parts of the system need to understand those arguments, and today neither can share what it knows with the other. - -Each database family's interpreter validates attributes with hand-written code that pulls raw argument text out of the AST, checks shapes, and reports diagnostics — the same patterns (parse a quoted string, split a bracketed list, reject an unknown named argument) repeated across the SQL and Mongo interpreters in slightly different ways. The **language server**, which powers editor features over PSL, has no description of attribute arguments at all. Attribute arguments are **opaque** to it: it cannot complete an argument name, offer the allowed values of `onDelete`, or jump from a field reference inside `@relation(fields: [...])` to the field it names. The knowledge exists — encoded in the interpreters' validation code — but in a form no other consumer can read. - -This ADR replaces that with a single declarative description per attribute, designed to be read by every consumer that needs it: the family interpreters that validate and lower attributes, and the language server that offers completion, go-to-definition, and find-usages over them. An author writes a relation like this: - -```prisma -model Post { - authorId Int - author User @relation(fields: [authorId], references: [id], onDelete: Cascade) -} -``` - -The framework describes `@relation` once, as data: +A PSL attribute carries positional and named arguments whose grammar must be validated before the family interpreter can lower them into a contract. That grammar is declared once as an `AttributeSpec`: ```ts const sqlRelation = fieldAttribute('relation', { positional: [{ key: 'name', type: optional(str()) }], named: { - name: optional(str()), - fields: optional(list(fieldRef('self'), { nonEmpty: true })), + name: optional(str()), + fields: optional(list(fieldRef('self'), { nonEmpty: true })), references: optional(list(fieldRef('referenced'), { nonEmpty: true })), - map: optional(str()), - onDelete: optional(oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault'))), - onUpdate: optional(oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault'))), + map: optional(str()), + onDelete: optional( + oneOf( + identifier('NoAction'), + identifier('Restrict'), + identifier('Cascade'), + identifier('SetNull'), + identifier('SetDefault'), + ), + ), }, refine: relationInvariants, }); ``` -At runtime, `interpretAttribute(node, sqlRelation, ctx)` turns the parsed AST node into a strongly-typed object — or a list of diagnostics. The output type is **inferred from the spec**, with no separate type declaration: - -```ts -// InferAttr -{ - name?: string; - fields?: string[]; // resolved against our model's fields - references?: string[]; // resolved against the referenced model's fields - map?: string; - onDelete?: 'NoAction' | 'Restrict' | 'Cascade' | 'SetNull' | 'SetDefault'; - onUpdate?: 'NoAction' | 'Restrict' | 'Cascade' | 'SetNull' | 'SetDefault'; -} -``` +`interpretAttribute(node, sqlRelation, ctx)` parses the attribute's existing `ExpressionAst` nodes and returns either a value whose shape is inferred from the spec or structured diagnostics. The interpreter consumes the inferred value without maintaining a second argument-shape interface. -Notice three things that the rest of this document builds up. The argument value types (`str()`, `oneOf(identifier(...))`, `list(fieldRef('self'))`) are **combinators** drawn from a fixed framework kit. A combinator like `fieldRef` carries a **scope** that says which entity a field name resolves against. And cross-argument rules that no single argument can express — `fields` and `references` must appear together — live in a `refine` step. - -That same spec is what the language server reads. Because `onDelete` is declared as `oneOf(identifier('NoAction'), ...)`, the editor enumerates the alternatives' pinned values; because `fields` is declared as `list(fieldRef('self'))`, the editor knows each entry names a field of the model and can resolve it to a definition or find its other uses — none of which the interpreter's hand-written validation could ever expose. - -The design covers the full spectrum of the current field-, model-, and generic-block-level attribute syntax. +The same declarative shape is intended to become the source for language-tooling features. That second consumer is follow-up work: the accepted implementation establishes interpreter-owned specs and the typed parsing substrate, but does not yet provide central registration or a fully traversable combinator graph for the language server. --- ## Decision -Every field-, model-, and generic-block-level attribute is described by a declarative `AttributeSpec` composed from a small, fixed kit of **argument combinators**. A single `interpretAttribute` function consumes a parsed AST node and a spec and returns a strongly-typed object whose shape is **inferred from the spec**, or structured diagnostics. The same spec is consumed by more than the interpreter: it is the description the language server reads to offer completion, go-to-definition, and find-usages over attribute arguments, which today it cannot do because that knowledge lives only as imperative validation code. Attributes are a PSL-only concept; the TypeScript builder authoring surface never uses them. The combinator kit therefore lives in the PSL authoring layer — the same target-agnostic layer that owns the PSL parser and symbol table — not in the framework core. Each family contributes the specs for the attributes it understands, registered by `(level, name)`; that layer dispatches generically and never learns an attribute's name. +Field- and model-level PSL attributes are described by family-owned declarative `AttributeSpec` values composed from a fixed kit of argument combinators in `psl-parser`. A shared `interpretAttribute` engine binds positional and named arguments, invokes each combinator against the parser's existing AST, applies optional defaults and cross-argument refinement, and returns a spec-derived output value or diagnostics. + +The SQL and Mongo family interpreters are the first consumers. They define their built-in specs locally and pass them explicitly to `interpretAttribute`; contributed SQL model attributes may provide their own specs through the existing contribution mechanism. A central registry for all built-in specs and language-server consumption are not part of this implementation. -The rest of this document develops the design: the principles that shape it, the combinator kit, how positional and named arguments are modelled and typed, how alternatives and function calls compose, the resolution context combinators draw on, cross-argument refinement, and the surface policy for collections. What the design deliberately excludes, and the alternatives weighed against it, are collected at the end. +The kit consumes `ExpressionAst` directly. No intermediate argument representation is introduced, and no combinator reparses flattened source text except `json()`, the deliberate quoted-JSON-object exception. + +Attributes are a PSL authoring concern, so the kit lives in `psl-parser` rather than framework core. The current constructors cover field and model attributes. `AttributeLevel` reserves a block level, but generic-block attribute construction and interpretation remain future work. --- ## Design principles -1. **A spec is data, not code.** Each attribute is described declaratively. Validation, type inference, PSL printing, and editor completion all read the same spec rather than re-deriving the attribute's shape. -2. **One description, many consumers.** The interpreters and the language server read the *same* spec. Knowledge about an attribute — what arguments it takes, what their values mean, which arguments name fields or models — is never trapped in one consumer where the others cannot reach it. -3. **The spec is the type.** The strongly-typed result of interpreting an attribute is inferred from its spec (`InferAttr`). There is no hand-written output interface to drift from the validation. -4. **Compose, don't special-case.** Generic `list` / `map` / `record`, a `oneOf` sum, and a recursive `funcCall` replace bespoke leaves like "a list of field references" or "a literal or a function call". New shapes are built by composition. -5. **Native surface by default.** Structured arguments are native PSL literals (`[…]`, `{…}`). Quoted-string-encoded values survive only where they earn it — an arbitrary JSON document — and that exception is isolated in one leaf. -6. **Leaf parsing is pure.** A combinator returns its diagnostics in a `Result` rather than pushing them into a shared sink, so alternatives can be tried and discarded without leaving stray errors behind. -7. **The PSL authoring layer owns the kit; families own the specs.** Attributes are PSL-specific, so the combinator vocabulary lives in the PSL authoring layer rather than the target-agnostic framework core; the attribute set is open and contributed, dispatched structurally. +1. **The spec is the argument grammar.** A migrated attribute has one declarative description of its accepted positional and named arguments. Hand-written parsing for the same shape is removed. +2. **The spec derives the output type.** Constructors compute the output object from their positional and named parameters, and `InferAttr` extracts it. Validation and the interpreter-facing type therefore evolve together. +3. **Compose instead of adding domain-specific leaves.** Native collections, alternatives, references, pinned literals, and typed function calls compose richer grammars such as defaults and Mongo index elements. +4. **Use native PSL literals for known structure.** Lists and records use `[…]` and `{…}`. A quoted string survives only for an arbitrary JSON object that the framework deliberately treats as opaque. +5. **Keep leaf parsing diagnostic-pure.** Every combinator returns a `Result`; it does not mutate a shared diagnostic sink. `oneOf` can therefore discard failed branches safely. +6. **Keep semantics at the right level.** A rule spanning arguments of one attribute belongs in `refine`. A rule spanning several attributes or entities remains in family-level semantic aggregation. +7. **Preserve future inspectability without claiming it already exists.** Specs expose their top-level argument structure and combinator kinds. Language tooling will require child combinators, function signatures, and reference metadata to become fully traversable. --- -## The combinator kit +## Core types -An argument combinator is an `ArgType`: it knows how to parse one AST argument into a value of type `T`, and it carries that type at the type level so the output object can be inferred. +An argument combinator parses one `ExpressionAst` into `T`: ```ts interface ArgType { - readonly kind: string; // discriminant for visitor dispatch (print, complete, doc-gen) - readonly label: string; // human-readable, for "expected …" diagnostics - readonly _out?: T; // phantom; never read at runtime - parse(arg: PslArgAst, ctx: InterpretCtx): Result; + readonly kind: string; + readonly label: string; + readonly _out?: T; + parse(arg: ExpressionAst, ctx: InterpretCtx): Result; } ``` -The kit divides into four groups. - -A combinator owns the work arktype cannot: parsing a PSL AST argument from source, resolving names against the symbol table and registries, anchoring diagnostics to source spans, and carrying the domain metadata (its `kind`, a reference's scope) the language server switches on. Where a leaf reduces to a context-free check on an already-parsed value — a literal, a numeric range, the shape of a JSON object — it delegates that check, and its type inference, to an arktype `Type` it wraps. So a pinned `identifier(name)` / `str(value)` / `num(value)` is backed by an arktype literal, `int({ min, max })` by arktype's numeric constraints, and `json()` by an arktype object schema, while the combinator around them supplies the parse, the context, the spans, and the metadata. This mirrors the existing authoring pattern, where a contributed entity's `validatorSchema` is an arktype `Type` that validates structured input before a factory runs. - -**Scalars** read a single token. `str()` parses any quoted string, and `str(value)` pins a specific one. `int({ min, max })` parses a number. `bool()` parses a boolean. `identifier(name)` matches a specific bare identifier, typed `ArgType`, and `num(value)` matches a specific number literal. There is no dedicated enum leaf: a fixed literal set is `oneOf` over these pinned matchers, and because each member is its own matcher the set may be homogeneous *or* mixed — with the quoted-vs-bare surface explicit per member rather than guessed from a value's JS type. Mongo's index `type`, which accepts the numbers `1`/`-1` and the strings `"text"`/`"2dsphere"`/`"2d"`/`"hashed"`, is `oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))`. `json()` reads an opaque JSON value from a quoted string; it is the one place a structured value is text-encoded (see [Surface policy](#surface-policy-native-literals-with-one-text-exception)). - -**References** resolve a name to an **entity coordinate** — the contract's uniform `(namespace, kind, name)` address for *any* entity, whether a model, an enum, or a pack-contributed entity kind (ADR 221, ADR 224). A reference is not special-cased to models. The coordinate carries one optional extension, a **`field`** element, for a reference that names a field *within* an entity. So `entityRef({ scope })` resolves an entity by name, and its field-bearing form `fieldRef({ scope })` resolves a field within the scoped entity and fills in the coordinate's `field` element; `scope` is `'self'` (the entity declaring the attribute), `'referenced'` (a relation's target entity), or `'document'` (a free path, for wildcard projections). `codecRef()` resolves a registered codec id — a registry reference, not a contract-entity coordinate. - -**Generic collections** lift element combinators over native literals. `list(of)` reads a `[…]` array literal into `T[]`, with options `{ nonEmpty, unique }`. `map(key, value)` reads a `{…}` object literal into `Record`; `record(value)` is the `map(str(), value)` shorthand. Every collection in the grammar is now a composition rather than a named leaf: +The context contains the source and family symbols needed by the shipped reference combinators: ```ts -list(fieldRef('self'), { nonEmpty: true, unique: true }) // @@id, @@unique field lists -record(str()) // SQL @@index options -map(fieldRef('self'), int()) // Mongo @@textIndex weights +interface InterpretCtx { + readonly level: 'field' | 'model' | 'block'; + readonly sourceId: string; + readonly sourceFile: SourceFile; + readonly selfModel: ModelSymbol; + resolveReferencedModel(): ModelSymbol | undefined; + readonly field?: FieldSymbol; +} ``` -**Sum and function call** are covered in their own section below, because they introduce alternatives and recursion. - -## Positional and named arguments - -A spec lists positional parameters in order and named parameters by key. Both write into the same output keyspace, so the inferred object is a flat merge. +A spec fixes the attribute level and name, declares its arguments, and may refine the parsed result: ```ts -interface PositionalParam { - readonly key: string; // output key this slot writes - readonly type: Param; - readonly variadic?: boolean; // trailing rest, for a list-as-positional -} - interface AttributeSpec { readonly level: 'field' | 'model' | 'block'; readonly name: string; readonly positional: readonly PositionalParam[]; - readonly named: Record>; - readonly refine?: (parsed: Out, ctx: InterpretCtx) => Diagnostic[]; + readonly named: Readonly>>; + readonly refine?: ( + parsed: Out, + ctx: InterpretCtx, + attributeNode: AstNode, + ) => readonly PslDiagnostic[]; } ``` -A `Param` is either a bare `ArgType` (required) or `optional(t)` / `optional(t, default)`. Three constructors — `fieldAttribute`, `modelAttribute`, `blockAttribute` — fix the `level` and determine which AST node the interpreter consumes and which context fields are guaranteed present. +`fieldAttribute` and `modelAttribute` infer `AttributeOut` when constructing a spec. `InferAttr` extracts that `Out` type. Optional parameters are `ArgType` values decorated by `optional(type)` or `optional(type, defaultValue)`; the engine detects the marker when finalizing absent arguments. -Two shapes in the grammar need a note. A **list-as-positional** is an ordinary positional whose type is a `list(...)`: `@@index([a, b])` is one positional bound to `list(indexField())`, and `@@base(Base, "v")` is two fixed positionals. An **alias** is an argument that may be written positionally *or* by name — the relation name is the only case. It is modelled by letting a positional with `key: 'name'` share the output key with the named `name`; the interpreter merges them and reports a conflict if both are present and disagree. The alias is purely about *where the value comes from*; it is unrelated to the `oneOf` combinator below, which is about *what shape a value takes*. +Positionals are fixed slots with an output key. Variadic positionals are not supported. Positional and named parameters may intentionally share a key, which supports the relation-name alias while allowing the engine to diagnose conflicting duplicate values. -## Type inference +--- -The output type is computed from the spec with mapped types. Optional parameters become optional properties; positional slots contribute their `key`. +## The combinator kit -```ts -type OutOf

= - P extends Optional ? T : - P extends ArgType ? T : never; +### Scalars and pinned literals + +- `str()` parses any string literal; `str(value)` matches one exact string and preserves its literal type. +- `num()` parses any number literal; `num(value)` matches one exact number and preserves its literal type. +- `int({ min, max })` parses an integer with optional inclusive bounds. +- `bool()` parses a boolean literal. +- `identifier(name)` matches one exact bare identifier and preserves its literal type. -type NamedOut>> = - { [K in keyof N as N[K] extends Optional ? never : K]: OutOf } & - { [K in keyof N as N[K] extends Optional ? K : never]?: OutOf }; +There is no enum-specific combinator. A fixed vocabulary is a `oneOf` over pinned matchers, making the source spelling explicit: -type InferAttr = S extends AttributeSpec - ? Simplify & NamedOut> - : never; +```ts +oneOf( + num(1), + num(-1), + str('text'), + str('2dsphere'), + str('2d'), + str('hashed'), +) ``` -`list`, `map`, and `oneOf` lift through `OutOf` like any other combinator, so a spec built from them infers a precise object type with no separate declaration. Principle #3 — the spec is the type — falls directly out of this. +These leaves perform direct AST checks. They do not wrap arktype schemas. -## Alternatives and function calls +### References -Two combinators express choice and nesting. +`fieldRef('self')` parses a field-name identifier and validates it against the declaring model. `fieldRef('referenced')` validates against the relation target when that model can be resolved; cross-space references may defer the existence check when no referenced model is locally available. Both forms return the authored field name as a string and expose their scope as combinator metadata. -`oneOf(...alts)` is a sum: it tries each alternative's `parse` in order and the first success wins; if all fail it emits one `expected ` diagnostic. This works because of principle #6 — each leaf returns its diagnostics in the `Result` rather than pushing them, so a failed branch leaves no trace and `oneOf` can backtrack cleanly. Ordered try-each is chosen over a separate recognition step: it keeps the leaf contract small (one `parse` method, plus a static `label` for the aggregate message). The cost is coarser diagnostics for malformed-but-clearly-intended input, an acceptable trade for a small, closed grammar. +`entityRef()` parses an unresolved model-name string. Existence and family semantics remain downstream concerns. -`funcCall(name, sig)` parses a function-call argument. A function call is structurally a named node with its own positional and named arguments — the same shape as an attribute — so `funcCall` **reuses the positional/named argument model recursively**, and its arguments may themselves be any combinator, including a nested `funcCall`. It pins the callee `name` and parses that call's arguments through `sig`; the output carries an `fn` discriminant so a `oneOf` over several functions, or downstream code, can switch on which one matched. An **open, contributed set** of functions — the shape behind PSL default functions — needs no dedicated combinator: it is expressed by composing `oneOf(funcCall(name, sig)…)` over the registered names (principle #4, compose don't special-case). +The current kit does not return declaration-bearing entity coordinates, provide a document-path scope, or include a codec reference combinator. Those would be separate additions if a future consumer requires them. -These two compose the attribute arguments that are neither a plain scalar nor a plain collection. A field default is "a literal that matches the field's type, or one of the default-function registry's calls" — composed per field from the registry: +### Native collections -```ts -const defaultValue = (registry: ControlMutationDefaultRegistry) => - oneOf(matchingScalarLiteral(), ...[...registry].map(([name, entry]) => funcCall(name, entry.signature))); -``` +`list(of, { nonEmpty, unique })` parses a native array literal, applies the element combinator to every item, and may enforce non-emptiness and uniqueness. -`matchingScalarLiteral()` is named to state its contract: it parses a scalar literal *and* checks it against the annotated field's type. It is constructible only inside `fieldAttribute(...)`, where the field is guaranteed in context. A Mongo index element — a bare field, a sorted field, or a wildcard — is likewise a `oneOf`: +`record(of)` parses a native object literal into `Record`, rejects duplicate keys, and applies `of` to each value. Keys are strings; the kit does not currently provide a generic `map(key, value)` combinator. + +Mongo text-index weights demonstrate the shipped record shape: ```ts -const indexField = () => oneOf(fieldRef('self'), sortedFieldRef('self'), wildcardPath()); +record(int({ min: 1, max: 99_999 })) ``` -## Resolution context +### Quoted JSON object + +`json()` accepts a quoted JSON string only when it decodes to a non-null, non-array object. It returns `Record`. + +This is intentionally narrower than an arbitrary JSON value. Its shipped use is Mongo's partial index filter, whose nested document is passed through rather than interpreted as a typed PSL record. + +### Alternatives + +`oneOf(first, ...rest)` tries its alternatives in order and returns the first success. If every alternative fails, it discards the branch diagnostics and emits one aggregate `Expected one of: …` diagnostic assembled from the alternatives' labels. + +This trade-off keeps the leaf contract small and allows backtracking, at the cost of less specific diagnostics for malformed input that resembles one particular branch. + +### Typed function calls -Reference and field-typed combinators draw on a single context object threaded through `parse`: +`funcCall(name, signature)` matches one unqualified function name and parses its positional and named arguments through the same parameter-binding engine used by attributes: ```ts -interface InterpretCtx { - level: 'field' | 'model' | 'block'; - symbols: SymbolTable; // model/field references → their declarations - selfModel: ModelSymbol; // declaring model; for fieldRef('self') - resolveReferencedModel(): ModelSymbol | undefined; // a relation's target; for fieldRef('referenced') - field?: ResolvedFieldDescriptor; // resolved declaring field; for matchingScalarLiteral - codecLookup: CodecLookup; // for codecRef - sourceId: string; +interface FuncCallSig { + readonly positional?: readonly PositionalParam[]; + readonly named?: Readonly>>; +} + +interface TypedFuncCall { + readonly fn: string; + readonly span: PslSpan; + readonly args: Readonly>; } ``` -The context holds the parser's `SymbolTable` (and the `ModelSymbol` / `FieldSymbol` it resolves to) rather than a flat `ReadonlySet` of model names. This matters because of the language-server reuse below: a set of names can only confirm that a referenced entity *exists*, but every symbol in the table carries its declaration `span` and AST `node`. So `entityRef()` and `fieldRef()` return a *resolvable* reference — a name plus the site it is declared at — which is what go-to-definition and find-usages need. Holding the symbol table directly is unproblematic here precisely because this machinery is PSL-specific, not part of the target-agnostic framework core; the symbol table is the PSL authoring layer's own type. +Function arguments may use any combinator, including nested `funcCall` values. Namespaced names are rejected at the function-call boundary. + +The result is typed as a normalized function-call envelope, not as a name-literal-discriminated or signature-derived object. `funcCallFrom` and an unpinned raw function-call combinator are not part of the design. + +--- -`field` is present only at the field level, which is what makes `matchingScalarLiteral()` and the field-default function call type-safe by construction: they are constructible only where the field they validate against is guaranteed available. +## Dynamic specifications -## One spec, two consumers: language-server features +A spec may be assembled from context known by the owning family before interpretation. This preserves a declarative grammar while allowing the accepted alternatives to reflect registries, enum members, or model fields. -The primary reason a spec is *declarative* rather than a parsing function is that a function can only be called — it cannot be inspected. A declarative spec can be read by a second consumer that has no interest in lowering an attribute to a contract: the language server. +### SQL defaults + +SQL constructs `@default` specs per field. Scalar fields accept flexible string, number, and boolean literals plus one pinned `funcCall(name, signature)` arm for every active default-function registry entry. List fields accept a list of those scalar literals plus the registry calls. Enum fields use one pinned `identifier(member)` arm per enum member. + +```ts +const functionArms = registryEntries.map(([name, entry]) => + funcCall(name, entry.signature), +); + +const scalarDefault = oneOf(str(), num(), bool(), ...functionArms); +const enumDefault = oneOf(...enumMembers.map(identifier)); +``` -Today attribute arguments are opaque to the language server. It can offer little more than the attribute name, because everything past the opening parenthesis is validated by interpreter code it cannot introspect. The same spec the interpreters run answers the questions an editor needs to ask: +Literal-to-codec compatibility remains a lowering concern. A `matchingScalarLiteral` combinator is not implemented. -- **Autocompletion.** The `named` map lists the legal argument names for an attribute, so the editor completes `fie` to `fields:` inside `@relation(...)`. A value typed `oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault'))` enumerates its alternatives' pinned values, so the editor offers exactly those after `onDelete:`. The combinator's `label` supplies the hover text. -- **Go-to-definition and find-usages.** A combinator declares not just that an argument is a name, but *what kind of name and where it resolves*. `fields: list(fieldRef('self'))` says each entry names a field of the enclosing model; `references: list(fieldRef('referenced'))` says each names a field of the relation's target model; `@@base`'s `entityRef()` names another entity. From that, the language server resolves the symbol under the cursor to its declaration, and finds every other attribute argument that references the same field or entity — neither of which the interpreter's hand-written validation could ever expose, because it discards that structure as soon as it has checked it. -- **Diagnostics parity.** The editor reports the *same* errors the interpreter would, from the same spec, rather than a thinner approximation maintained separately. +### Mongo index elements -This is why the reference combinators carry a **scope** ([resolution context](#resolution-context)) rather than treating every field name alike. The scope is the fact the language server needs to resolve a reference correctly: a name in `references:` must be looked up in the target model, not the local one. Encoding that in the spec is what turns a field reference into a navigable symbol. +Mongo constructs its index field-element grammar from the declaring model's field names: + +```ts +const sortSig = { + named: { sort: oneOf(identifier('Asc'), identifier('Desc')) }, +} satisfies FuncCallSig; + +const indexFieldElement = oneOf( + fieldRef('self'), + funcCall('wildcard', { + positional: [{ key: 'scope', type: optional(entityRef()) }], + }), + ...fieldNames.map((name) => funcCall(name, sortSig)), +); +``` + +This composition covers bare fields, sorted field calls, and wildcard calls without dedicated `sortedFieldRef` or `wildcardPath` combinators. + +--- ## Cross-argument refinement -Some rules span several arguments and cannot be expressed by any single combinator. They live in a `refine(parsed, ctx)` step that runs after every argument parses and sees the fully-typed result. This is where the relation's "`fields` and `references` are both-or-neither" rule lives, along with the SQL index's "`options` requires `type`" and the Mongo index's wildcard, projection, and collation constraints. Rules that span *multiple attributes on one object* — at most one `@@textIndex` per collection — are not attribute-level at all; they belong to a model-level aggregator above the individual specs. +`refine(parsed, ctx, attributeNode)` runs after every argument has parsed successfully. It handles rules that no single argument can express and may anchor diagnostics to the complete attribute node. + +Examples include relation arguments that must appear together and SQL or Mongo index constraints involving several options. Rules spanning multiple attributes, such as allowing at most one Mongo text index per collection, remain in the model-level semantic aggregation above individual attribute interpretation. + +--- ## Surface policy: native literals with one text exception -PSL's expression grammar supports native array and object literals recursively, and the SQL `@@index options` argument already uses a native object literal. Structured arguments therefore use native literals as a rule: `include: [metadata, tags]`, `weights: { title: 10 }`. The one justified exception is a value that is genuinely an arbitrary, nested document the framework does not interpret — a Mongo partial-filter expression — where a JSON string preserves exact fidelity and avoids making the PSL object grammar a JSON superset. That exception is confined to the `json()` leaf used by `filter`; every other collection reads a native literal. +Known structured shapes use native PSL literals so the parser owns their structure and spans: -The benefit is uniform: a native literal is parsed by the PSL parser, so it carries real spans and diagnostics and supports editor completion, where a quoted-string-encoded list or map is opaque to all of that. +```prisma +@@index([wildcard()], include: ["metadata", "nested.path"]) +@@textIndex([title, body], weights: { title: 10, body: 5 }) +``` + +A Mongo partial-filter expression remains quoted JSON: + +```prisma +@@index([status], filter: "{\"status\": {\"$ne\": \"archived\"}}") +``` + +The distinction is semantic: projections and weights have a known grammar the spec can describe, while the filter is an arbitrary nested Mongo document that the interpreter passes through. + +--- + +## Language-tooling follow-up + +The interpreter implementation proves that attribute grammars can be represented as values and consumed without hand-written argument parsing. It does not yet make those values a complete language-server API. + +A language-tooling consumer will require additional work: + +- a central way to discover built-in and contributed specs by level and attribute name; +- traversable child metadata for `list`, `record`, `oneOf`, and `funcCall`, whose children are currently captured by parse closures; +- reference metadata and resolution results sufficient for go-to-definition and find-usages; +- completion and hover behavior over named arguments, pinned alternatives, and nested function signatures; +- diagnostics parity tests between editor and interpreter consumers. + +These additions should extend the declarative values rather than create a second attribute grammar. Language-server integration remains a separate delivery scope. + +--- ## Removed storage-type attribute channel -The former `@db.*` named-type attribute channel is removed. Storage types are authored only through type-position constructors: `@db.Uuid` becomes `Uuid`, and `@db.VarChar(191)` becomes `VarChar(191)`. Remaining source that uses the removed spelling receives an actionable diagnostic directing that rewrite. Because storage selection is no longer an attribute surface, this declarative attribute design needs no named-type exception. [ADR 241](ADR%20241%20-%20Scalar%20types%20use%20the%20authoring%20type-constructor%20channel.md) records the unified constructor channel. +Storage types are authored through type-position constructors rather than `@db.*` named-type attributes: `@db.Uuid` becomes `Uuid`, and `@db.VarChar(191)` becomes `VarChar(191)`. Because storage selection is not an attribute surface, this design requires no named-type exception. ADR 241 records the constructor channel. --- ## Consequences -The validation for an attribute is one declarative value. The patterns that recur across today's interpreters — unwrap a quoted string, split a bracketed list, reject an unknown named argument, resolve a field name against a model — exist once in the combinator kit, not once per attribute per family. +SQL and Mongo attribute argument grammars now live in family-owned specs, and migrated interpreter paths consume typed values from the shared engine. Repeated parsing concerns—argument binding, optional defaults, unknown names, duplicate values, native collection traversal, alternatives, and function-call signatures—are implemented once in `psl-parser`. -The output type cannot drift from the validation, because there is no separately written output type: `InferAttr` is derived from the same spec the interpreter runs. Adding or renaming an argument changes both at once. +The interpreter-facing output type cannot independently drift from the spec because constructors derive it from the same positional and named parameter declarations. -The spec registry is the single source of truth for the attribute surface. The same data that drives validation in the interpreters drives PSL printing, language-server features (completion, go-to-definition, find-usages, hovers), and generated reference documentation. The editor's understanding of an attribute can no longer fall out of step with what the interpreter accepts, because both read one description. +Dynamic composition keeps open registries and document-specific vocabularies declarative. Families build `oneOf` alternatives from the registry, enum, or model context they own instead of adding registry-specific framework combinators. -Adding an attribute is additive and local to a family or target: register a new `(level, name)` spec. The framework parser, the interpreter, and the dispatch learn nothing new. +The cost is a new vocabulary and a deliberate distinction between parsing and semantic validation. Contributors must decide whether a rule belongs in one combinator, attribute-level refinement, or family-level aggregation. -The cost is a new layer of indirection. Reading what an attribute accepts means reading a spec built from combinators rather than imperative code, and contributors must learn the kit. The kit is small and closed, which bounds that cost, but it is a real shift in how attribute logic is read and written. +The current implementation is sufficient for interpreter consumption but not yet for language tooling. Accepting this ADR commits future consumers to extend and inspect the same specs rather than re-derive attribute grammars elsewhere. --- -## Alternatives considered +## Follow-up work -**Keep hand-written validation per attribute.** The status quo: each interpreter parses and checks its attributes directly. Rejected as the thing this ADR exists to replace — it duplicates the same parsing patterns across families, lets the output type drift from the checks, and gives the PSL printer and editor no shared description to read. +- Add central spec discovery and traversable combinator metadata for language-tooling consumers. +- Decide whether reference combinators should expose declaration-bearing results while preserving the interpreter's string-oriented lowering needs. +- Add block-level construction and interpretation if generic-block attributes adopt this mechanism. +- Revisit signature-derived `TypedFuncCall` output types if downstream code needs statically discriminated call unions. +- Decide whether literal-to-field-type compatibility should remain in lowering or gain a dedicated field-context combinator. -**Dispatch `oneOf` with a recognition predicate.** Give each combinator a `recognizes(arg)` method so `oneOf` commits to one branch by AST shape before parsing, yielding more targeted errors. Rejected for now: it doubles the leaf contract (a recognizer that must stay in sync with the parser) for a benefit — sharper errors on malformed input — that a small closed grammar does not need. Ordered try-each over diagnostic-pure branches is simpler, and a recognizer can be added later without changing the leaf contract if error quality demands it. +--- -**A dedicated enum leaf.** A single combinator for a fixed literal set, deciding each member's token surface from its JS type. Rejected in favour of `oneOf` over `identifier` / pinned `str` / `num`: composition (principle #4) expresses homogeneous and mixed sets uniformly, makes the quoted-vs-bare surface explicit per member rather than inferred from a value's type, and reuses the `oneOf` sum the design already needs for `@default` and index elements. Mixed string/number sets — Mongo's index `type` — remain expressible, now as `oneOf(num(1), num(-1), str('text'), …)`. +## Alternatives considered -**`json(codecId)` validated by a codec.** Let the JSON leaf decode through a named codec. Rejected: no attribute in scope needs codec-validated JSON — `filter` is opaque pass-through, and `@@type` takes a codec *id* (a `codecRef`), not a codec-validated value. Codec-bound decoding is a separate concern that belongs to generic-block parameters and enum member values, and if those are folded in later they get their own primitive rather than overloading `json()`. +**Keep hand-written validation per attribute.** Rejected because it duplicates argument binding and parsing across families, allows output types to drift from validation, and leaves no reusable grammar value for future consumers. -**Quoted-string surfaces for lists and maps.** Accept `include: "[a, b]"` and `weights: "{…}"` as the encoding. Rejected for everything except an arbitrary document (`filter`): native literals already work, carry spans and diagnostics, and support completion, where quoted strings are opaque. The string surface is kept only where the value is genuinely an uninterpreted JSON document. +**Use a dedicated enum combinator.** Rejected in favor of `oneOf` over pinned `identifier`, `str`, and `num` matchers. Composition supports homogeneous and mixed literal sets while making each member's source spelling explicit. -**Monomorphic collection leaves.** Bespoke `fieldRefList`, `stringMap`, and the like instead of generic `list` / `map`. Rejected: generic combinators compose the same coverage from fewer pieces (`list(fieldRef('self'))`, `record(str())`), absorb today's ad-hoc checks as list options (`nonEmpty`, `unique`), and let map keys be validated too (`map(fieldRef('self'), int())`). +**Provide registry-specific function-call combinators.** Rejected because a family with a registry can build `oneOf(funcCall(name, signature), …)` directly. `funcCallFrom` would duplicate composition already available to the owner of the registry. -**Express the whole spec in arktype.** Drop the custom kit and describe each attribute as one arktype `Type`, reusing its validation and type inference. Rejected on three counts that arktype is not built to carry. Its input is a JavaScript value, not a PSL AST with source spans, so parsing surfaces and mini-grammars — and anchoring diagnostics to offsets in the `.prisma` file — fall outside it. Its validation is context-free: a `Type` cannot be handed the symbol table, codec registry, or a field's resolved type, so reference and field-typed combinators would have to rebuild types per document via closures, forfeiting the static, registered-once spec. And, decisively, a reference encoded as an arktype morph is an opaque function to arktype's introspection — the language server could learn "a string that passed a predicate" but never "a reference to a field in the relation's target model," which is the navigable structure this design exists to expose. arktype is therefore used *inside* the value-shape leaves, not as the whole engine. +**Keep an unpinned raw function-call parser.** Rejected because it accepts names and argument shapes before the owning registry has described them, forcing validation back into lowering. Pinned typed calls reject unknown names and malformed arguments at the grammar boundary. ---- +**Add bespoke sorted-field and wildcard-path combinators.** Rejected because model-specific `funcCall` arms compose those shapes from the existing kit. + +**Use quoted strings for lists and records.** Rejected for known structure because native literals preserve AST structure, spans, diagnostics, and future completion opportunities. Quoted JSON remains only for an opaque object. -## Open questions +**Provide a generic `map(key, value)` immediately.** Deferred because current consumers require only string-keyed records. A generic key combinator can be added when a concrete attribute grammar needs it. -- **Field-type matching is a runtime check.** `matchingScalarLiteral()` validates a literal against the field's type at parse time via `ctx.field`; the static output stays the general default-value union, because the field's concrete type is not known where the spec is defined. -- **Index-element strictness.** Whether `indexField()` should reject unknown modifier keys or values (`field(order: Desc)`, `sort: descending`) rather than silently degrading to ascending. Rejecting is the safer default. -- **Alias ergonomics.** Whether the positional-or-named relation name warrants a first-class `aliased('name')` combinator or stays the positional/named key-collision convention. -- **Generic-block parameters.** Whether the same kit should also type a generic block's `key = value` entries and variadic members (e.g. enum members), unifying extension-block validation with this engine. Codec-bound member decoding would gain its own `codecValue(...)` primitive if so. -- **Collection-level invariants.** Where the model-level aggregator that enforces rules like "at most one `@@textIndex` per collection" lives, since it sits above individual attribute specs. +**Express the entire spec in arktype.** Rejected because arktype validates JavaScript values rather than PSL AST nodes with source spans and interpretation context. Direct combinators own AST recognition, diagnostics, and model-aware resolution. --- ## References -- [ADR 225 — Three-layer extensibility for pack-contributed entity kinds](ADR%20225%20-%20Three-layer%20extensibility%20for%20pack-contributed%20entity%20kinds.md) — the contribution model this design follows: a framework-defined extension point that families and targets register into, dispatched structurally so the framework learns no per-kind names. Attribute specs are registered the same way entity kinds are. -- [ADR 224 — Control policy: a framework-locked vocabulary with family-owned dispatch](ADR%20224%20-%20Control%20Policy%20—%20framework-locked%20vocabulary%20and%20family-owned%20dispatch.md) — the `@@control()` attribute whose value set this design types as `oneOf(identifier('managed'), identifier('tolerated'), identifier('external'), identifier('observed'))`, and the framework-vocabulary / family-dispatch split this design mirrors, here as a PSL-layer kit with family-owned specs. -- [ADR 221 — Contract IR: two planes with a uniform entity coordinate](ADR%20221%20-%20Contract%20IR%20two%20planes%20with%20uniform%20entity%20coordinate%20and%20pack-contributed%20entity%20kinds.md) — the coordinate model attribute resolution writes into. -- [ADR 126 — PSL top-level block SPI](ADR%20126%20-%20PSL%20top-level%20block%20SPI.md) — the descriptor SPI for generic blocks, whose `key = value` parameters are the subject of an open question above. -- [Pattern: Frozen-class AST + visitor](../patterns/frozen-class-ast.md) — the dispatch pattern for the `ArgType` combinator union across parse, print, and completion sites. -- [Pattern: Three-layer polymorphic IR](../patterns/three-layer-polymorphic-ir.md) — the framework-vocabulary → family-dispatch layering instantiated by the framework kit and family-owned specs. +- [ADR 225 — Three-layer extensibility for pack-contributed entity kinds](ADR%20225%20-%20Three-layer%20extensibility%20for%20pack-contributed%20entity%20kinds.md) +- [ADR 224 — Control Policy: framework-locked vocabulary and family-owned dispatch](ADR%20224%20-%20Control%20Policy%20—%20framework-locked%20vocabulary%20and%20family-owned%20dispatch.md) +- [ADR 221 — Contract IR: two planes with a uniform entity coordinate](ADR%20221%20-%20Contract%20IR%20two%20planes%20with%20uniform%20entity%20coordinate%20and%20pack-contributed%20entity%20kinds.md) +- [ADR 126 — PSL top-level block SPI](ADR%20126%20-%20PSL%20top-level%20block%20SPI.md) +- [ADR 241 — Scalar types use the authoring type-constructor channel](ADR%20241%20-%20Scalar%20types%20use%20the%20authoring%20type-constructor%20channel.md) +- [Pattern: Frozen-class AST + visitor](../patterns/frozen-class-ast.md) +- [Pattern: Three-layer polymorphic IR](../patterns/three-layer-polymorphic-ir.md) diff --git a/projects/typed-attribute-parsers/close-out.md b/projects/typed-attribute-parsers/close-out.md new file mode 100644 index 000000000000..eece5fba15b0 --- /dev/null +++ b/projects/typed-attribute-parsers/close-out.md @@ -0,0 +1,43 @@ +# Typed attribute parsers — close-out ledger + +## Delivery state + +| Slice | Evidence | State | +| --- | --- | --- | +| Attribute-spec kit + SQL `@relation` | [prisma/prisma-next#891](https://github.com/prisma/prisma-next/pull/891) | Merged | +| Remaining non-default SQL attributes | [prisma/prisma-next#932](https://github.com/prisma/prisma-next/pull/932) | Merged | +| SQL `@default` dynamic specs and typed function calls | [prisma/prisma-next#938](https://github.com/prisma/prisma-next/pull/938) | Merged | +| Mongo attributes | [prisma/orm#29833](https://github.com/prisma/orm/pull/29833) | Approved and queued for merge; final close gate | + +The original plan named three slices. SQL `@default` became a fourth slice when its registry-sensitive function grammar and enum path proved independently reviewable. + +## Project DoD verification + +- [ ] **Team DoD floor:** Final repo-wide gates, project deletion, and Linear completion wait for the Mongo merge and the close-out branch. +- [x] **Attribute-spec substrate:** PR #891 delivered the combinator kit, `AttributeSpec`, `interpretAttribute`, `InferAttr`, unit coverage, and the first SQL consumer. +- [x] **SQL attributes:** PRs #891, #932, and #938 migrated SQL relation, non-default attributes, and both `@default` paths. Typed function-call parsing and dynamic registry/enum specs replaced the legacy default-call parser. +- [ ] **Mongo attributes:** PR #29833 migrates the Mongo family and removes legacy argument parsers. The PR is approved, all 20 CI checks pass, and it is queued for merge; mark complete only after the merge commit is on `main`. +- [x] **Validation evidence on the final Mongo PR head:** package tests, full integration tests, retail tests, build, fixtures, upgrade coverage, DCO, lint, typecheck, e2e, security, and all CI shards pass. +- [x] **Legacy parser retirement:** Each slice removed the parsing helpers whose final consumers migrated; final PR review and typechecking report no remaining consumers. +- [x] **ADR audit:** ADR 231 is reconciled with the shipped API, separates language-tooling follow-up from current behavior, and is `Accepted`. +- [x] **Mandatory final retro:** `projects/typed-attribute-parsers/retros.md` records the project retro and lands the durable architecture in ADR 231. +- [x] **Manual QA roll-up:** N/A for interactive workflow QA. The project changes interpreter parsing rather than a runnable user flow; parser tests, interpreter tests, integration tests, fixtures, example tests, and executable upgrade instructions cover the observable schema-syntax changes. + +## Classification + +All files under `projects/typed-attribute-parsers/` are transient coordination artifacts and will be deleted after the Mongo merge is confirmed. + +- `spec.md`, `plan.md`, `close-out.md`, and `retros.md`: transient project shaping, sequencing, verification, and retro records. +- Every slice `spec.md` and `plan.md`: transient slice scope and sequencing records. +- Every `dispatches/*.md`: transient implementation briefs. + +No project-local file migrates to `docs/`. The durable technical outcome has already landed in `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md`. + +## Remaining close gates + +1. Confirm PR #29833 is merged and update local `main`. +2. Run the final repo-wide validation gates on the close-out branch. +3. Confirm the umbrella Linear issue reflects the merged delivery; do not manually complete it before GitHub integration runs. +4. Remove external references to `projects/typed-attribute-parsers/` without using a repository search tool; inspect known durable surfaces and use build/link validation. +5. Delete `projects/typed-attribute-parsers/`. +6. Commit and open the close-out PR referencing TML-2956. diff --git a/projects/typed-attribute-parsers/retros.md b/projects/typed-attribute-parsers/retros.md new file mode 100644 index 000000000000..31be620d269a --- /dev/null +++ b/projects/typed-attribute-parsers/retros.md @@ -0,0 +1,50 @@ +## 2026-08-27 — Typed attribute parsers project close + +**Trigger:** Mandatory final retro at project close per invariant I10. + +**What happened:** The project delivered the shared attribute-spec engine and migrated SQL and Mongo interpreter attributes through four PR-sized slices. The original three-slice plan gained a dedicated SQL `@default` slice when registry-driven function calls and enum defaults proved too large and distinct for the general SQL migration. + +**Root cause:** The substrate design was implemented before its composition rules and consumer packaging boundary were fully settled. Review therefore discovered structural decisions—literal alternatives, function-call signatures, dynamic specs, diagnostic policy, and comment placement—that should have been reconciled before later dispatches. Delegation briefs also weakened an operator-scoped tool constraint by treating “no search” as merely “no search MCP,” which authorized commands the operator had explicitly forbidden. + +**Landing surface(s):** + +- ADR: `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md` — reconciled the accepted interpreter architecture with the shipped combinators, dynamic specs, typed function calls, native Mongo literals, and explicit language-tooling follow-up. + +### What went well + +- The substrate landed with a real SQL `@relation` consumer, proving the interpretation seam before the migration fanned out. +- Attribute-by-attribute migration kept semantic validation separate from argument grammar and allowed legacy helpers to be removed as their final consumers moved. +- Splitting SQL `@default` preserved review coherence instead of forcing registry-sensitive function-call work into the general SQL slice. +- Dynamic composition from registries, enum members, and model fields eliminated proposed special-purpose combinators while keeping family ownership explicit. +- Dist-consuming and fixture validation caught failures that source-resolved package tests could not see, including duplicated parser classes and stale encoded Mongo syntax. +- Consumer upgrade instructions now accompany the intentional Mongo projection and weight syntax changes. + +### What surprised us + +- The first slice required repeated structural review rounds rather than ordinary polish. The combinator vocabulary, diagnostics policy, and engine shape were not sufficiently settled at implementation start. +- Several combinators or variants were introduced and then removed within the project: `enumOf`, flexible/raw function calls, `funcCallFrom`, scalar-literal helpers, and proposed Mongo-specific index leaves. +- The parent plan was not amended when SQL `@default` became a fourth slice, leaving the project-level record stale. +- “Search-free” delegation briefs still authorized terminal search commands. The durable lesson is to preserve task-scoped constraints verbatim in every delegation and avoid contradictory completion gates. The operator's personal no-search preference remains an operator instruction and is not committed as a project-wide repository rule. +- Historical retail migration inputs retained encoded text-index weights after the current example and integration fixture had moved to native records, so the full fixture pipeline found the remaining consumer lineage late. + +### Calibration lessons + +- A substrate slice that receives structural API feedback should pause for design reconciliation before spawning a sequence of review-fix dispatches. +- A slice boundary change must update the parent plan immediately; sibling slice specs alone are not a sufficient project record. +- Changes crossing package or bundling boundaries need at least one dist-consuming consumer test before the substrate is considered review-ready. +- A delivery brief must not silently weaken an operator-scoped tool constraint. If a completion gate requires a prohibited tool, the orchestrator owns an alternative verification path rather than delegating the contradiction. +- Fixture validation must include historical migration sources when a user-facing syntax changes, not only current examples and integration schemas. + +These process lessons remain in this transient retro because no repo-wide policy change is justified solely by this project. The accepted architectural outcome is preserved in ADR 231. + +### Deferred work + +Language-server discovery, traversal, completion, navigation, and diagnostic reuse over attribute specs remain a separate follow-up under the Language Tools project. This project deliberately delivered the interpreter substrate only. + +### ADR audit + +ADR 231 contained several aspirational or superseded API shapes. It has been rewritten and accepted to distinguish the shipped interpreter architecture from future language-tooling requirements. No additional architectural decision requires a new ADR. + +### Team summary + +SQL and Mongo interpreters now derive attribute argument parsing from typed declarative specs, and ADR 231 records the shipped substrate that future PSL language tooling can extend. From 99e64d62aaeaac51254229d6e85f9022de625eb2 Mon Sep 17 00:00:00 2001 From: Steven McClankerton Date: Thu, 27 Aug 2026 14:30:41 +0000 Subject: [PATCH 2/2] chore: close typed attribute parsers project Signed-off-by: Steven McClankerton --- projects/typed-attribute-parsers/close-out.md | 43 --- projects/typed-attribute-parsers/plan.md | 46 --- projects/typed-attribute-parsers/retros.md | 50 --- .../dispatches/02-combinators.md | 47 --- .../dispatches/03-migrate-relation.md | 66 ---- .../dispatches/04-address-review-r1.md | 57 --- .../dispatches/06-oneof-identifier.md | 49 --- .../dispatches/07-adr-reconcile.md | 43 --- .../dispatches/08-address-review-r2.md | 54 --- .../dispatches/09-prune-comments.md | 51 --- .../dispatches/10-unify-duplicate-check.md | 40 --- .../dispatches/11-vocab-ratchet.md | 35 -- .../dispatches/12-vocab-allowlist.md | 45 --- .../dispatches/13-comment-revision.md | 57 --- .../dispatches/14-unite-diagnostic-code.md | 38 -- .../slices/attribute-spec-kit/plan.md | 31 -- .../slices/attribute-spec-kit/spec.md | 83 ----- .../dispatches/01-mongo-wiring-map.md | 102 ------ .../dispatches/02-mongo-relation.md | 76 ---- .../dispatches/03-mongo-polymorphism.md | 93 ----- .../dispatches/04-kit-str-value-json.md | 96 ----- .../dispatches/05-mongo-index.md | 112 ------ .../dispatches/06-mongo-textindex-cleanup.md | 88 ----- .../slices/mongo-attributes/plan.md | 31 -- .../slices/mongo-attributes/spec.md | 77 ---- .../dispatches/01-model-attribute-kit.md | 37 -- .../sql-attributes/dispatches/02-map.md | 38 -- .../sql-attributes/dispatches/03-id-unique.md | 56 --- .../sql-attributes/dispatches/04-index.md | 53 --- .../sql-attributes/dispatches/05-control.md | 43 --- .../dispatches/06-polymorphism.md | 52 --- .../slices/sql-attributes/plan.md | 49 --- .../slices/sql-attributes/spec.md | 77 ---- .../dispatches/01-kit-scalar-funccall.md | 47 --- .../dispatches/02-migrate-nonenum.md | 78 ---- .../sql-default/dispatches/03-migrate-enum.md | 93 ----- .../04-delete-legacy-funccall-parser.md | 50 --- .../dispatches/05-dynamic-nonenum.md | 56 --- .../sql-default/dispatches/06-dynamic-enum.md | 45 --- .../07-remove-superseded-combinators.md | 50 --- .../dispatches/08-funccall-signatures-kit.md | 55 --- .../09-typed-funccall-signatures.md | 340 ------------------ .../dispatches/10-remove-raw-funccall.md | 75 ---- .../slices/sql-default/plan.md | 55 --- .../slices/sql-default/spec.md | 107 ------ projects/typed-attribute-parsers/spec.md | 103 ------ 46 files changed, 3069 deletions(-) delete mode 100644 projects/typed-attribute-parsers/close-out.md delete mode 100644 projects/typed-attribute-parsers/plan.md delete mode 100644 projects/typed-attribute-parsers/retros.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/02-combinators.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/03-migrate-relation.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/04-address-review-r1.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/06-oneof-identifier.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/07-adr-reconcile.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/08-address-review-r2.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/09-prune-comments.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/10-unify-duplicate-check.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/11-vocab-ratchet.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/12-vocab-allowlist.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/13-comment-revision.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/14-unite-diagnostic-code.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/plan.md delete mode 100644 projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/01-mongo-wiring-map.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/02-mongo-relation.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/03-mongo-polymorphism.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/04-kit-str-value-json.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/05-mongo-index.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/06-mongo-textindex-cleanup.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/plan.md delete mode 100644 projects/typed-attribute-parsers/slices/mongo-attributes/spec.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/01-model-attribute-kit.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/02-map.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/03-id-unique.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/04-index.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/05-control.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/dispatches/06-polymorphism.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/plan.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-attributes/spec.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/01-kit-scalar-funccall.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/02-migrate-nonenum.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/03-migrate-enum.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/04-delete-legacy-funccall-parser.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/05-dynamic-nonenum.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/06-dynamic-enum.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/07-remove-superseded-combinators.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/08-funccall-signatures-kit.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/09-typed-funccall-signatures.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/dispatches/10-remove-raw-funccall.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/plan.md delete mode 100644 projects/typed-attribute-parsers/slices/sql-default/spec.md delete mode 100644 projects/typed-attribute-parsers/spec.md diff --git a/projects/typed-attribute-parsers/close-out.md b/projects/typed-attribute-parsers/close-out.md deleted file mode 100644 index eece5fba15b0..000000000000 --- a/projects/typed-attribute-parsers/close-out.md +++ /dev/null @@ -1,43 +0,0 @@ -# Typed attribute parsers — close-out ledger - -## Delivery state - -| Slice | Evidence | State | -| --- | --- | --- | -| Attribute-spec kit + SQL `@relation` | [prisma/prisma-next#891](https://github.com/prisma/prisma-next/pull/891) | Merged | -| Remaining non-default SQL attributes | [prisma/prisma-next#932](https://github.com/prisma/prisma-next/pull/932) | Merged | -| SQL `@default` dynamic specs and typed function calls | [prisma/prisma-next#938](https://github.com/prisma/prisma-next/pull/938) | Merged | -| Mongo attributes | [prisma/orm#29833](https://github.com/prisma/orm/pull/29833) | Approved and queued for merge; final close gate | - -The original plan named three slices. SQL `@default` became a fourth slice when its registry-sensitive function grammar and enum path proved independently reviewable. - -## Project DoD verification - -- [ ] **Team DoD floor:** Final repo-wide gates, project deletion, and Linear completion wait for the Mongo merge and the close-out branch. -- [x] **Attribute-spec substrate:** PR #891 delivered the combinator kit, `AttributeSpec`, `interpretAttribute`, `InferAttr`, unit coverage, and the first SQL consumer. -- [x] **SQL attributes:** PRs #891, #932, and #938 migrated SQL relation, non-default attributes, and both `@default` paths. Typed function-call parsing and dynamic registry/enum specs replaced the legacy default-call parser. -- [ ] **Mongo attributes:** PR #29833 migrates the Mongo family and removes legacy argument parsers. The PR is approved, all 20 CI checks pass, and it is queued for merge; mark complete only after the merge commit is on `main`. -- [x] **Validation evidence on the final Mongo PR head:** package tests, full integration tests, retail tests, build, fixtures, upgrade coverage, DCO, lint, typecheck, e2e, security, and all CI shards pass. -- [x] **Legacy parser retirement:** Each slice removed the parsing helpers whose final consumers migrated; final PR review and typechecking report no remaining consumers. -- [x] **ADR audit:** ADR 231 is reconciled with the shipped API, separates language-tooling follow-up from current behavior, and is `Accepted`. -- [x] **Mandatory final retro:** `projects/typed-attribute-parsers/retros.md` records the project retro and lands the durable architecture in ADR 231. -- [x] **Manual QA roll-up:** N/A for interactive workflow QA. The project changes interpreter parsing rather than a runnable user flow; parser tests, interpreter tests, integration tests, fixtures, example tests, and executable upgrade instructions cover the observable schema-syntax changes. - -## Classification - -All files under `projects/typed-attribute-parsers/` are transient coordination artifacts and will be deleted after the Mongo merge is confirmed. - -- `spec.md`, `plan.md`, `close-out.md`, and `retros.md`: transient project shaping, sequencing, verification, and retro records. -- Every slice `spec.md` and `plan.md`: transient slice scope and sequencing records. -- Every `dispatches/*.md`: transient implementation briefs. - -No project-local file migrates to `docs/`. The durable technical outcome has already landed in `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md`. - -## Remaining close gates - -1. Confirm PR #29833 is merged and update local `main`. -2. Run the final repo-wide validation gates on the close-out branch. -3. Confirm the umbrella Linear issue reflects the merged delivery; do not manually complete it before GitHub integration runs. -4. Remove external references to `projects/typed-attribute-parsers/` without using a repository search tool; inspect known durable surfaces and use build/link validation. -5. Delete `projects/typed-attribute-parsers/`. -6. Commit and open the close-out PR referencing TML-2956. diff --git a/projects/typed-attribute-parsers/plan.md b/projects/typed-attribute-parsers/plan.md deleted file mode 100644 index ec0b873fa42d..000000000000 --- a/projects/typed-attribute-parsers/plan.md +++ /dev/null @@ -1,46 +0,0 @@ -# typed-attribute-parsers — Plan - -**Spec:** `projects/typed-attribute-parsers/spec.md` -**Linear issue:** [TML-2956](https://linear.app/prisma-company/issue/TML-2956) (under project _Language Tools Support Prisma Next PSL_) - -## At a glance - -Three slices in a substrate-then-consumers shape: slice 1 lands the combinator kit + `interpretAttribute` in `psl-parser`, proven by migrating `@relation` end-to-end in the SQL family; slices 2 and 3 then migrate the remaining SQL and Mongo attributes respectively, in parallel, each deleting its family's legacy parsing helpers. - -## Composition - -### Stack (deliver in order) - -1. **Slice `attribute-spec-kit`** — Linear: _TBD_ - - **Outcome:** The combinator kit (`str`, `int`, `bool`, `enumOf`, `json`, `entityRef`, `fieldRef`, `codecRef`, `list`, `map`, `record`, `oneOf`, `funcCall`/`funcCallFrom`), `AttributeSpec`, the three constructors (`fieldAttribute`/`modelAttribute`/`blockAttribute`), `interpretAttribute`, `InferAttr`, and the `InterpretCtx` contract exist in `psl-parser`, consuming the parser's `ExpressionAst` directly. `@relation` in the **SQL** family is validated and lowered via a spec through `interpretAttribute`, producing byte-identical contract output and identical diagnostics to the hand-written path it replaces. - - **Builds on:** None. - - **Hands to:** (a) the kit + `interpretAttribute` + `InferAttr` API exported from `psl-parser`; (b) the `InterpretCtx` wiring recipe — how a family interpreter assembles `SymbolTable` / declaring model / referenced-model resolver / declaring field / codec lookup / default-fn registry at an attribute call site; (c) the migration recipe — route a call site from `readResolvedArgList` + string helpers to `interpretAttribute(cstNode, spec, ctx)`, then retire the now-dead helper. - - **Focus:** The generic engine and exactly one representative attribute (`@relation` — the richest: positional+named alias, `fieldRef('self')`/`fieldRef('referenced')` scopes, `enumOf` actions, `list` with `nonEmpty`, and a `refine` for the both-or-neither rule). The remaining SQL attributes and all Mongo attributes are deliberately left to slices 2 and 3. No language-server consumer (project non-goal). - -### Parallel group A (builds on slice 1; independent of group B) - -- **Slice `sql-attributes`** — Linear: _TBD_ - - **Outcome:** Every remaining field-, model-, and block-level attribute the **SQL** family interprets — `@id`/`@@id`, `@unique`/`@@unique`, `@@index`, `@default`, `@map`/`@@map`, `@@control`, `@@discriminator`, `@@base` — is described by a spec and lowered via `interpretAttribute`. The SQL family's hand-written argument-parsing helpers (`psl-attribute-parsing.ts` string parsers, and the per-attribute `getNamedArgument`/`getPositionalArgument` re-parsing in `interpreter.ts`, `psl-field-resolution.ts`, `psl-relation-resolution.ts`) are deleted for every migrated attribute. - - **Builds on:** Slice 1's kit API + `InterpretCtx` wiring recipe + migration recipe. - - **Hands to:** SQL family fully spec-driven; no legacy SQL attribute-argument parser remains (grep gate). - - **Focus:** SQL family only (`packages/2-sql/2-authoring/contract-psl`). The model-level aggregation that enforces cross-attribute rules stays untouched (spec decision); only single-attribute cross-argument rules move into each spec's `refine`. `@db.*` native types remain out of scope. - -### Parallel group B (builds on slice 1; independent of group A) - -- **Slice `mongo-attributes`** — Linear: _TBD_ - - **Outcome:** Every field-, model-, and block-level attribute the **Mongo** family interprets — `@id`, `@unique`/`@@unique`, `@@index`, `@@textIndex`, `@relation`, `@map`/`@@map`, `@@discriminator`, `@@base` — is described by a spec and lowered via `interpretAttribute`, including the mixed string/number index `type` as a single `enumOf` and the index-element `oneOf`. The Mongo family's hand-written helpers (`psl-helpers.ts` parsers, `parseIndexFieldList`, the local `parseRelationAttribute`) are deleted for every migrated attribute. - - **Builds on:** Slice 1's kit API + `InterpretCtx` wiring recipe + migration recipe. - - **Hands to:** Mongo family fully spec-driven; no legacy Mongo attribute-argument parser remains (grep gate). - - **Focus:** Mongo family only (`packages/2-mongo-family/2-authoring/contract-psl`). Mongo migrates its own `@relation` spec (distinct value shapes from SQL's). The "at most one `@@textIndex` per collection" rule stays in Mongo's existing model-level aggregation, not in a per-attribute `refine` (spec decision). - - **Carry-in from slice 1 (D6):** `enumOf` was **removed**; enums are now `oneOf` over per-member matchers (`identifier(name)` for bare identifiers; pinned `str(value)` / `num(value)` for literals). Mongo's index `type` set (`1`, `-1`, `"text"`, `"2dsphere"`, `"2d"`, `"hashed"`) becomes `oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))`. Slice 1 built `oneOf` + `identifier`; **slice 3 builds the pinned `str(value)` / `num(value)` forms** (their first consumer is this index-type set — digit-leading members like `"2dsphere"` can't be bare identifiers, so they're quoted-string literals). - -## Dependencies (external) - -- [x] **Linear tracking** — single umbrella issue [TML-2956](https://linear.app/prisma-company/issue/TML-2956) under the _Language Tools Support Prisma Next PSL_ project, assigned to @tatarintsev. (Per-slice sub-issues not yet created; the operator opted for one umbrella ticket over a Linear Project + three sub-issues.) -- [x] **ADR 231 — Declarative attribute specifications** — settled (`Proposed`); this project is its first implementation and advances its status at close-out. - -## Sequencing rationale - -Slice 1 is the substrate every consumer depends on, so it must land first — this is the migration-shaped "substrate change → consumer migration" pattern, which always serialises at the substrate boundary. `@relation` is folded into slice 1 (rather than a pure kit-only slice) so the slice is *Valuable* on its own: it ships a working consumer and proves the seam end-to-end, not "preparation for slice 2." - -Slices 2 and 3 run in parallel because they touch **disjoint family packages** (`packages/2-sql` vs `packages/2-mongo-family`) and share no mutable surface beyond slice 1's already-merged kit — the "different operation families parallelise well" heuristic. Neither consumes the other's hand-off. Serializing them would forfeit throughput the dependency graph permits. diff --git a/projects/typed-attribute-parsers/retros.md b/projects/typed-attribute-parsers/retros.md deleted file mode 100644 index 31be620d269a..000000000000 --- a/projects/typed-attribute-parsers/retros.md +++ /dev/null @@ -1,50 +0,0 @@ -## 2026-08-27 — Typed attribute parsers project close - -**Trigger:** Mandatory final retro at project close per invariant I10. - -**What happened:** The project delivered the shared attribute-spec engine and migrated SQL and Mongo interpreter attributes through four PR-sized slices. The original three-slice plan gained a dedicated SQL `@default` slice when registry-driven function calls and enum defaults proved too large and distinct for the general SQL migration. - -**Root cause:** The substrate design was implemented before its composition rules and consumer packaging boundary were fully settled. Review therefore discovered structural decisions—literal alternatives, function-call signatures, dynamic specs, diagnostic policy, and comment placement—that should have been reconciled before later dispatches. Delegation briefs also weakened an operator-scoped tool constraint by treating “no search” as merely “no search MCP,” which authorized commands the operator had explicitly forbidden. - -**Landing surface(s):** - -- ADR: `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md` — reconciled the accepted interpreter architecture with the shipped combinators, dynamic specs, typed function calls, native Mongo literals, and explicit language-tooling follow-up. - -### What went well - -- The substrate landed with a real SQL `@relation` consumer, proving the interpretation seam before the migration fanned out. -- Attribute-by-attribute migration kept semantic validation separate from argument grammar and allowed legacy helpers to be removed as their final consumers moved. -- Splitting SQL `@default` preserved review coherence instead of forcing registry-sensitive function-call work into the general SQL slice. -- Dynamic composition from registries, enum members, and model fields eliminated proposed special-purpose combinators while keeping family ownership explicit. -- Dist-consuming and fixture validation caught failures that source-resolved package tests could not see, including duplicated parser classes and stale encoded Mongo syntax. -- Consumer upgrade instructions now accompany the intentional Mongo projection and weight syntax changes. - -### What surprised us - -- The first slice required repeated structural review rounds rather than ordinary polish. The combinator vocabulary, diagnostics policy, and engine shape were not sufficiently settled at implementation start. -- Several combinators or variants were introduced and then removed within the project: `enumOf`, flexible/raw function calls, `funcCallFrom`, scalar-literal helpers, and proposed Mongo-specific index leaves. -- The parent plan was not amended when SQL `@default` became a fourth slice, leaving the project-level record stale. -- “Search-free” delegation briefs still authorized terminal search commands. The durable lesson is to preserve task-scoped constraints verbatim in every delegation and avoid contradictory completion gates. The operator's personal no-search preference remains an operator instruction and is not committed as a project-wide repository rule. -- Historical retail migration inputs retained encoded text-index weights after the current example and integration fixture had moved to native records, so the full fixture pipeline found the remaining consumer lineage late. - -### Calibration lessons - -- A substrate slice that receives structural API feedback should pause for design reconciliation before spawning a sequence of review-fix dispatches. -- A slice boundary change must update the parent plan immediately; sibling slice specs alone are not a sufficient project record. -- Changes crossing package or bundling boundaries need at least one dist-consuming consumer test before the substrate is considered review-ready. -- A delivery brief must not silently weaken an operator-scoped tool constraint. If a completion gate requires a prohibited tool, the orchestrator owns an alternative verification path rather than delegating the contradiction. -- Fixture validation must include historical migration sources when a user-facing syntax changes, not only current examples and integration schemas. - -These process lessons remain in this transient retro because no repo-wide policy change is justified solely by this project. The accepted architectural outcome is preserved in ADR 231. - -### Deferred work - -Language-server discovery, traversal, completion, navigation, and diagnostic reuse over attribute specs remain a separate follow-up under the Language Tools project. This project deliberately delivered the interpreter substrate only. - -### ADR audit - -ADR 231 contained several aspirational or superseded API shapes. It has been rewritten and accepted to distinguish the shipped interpreter architecture from future language-tooling requirements. No additional architectural decision requires a new ADR. - -### Team summary - -SQL and Mongo interpreters now derive attribute argument parsing from typed declarative specs, and ADR 231 records the shipped substrate that future PSL language tooling can extend. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/02-combinators.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/02-combinators.md deleted file mode 100644 index 16a91510d377..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/02-combinators.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brief: D2 — the `@relation` combinators - -> Implementer note: you are a **fresh** implementer (the prior D1 implementer's session became inaccessible). You have no project transcript — read the context paths below, especially the on-disk D1 engine, before editing. - -## Context paths (read before editing) -- **The D1 engine you build on** (committed, on disk): `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` — read `types.ts`, `interpret.ts`, `optional.ts`, `field-attribute.ts`, and exports in `src/exports/index.ts`. Tests: `packages/1-framework/2-authoring/psl-parser/test/attribute-spec.test.ts` + `attribute-spec.test-d.ts`. -- Slice spec: `projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md`; slice plan §Dispatch 2: `projects/typed-attribute-parsers/slices/attribute-spec-kit/plan.md`. -- ADR 231: `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md`. -- CST types exported from `packages/1-framework/2-authoring/psl-parser/src/exports/syntax.ts` (`StringLiteralExprAst`, `NumberLiteralExprAst`, `ArrayLiteralAst`, `IdentifierAst`, `ExpressionAst`, …). Span helper `nodePslSpan(node, sourceFile)` in `src/resolve.ts`. Diagnostics are `PslDiagnostic`; failure channel `Result` from `@internal/utils/result`. - -Engine facts (verify against the code): `ArgType { kind; label; _out?; parse(arg: ExpressionAst, ctx: InterpretCtx): Result }`. `InterpretCtx` currently `{ level, sourceId, sourceFile, symbols, selfModel, resolveReferencedModel(), field? }`. `AttributeSpec` has `diagnosticCode?` (defaults `PSL_INVALID_ATTRIBUTE_SYNTAX`). - -## Task -Author the four domain combinators `@relation` needs, as `ArgType`s over `ExpressionAst`, in a new module beside the engine (e.g. `src/attribute-spec/combinators/`); export each from the package public surface; unit-test each: - -- **`str()`** — `StringLiteralExprAst` → string value; non-string-literal → diagnostic. -- **`enumOf(...values)`** — `StringLiteralExprAst` or `NumberLiteralExprAst` whose value is a member of the fixed set (members may be mixed string/number per ADR 231); non-member / wrong-token → diagnostic. Build it generically; whether `@relation` uses it for `onDelete`/`onUpdate` is a D3 wiring decision. -- **`fieldRef(scope)`**, scope `'self' | 'referenced'` — bare `IdentifierAst` → the field **name string**. **Do NOT resolve or validate field existence at parse time** — the SQL interpreter validates existence downstream; a parse-time check would emit new diagnostics and break `@relation` parity. Carry `scope` as combinator metadata (for the future language server); the parsed value is just the name. Non-identifier → diagnostic. -- **`list(of, opts?)`**, `opts?: { nonEmpty?: boolean; unique?: boolean }` — reads an `ArrayLiteralAst`, maps each element through the element `ArgType` `of`, returns `T[]`; `nonEmpty` → diagnostic on empty; `unique` → diagnostic on duplicates; non-array → diagnostic. Build `unique` too (slices 2–3 need it). - -## Codes parity (load-bearing) -Diagnostic **codes** must stay identical; legacy `@relation` errors all use `PSL_INVALID_RELATION_ATTRIBUTE`. Leaf-emitted diagnostics must carry the **attribute's** code, not a hard-coded generic. Thread the spec's `diagnosticCode` to the leaves — cleanest shape: add `diagnosticCode` to `InterpretCtx` and have `interpretAttribute` populate it from the spec before calling any leaf's `parse`, so each combinator emits with `ctx.diagnosticCode`. Pick the cleanest shape against the D1 engine; name it in your report (it's the D3 hand-off). Leaf-diagnostic spans anchor to the offending element/arg node via `nodePslSpan(node, ctx.sourceFile)`. - -## Scope -**In:** the four combinators + their unit tests; the `diagnosticCode` threading (or equivalent); exports in `src/exports/`. -**Out:** ANY interpreter change and the `sqlRelation` spec itself (that's D3); the rest of ADR 231's alphabet (`int`, `bool`, `json`, `map`, `record`, `entityRef`, `codecRef`, `oneOf`, `funcCall`, `modelAttribute`, `blockAttribute`); `@db.*`; field-existence resolution. - -## Completed when -- [ ] `str`, `enumOf`, `fieldRef`, `list` exported from `psl-parser` and usable as `Param`s in an `AttributeSpec`. -- [ ] Unit tests per combinator: parse success + each diagnostic path; `enumOf` covers a mixed string/number set; `list` covers `nonEmpty` + `unique` + element-error propagation; `fieldRef` returns the name and emits NO existence diagnostic. -- [ ] A test proves a leaf diagnostic carries the attribute's `diagnosticCode` end-to-end through `interpretAttribute`. -- [ ] Gate green: `pnpm --filter @internal/psl-parser typecheck && pnpm --filter @internal/psl-parser test && pnpm --filter @internal/psl-parser lint`. - -## Standing instruction -Stay focused on the goal; control scope. Trivial-and-related fixes that serve the goal go in with a one-line note in your wrap-up; anything pulling you off the goal halts and surfaces. - -## Constraints -- No `any`; no bare `as` (narrow `blindCast`/`castAs` from `@internal/utils/casts` with a reason, or types that avoid the cast); arktype not zod — where a leaf reduces to a context-free value check (`enumOf`'s literal set), ADR 231 suggests backing it with an arktype `Type`; use judgment for a small fixed set vs a plain membership check, and note the choice; no file-extension imports; tests-first. -- Explicit-staging commits (`git add `, never `-A`/`.`); no amend; **no push**. -- Read-only on `projects/typed-attribute-parsers/reviews/**`, `spec.md`, plan files. -- Run the "no transient project IDs in code" scan on your `+` diff before declaring done. - -## Operational metadata -- **Model tier:** mid (routine combinators against a settled contract). -- **Halt conditions:** a combinator can't emit code-parity diagnostics without an engine change you can't make cleanly; the diff drifts into interpreter / `sqlRelation` territory (that's D3); an `ExpressionAst` shape you need isn't exported. - -Return the structured report per your persona's § Return shape; note the `diagnosticCode`-threading shape you landed and the final exported combinator signatures (the D3 hand-off). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/03-migrate-relation.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/03-migrate-relation.md deleted file mode 100644 index 37d2216b7e6d..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/03-migrate-relation.md +++ /dev/null @@ -1,66 +0,0 @@ -# Brief: D3 — migrate SQL `@relation` to a spec; delete `parseRelationAttribute` - -> Fresh implementer (session resume is unavailable). Read the context paths first; all prior work is committed. - -## Context paths (read before editing) -- **The kit you consume** (committed): `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` — `types.ts` (`ArgType`, `AttributeSpec`, `Param`, `InterpretCtx`, `InferAttr`), `interpret.ts` (`interpretAttribute`), `field-attribute.ts` (`fieldAttribute`), `optional.ts`, `combinators/` (`str`, `enumOf`, `fieldRef`, `list`). All exported from `@internal/psl-parser` (`src/exports/index.ts`). -- **The code you replace:** `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` — `parseRelationAttribute` (the hand-written parser) and `normalizeReferentialAction` (KEEP this — it stays the referential-action validator). Read both in full. -- **The call sites:** `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` — three `parseRelationAttribute({ attribute, modelName, fieldName, sourceId, diagnostics })` calls (around the `buildModelNodeFromPsl` relation paths). Also `psl-field-resolution.ts` / `psl-relation-resolution.ts` `validateNavigationListFieldAttributes` for surrounding context. -- Slice spec (esp. § Resolved decisions): `projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md`. ADR 231. -- The interpreter receives `symbolTable: SymbolTable` + `sourceFile: SourceFile` (see `InterpretPslDocumentToSqlContractInput`) — so the CST attribute node and everything `InterpretCtx` needs is in reach. Diagnostics here are `ContractSourceDiagnostic` (`{ code, message, sourceId, span }`); `PslDiagnostic` has the same shape — map between them at the call site as needed. - -## Task -Replace the hand-written `parseRelationAttribute` with a declarative `sqlRelation` `AttributeSpec` lowered through `interpretAttribute`, preserving **byte-identical diagnostic codes and spans** for every `@relation` error path (message text may change per the project parity bar). Then delete `parseRelationAttribute` and any helper it alone used. - -### The spec -Define `sqlRelation` (e.g. in a new `src/attribute-specs.ts` or co-located in `psl-relation-resolution.ts`): -``` -fieldAttribute('relation', { - positional: [{ key: 'name', type: optional(str()) }], // positional-or-named alias for name - named: { - name: optional(str()), - fields: optional(list(fieldRef('self'), { nonEmpty: true })), - references: optional(list(fieldRef('referenced'), { nonEmpty: true })), - map: optional(str()), - onDelete: optional(), - onUpdate: optional(), - }, - refine: relationInvariants, - diagnosticCode: 'PSL_INVALID_RELATION_ATTRIBUTE', -}) -``` -- **`onDelete`/`onUpdate` (resolved decision):** these are **bare identifiers** (`onDelete: Cascade`), and the action set is validated **downstream** by the existing `normalizeReferentialAction` (which emits `PSL_UNSUPPORTED_REFERENTIAL_ACTION`). Do NOT use `enumOf` for them (it would change the code at parse time and break parity). Parse them to the **raw identifier name string** (no set check) and route the result to `normalizeReferentialAction` unchanged. If the kit has no bare-identifier-name leaf, add a small one in `psl-parser` combinators (reads an `IdentifierAst` → its name, no validation; analogous to `fieldRef` minus the scope) and export it; unit-test it. -- **`refine: relationInvariants`** holds the cross-argument rules: `fields`/`references` both-or-neither (legacy code `PSL_INVALID_RELATION_ATTRIBUTE`, anchored to the attribute span). The positional-vs-named `name` conflict is handled by the **engine's alias mechanism** (already built) — verify it emits with `diagnosticCode` + attribute-span; if its span/anchoring diverges from legacy, reconcile. -- The interpreted output (`InferAttr` = `{ name?, fields?, references?, map?, onDelete?, onUpdate? }`) is mapped at the call site to today's `ParsedRelationAttribute` (`name → relationName`, `map → constraintName`, rest 1:1). - -### InterpretCtx assembly -At each call site, build an `InterpretCtx` from interpreter state: `level: 'field'`, `sourceId`, `sourceFile`, `symbols` (the SymbolTable), `selfModel` (the declaring model symbol), `resolveReferencedModel()` (the relation's target model — use the field's type name to resolve, as the interpreter already does elsewhere), optional `field`, and a baseline `diagnosticCode` (the engine overrides it from the spec). Factor the assembly into a small helper if it's repeated across the three call sites. - -## Parity reconciliation (the load-bearing carry-overs) -Verify against the diagnostics + relations fixtures/tests and reconcile: -1. **Codes + spans byte-identical** for every `@relation` error path: positional-name-not-a-string, named-name-not-a-string, conflicting names, unknown argument, fields-xor-references, empty/non-bracketed fields or references, map-not-a-string, too-many-positional, bad referential action. For each, confirm the legacy code + span are reproduced. Where the engine anchors a span differently than legacy, prefer adjusting the spec/call-site; a minimal, noted engine span tweak is acceptable only if unavoidable. -2. **Aggregate-all vs first-error:** the engine returns ALL diagnostics; legacy returned on the FIRST error (then the caller skipped the field). If a fixture has a `@relation` with multiple simultaneous errors, the diagnostic SET may grow. If you find such a case, **halt and surface** the specific fixture delta rather than silently rewriting it — the orchestrator decides whether the richer diagnostics are an acceptable, intentional fixture update. -3. **Duplicate named args:** the engine silently drops duplicates (no diagnostic). Confirm this matches legacy `@relation` behaviour (legacy used `getNamedArgument` = first match) or that the upstream parser already rejects duplicates. Note the finding. - -## Scope -**In:** `sqlRelation` spec; route the three `@relation` call sites through `interpretAttribute` + map the output to `ParsedRelationAttribute`; `InterpretCtx` assembly helper; delete `parseRelationAttribute` and any now-dead helper it alone used (check `getPositionalArgumentEntry`, `parseFieldList`, etc. — delete only if `@relation` was their sole caller; otherwise leave for slice 2); add the bare-identifier-name leaf to `psl-parser` if needed. Keep `normalizeReferentialAction`. -**Out:** all other SQL attributes (`@id`, `@unique`, `@@index`, `@default`, `@map`, `@@control`, `@@discriminator`, `@@base`) — slice 2; Mongo — slice 3; the rest of ADR 231's alphabet; `@db.*`. - -## Completed when -- [ ] `@relation` is validated + lowered via `interpretAttribute(sqlRelation)`; `parseRelationAttribute` deleted. -- [ ] `rg "parseRelationAttribute"` returns zero results (outside this brief's own text). -- [ ] Diagnostic **codes + spans** byte-identical for every `@relation` error path (verified against `interpreter.relations.test.ts`, `interpreter.relations.many-to-many.test.ts`, `interpreter.diagnostics.test.ts`). -- [ ] Gate green: `pnpm --filter @internal/contract-psl-sql test` (or the package's actual name — confirm via its `package.json`); `pnpm fixtures:check`; and after `pnpm --filter @internal/psl-parser build`, a workspace `pnpm typecheck` (cross-package consumer check, since `psl-parser`'s exported types changed). `pnpm --filter @internal/psl-parser test` + lint if you added the bare-identifier leaf. - -## Standing instruction -Stay focused on the goal; control scope. Trivial-and-related fixes serving the goal go in with a one-line note; anything pulling you off the goal — especially migrating a second attribute — halts and surfaces. - -## Constraints -- No `any`; no bare `as` (narrow `blindCast`/`castAs` with reason, or types that avoid it); no file-extension imports; no reexport outside `exports/`; tests-first where you add new kit surface. -- Explicit-staging commits, no amend, **no push**. Read-only on `projects/typed-attribute-parsers/reviews/**`, `spec.md`, plan files. Run the transient-ID scan on your `+` diff. - -## Operational metadata -- **Model tier:** thorough (parity-critical, judgment-heavy migration across packages). -- **Halt conditions:** a fixture's `@relation` diagnostic SET changes (aggregate-all case — surface it, decision #2 above); a span can't be reproduced without a non-trivial engine change; deleting a helper would break a non-`@relation` caller (leave it, note it for slice 2); the diff drifts into a second attribute. - -Return the structured report per § Return shape; explicitly report the parity verification (each error path: code + span identical?), the duplicate-named-arg finding, and any fixture delta you surfaced. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/04-address-review-r1.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/04-address-review-r1.md deleted file mode 100644 index 525e1422c297..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/04-address-review-r1.md +++ /dev/null @@ -1,57 +0,0 @@ -# Brief: D4 — address PR #891 review round 1 - -> Fresh implementer (session resume unavailable). Read the committed kit + the commented files first. This addresses a review round on the open slice-1 PR; changes land on branch `tml-2956-typed-attribute-parsers` and update the PR. **Do NOT post to GitHub or resolve review threads** — only push commits. - -## Context -- PR #891 (slice 1). Reviewers: CodeRabbit (bot) + the operator (SevInf). Files: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` (engine + combinators), `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (the `sqlRelation` spec), `interpreter.ts`. -- Slice spec + the project parity bar: `projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md`, `projects/typed-attribute-parsers/spec.md` (codes byte-identical; spans no-coarser; messages may change; stricter rejection of malformed input is allowed). - -## Tasks (address each; commit coherently) - -### T1 — Reject duplicate arguments unconditionally (interpret.ts) -- The named-argument loop (`if (namedSeen.has(key)) continue;`, ~line 55) currently **silently drops** a duplicate named key. Change it to emit a structural diagnostic (code `ctx`/`spec.diagnosticCode`, anchored to the duplicate arg's span via `nodePslSpan(arg.syntax, ctx.sourceFile)`) and not count the duplicate as a successful parse. -- The positional-vs-named alias merge in `resolveKey` (~lines 107-123) currently only emits a conflict when the two values **differ** (`!argValuesEqual(...)`). Per the operator: a key supplied **both** positionally and by name is a duplicate — reject it **regardless of value equality**. Emit the conflict/duplicate diagnostic whenever `fromPositional && fromNamed`. -- **Remove the now-dead `argValuesEqual` + `isPlainRecord` helpers** (they existed only for the value-equality escape hatch). -- Add/adjust unit tests: `name: "A", name: "B"` and `name: "A", name: "A"` both rejected; `@rel("Foo", name: "Foo")` (positional + named, equal) rejected. - -### T2 — `unique: true` on relation lists (psl-relation-resolution.ts, the `sqlRelation` spec) -- Change `fields: optional(list(fieldRef('self'), { nonEmpty: true }))` → add `unique: true`; same for `references`. So duplicate FK column names can't reach `foreignKeyNodes`. - -### T3 — Drop the unnecessary list copy (list.ts:27) -- `const elements = [...arg.elements()]` copies the iterable. If `ArrayLiteralAst.elements()` already returns an array, iterate it directly (drop the spread). If it's a generator and you only need it for the `nonEmpty` length check, track an element count in the existing parse loop instead of materialising a second array. Keep behaviour identical; just remove the redundant allocation. - -### T4 — Use `enumOf` for referential actions; delete `identifierName` (operator question identifier-name.ts:13) -- The operator's point: a referential action (`onDelete: Cascade`) is a bare-identifier enum and should go through `enumOf`, not a bespoke `identifierName` leaf. **Extend `enumOf`** to also accept a bare `IdentifierAst` whose text matches a **string** member (in addition to the existing `StringLiteralExprAst`/`NumberLiteralExprAst` handling) — additive, must not regress existing `enumOf` tests. -- In `sqlRelation`, change `onDelete`/`onUpdate` to `optional(enumOf('NoAction', 'Restrict', 'Cascade', 'SetNull', 'SetDefault'))`. Map the validated action to the `ReferentialAction` via the existing `REFERENTIAL_ACTION_MAP` (keep `normalizeReferentialAction` as the pure token→action mapper, or inline the map — your call; do not keep a redundant second validation path). -- **Delete `identifierName` + its tests + its export.** -- **Parity flag (report this):** a bad referential action now errors at parse via `enumOf` with the attribute's code (`PSL_INVALID_RELATION_ATTRIBUTE`) instead of downstream `PSL_UNSUPPORTED_REFERENTIAL_ACTION`. If any test/fixture asserts `PSL_UNSUPPORTED_REFERENTIAL_ACTION`, update it intentionally and **report the exact count + files** so the orchestrator can relay to the operator. If that code turns out to be load-bearing elsewhere (non-`@relation`), **halt and surface** instead of deleting its only producer. - -### T5 — `fieldRef` resolves via the symbol table (operator question field-ref.ts:30) -- The operator wants `fieldRef` to actually resolve the field against the symbol table it has in `ctx` (`selfModel` for `'self'`, `resolveReferencedModel()` for `'referenced'`), not treat the name as opaque. Implement resolution: look the field up on the scoped model; **if it doesn't resolve, emit the field-existence diagnostic here** (code = `ctx.diagnosticCode`, span = the identifier's span). -- **Reconcile downstream to avoid double diagnostics:** the SQL interpreter currently validates relation `fields`/`references` existence downstream (the `localColumns`/`referencedColumns` resolution in `interpreter.ts`). With `fieldRef` now validating, remove/skip that **duplicate** existence check **for the relation `@relation` path only**, so a missing field yields exactly one diagnostic. Keep the column-name mapping (the resolved field still maps to its column). -- Preserve diagnostic **code + span** parity for the missing-field case (verify against `interpreter.relations.test.ts` / `interpreter.diagnostics.test.ts`); if the diagnostic's code/span/source must shift, update assertions intentionally and report. -- **Halt and surface if** this reconciliation requires large interpreter surgery, touches non-relation field resolution, or can't preserve the cross-space/referenced-model resolution the interpreter already does. Better to surface than to sprawl. -- Keep `fieldRef`'s parsed value as the **name string** (so the `ParsedRelationAttribute` mapping is unchanged); resolution drives validation, not the return shape. - -## Scope -**In:** the five tasks above (psl-parser engine + `list`/`enumOf`/`field-ref` combinators, delete `identifier-name`; the `sqlRelation` spec + the relation call-site existence-check reconciliation in `interpreter.ts`). Tests for each. -**Out:** other attributes (slices 2–3); the rest of ADR 231's alphabet; Mongo; `@db.*`. Do not migrate a second attribute. - -## Completed when -- [ ] T1–T5 done; `identifierName` fully removed (`rg identifierName` zero). -- [ ] `rg "argValuesEqual"` zero (helper removed). -- [ ] Unit tests updated/added for T1, T3, T4 (enumOf bare-identifier), T5 (fieldRef resolution: resolves a real field; emits one diagnostic for a missing field). -- [ ] Gates green: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test` (or the package's real name — confirm); `pnpm fixtures:check`; after `pnpm --filter @internal/psl-parser build`, workspace `pnpm typecheck`. -- [ ] Report: the T4 parity flag (code change + any updated assertions/fixtures, with counts) and the T5 reconciliation (what downstream check was removed, diagnostic parity result). - -## Standing instruction -Stay focused on the goal; control scope. Halt + surface (don't sprawl) if T5's downstream reconciliation balloons or T4's code change hits load-bearing non-relation uses. - -## Constraints -No `any`; no bare `as` (narrow `blindCast`/`castAs` with reason, or types that avoid it); no file-ext imports; no reexport outside `exports/`; tests-first for new behaviour. Explicit-staging commits, no amend, **no push** (the orchestrator pushes). Read-only on `projects/**/reviews/**`, `spec.md`, plan files. Run the transient-ID scan on your `+` diff. Do NOT post to GitHub or touch review threads. - -## Operational metadata -- **Model tier:** thorough (parity-sensitive, cross-package, two design changes). -- **Halt conditions:** T5 downstream reconciliation requires large surgery / touches non-relation paths; T4 reveals `PSL_UNSUPPORTED_REFERENTIAL_ACTION` is load-bearing elsewhere; any fixture's contract output (not just diagnostics) changes. - -Return the structured report per § Return shape, with explicit per-task results (T1–T5), the parity flags, and commit SHAs. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/06-oneof-identifier.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/06-oneof-identifier.md deleted file mode 100644 index f3c4b83bf22e..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/06-oneof-identifier.md +++ /dev/null @@ -1,49 +0,0 @@ -# Brief: D6 — replace `enumOf` with `oneOf` + `identifier` - -> Fresh implementer (session resume unavailable). On the open slice-1 PR #891 branch `tml-2956-typed-attribute-parsers`. Operator-directed design change. Do NOT push or touch GitHub — the orchestrator pushes. - -## Context -- Kit: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` — engine (`interpret.ts`), combinators (`combinators/`: `str`, `enumOf`, `fieldRef`, `list`, `diagnostic`), types (`types.ts`), exports (`src/exports/index.ts`). Tests under the package's `test/`. -- `enumOf`'s only consumer is `sqlRelation`'s `onDelete`/`onUpdate` (`packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`), matching bare-identifier actions; the validated name maps to a `ReferentialAction` via `normalizeReferentialAction` (now a pure token→action map). -- Leaves are `Result`-pure (return diagnostics, never push a sink) — this is what lets `oneOf` backtrack. Leaf diagnostics carry `ctx.diagnosticCode` and anchor via `nodePslSpan(node, ctx.sourceFile)`. - -## Decision (operator) -Replace the bespoke `enumOf` with two composable primitives joined by `oneOf` — ADR 231 principle #4 (compose, don't special-case). - -## Tasks - -### T1 — `oneOf(...alts)` combinator -- New combinator: tries each alternative's `parse` in order; **first success wins**; if all fail, emits **one** diagnostic (e.g. `Expected one of: `, aggregating the alternatives' `label`s) with `ctx.diagnosticCode`, anchored to the arg node. Because leaves are `Result`-pure, a failed branch leaves no diagnostics behind — do not leak the alternatives' internal failures; emit only the single aggregate. -- Type: `oneOf[]>(...alts: Alts): ArgType>` (union of the alternatives' output types; `OutOf = A extends ArgType ? X : never`). Confirm the inferred output is the union of members. -- Unit tests: first-match-wins; all-fail → single aggregate diagnostic (code = the threaded `diagnosticCode`, span = arg node); type-level test that the output is the union of the alternatives. - -### T2 — `identifier(name)` combinator -- New combinator: matches a bare `IdentifierAst` whose name **equals** `name`; returns that name. Pinned-only (no open form). Non-identifier OR identifier with a different name → diagnostic (`ctx.diagnosticCode`, span = arg node). -- Type: `identifier(name: N): ArgType` (so `oneOf` over several `identifier`s infers the precise union). -- Unit tests: matches the exact identifier; rejects a different identifier; rejects a non-identifier (e.g. a quoted string / number); the returned value is the pinned literal type. - -### T3 — Rewire referential actions -- In `sqlRelation`, change `onDelete`/`onUpdate` from `optional(enumOf('NoAction', …))` to: - `optional(oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault')))`. -- The inferred output union (`'NoAction' | 'Restrict' | 'Cascade' | 'SetNull' | 'SetDefault'`) must be unchanged from what `enumOf` produced, so the call-site mapping through `normalizeReferentialAction` is untouched. Verify `onDelete: Cascade` parses and maps; a bad action (`WeirdAction`) yields one diagnostic with code `PSL_INVALID_RELATION_ATTRIBUTE` (the existing assertion in `interpreter.relations.test.ts` — message may differ, code must hold). - -### T4 — Delete `enumOf` -- Remove `combinators/enum-of.ts`, its export, and its unit tests (the behaviour is now covered by `oneOf` + `identifier` tests). `rg "enumOf"` → zero. - -## Scope -**In:** `oneOf` + `identifier` combinators + tests; rewire `sqlRelation` actions; delete `enumOf`; exports. **Out:** the pinned `str(value)`/`num(value)` literal matchers (their first consumer is Mongo's index `type` in slice 3 — do NOT build them now, no caller); `str()` stays the open string matcher unchanged; everything else (other attributes, Mongo, `@db.*`). - -## Completed when -- [ ] `oneOf` + `identifier` exported and usable as `Param`s; `enumOf` gone (`rg enumOf` zero). -- [ ] `onDelete`/`onUpdate` use `oneOf(identifier(...))`; output union unchanged; SQL relations + diagnostics suites green (the bad-action case still emits `PSL_INVALID_RELATION_ATTRIBUTE`). -- [ ] Unit + type-level tests for `oneOf` (union inference, first-match, aggregate diagnostic) and `identifier` (pinned match/mismatch, literal type). -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check`; after `pnpm --filter @internal/psl-parser build`, workspace `pnpm typecheck`. - -## Constraints -No `any`; no bare `as` (narrow `blindCast`/`castAs` with reason, or types that avoid it); no file-ext imports; no reexport outside `exports/`; tests-first. Explicit-staging commits, no amend, **no push**. Read-only on `projects/**/reviews/**`, `spec.md`, plan files. Transient-ID scan on the `+` diff. Do NOT post to GitHub. - -## Operational metadata -- **Model tier:** mid (two combinators against a settled contract + a one-line spec rewire). -- **Halt conditions:** `oneOf`'s union type inference can't be expressed without `any`/a broad cast; deleting `enumOf` breaks a consumer you didn't expect (grep first); the spec rewire changes the `onDelete`/`onUpdate` output type (it must stay the same union). - -Return the structured report per § Return shape: per-task results, the `oneOf` diagnostic shape + type-inference approach, confirmation the action union is unchanged, and commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/07-adr-reconcile.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/07-adr-reconcile.md deleted file mode 100644 index 74d0f4a30903..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/07-adr-reconcile.md +++ /dev/null @@ -1,43 +0,0 @@ -# Brief: D7 — reconcile ADR 231 with the shipped `oneOf`/`identifier` design - -> Fresh implementer. Documentation-only dispatch on the slice-1 PR branch. Editing the architecture doc `docs/architecture docs/ADR 231 - Declarative attribute specifications.md` is operator-authorised (overrides the `AGENTS.md` "ask first" default for this edit). Do NOT push or touch GitHub. - -## Why -The shipped kit replaced the bespoke `enumOf` combinator with **composition**: enums are now `oneOf` over per-member matchers — `identifier(name)` for bare-identifier members, and pinned `str(value)` / `num(value)` for quoted-string / number-literal members. ADR 231 still documents `enumOf` throughout. Update the ADR so it matches what shipped (ADR principle #4, "compose, don't special-case", taken to its conclusion: there is no enum leaf). - -## The shipped design (what the ADR should now say) -- **No `enumOf` combinator.** An enum is expressed by `oneOf` over matchers: - - `identifier('Cascade')` — matches the bare identifier `Cascade` (typed `ArgType<'Cascade'>`). - - `str('text')` — matches the quoted string `"text"`; `str()` (no arg) remains the open "any string literal" matcher. - - `num(1)` / `num(-1)` — matches a specific number literal. - - (No `bool` matcher — not needed.) -- A bare-identifier enum (referential actions): `oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault'))`. -- A mixed quoted-string/number set (Mongo index `type`): `oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))`. The quoted-vs-bare surface is now **explicit per member** (`str('text')` vs `identifier('Cascade')`), which is strictly more precise than `enumOf` guessing from the member's JS type. -- `oneOf` (already in the ADR) is the sum: ordered try-each over `Result`-pure leaves, first success wins, one aggregate `expected one of …` diagnostic on total failure. Its output type is the union of the alternatives' output types — so an editor can still enumerate the legal completions (each alternative is a pinned matcher with a known value). - -## Edits to make (find every `enumOf` mention; these are the known sites — search for `enumOf` to be exhaustive) -1. **§ At a glance** — the `sqlRelation` code sample: `onDelete`/`onUpdate` change from `optional(enumOf('NoAction', …))` to `optional(oneOf(identifier('NoAction'), identifier('Restrict'), identifier('Cascade'), identifier('SetNull'), identifier('SetDefault')))`. Update the `InferAttr` comment block if it references the enum shape (the union type is unchanged). -2. **§ At a glance** narrative ("Notice three things…") — where it lists `enumOf(...)` as an example combinator, replace with the `oneOf(identifier(...))` composition; keep the point that the value types are combinators. -3. **§ At a glance** — "because `onDelete` is declared as `enumOf(...)`, the editor can complete its values" → reframe: declared as `oneOf(identifier('NoAction'), …)`, the editor enumerates the alternatives' pinned values. -4. **§ The combinator kit → Scalars** — the sentence introducing `enumOf(...values)` and the Mongo `enumOf(1, -1, 'text', …)` example. Replace with the `str(value?)` / `num(value?)` / `identifier(name)` matchers and the `oneOf(...)`-composes-enums explanation; Mongo index `type` becomes the `oneOf(num(1), num(-1), str('text'), …)` form. Note `str()` open vs `str(value)` pinned. -5. **§ One spec, two consumers (language-server)** — the `enumOf('NoAction', …)` completion example → `oneOf(identifier('NoAction'), …)`; the editor still derives completions from the alternatives' pinned values. -6. **§ Alternatives considered → "Separate `enumOf` and `numEnum`"** — this rejected-alternative is now obsolete (there is no enum leaf at all). Replace it with a rejected-alternative entry that records the actual decision: **"A dedicated `enumOf` leaf"** — rejected in favour of `oneOf` over `identifier` / pinned `str` / `num`, because composition (principle #4) expresses homogeneous and mixed sets uniformly, makes the quoted-vs-bare surface explicit per member, and reuses the `oneOf` sum the design already needs for `@default` and index elements. (Preserve the insight that mixed string/number sets must be expressible — now via `oneOf(num(...), str(...))`.) -7. **§ References (ADR 224 line)** — `@@control(...)` "value set this design types as `enumOf('managed', 'tolerated', 'external', 'observed')`" → `oneOf(identifier('managed'), identifier('tolerated'), identifier('external'), identifier('observed'))`. -8. Any other `enumOf` occurrence the search turns up — reconcile consistently. - -## Scope -**In:** `docs/architecture docs/ADR 231 - Declarative attribute specifications.md` only — prose + code samples reconciled to the `oneOf`/`identifier`/`str`/`num` design. **Out:** code, tests, other docs, ADR status line (leave `Status: Proposed` as-is unless it already says otherwise — implementation tracking is a close-out concern). Do not invent design beyond what's described here; if you find an `enumOf` use case this model can't express, **halt and surface** rather than guessing. - -## Completed when -- [ ] `rg "enumOf" "docs/architecture docs/ADR 231 - Declarative attribute specifications.md"` → zero. -- [ ] Every code sample + narrative uses `oneOf` / `identifier` / `str` / `num` consistently; the doc reads coherently (no dangling references to a removed leaf). -- [ ] No other file changed. - -## Constraints -- Markdown only; no code/test changes. Follow `markdown-no-artificial-line-wraps` (don't hard-wrap prose). Explicit-staging commit (`git add` the ADR path only), no amend, **no push**. Do NOT touch GitHub. Transient-ID scan is N/A (no source), but don't introduce `projects/…` paths into the ADR. - -## Operational metadata -- **Model tier:** mid (bounded doc reconciliation, but requires faithful design understanding). -- **Halt conditions:** an `enumOf` use case that `oneOf` + the matchers can't express; any temptation to change code/tests to match the doc (the doc follows the code, not vice-versa). - -Return: the list of sites changed, the `rg enumOf` result, and the commit SHA. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/08-address-review-r2.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/08-address-review-r2.md deleted file mode 100644 index 90c905f8f644..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/08-address-review-r2.md +++ /dev/null @@ -1,54 +0,0 @@ -# Brief: D8 — address PR #891 review round 2 (engine simplification) - -> Fresh implementer. On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. These are the operator's (and CodeRabbit's) **unresolved** review comments — all to be addressed. The 509-test psl-parser suite + the SQL suites are your safety net; keep them green and update them where the reshape demands. Do NOT push or touch GitHub. - -## Context -- Engine + types: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` — `interpret.ts`, `types.ts`, `optional.ts`, `combinators/one-of.ts`. Tests under the package's `test/`. -- The SQL `@relation` consumer: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (the `sqlRelation` spec + `interpretRelationAttribute` wrapper) and its call sites in `interpreter.ts`. -- Parity bar (operator-amended): contract output + diagnostic **codes** identical; **spans** no-coarser; **messages** may change. - -## Tasks - -### T1 — Single-pass engine (`interpret.ts`) — operator's main note -The engine currently does one pass over positional args, one over named args, then a third `resolveKey` merge pass. Rewrite `interpretAttribute` as **a single pass that builds one output map and reports duplicates as it goes**: -- Walk the attribute's args in source order. A positional arg binds to the next unconsumed positional slot's `key`; a named arg binds to its `key`. -- Maintain one `output` map + a `seen` set. If a key is already `seen` when you go to set it (whether from positional-then-named alias, or a repeated named key), emit the duplicate/conflict diagnostic **inline** and skip — this subsumes the old alias-conflict and duplicate-named handling (already operator-confirmed: reject duplicates regardless of value equality). -- Unknown named key → diagnostic; excess positional (no slot left) → diagnostic. -- After the pass: apply `optional` defaults for absent keys; emit missing-required diagnostics; then run `refine`. -- Delete the now-dead `positionalParsed`/`namedParsed`/`resolveKey` machinery. The behaviour (codes, spans, the set of diagnostics for each error path) must stay within the parity bar; keep all engine + relation tests green. - -### T2 — Reuse shared type helpers (`types.ts:8,10`) -`Simplify` and `UnionToIntersection` are redefined locally but exist in ~7 places across the repo with no canonical home. **Centralize** them in `@internal/utils` (psl-parser already depends on it — add a small `types` module/export there, e.g. `@internal/utils/types`) and import them in `attribute-spec/types.ts`; do not keep the local redefinitions. Scope: just centralize + import here — do NOT migrate the other 6 copies (out of scope; note as a possible future cleanup). If `@internal/utils` is the wrong home per `lint:deps` layering, surface the finding rather than forcing a bad dependency. - -### T3 — Remove variadic positional support (`types.ts:85`) -No attribute uses a variadic positional (`@@index([a,b])` is a single positional bound to a `list`, not a variadic; `@@base(Base, "v")` is two fixed positionals). Remove `PositionalParam.variadic`, the variadic branch in `PosEntryObject`/`PosOut`, and the engine's variadic handling. YAGNI — re-add only when a real variadic attribute appears. - -### T4 — `optional` is an `ArgType` (`optional.ts:8`) -Model `optional(t)` as an `ArgType`, not a separate `OptionalParam`/`Param` union member. Target shape: an `OptionalArgType extends ArgType` carrying `{ optional: true; hasDefault: boolean; defaultValue?: T }`, so `Param` collapses to just `ArgType` (an optional param is a flavoured `ArgType`). The engine detects optionality via the marker on the `ArgType`; `NamedOut`/`PosOut` key their optional-property mapping off `OptionalArgType` instead of `OptionalParam`. Keep `optional(t)` / `optional(t, default)` call-shape and inferred types unchanged for consumers (`sqlRelation` must still type-check identically and infer the same output union). - -### T5 — Remove the `map`→`constraintName` rename wrapper (`psl-relation-resolution.ts:217`) -`interpretRelationAttribute`'s output mapping only renames `name`→`relationName` and `map`→`constraintName`. Remove that renaming layer and consume `interpretAttribute`'s result **directly**: align the downstream shape with the spec output keys (`name`, `map`, `fields`, `references`, `onDelete`, `onUpdate`) — update `ParsedRelationAttribute` (rename its `relationName`→`name`, `constraintName`→`map`, or drop it in favour of `SqlRelationOutput`) and the downstream field accesses across `interpreter.ts`. Keep the genuinely-needed plumbing (`findRelationAttributeNode`, `InterpretCtx` assembly) — inline or as small helpers — but no value-renaming pass. (The "same for Mongo" note is slice 3 — Mongo isn't migrated yet; ignore here.) **Halt and surface** if the downstream `relationName`/`constraintName` consumers fan out further than the relation path expects. - -### T6 — `oneOf` requires ≥1 alternative (`one-of.ts:19`) -Make the rest parameter a non-empty tuple so `oneOf()` (zero args) is a **compile error**: `oneOf, ...ArgType[]]>(...alts: Alts)`. Keep the union-output typing. - -## Orchestrator decision on the remaining CodeRabbit comment (do NOT re-add) -CodeRabbit `psl-relation-resolution.ts:89` asks to restore `PSL_UNSUPPORTED_REFERENTIAL_ACTION`. **Decision: do not restore it.** The operator deliberately moved referential-action validation into `oneOf(identifier(...))` (D6); a bad action now reports the attribute's `PSL_INVALID_RELATION_ATTRIBUTE` at parse, which is consistent with the operator's simplification and the amended parity bar. Restoring the specific code would re-add downstream validation or a per-argument code override — exactly the special-casing being removed. Leave as-is; do not change the test assertion back. - -## Completed when -- [ ] Engine is single-pass; `resolveKey`/`positionalParsed`/`namedParsed` gone (`rg "resolveKey\|positionalParsed\|namedParsed"` zero). -- [ ] `Simplify`/`UnionToIntersection` imported from a shared home; not redefined in `attribute-spec/types.ts`. -- [ ] `variadic` removed everywhere (`rg variadic packages/1-framework/2-authoring/psl-parser` zero). -- [ ] `optional` returns an `ArgType`; `OptionalParam`/`Param`-union collapsed; `sqlRelation` infers the same output type. -- [ ] `interpretRelationAttribute` renaming layer gone; downstream uses the spec output keys; relation suites green. -- [ ] `oneOf()` with zero args is a compile error. -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check`; after `pnpm --filter @internal/psl-parser build` (and `@internal/utils` build if you added an export there), workspace `pnpm typecheck`; `pnpm lint:deps` (T2 adds a dependency edge — verify it's clean). - -## Constraints -No `any`; no bare `as` (narrow `blindCast`/`castAs` with reason, or types that avoid it); no file-ext imports; no reexport outside `exports/`; tests-first for the reshaped surfaces. Explicit-staging commits (one per task or coherent group), no amend, **no push**. Read-only on `projects/**/reviews/**`, `spec.md`, plan files. Transient-ID scan on the `+` diff. Do NOT post to GitHub or resolve threads. - -## Operational metadata -- **Model tier:** thorough (core-engine reshape + cross-package consumer change). -- **Halt conditions:** T4 (optional-as-ArgType) can't preserve the inferred output types without `any`/broad casts; T5 downstream renaming fans out beyond the relation path; T2 reuse would force a layering violation; any task changes contract output (not just diagnostics). - -Return the structured report per § Return shape: per-task (T1–T6) results, the single-pass + optional-as-ArgType designs you landed, confirmation `sqlRelation` infers the same output, the CodeRabbit:89 disposition restated, and commit SHAs. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/09-prune-comments.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/09-prune-comments.md deleted file mode 100644 index 6f2d79c1a429..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/09-prune-comments.md +++ /dev/null @@ -1,51 +0,0 @@ -# Brief: D9 — prune added comments on PR #891 - -> On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. **Comment-deletion only — do not change any code, types, or behaviour.** Do NOT push or touch GitHub. - -## Task -Go through **every comment ADDED by this branch** and remove the ones that don't earn their place. Get the added comments with: -`git --no-pager diff origin/main...HEAD --diff-filter=d -- 'packages/**/*.ts'` (source + test `.ts` under `packages/**`). Work file-by-file through the added `//` and `/** */` comments. - -### Remove a comment if it does any of these (the operator's criteria): -1. **Narrates what a function/method body does** (step-by-step of the implementation). -2. **Restates information already obvious from the signature or type** (name, params, return type). -3. **Enumerates the usages** of an internal type/function. -4. **Refers to transient artifacts** — specs, Linear issues, review comments, dispatch/project docs, operator decisions. -5. **Refers to removed code or an overruled decision** — e.g. "the previous engine", "was X before", "no longer …". - -### Keep a comment ONLY if it answers **why** a specific line/function exists **and that why is not obvious**. -- If a comment is **mixed** (some narration + a real non-obvious why), **trim it to just the why** rather than deleting wholesale. -- When in doubt between keep and remove, **remove** — the operator wants a lean result. - -## Calibration (from this diff — apply the same judgment everywhere) - -**REMOVE (narration / restatement):** -- `str.ts`: "Parses a quoted string-literal argument into its decoded value." (restates `str(): ArgType`). -- `list.ts` JSDoc: "Lifts an element combinator over a […] array literal into T[], threading each element through `of` and enforcing the optional surface constraints. Element errors are collected and propagated; nonEmpty and unique add their own diagnostics…" (narrates the body). -- `interpret.ts` `interpretAttribute` JSDoc: the paragraph narrating the single-pass algorithm ("A positional argument binds to the next unconsumed slot… After the pass, optional defaults fill…") — body narration. -- `field-attribute.ts`: "Builds a field-level AttributeSpec. The output type is inferred from the positional and named parameters…" (restates signature). -- `types.ts` field docs that restate: "Human-readable label, for 'expected …' diagnostics", "Identifier of the source… stamped onto diagnostics", "The output key this slot writes into", "The declaring model; the resolution target for a self-scoped field reference" (restate the field name/type). -- `utils/src/types.ts`: "Flattens an intersection of mapped types into a single readable object type." / "Collapses a union into the intersection of its members." (WHAT, not WHY). - -**KEEP (non-obvious why):** -- `types.ts` `_out` field: "Phantom carrier for T; never read at runtime." (explains why an unused field exists). -- `types.ts` `InterpretCtx` "Deliberately lean: codec-lookup / default-function-registry handles are added only once a combinator needs them, so the kit does not pull those dependencies into the parser layer before they are used." (why the ctx is minimal). -- `types.ts` `diagnosticCode` field: "…so a combinator emits with the attribute's code rather than a hard-coded generic." (why the field is threaded). -- `types.ts` `InferAttr`: "The parameter is intentionally unconstrained: Out sits contravariantly in refine, so a constraint would reject every spec that uses a cross-argument refine." (non-obvious type why). -- `field-ref.ts`: the note that a cross-space referenced model is out of scope so existence is carried through unchecked (non-obvious why the miss is silent) — trim any body-narration around it, keep the why. - -**TRIM (mixed):** -- `interpret.ts` `duplicateDiagnostic` JSDoc: "…keeping each error path's span no coarser than the previous engine." — "the previous engine" is a removed-code reference (criterion 5). Keep the *why* the span anchoring differs (repeated-named → arg node; alias collision → whole attribute) only if non-obvious; drop the "previous engine" reference. If what remains is just narration, remove it. -- `one-of.ts` JSDoc: mostly narrates "tries each in order, first wins". Trim to the one non-obvious point (Result-pure leaves ⇒ a discarded branch leaves no stray diagnostics) if you judge it non-obvious; otherwise remove. - -## Constraints -- **Only delete/trim comments.** No code, type, signature, or behaviour change. A JSDoc that's the only thing above an export may be removed entirely — that's fine. -- Don't touch comments that existed before this branch (only ones ADDED by `origin/main...HEAD`). -- Explicit-staging commit(s) with sign-off (`git commit -s`), no push. Read-only on `projects/**`, `spec.md`, plan files (this task is about `packages/**` code comments, not the project docs). - -## Completed when -- [ ] Every added comment reviewed against the criteria; narration/restatement/enumeration/transient-ref/removed-ref comments removed; mixed ones trimmed to the why; only non-obvious why-comments remain. -- [ ] Gates green (proving no code was accidentally deleted): `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm --filter @internal/utils typecheck`. - -## Report -Return a per-file summary: which comments you REMOVED, which you TRIMMED (before→after gist), and which you KEPT with the one-line why each survived. Commit SHA(s). Flag any comment you were genuinely torn on for the orchestrator's call. Model tier: thorough (judgment per comment). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/10-unify-duplicate-check.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/10-unify-duplicate-check.md deleted file mode 100644 index 2302134f1ca0..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/10-unify-duplicate-check.md +++ /dev/null @@ -1,40 +0,0 @@ -# Brief: D10 — unify the engine's duplicate-argument check (PR #891 review) - -> On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. Operator review comments on `interpret.ts`. Do NOT push or touch GitHub. - -## Context -`packages/1-framework/2-authoring/psl-parser/src/attribute-spec/interpret.ts` — the single-pass `interpretAttribute`. It currently has **two separate `seen` checks** (one in the positional branch ~L53, one in the named branch ~L86) and tracks an `Origin` (`'positional' | 'named'`) so `duplicateDiagnostic` can vary its message/span by how each side was bound. - -Operator comments: -- **`interpret.ts:86`** — "Can't you check this once at the start of the loop instead of doing separate check for named and positional args? `const key = param.key ?? arg.name(); if (seen.has(key)) { … }`" -- **`interpret.ts:92`** — "Why does it matter if the argument is named?" (i.e. drop the named-vs-positional distinction in the duplicate handling). - -## Task -Refactor the loop so the duplicate check happens **once**, uniformly: -- Per iteration, resolve `(key, param)` in the two entry branches, keeping their necessary bailouts: - - **Positional** (`arg.name()` undefined): take `spec.positional[positionalSlot]`; if none, emit the "too many positional arguments" diagnostic (still once, flag-guarded) and `continue`; else `key = posParam.key`, `param = posParam.type`, advance `positionalSlot`. - - **Named**: look up `spec.named[name]`; if unknown, emit the "unknown argument" diagnostic and `continue`; else `key = name`, `param = that`. -- **Then a single** `if (seen.has(key))` → emit **one uniform** duplicate diagnostic **anchored to the current arg node** (`nodePslSpan(arg.syntax, ctx.sourceFile)`), and `continue`. Otherwise `seen.add(key)` and parse the value into `output[key]`. -- **Drop the `Origin` type**, make `seen` a `Set`, and **delete `duplicateDiagnostic`'s two-branch logic** — replace with one message (e.g. `Attribute "" received duplicate argument ""`). The named-vs-positional distinction goes away entirely (answers comment :92: it doesn't matter). - -## Parity / test impact (verify + update intentionally) -- **Repeated named key** (`name: "A", name: "B"`): still arg-node span, "duplicate argument" message — unchanged. -- **Positional+named alias collision** (e.g. `@relation("Foo", name: "Bar")`): was anchored to the **whole attribute** with a "both positionally and by name" message; now anchors to the **offending arg node** (narrower → within the "spans no-coarser" bar) with the uniform message (messages may change). Update any unit test / interpreter test / fixture assertion for this case intentionally, and confirm `@relation`'s positional-or-named `name` still works and a real conflict still produces exactly one diagnostic with code `PSL_INVALID_RELATION_ATTRIBUTE`. - -## Out of scope -- The `interpretRelationAttribute` wrapper (`psl-relation-resolution.ts:192`): its original complaint (renaming `map`→`constraintName`) was already removed in an earlier dispatch — it now returns `SqlRelationOutput` directly and only assembles the `InterpretCtx` + threads diagnostics. **Leave it** (it's necessary glue, not renaming). Do not remove it. -- Everything else (other attributes, Mongo, the rest of the kit). - -## Completed when -- [ ] One `seen` check in the loop; `Origin` gone; `seen` is a `Set`; `duplicateDiagnostic`'s branching removed (one uniform duplicate diagnostic anchored to the arg node). -- [ ] `rg "Origin\b" packages/1-framework/2-authoring/psl-parser/src/attribute-spec/interpret.ts` → zero (the removed type). -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check`; after `pnpm --filter @internal/psl-parser build`, workspace `pnpm typecheck`. - -## Constraints -No `any`; no bare `as` (keep the existing justified `blindCast` on the output object); no file-ext imports; tests-first for the changed behaviour. Explicit-staging commit with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Transient-ID scan on the `+` diff. Do NOT post to GitHub. - -## Operational metadata -- **Model tier:** mid (bounded engine refactor with test updates). -- **Halt conditions:** unifying the span breaks a case that can't stay within the "spans no-coarser" bar (surface it); a fixture's contract output (not just diagnostics) changes. - -Return the structured report: the unified-loop shape you landed, which test assertions changed (alias case), confirmation `@relation` still validates, and commit SHA. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/11-vocab-ratchet.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/11-vocab-ratchet.md deleted file mode 100644 index 36a78cf30584..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/11-vocab-ratchet.md +++ /dev/null @@ -1,35 +0,0 @@ -# Brief: D11 — satisfy the framework-vocabulary ratchet (post-rebase) - -> On the rebased slice-1 branch `tml-2956-typed-attribute-parsers`. Main's new PR #918 added `pnpm lint:framework-vocabulary` (`scripts/lint-framework-vocabulary.mjs` + `.config.json`), which forbids family terms from growing in `packages/1-framework`. Our new code trips it by **+3** (count 970 vs threshold 967) — all false positives. Fix honestly, minimize any threshold bump. Do NOT push or touch GitHub. - -## The 3 false-positive hits (all in `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/types.ts`) -1. Line ~5 — `import type { …, SymbolTable } from '../symbol-table'` → matches "table". -2. Line ~39 — `readonly symbols: SymbolTable;` on `InterpretCtx` → matches "table". -3. Line ~100 — doc comment "…a constraint would reject every spec…" → matches "constraint" (it means a TypeScript generic bound, not SQL CONSTRAINT). - -## Tasks -### T1 — Remove the dead `InterpretCtx.symbols` field -`InterpretCtx.symbols: SymbolTable` is **never read** anywhere in the attribute-spec kit (combinators resolve via `selfModel` / `resolveReferencedModel()`; verify with `rg "\\.symbols" packages/1-framework/2-authoring/psl-parser/src/attribute-spec`). Remove it: -- Delete the `symbols: SymbolTable` field from `InterpretCtx` in `types.ts`, and drop `SymbolTable` from that file's `../symbol-table` import (keep `FieldSymbol`, `ModelSymbol` — still used). This also tightens the "deliberately lean" ctx as its own doc claims. -- In `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`, `buildRelationInterpretCtx` sets `symbols: input.symbols` — remove that property from the returned ctx. Keep the `input.symbols` parameter (the `resolveReferencedModel` closure still captures it). Confirm no other `InterpretCtx` construction sets `symbols`. - -### T2 — Reword the "constraint" prose -In `types.ts`, the `InferAttr` doc comment uses "constraint" for a TS generic bound. Reword to a framework-neutral term (e.g. "an upper bound would reject" / "a type bound would reject") — behaviour-neutral, and honestly avoids an SQL-ish word in a framework package. - -### T3 — Re-run the ratchet; minimize any residual -Run `pnpm lint:framework-vocabulary`. After T1+T2 the count should drop by ~2–3. If it now **passes**, do nothing to the config. If a small **irreducible** residual remains (e.g. the `'../symbol-table'` module path still contributes one "table" hit that can't be removed without renaming a shared framework module — do NOT rename it), then lower the `threshold` in `scripts/lint-framework-vocabulary.config.json` by **exactly** that residual, and add a one-line justification in your report (the residual is a false positive on the framework-neutral `symbol-table` module path). Report the before/after threshold and the exact residual. Do not bump the threshold for anything you could have removed via T1/T2. - -## Completed when -- [ ] `InterpretCtx.symbols` removed (`rg "symbols" packages/1-framework/2-authoring/psl-parser/src/attribute-spec/types.ts` → zero); `buildRelationInterpretCtx` no longer sets it. -- [ ] "constraint" reworded in the `InferAttr` comment. -- [ ] `pnpm lint:framework-vocabulary` passes (with a minimal, justified threshold delta only if an irreducible residual remains). -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; after `pnpm --filter @internal/psl-parser build`, workspace `pnpm typecheck`. - -## Constraints -No `any`; no bare `as`; no file-ext imports; no behaviour change (removing a dead field + rewording a comment must not alter parsing). Explicit-staging commit(s) with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT rename the `symbol-table` module or any shared type. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** mid. -- **Halt conditions:** `ctx.symbols` turns out to be read somewhere (then it's not dead — surface it); the ratchet can't be satisfied without renaming a shared framework module or a threshold bump larger than the irreducible residual. - -Return: confirmation `ctx.symbols` was dead + removed, the reworded comment, the final `lint:framework-vocabulary` result (and any threshold delta with justification), all gate results, and commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/12-vocab-allowlist.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/12-vocab-allowlist.md deleted file mode 100644 index c076b553efd7..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/12-vocab-allowlist.md +++ /dev/null @@ -1,45 +0,0 @@ -# Brief: D12 — add a proper allowlist to the framework-vocabulary ratchet - -> On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. Operator-authorised change to main's PR #918 tooling. Replaces the earlier `threshold` bump (a blunt +1) with a real allowlist so framework-neutral compounds like `SymbolTable` stop being false-positive "table" hits. Do NOT push or touch GitHub. - -## Background -`scripts/lint-framework-vocabulary.mjs` counts distinct lines in `packages/1-framework` where a forbidden term's token sequence appears (tokenizer splits camelCase + non-alphanumerics, lowercases). `SymbolTable` and the module path `symbol-table` both tokenize to `['symbol','table']`, so they match the forbidden term `table` even though `SymbolTable` is a framework-neutral PSL parser type, not SQL-family vocabulary. Today the only knob is `threshold`; an earlier dispatch bumped it `967→968` to absorb one such false positive. The operator wants a proper allowlist instead. - -## Tasks - -### T1 — Add allowlist support to the script (backward-compatible) -In `scripts/lint-framework-vocabulary.mjs`, add support for an optional per-scope `allow: string[]` of framework-neutral **compound terms**. Semantics: -- An `allow` term tokenizes the same way as a forbidden term (reuse `termTokens`). -- On each line, compute the token ranges covered by any `allow` term occurrence (consecutive-subsequence match, same matcher as forbidden). -- A forbidden-term match counts **only if its matched token range is NOT fully contained within an allowed range**. A line counts if it has ≥1 such uncovered forbidden match. -- So `allow: ["SymbolTable"]` (tokens `['symbol','table']`) shields the `table` token wherever it is immediately preceded by `symbol` (covering both the `SymbolTable` identifier and the `symbol-table` module path), while a bare `table` (real SQL vocabulary) elsewhere still counts. -- Absent/empty `allow` ⇒ current behaviour exactly (backward-compatible). Keep all existing exports and their signatures; `findMatchingLines(content, scope)` already receives `scope`, so read `scope.allow` there. - -### T2 — Add the allowlist to the config + recompute the threshold -In `scripts/lint-framework-vocabulary.config.json`: -- Add `"allow": ["SymbolTable"]` to the `packages/1-framework` scope. (Only add entries that are genuine framework-neutral false positives; `SymbolTable` is the known one. Do not allow bare forbidden terms.) -- Run `node scripts/lint-framework-vocabulary.mjs --list`, get the new accurate `count` (it will drop — every `SymbolTable`/`symbol-table` line across framework, pre-existing + ours, is now shielded), and set `"threshold"` to that new count. This **replaces** the earlier `967→968` bump with the accurate lower number. Report the old and new threshold + how many lines the allowlist shielded. - -### T3 — Extend the ratchet's own test -`scripts/lint-framework-vocabulary.test.mjs` covers the script. Add a focused test for the allow behaviour: a line containing an allowed compound (`SymbolTable` / `symbol-table`) does NOT count, while a line with the bare forbidden term (`table`) still does. Keep existing tests passing. - -## Do NOT -- Do not revert the earlier honest cleanups (the removed dead `InterpretCtx.symbols` field stays removed; the "type bound" comment reword stays). The allowlist is about the ratchet's accuracy, not undoing those. -- Do not allow-list bare forbidden terms or anything that would mask genuine family-vocabulary leakage. -- Do not rename any shared module or type. - -## Completed when -- [ ] `scripts/lint-framework-vocabulary.mjs` supports `scope.allow` with the range-shielding semantics above; absent `allow` = unchanged behaviour. -- [ ] Config has `"allow": ["SymbolTable"]` and a `threshold` recomputed to the new accurate count. -- [ ] `node scripts/lint-framework-vocabulary.test.mjs` (however the repo runs it — check `package.json` / how #918 wired it; likely `node --test` or a vitest) passes, including the new allow test. -- [ ] `pnpm lint:framework-vocabulary` passes (count === threshold). -- [ ] Gates unaffected: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check`; workspace `pnpm typecheck` (after psl-parser build). - -## Constraints -No `any` in the script beyond its existing style (it's plain JS/ESM — match the file's conventions). Explicit-staging commit(s) with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** thorough (touches shared tooling + its test; correctness of the shielding logic matters). -- **Halt conditions:** the allow-shielding would mask a genuine forbidden-term line (i.e. `SymbolTable` allow accidentally hides a real `table` violation) — surface it; the ratchet's test harness can't be run/extended cleanly. - -Return: the shielding logic you implemented, the old→new threshold + shielded-line count, the new test, all gate results, and commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/13-comment-revision.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/13-comment-revision.md deleted file mode 100644 index 5b70e423eee2..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/13-comment-revision.md +++ /dev/null @@ -1,57 +0,0 @@ -# Brief: D13 — full revision of comments added in this PR - -> On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. Comment-only revision — change no code/behaviour. Do NOT push or touch GitHub. - -## Operator direction (verbatim intent) -The "why" comments kept so far are written as **JSDoc doc-comments** (above the function/type), often with **opaque jargon** and sometimes explaining *another* symbol's behaviour. Do a **full revision of every comment this PR added**: -1. A genuine, non-obvious **"why" belongs as a terse inline comment at the specific line that implements that behaviour** — not as a doc-header on the whole function/type. -2. **Plain language, no unexplained jargon.** Kill "pinned-only", "open form", "Result-pure", etc. — either explain the mechanism in ordinary words or drop it. -3. A comment must not document a *different* symbol's behaviour (e.g. `oneOf`'s rationale living on `identifier`). -4. Remaining doc-headers that merely narrate/restate get removed (as in the earlier pruning pass) — this pass additionally *relocates* the survivors inline. - -Operator's three explicit complaints (address these exactly): -- `identifier.ts` — "Wtf is pinned only? Wtf is open form? Why is doc for `oneOf` in the `identifier` method?" -- `field-ref.ts` — the function doc-comment "does not belong to a doc comment". -- `one-of.ts` — "wtf is result-pure?" - -## Per-file target relocations (apply this, then sweep the rest of the added comments the same way) - -**`combinators/identifier.ts`** — delete the header doc. The only non-obvious thing is the `const N` type param; put a short inline comment on the signature line, e.g.: -`// `const N` keeps each name's literal type, so `oneOf(identifier('a'), identifier('b'))` infers `'a' | 'b'`.` - -**`combinators/field-ref.ts`** — delete the header doc; delete the `FieldRefScope` doc (its purpose is obvious from `scope === 'self' ? ctx.selfModel : ctx.resolveReferencedModel()`). Put the real why **inline at the `if (model !== undefined && …)` existence check**, plain, e.g.: -`// A cross-space target can't be resolved here (resolveReferencedModel returns undefined); skip the existence check — it runs where the target model is known.` - -**`combinators/one-of.ts`** — delete the header doc. Put a plain inline comment at the fallthrough `return notOk([… aggregate …])` (and/or the loop) explaining the mechanism without "Result-pure", e.g.: -`// Each alternative returns its own diagnostics rather than writing to a shared list, so failed attempts leave nothing behind; if none match we report a single aggregate error.` - -**`attribute-spec/types.ts`** — this file is mostly type declarations; relocate the survivors and drop the rest: -- `ArgType` header ("Parsing is pure…"): drop the header; if worth keeping, a short inline note on the `parse(...)` line — "returns diagnostics rather than pushing to a shared list, so `oneOf` can discard a failed branch". Plain. -- `kind` field: reduce to a terse inline "discriminant for print/completion dispatch" or drop. -- `_out` field: keep terse inline "phantom carrier for `T`; never read at runtime" (it explains a genuinely puzzling unused field). -- `InterpretCtx` "Deliberately lean…" header: **drop** — it defends an absence (future fields), not a why about existing code. -- `diagnosticCode` field doc: move the why to **`interpret.ts`** at the `const leafCtx = { ...ctx, diagnosticCode: code }` line — "stamp the spec's code onto ctx so leaf diagnostics carry the attribute's code, not a generic one". Leave the field itself with at most a terse note. -- `OptionalArgType` "Because it extends ArgType…" header: reduce to a terse inline note on the `optional: true` marker, or drop (the engine's `'optional' in param` check makes it clear). -- `AttributeSpec.diagnosticCode` doc ("Defaults to…"): drop or reduce to terse; the default lives in `interpret.ts` (`spec.diagnosticCode ?? DEFAULT_STRUCTURAL_CODE`). -- `InferAttr` header (the contravariance why): this **is** a why about that exact line — move it inline onto the `export type InferAttr = …` line, condensed + plain, e.g.: - `// S is unconstrained on purpose: refine makes Out contravariant, so `S extends AttributeSpec` would reject every spec that uses refine.` - -**`combinators/diagnostic.ts`** — reduce `leafDiagnostic`'s doc to a terse note (or inline at the stamping line): it stamps `ctx.diagnosticCode` so every leaf diagnostic carries the attribute's code. - -**Sweep everything else** added by this PR (`git --no-pager diff origin/main...HEAD -- 'packages/**/*.ts'`, plus `psl-relation-resolution.ts`'s `relationInvariants` / `normalizeReferentialAction` notes): apply the same rule — inline terse why at the implementing line, plain language, or remove. - -## Constraints & gotchas -- **Comment-only.** No code, signature, or behaviour change. (Cast-reason strings inside `blindCast` are code arguments, not comments — leave them.) -- **Re-run the vocabulary ratchet.** The ratchet counts *file lines* containing forbidden tokens, so editing comments can change the count. After the revision run `pnpm lint:framework-vocabulary`; if `count` changed, set `threshold` in `scripts/lint-framework-vocabulary.config.json` to the new count (the `allow: ["SymbolTable"]` entry stays). Report the old→new threshold. -- Explicit-staging commit with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Completed when -- [ ] Every added comment reviewed; non-obvious whys are terse **inline** comments at their implementing line in plain language; no `pinned-only`/`open form`/`Result-pure`-style jargon remains; no comment documents another symbol's behaviour. -- [ ] `pnpm lint:framework-vocabulary` passes (threshold updated if the count moved). -- [ ] Gates (prove no code changed): `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check`; workspace `pnpm typecheck` after psl-parser build. - -## Operational metadata -- **Model tier:** thorough (judgment per comment + placement). -- **Halt conditions:** a relocation would require a code change to make sense (surface it); the ratchet can't be satisfied by a threshold update matching the new count. - -Return a per-file summary: what moved inline (before doc → after inline, with the line it now sits on), what was dropped, the ratchet old→new threshold, gate results, and commit SHA. diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/14-unite-diagnostic-code.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/14-unite-diagnostic-code.md deleted file mode 100644 index 1f4a63c4b4a8..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/dispatches/14-unite-diagnostic-code.md +++ /dev/null @@ -1,38 +0,0 @@ -# Brief: D14 — unite on a single diagnostic code + purge not-taken-alternative comments - -> On the slice-1 PR branch `tml-2956-typed-attribute-parsers`. Operator review batch. Do NOT push or touch GitHub. - -## Part A — comment purge (comment-only) -The operator wants comments that explain by **contrasting with an alternative we never wrote** removed, plus two specific ones: -- **`combinators/identifier.ts`** — remove the `const N` comment ("just explains how TS works"). -- **`combinators/one-of.ts`** — remove the inline comment on the fallthrough `return notOk(...)` ("doesn't add value on top of the code"; it also uses the "shared list" contrast). -- **Sweep every remaining comment** added by this PR (`git --no-pager diff origin/main...HEAD -- 'packages/**/*.ts'`) that explains via a not-taken alternative — phrases like "rather than pushing to a shared list", "instead of writing to a shared list/sink", "rather than a hard-coded generic". Remove them. In particular check `attribute-spec/types.ts` (the `ArgType.parse` note) and `combinators/diagnostic.ts` / `interpret.ts`. - -## Part B — unite on a single diagnostic code (code change; operator-directed) -The operator asks: "Why does `diagnosticCode` need to be customizable? Can't we just unite on a single `PSL_INVALID_ATTRIBUTE`?" The per-attribute `diagnosticCode` exists only to preserve legacy codes-parity for `@relation` (`PSL_INVALID_RELATION_ATTRIBUTE`). Remove the customization; use **one constant code** for all attribute-spec structural + leaf diagnostics. - -- Pick the single code: check `PslDiagnosticCode` (in `@internal/framework-components/psl-ast`) for the best existing generic — prefer one literally meaning "invalid attribute". If a `PSL_INVALID_ATTRIBUTE` exists, use it; otherwise use the current default `PSL_INVALID_ATTRIBUTE_SYNTAX` uniformly. Note which you chose and why. -- **Remove `AttributeSpec.diagnosticCode`** (types.ts) and its usage; **remove `InterpretCtx.diagnosticCode`** and the `leafCtx = { ...ctx, diagnosticCode: code }` threading in `interpret.ts` — the engine emits structural diagnostics with the single constant directly, and passes `ctx` (unchanged) to leaves. -- **Leaves** (`leafDiagnostic` in `combinators/diagnostic.ts`) stamp the single constant instead of `ctx.diagnosticCode`. If `leafDiagnostic` no longer needs anything attribute-specific from ctx for the code, simplify accordingly. -- **SQL `sqlRelation`** (`psl-relation-resolution.ts`): remove `diagnosticCode: 'PSL_INVALID_RELATION_ATTRIBUTE'` from the spec; `relationInvariants` (the both-or-neither refine) currently hard-codes `PSL_INVALID_RELATION_ATTRIBUTE` — change it to the single unified code; `buildRelationInterpretCtx` — remove the `diagnosticCode` property. -- **Tests/fixtures:** any assertion expecting `PSL_INVALID_RELATION_ATTRIBUTE` for a `@relation` error path now expects the unified code. Update them intentionally (this is authorised — it further relaxes the codes-parity bar). Report the count of changed assertions and whether any fixture changed (contract output must NOT change — only diagnostic codes in tests). - -## Parity note -This changes `@relation` error **codes** from `PSL_INVALID_RELATION_ATTRIBUTE` to the single generic code. Contract output is unaffected. `pnpm fixtures:check` must stay clean (fixtures are valid schemas; they don't exercise these error paths). If any fixture *does* change, halt and surface. - -## Vocabulary ratchet -Removing the `diagnosticCode` field/comments involves no forbidden vocabulary, so the count likely stays 906. Re-run `pnpm lint:framework-vocabulary`; if the count moved, update `threshold` to the new count (keep `allow: ["SymbolTable"]`). - -## Completed when -- [ ] Part A: the two named comments removed; no "rather than/instead of … shared list/sink" or "rather than a hard-coded generic" comment remains (`rg -n "shared list|shared sink|hard-coded generic|rather than pushing" packages/1-framework/2-authoring/psl-parser` → zero). -- [ ] Part B: `AttributeSpec.diagnosticCode` and `InterpretCtx.diagnosticCode` gone (`rg -n "diagnosticCode" packages/1-framework/2-authoring/psl-parser/src/attribute-spec packages/2-sql/2-authoring/contract-psl/src` → zero); all attribute-spec diagnostics use one constant code; `sqlRelation` no longer sets a per-attribute code. -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm --filter @internal/sql-contract-psl test`; `pnpm fixtures:check` (clean); `pnpm lint:framework-vocabulary`; workspace `pnpm typecheck` after psl-parser build. - -## Constraints -No `any`; no bare `as`; no file-ext imports; tests-first where behaviour (the code emitted) changes. Explicit-staging commit(s) with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** thorough (engine + SQL-spec + test change with a diagnostic-code semantics shift). -- **Halt conditions:** removing the per-attribute code breaks a NON-`@relation` consumer that depends on a specific code (surface it); a fixture's contract output changes; no single generic code fits cleanly (surface the `PslDiagnosticCode` options). - -Return: the single code you chose (+ why), confirmation `diagnosticCode` is fully gone, the count of updated test assertions, ratchet result, all gate results, and commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/plan.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/plan.md deleted file mode 100644 index 516bd3ad6b37..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/plan.md +++ /dev/null @@ -1,31 +0,0 @@ -# Slice: attribute-spec-kit — Dispatch plan - -**Slice spec:** `projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md` - -Sandwich shape: engine → combinators → consumer migration. 3 dispatches, sequential. - -### Dispatch 1: Engine + core types - -- **Outcome:** `psl-parser` exports `ArgType`, `AttributeSpec`, `Param`/`optional`, `fieldAttribute`, `interpretAttribute`, `InferAttr`, and `InterpretCtx`. The engine parses positional + named arguments (including the positional-or-named alias) into a flat typed object, runs the optional `refine`, and returns `Result, Diagnostic[]>` — proven against a trivial in-test stub `ArgType` and `InferAttr` type-level tests. No domain combinators yet. -- **Builds on:** The spec's chosen design; the `ExpressionAst` exports. -- **Hands to:** The `ArgType` contract (`parse(arg: ExpressionAst, ctx) → Result`) + the engine, so dispatch 2 can author real combinators against a stable interface. -- **Focus:** Engine + types only. Message-templating machinery is included only if Open Question 1 resolves to "strict message parity." -- **Gate:** `cd packages/1-framework/2-authoring/psl-parser && pnpm typecheck && pnpm test`; `pnpm --filter @internal/psl-parser lint`. - -### Dispatch 2: The `@relation` combinators - -- **Outcome:** `str`, `enumOf(...values)`, `fieldRef(scope)`, and `list(of, { nonEmpty })` exist as `ArgType`s over `ExpressionAst`, each with unit tests covering parse success + each diagnostic path. `fieldRef` carries its scope (`'self'` / `'referenced'`) and resolves against `InterpretCtx`. -- **Builds on:** Dispatch 1's `ArgType` contract + engine. -- **Hands to:** The combinator set sufficient to express `sqlRelation`. -- **Focus:** Only the four combinators `@relation` needs. The rest of ADR 231's alphabet is out (slices 2–3). -- **Gate:** psl-parser typecheck + test + lint. - -### Dispatch 3: Migrate SQL `@relation`; delete the legacy parser - -- **Outcome:** `sqlRelation` spec defined; the `@relation` call sites in `packages/2-sql/.../interpreter.ts` + `psl-relation-resolution.ts` route through `interpretAttribute` with an assembled `InterpretCtx`; `parseRelationAttribute` (and helpers it alone used) deleted; diagnostic codes + spans byte-identical (message-text per Open Question 1). -- **Builds on:** Dispatch 2's combinator set. -- **Hands to:** SQL `@relation` validated via spec; legacy parser gone — the migration recipe slices 2–3 follow. -- **Focus:** `@relation` only. Other SQL attributes stay on their legacy paths (slice 2). -- **Gate:** `pnpm --filter @internal/contract-psl-sql test` (relations + diagnostics suites); `pnpm fixtures:check`; `rg "parseRelationAttribute"` empty; workspace `pnpm typecheck` after `psl-parser` build (cross-package consumer check). - -_(Final `hands to` ⊇ slice-DoD: legacy parser removed (D3), kit exported + tested (D1–D2), parity gates green (D3). Complete.)_ diff --git a/projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md b/projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md deleted file mode 100644 index a959ad46c552..000000000000 --- a/projects/typed-attribute-parsers/slices/attribute-spec-kit/spec.md +++ /dev/null @@ -1,83 +0,0 @@ -# Slice: attribute-spec-kit - -_(In-project slice. Parent: `projects/typed-attribute-parsers/`. Outcome it contributes: stands up the declarative-attribute engine the whole project builds on, proven by one real attribute.)_ - -## At a glance - -Build the combinator kit + `interpretAttribute` + `InferAttr` + `InterpretCtx` in `psl-parser`, and migrate the SQL family's `@relation` from the hand-written `parseRelationAttribute` (in `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts`) to a declarative `AttributeSpec` lowered through `interpretAttribute`. After this slice, `@relation` (SQL) is validated and lowered via a spec, and the engine + the combinators `@relation` needs exist for slices 2–3 to consume. - -## Chosen design - -The engine consumes the parser's `ExpressionAst` CST directly (already exported from `psl-parser`). An `ArgType` parses one argument; an `AttributeSpec` lists positional params + named params + an optional `refine`; `interpretAttribute(attrNode, spec, ctx)` returns `Result, Diagnostic[]>`. - -The SQL `@relation` spec replaces `parseRelationAttribute`: - -```ts -const sqlRelation = fieldAttribute('relation', { - positional: [{ key: 'name', type: optional(str()) }], // positional-or-named alias for `name` - named: { - name: optional(str()), - fields: optional(list(fieldRef('self'), { nonEmpty: true })), - references: optional(list(fieldRef('referenced'), { nonEmpty: true })), - map: optional(str()), - onDelete: optional(enumOf('NoAction', 'Restrict', 'Cascade', 'SetNull', 'SetDefault')), - onUpdate: optional(enumOf('NoAction', 'Restrict', 'Cascade', 'SetNull', 'SetDefault')), - }, - refine: relationInvariants, // fields/references both-or-neither; positional/named name conflict -}); -``` - -`interpretAttribute(relationNode, sqlRelation, ctx)` yields the same shape `ParsedRelationAttribute` carries today (`{ relationName?, fields?, references?, constraintName?, onDelete?, onUpdate? }`), mapped at the call site. `InterpretCtx` is assembled from data the SQL interpreter already holds (symbol table, declaring model, referenced-model resolver, declaring field, source id). - -**Minimal kit, grown by consumers.** Slice 1 ships the engine plus only the combinators `@relation` needs — `str`, `enumOf`, `fieldRef(scope)`, `list({ nonEmpty })`, `optional`, `fieldAttribute`. The rest of ADR 231's alphabet (`int`, `bool`, `json`, `map`, `record`, `entityRef`, `codecRef`, `oneOf`, `funcCall`/`funcCallFrom`, `modelAttribute`, `blockAttribute`) is added by slices 2–3 as the attributes they migrate require it. This keeps slice 1 reviewable. - -## Coherence rationale - -One reviewer holds it in one sitting: a new authoring surface (`psl-parser` kit) plus its first consumer (`@relation`), reviewed together so the engine is judged against a real attribute rather than in the abstract. The legacy `parseRelationAttribute` is deleted in the same PR, so there is never a second live validation path for `@relation`. - -## Scope - -**In:** -- `psl-parser`: `ArgType`, `AttributeSpec`, `Param`/`optional`, `fieldAttribute`, `interpretAttribute`, `InferAttr`, `InterpretCtx`, and the combinators `str`, `enumOf`, `fieldRef`, `list` — with unit + type-level tests; exported from `psl-parser`'s public surface. -- `packages/2-sql/2-authoring/contract-psl`: `sqlRelation` spec; route the `@relation` call sites in `interpreter.ts` / `psl-relation-resolution.ts` through `interpretAttribute`; assemble `InterpretCtx`; delete `parseRelationAttribute` (and any now-dead helpers it alone used). - -**Out:** -- All other SQL attributes (`@id`, `@unique`, `@@index`, `@default`, `@map`, `@@control`, `@@discriminator`, `@@base`) — slice 2. -- All Mongo attributes — slice 3. -- The unused-by-`@relation` combinators (`int`, `bool`, `json`, `map`, `record`, `entityRef`, `codecRef`, `oneOf`, `funcCall`, `modelAttribute`, `blockAttribute`). -- Language-server consumers; `@db.*`; the TS builder surface. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| Positional + named `name` both present and disagreeing | Must preserve | Existing code emits `PSL_INVALID_RELATION_ATTRIBUTE` "conflicting positional and named relation names"; reproduce via `refine` or the alias merge. | -| `fields` without `references` (or vice-versa) | Must preserve | Existing both-or-neither check, code `PSL_INVALID_RELATION_ATTRIBUTE`; lives in `refine`. | -| Unknown named argument (e.g. `@relation(foo: 1)`) | Must preserve | Existing code rejects with `PSL_INVALID_RELATION_ATTRIBUTE`; the engine's named-map closedness must reject it (see Open Question on message text). | -| `onDelete`/`onUpdate` value not in the action set | Must preserve code | Today `PSL_UNSUPPORTED_REFERENTIAL_ACTION` is raised *downstream* by `normalizeReferentialAction`, not at parse; decide whether `enumOf` raises at parse with the same code or the value passes through to the existing normaliser. | - -## Slice-specific done conditions - -- [ ] `rg "parseRelationAttribute"` returns zero results outside its deleted definition. -- [ ] `pnpm fixtures:check` clean and the SQL interpreter relations suites pass (`interpreter.relations.test.ts`, `interpreter.relations.many-to-many.test.ts`, `interpreter.diagnostics.test.ts`). -- [ ] Diagnostic **codes and spans** for every `@relation` error path are byte-identical to pre-slice behaviour. Message text may change to the kit's phrasing (see Resolved decision 1) but must stay clear and actionable; updated test assertions are reviewed as intentional. - -## Resolved decisions - -1. **Diagnostic-message parity — codes + spans only (operator-authorised, 2026-06-29).** Codes + spans are the hard parity gate. Cross-argument messages emitted from hand-written `refine` (both-or-neither, name-conflict) should stay close to the existing text, but generic-combinator-emitted messages (unknown-arg, malformed-list, bad-enum-value) may adopt the kit's phrasing as long as they remain clear. Affected interpreter-test message assertions are updated as intentional, reviewer-approved changes. The engine does **not** need subject-label message-templating in slice 1. This relaxes the project's original "identical messages" cross-cutting requirement, which has been amended accordingly. - -## Resolved decisions (cont.) - -2. **`onDelete`/`onUpdate` keep `normalizeReferentialAction` as validator (resolved at D3).** `onDelete: Cascade` is a **bare identifier**, and legacy validates the action set *downstream* via `normalizeReferentialAction`, emitting `PSL_UNSUPPORTED_REFERENTIAL_ACTION`. Using `enumOf` (set-validation at parse) would change that code and break the codes-parity bar. So the `sqlRelation` spec parses `onDelete`/`onUpdate` to the **raw identifier name** (a bare-identifier leaf, no parse-time set check) and routes the value to the existing `normalizeReferentialAction` unchanged — exact code/span/message parity. `enumOf` is NOT used for `@relation` actions (it remains for slices 2–3). D3 adds the small bare-identifier-name leaf if one isn't already present. - -## Open Questions - -None — all resolved. - -## References - -- Parent project: `projects/typed-attribute-parsers/spec.md` -- Linear issue: [TML-2956](https://linear.app/prisma-company/issue/TML-2956) -- ADR 231 — `docs/architecture docs/adrs/ADR 231 - Declarative attribute specifications.md` -- Legacy parser being replaced: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (`parseRelationAttribute`, `normalizeReferentialAction`) -- Engine input type: `ExpressionAst` and friends, exported from `packages/1-framework/2-authoring/psl-parser/src/exports/syntax.ts` diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/01-mongo-wiring-map.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/01-mongo-wiring-map.md deleted file mode 100644 index 962965d1bb38..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/01-mongo-wiring-map.md +++ /dev/null @@ -1,102 +0,0 @@ -# Brief: D1 — Mongo `InterpretCtx` wiring + `@map`/`@@map` migration - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes` (off `origin/main`). Do NOT push or touch GitHub. ONE signed commit. Tests-first. - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks the run.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files/line-ranges with the file reader is fine. If genuinely under-specified, STOP and report "brief under-specified: ". - -## Why -The Mongo family still parses every attribute imperatively off `ResolvedAttribute` + string helpers; it does not use the declarative kit at all. This dispatch lands the Mongo-side kit wiring (a `mongo-attribute-specs.ts` mirroring the SQL family's) and migrates the simplest attribute — `@map`/`@@map` — end-to-end through `interpretAttribute`, proving the seam. No new kit combinators are needed (`str`, `fieldAttribute`, `modelAttribute`, `interpretAttribute` already exist). No `packages/1-framework` or `packages/2-sql` changes. - -## The pattern to mirror -The SQL family's wiring lives in `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` (lines ~36-146): `findModelAttributeNode`, `findFieldAttributeNode`, `buildModelInterpretCtx`, `buildFieldInterpretCtx`, `interpretModelAttribute`, `interpretFieldAttribute`, and the `mapModelSpec`/`mapFieldSpec` constants. **Read that file first** — you will reproduce those functions in the Mongo package (they are family-agnostic: they take `ModelSymbol`/`FieldSymbol`/`SourceFile`/`sourceId`/`diagnostics`). Do NOT import them from `@prisma-next/sql-contract-psl` — that is a forbidden cross-family dependency; copy the wiring into the Mongo package. - -## Step 1 — new file `packages/2-mongo-family/2-authoring/contract-psl/src/mongo-attribute-specs.ts` -Reproduce, adapted to this package, from the SQL template: -- `findModelAttributeNode(model, name)` and `findFieldAttributeNode(field, name)` (iterate `model.node.attributes()` / `field.node.attributes()`, return the AST node whose `.name()?.isSimpleName(name) === true`). -- `buildModelInterpretCtx({ selfModel, sourceFile, sourceId })` → `InterpretCtx` (level `'model'`, `resolveReferencedModel: () => undefined`). -- `buildFieldInterpretCtx({ selfModel, field, sourceFile, sourceId, resolveReferencedModel? })` → `InterpretCtx` (level `'field'`). -- `interpretModelAttribute({ node, spec, model, sourceFile, sourceId, diagnostics })` and `interpretFieldAttribute({ node, spec, model, field, sourceFile, sourceId, diagnostics, resolveReferencedModel? })` — call `interpretAttribute(node, spec, ctx)`, drain `result.failure` into `diagnostics` and return `undefined` on failure, else `result.value`. -- The two map specs: -```ts -export const mapModelSpec = modelAttribute('map', { positional: [{ key: 'name', type: str() }] }); -export const mapFieldSpec = fieldAttribute('map', { positional: [{ key: 'name', type: str() }] }); -``` -Imports: from `@prisma-next/psl-parser` — `fieldAttribute, modelAttribute, str, interpretAttribute` (values) and `type { AttributeSpec, FieldSymbol, InterpretCtx, ModelSymbol }`; from `@prisma-next/psl-parser/syntax` — `type { FieldAttributeAst, ModelAttributeAst, SourceFile }`; from `@prisma-next/config/config-types` — `type { ContractSourceDiagnostic }`. (Match the exact import specifiers the SQL file uses.) - -## Step 2 — migrate the three `getMapName` sites in `interpreter.ts` -(a) **Field `@map`** — `resolveFieldMappings` (currently ~L132-139). Change its signature to take the interpret context and use the spec: -```ts -function resolveFieldMappings(input: { - readonly model: ModelSymbol; - readonly sourceFile: SourceFile; - readonly sourceId: string; - readonly diagnostics: ContractSourceDiagnostic[]; -}): FieldMappings { - const { model, sourceFile, sourceId, diagnostics } = input; - const pslNameToMapped = new Map(); - for (const field of Object.values(model.fields)) { - const mapNode = findFieldAttributeNode(field, 'map'); - const mapped = - (mapNode - ? interpretFieldAttribute({ node: mapNode, spec: mapFieldSpec, model, field, sourceFile, sourceId, diagnostics })?.name - : undefined) ?? field.name; - pslNameToMapped.set(field.name, mapped); - } - return { pslNameToMapped }; -} -``` -(b) **Collection `@@map`** — `resolveCollectionName` (currently ~L141-143): -```ts -function resolveCollectionName(input: { - readonly model: ModelSymbol; - readonly sourceFile: SourceFile; - readonly sourceId: string; - readonly diagnostics: ContractSourceDiagnostic[]; -}): string { - const { model, sourceFile, sourceId, diagnostics } = input; - const mapNode = findModelAttributeNode(model, 'map'); - const name = mapNode - ? interpretModelAttribute({ node: mapNode, spec: mapModelSpec, model, sourceFile, sourceId, diagnostics })?.name - : undefined; - return name ?? lowerFirst(model.name); -} -``` -(c) **Variant presence check** — currently `interpreter.ts` ~L343: `const hasExplicitMap = getMapName(variantModelView.attributes) !== undefined;`. `variantModelView` is a resolved model view (not a `ModelSymbol` with `.node`), so keep this a presence check via the surviving `getAttribute` helper: -```ts -const hasExplicitMap = getAttribute(variantModelView.attributes, 'map') !== undefined; -``` -(For a well-formed `@@map("x")` both forms are equivalent; a valueless `@@map()` is invalid PSL upstream.) - -## Step 3 — thread context into the callers -The two call sites are `interpreter.ts` ~L1017-1018: -```ts -const collectionName = resolveCollectionName(pslModel); -const fieldMappings = resolveFieldMappings(pslModel); -``` -Read the enclosing function (`interpretPslDocumentToMongoContract`, ~L952 onward) to find the in-scope `sourceFile`, `sourceId`, and the `diagnostics` accumulator (they exist — the entry emits diagnostics with `sourceId`/`sourceFile` already). Pass them: -```ts -const collectionName = resolveCollectionName({ model: pslModel, sourceFile, sourceId, diagnostics }); -const fieldMappings = resolveFieldMappings({ model: pslModel, sourceFile, sourceId, diagnostics }); -``` -If `resolveFieldMappings`/`resolveCollectionName` are called from any other site, thread context there too (grep in the terminal to confirm the call sites). - -## Step 4 — retire `getMapName` -Delete `getMapName` from `packages/2-mongo-family/2-authoring/contract-psl/src/psl-helpers.ts` and remove it from the `interpreter.ts` import list. **Keep** `getAttribute`, `stripQuotes`, `lowerFirst`, and the other helpers (still used by not-yet-migrated attributes). Confirm with `rg -n "getMapName" packages/2-mongo-family` → zero after the edit. - -## Tests -`@map`/`@@map` behaviour must be **byte-identical** — the existing Mongo contract-psl suite + `fixtures:check` are the primary signal (a real `str()`-decoded map name equals the old `stripQuotes` value). Run them. If the suite has no direct coverage of a mapped field/collection name, add one concise case (a model with `@@map("things")` and a field with `@map("_id")`) asserting the emitted `storage.collection` and the mapped field name. Tests-first for any added case. - -## Scope -**In:** the new `mongo-attribute-specs.ts` wiring + map specs; the three `interpreter.ts` migration sites + caller threading; deleting `getMapName`. **Out:** every other Mongo attribute (later dispatches); any `packages/1-framework` or `packages/2-sql` change; the interpreter's semantic checks. - -## Constraints -No `any`; no bare `as` (a narrow justified `blindCast` is acceptable only if the SQL template itself uses one at the same spot — mirror it exactly, no wider); no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/mongo-contract-psl build && typecheck && test` (confirm the exact package name from `packages/2-mongo-family/2-authoring/contract-psl/package.json`; use that in the filter) -2. `pnpm fixtures:check` — clean, no Mongo contract drift -3. `pnpm lint:deps` (0 — catches any forbidden cross-family import) and `pnpm lint:framework-vocabulary` (threshold unchanged; the edits are in `packages/2-mongo-family`, not `1-framework`, so it should not move) - -## Report back -The `mongo-attribute-specs.ts` exports; the three migration sites + how you threaded `sourceFile`/`sourceId`/`diagnostics` (and the exact local names you found in the entry); confirmation `getMapName` is gone (`rg` → zero) and `getAttribute`/`stripQuotes` retained; whether you added a map test or relied on existing coverage; all gate results; the commit SHA. If the SQL template uses an import specifier or a `blindCast` you can't cleanly mirror, or a caller can't reach `diagnostics`, STOP and report rather than guessing. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/02-mongo-relation.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/02-mongo-relation.md deleted file mode 100644 index aa8d3088ca46..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/02-mongo-relation.md +++ /dev/null @@ -1,76 +0,0 @@ -# Brief: D2 — migrate Mongo `@relation` to a spec - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes`. Do NOT push or touch GitHub. ONE signed commit. Tests-first. Builds on D1 (the Mongo `mongo-attribute-specs.ts` wiring already exists). - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks the run.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files/line-ranges is fine. If under-specified, STOP and report. - -## Why -Mongo `@relation` is parsed by the hand-written `parseRelationAttribute` (string extraction of `name`/`fields`/`references`). Replace it with a declarative spec through `interpretAttribute`, reusing D1's `mongo-attribute-specs.ts` wiring. The SQL family already did this — its spec is the template. - -## The template -`packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (lines ~99-126) defines `sqlRelation`. Read it. The Mongo spec is a **subset**: Mongo `@relation` carries only `name` (alias) + `fields` + `references` — **no `map`, no `onDelete`/`onUpdate`, and no `refine`** (see the behaviour note below). Also note (from `combinators/field-ref.ts`): `fieldRef(scope)` returns `ArgType` — it yields the field-name string (with an existence check against the scoped model), so `list(fieldRef('self'))` produces `string[]`, matching the old parser's output. - -## Step 1 — add the Mongo relation spec to `mongo-attribute-specs.ts` -```ts -export const relationFieldSpec = fieldAttribute('relation', { - positional: [{ key: 'name', type: optional(str()) }], - named: { - name: optional(str()), - fields: optional(list(fieldRef('self'), { nonEmpty: true, unique: true })), - references: optional(list(fieldRef('referenced'), { nonEmpty: true, unique: true })), - }, -}); -export type RelationFieldOutput = InferAttr; -``` -Add the needed imports to `mongo-attribute-specs.ts`: `fieldRef, list, optional` (values) and `type { InferAttr }` from `@prisma-next/psl-parser` (mirror the SQL file's specifiers). Output shape: `{ name?: string; fields?: string[]; references?: string[] }`. - -**No `refine`.** SQL's `relationInvariants` errors when `fields` XOR `references` is present; Mongo must NOT adopt that — the Mongo interpreter (below) treats "not both `fields` and `references`" as a **backrelation candidate**, not an error. Adding the refine would change Mongo behaviour. Omit it. - -## Step 2 — migrate the consumption in `interpreter.ts` (~L1088-1133) -The single call site is `const relation = parseRelationAttribute(field.attributes);` (~L1090), inside `for (const field of Object.values(pslModel.fields))` guarded by `isRelationField(field, modelNames)`. Replace with: -```ts -const relationNode = findFieldAttributeNode(field, 'relation'); -const relation = relationNode - ? interpretFieldAttribute({ - node: relationNode, - spec: relationFieldSpec, - model: pslModel, - field, - sourceFile, - sourceId, - diagnostics, - resolveReferencedModel: () => allModels.find((m) => m.name === field.typeName), - }) - : undefined; -``` -Add `relationFieldSpec` (and `findFieldAttributeNode`, `interpretFieldAttribute` if not already imported) to the `import { … } from './mongo-attribute-specs'` line. `allModels`, `sourceFile`, `sourceId`, `diagnostics` are all in scope here (confirm by reading ~L1016-1090). - -Then update the two reads of the old `relationName` key — the spec output uses `name`, not `relationName`. The **output** key stays `relationName`: -- `...ifDefined('relationName', relation?.relationName)` → `...ifDefined('relationName', relation?.name)` (the backrelation-candidate push, ~L1097) -- `...ifDefined('relationName', relation.relationName)` → `...ifDefined('relationName', relation.name)` (the FK-relation push, ~L1128) - -`relation?.fields` and `relation?.references` are `string[]` exactly as before — the `.map((f) => fieldMappings.pslNameToMapped.get(f) ?? f)` lines are unchanged, as is the `if (field.list || !(relation?.fields && relation?.references))` backrelation branch. - -## Behaviour note — new field-existence validation -The old `parseRelationAttribute` did no validation; `fieldRef('self')`/`fieldRef('referenced')` now check that each named field exists on the self / referenced model (the referenced check is skipped when `resolveReferencedModel()` returns `undefined`, e.g. a cross-space target). For **valid** schemas this is byte-identical (the names resolve and the same `string[]` comes out). For a schema naming a non-existent relation field, a `PSL_INVALID_ATTRIBUTE_SYNTAX` now fires at parse time. Run the suite: if an existing test asserted a different code/behaviour for a bad relation field ref, update it per operator "Option A" (shape/existence → `PSL_INVALID_ATTRIBUTE_SYNTAX`) and note it; if no such test exists, rely on the green suite + `fixtures:check`. - -## Step 3 — retire the dead parser -Delete `parseRelationAttribute` and the `ParsedRelationAttribute` interface from `psl-helpers.ts`, and remove `parseRelationAttribute` from the `interpreter.ts` import list. Then `rg` in the terminal for the now-possibly-unused helpers: **`parseRelationAttribute`** must be zero. `stripQuotes` was used only by the (already-deleted) `getMapName` and by `parseRelationAttribute` — if `rg -n "stripQuotes" packages/2-mongo-family` is now zero outside its own definition, delete `stripQuotes` too. **Keep `parseFieldList`** (still used by `parseIndexFieldList`, migrated in a later dispatch) and `getAttribute`/`lowerFirst`/`getPositionalArgument`/`getNamedArgument`. - -## Tests -`@relation` lowering must stay byte-identical for valid schemas — the Mongo contract-psl suite + `fixtures:check` are the primary signal. If the suite lacks a direct FK-relation case (a model with `@relation(fields: [x], references: [y])` producing `relations[...]` with `on.localFields`/`on.targetFields`) or a named-relation/backrelation case, add one concise case. Tests-first for anything added. - -## Scope -**In:** the Mongo `relationFieldSpec`; the `interpreter.ts` relation call-site migration + the `relationName`→`name` read changes; deleting `parseRelationAttribute`/`ParsedRelationAttribute` (+ `stripQuotes` if now dead). **Out:** every other Mongo attribute; `packages/1-framework` / `packages/2-sql` changes; the interpreter's relation semantics (backrelation matching, FK indexing) — only the argument parse changes. - -## Constraints -No `any`; no bare `as` (a narrow justified `blindCast` only if the SQL template uses one at the same spot); no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/mongo-contract-psl build && typecheck && test` (use the exact package name from its package.json) -2. `pnpm fixtures:check` — clean, no Mongo contract drift -3. `pnpm lint:deps` (0) and `pnpm lint:framework-vocabulary` (threshold unchanged) - -## Report back -The `relationFieldSpec` shape + confirmation no `refine` was added; the migration site + `relationName`→`name` read changes; whether any field-existence-validation test shifted (and how); `parseRelationAttribute` gone (`rg` → zero) and whether `stripQuotes` was also removed; the test path (added vs relied-on); all gate results; the commit SHA. If the SQL template diverges from this brief, or `stripQuotes` turns out still-used, or a gate goes red you can't resolve from the brief, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/03-mongo-polymorphism.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/03-mongo-polymorphism.md deleted file mode 100644 index 1d3378111387..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/03-mongo-polymorphism.md +++ /dev/null @@ -1,93 +0,0 @@ -# Brief: D3 — migrate Mongo `@@discriminator` / `@@base` to specs - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes`. Do NOT push or touch GitHub. ONE signed commit. Tests-first. Builds on D1 (the `mongo-attribute-specs.ts` wiring exists) and D2 (already merged into the branch). - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files/line-ranges is fine. If under-specified, STOP and report. - -## Why -Mongo `@@discriminator` / `@@base` argument shapes are parsed imperatively in `collectPolymorphismDeclarations` (`interpreter.ts`) via `getPositionalArgument` + `parseQuotedStringLiteral`. Migrate the **argument parsing** to specs through `interpretAttribute`. The polymorphism cross-model semantics (`resolvePolymorphism`) are untouched — only the per-attribute argument parse changes. - -## The templates -SQL's `sql-attribute-specs.ts` already defines `discriminatorModelSpec` and `baseModelSpec` (near the bottom of the file). Read them. They are exactly what Mongo needs: -```ts -export const discriminatorModelSpec = modelAttribute('discriminator', { - positional: [{ key: 'field', type: fieldRef('self') }], -}); -export const baseModelSpec = modelAttribute('base', { - positional: [ - { key: 'base', type: entityRef() }, - { key: 'value', type: str() }, - ], -}); -``` -`fieldRef('self')` → the field-name string (validates it exists on the model); `entityRef()` → the model-name string (existence deferred downstream, exactly as today); `str()` → the decoded quoted-string value. - -## Step 1 — add the two specs to `mongo-attribute-specs.ts` -Copy the two spec constants above into `mongo-attribute-specs.ts` (do NOT import from the SQL package). Add imports `entityRef, fieldRef` to the `@prisma-next/psl-parser` value import (it already imports `modelAttribute`, `str`). Export both. - -## Step 2 — migrate `collectPolymorphismDeclarations` (`interpreter.ts` ~L208-274) -The function loops `for (const model of models) { for (const attr of model.attributes) { if (attr.name === 'discriminator') {…} if (attr.name === 'base') {…} } }`. Replace the inner `attr`-loop with two `findModelAttributeNode` lookups per model (there is at most one of each): - -```ts -for (const model of models) { - const discNode = findModelAttributeNode(model, 'discriminator'); - if (discNode) { - const parsed = interpretModelAttribute({ node: discNode, spec: discriminatorModelSpec, model, sourceFile, sourceId, diagnostics }); - if (parsed) { - const fieldName = parsed.field; - const discField = model.fields[fieldName]; - // Semantic check — stays: the discriminator field must be a String. - if (discField && discField.typeName !== 'String') { - diagnostics.push({ - code: 'PSL_INVALID_ATTRIBUTE_ARGUMENT', - message: `Discriminator field "${fieldName}" on model "${model.name}" must be of type String, but is "${discField.typeName}"`, - sourceId, - span: nodePslSpan(discNode.syntax, sourceFile), - }); - } else { - discriminatorDeclarations.set(model.name, { fieldName, span: nodePslSpan(discNode.syntax, sourceFile) }); - } - } - } - const baseNode = findModelAttributeNode(model, 'base'); - if (baseNode) { - const parsed = interpretModelAttribute({ node: baseNode, spec: baseModelSpec, model, sourceFile, sourceId, diagnostics }); - if (parsed) { - const collectionName = resolveCollectionName({ model, sourceFile, sourceId, diagnostics }); - baseDeclarations.set(model.name, { baseName: parsed.base, value: parsed.value, collectionName, span: nodePslSpan(baseNode.syntax, sourceFile) }); - } - } -} -``` -- Add `discriminatorModelSpec`, `baseModelSpec` (and `interpretModelAttribute`, `findModelAttributeNode` if not already) to the `import { … } from './mongo-attribute-specs'` line. `nodePslSpan` is already imported from `@prisma-next/psl-parser`. -- The `discField.typeName !== 'String'` check keeps its `PSL_INVALID_ATTRIBUTE_ARGUMENT` code (it is genuinely semantic, not arg-shape). -- Delete the now-dead imperative bodies (the `getPositionalArgument(attr)` / `getPositionalArgument(attr, 0|1)` / `parseQuotedStringLiteral(rawValue)` blocks and their `PSL_INVALID_ATTRIBUTE_ARGUMENT` "requires …" / "must be a quoted string literal" diagnostics). - -## Diagnostic-code shifts (operator "Option A") -These arg-shape errors move from `PSL_INVALID_ATTRIBUTE_ARGUMENT` to the grammar's `PSL_INVALID_ATTRIBUTE_SYNTAX`: -- `@@discriminator` with no field arg → missing-required-arg (was "requires a field name argument"). -- `@@discriminator` naming a non-existent field → `fieldRef` existence failure (previously silently recorded). -- `@@base` with fewer than two args → missing-required-arg (was "requires two arguments"). -- `@@base` whose value isn't a quoted string → `str()` rejection (was "must be a quoted string literal"). -The base-model-existence check and the discriminator⇄base consistency checks in `resolvePolymorphism` are unchanged. **Grep the Mongo test suite for these cases and update the asserted codes/messages per Option A** (search for `discriminator`/`@@base`/`PSL_INVALID_ATTRIBUTE_ARGUMENT` in `packages/2-mongo-family/2-authoring/contract-psl/test`), keeping the String-type-check test (`PSL_INVALID_ATTRIBUTE_ARGUMENT`) as-is. Tests-first for any new case. - -## Step 3 — no parser deletions yet -`getPositionalArgument` and `parseQuotedStringLiteral` are still used by the index attributes (migrated in D4). Do NOT delete them here — only their `@@discriminator`/`@@base` call sites go away. Confirm they are still imported/used after your edit. - -## Tests -Polymorphism lowering must be byte-identical for valid schemas — the Mongo contract-psl suite + `fixtures:check` are the primary signal. Update any bad-arg diagnostic-code assertions per the shifts above. If a valid `@@base`/`@@discriminator` round-trip case is missing, add one. - -## Scope -**In:** the two Mongo polymorphism specs; the `collectPolymorphismDeclarations` argument-parse migration; the diagnostic-code-shift test updates. **Out:** `resolvePolymorphism` semantics; every other attribute; `packages/1-framework` / `packages/2-sql`; helper deletions (D6). - -## Constraints -No `any`; no bare `as` (mirror the SQL template's justified `blindCast` only if it uses one at the same spot); no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/mongo-contract-psl build && typecheck && test` -2. `pnpm fixtures:check` — clean, no Mongo contract drift -3. `pnpm lint:deps` (0) and `pnpm lint:framework-vocabulary` (threshold unchanged) - -## Report back -The two specs added; the `collectPolymorphismDeclarations` migration + confirmation the String-type check kept `PSL_INVALID_ATTRIBUTE_ARGUMENT`; which bad-arg tests shifted to `PSL_INVALID_ATTRIBUTE_SYNTAX`; confirmation `getPositionalArgument`/`parseQuotedStringLiteral` are still used (not deleted); the test path; all gate results; the commit SHA. If a span change breaks a diagnostic-span assertion, or the SQL template diverges, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/04-kit-str-value-json.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/04-kit-str-value-json.md deleted file mode 100644 index a20a64c538a7..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/04-kit-str-value-json.md +++ /dev/null @@ -1,96 +0,0 @@ -# Brief: D4 — kit combinators `str(value)` + `json()` for the Mongo index surface - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes`. Do NOT push or touch GitHub. ONE signed commit. Tests-first. **psl-parser (kit) only — no Mongo/SQL package changes.** - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files is fine. If under-specified, STOP and report. - -## Why -The Mongo `@@index`/`@@unique`/`@@textIndex` argument surface (migrated in later dispatches) needs two leaf combinators the kit lacks: -- **`str(value)`** — a pinned string literal, for the index `type` set (`type: "hashed"`, `"2dsphere"`, `"2d"`, `"text"` — digit-leading, so they can't be bare identifiers). The ADR calls for this alongside `num(value)`/`identifier(name)`. -- **`json()`** — reads an opaque JSON **object** from a quoted JSON string (`filter: "{\"status\": \"active\"}"`, `weights: "{\"title\": 10}"`). ADR § "Surface policy" names this the one text-encoded exception. It replaces the interpreter's `parseJsonArg`. - -The field-element grammar (`name(sort: Desc)`, `wildcard(scope)`) needs **no** new combinator — it composes from the existing `funcCall(name, sig)` dynamically over the model's fields (done in D5). This dispatch is just the two leaves. - -## Part A — `str(value)` overload (`combinators/str.ts`) -Today `str()` returns `ArgType` (any string literal). Add a pinned overload, mirroring `combinators/num.ts` (`num()` / `num(value)`) exactly: -```ts -export function str(): ArgType; -export function str(value: string): ArgType; -export function str(value?: string): ArgType { - return { - kind: 'str', - label: value === undefined ? 'string' : JSON.stringify(value), - parse: (arg, ctx): Result => { - if (arg instanceof StringLiteralExprAst) { - const parsed = arg.value(); - if (parsed !== undefined && (value === undefined || parsed === value)) return ok(parsed); - } - const message = value === undefined ? 'Expected a string literal' : `Expected ${JSON.stringify(value)}`; - return notOk([leafDiagnostic(ctx, arg, message)]); - }, - }; -} -``` -(The unpinned `str()` behaviour is unchanged — all existing callers keep working. Use `JSON.stringify(value)` for the label/message so the quotes show, e.g. `Expected "hashed"`.) - -## Part B — `json()` combinator (new file `combinators/json.ts`) -Reads a quoted JSON string literal and parses it to a JSON **object**. Behaviour matches the interpreter's current `parseJsonArg` (decoded string → `JSON.parse` → must be a non-array object): -```ts -import type { PslDiagnostic } from '@prisma-next/framework-components/psl-ast'; -import { notOk, ok, type Result } from '@prisma-next/utils/result'; -import { StringLiteralExprAst } from '../../syntax/ast/expressions'; -import type { ArgType } from '../types'; -import { leafDiagnostic } from './diagnostic'; - -// Reads an opaque JSON object from a quoted JSON string — the ADR's one text-encoded surface -// exception (e.g. a Mongo index `filter` / `weights`). The string is decoded by the parser, then -// JSON-parsed; a non-object (array/scalar) or invalid JSON is a diagnostic. -export function json(): ArgType> { - return { - kind: 'json', - label: 'JSON object', - parse: (arg, ctx): Result, readonly PslDiagnostic[]> => { - if (!(arg instanceof StringLiteralExprAst)) { - return notOk([leafDiagnostic(ctx, arg, 'Expected a JSON object string')]); - } - const raw = arg.value(); - if (raw !== undefined) { - try { - const parsed: unknown = JSON.parse(raw); - if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { - return ok(blindCast, 'JSON.parse of a validated non-array object literal is a string-keyed record'>(parsed)); - } - } catch { - // fall through to the diagnostic - } - } - return notOk([leafDiagnostic(ctx, arg, 'Expected a valid JSON object')]); - }, - }; -} -``` -Import `blindCast` from `@prisma-next/utils/casts` (the one justified narrow cast — `JSON.parse` returns `any`/`unknown`; narrowing a validated object to `Record` needs it). No bare `as`. - -## Part C — export `json` -Add `export { json } from '../attribute-spec/combinators/json';` to `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts` (alphabetical, near `int`/`identifier`). `str` is already exported (the overload needs no export change). - -## Tests — `test/attribute-spec-combinators.test.ts` -Add focused unit tests (tests-first): -- **`str(value)`**: `str('hashed')` accepts `"hashed"` → `'hashed'`; rejects `"2dsphere"` and a bare identifier and a number; the unpinned `str()` still accepts any string. (Mirror the existing `num(value)` test block's structure.) -- **`json()`**: accepts `"{\"a\": 1}"` → `{ a: 1 }`; rejects a non-object JSON string (`"[1,2]"`, `"5"`), an invalid-JSON string, and a bare identifier / number literal. -Use the existing `argOf(...)` helper in that test file to build the expression + ctx. - -## Scope -**In:** `str(value)` overload; the `json()` combinator + its export; their unit tests. **Out:** any Mongo/SQL package change; the index migration (D5); wiring `json()` into any spec. - -## Constraints -No `any` (use `unknown` + the single justified `blindCast` in `json`); no other bare `as`; no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/psl-parser build && typecheck && test` -2. `pnpm --filter @prisma-next/sql-contract-psl typecheck && test` and `pnpm --filter @prisma-next/mongo-contract-psl typecheck && test` (must stay green with NO edits — the `str()` overload + new `json` export are additive) -3. `pnpm lint:deps` (0) and `pnpm lint:framework-vocabulary` (bump threshold to the exact new count ONLY if the two combinators' comments move it; prefer rewording) - -## Report back -The `str(value)` overload + `json()` shape; confirmation the unpinned `str()` and all existing psl-parser/sql/mongo tests stay green with no edits; how you handled the `json` cast (the single `blindCast`); the new unit tests; all gate results; the commit SHA. If `arg.value()` on a `StringLiteralExprAst` does NOT return the JSON-decoded (unescaped) content — so `JSON.parse` would need different pre-processing — STOP and report what it returns. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/05-mongo-index.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/05-mongo-index.md deleted file mode 100644 index 554f7723f89f..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/05-mongo-index.md +++ /dev/null @@ -1,112 +0,0 @@ -# Brief: D5 — migrate Mongo `@@index` / `@@unique` (model) to specs - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes`. Do NOT push or touch GitHub. ONE signed commit. Tests-first. Builds on D1–D4 (the `mongo-attribute-specs.ts` wiring + `str(value)`/`json()` combinators are on the branch). - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files/line-ranges is fine. If under-specified, STOP and report. - -## Why -Migrate the **argument parsing** of the model-level `@@index` and `@@unique` attributes off the imperative string helpers onto specs through `interpretAttribute`. The dense index-shape **validation stays** in the interpreter — only the arg *source* changes. `@@textIndex` stays on its current path in this dispatch (migrated in D6); the loop branches to keep textIndex working. - -All the code lives in `collectIndexes` (`packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts`, ~L626-880). Read that whole function first — you will preserve all of its validation, key-building, and `MongoIndex` construction; you only replace where the argument *values* come from. - -## The argument surface (grounded) -Positional `fields` = an array of index elements, each one of: -- a bare field → `fieldRef('self')` → the field-name string; -- `field(sort: Asc|Desc)` → a per-field sorted ref; -- `wildcard()` / `wildcard(scope)` → a wildcard path. - -Named args (`@@index`/`@@unique`): `type` (`1`/`-1`/`"text"`/`"2dsphere"`/`"2d"`/`"hashed"`), `sparse` (bool), `expireAfterSeconds` (number), `filter` (quoted JSON → object), `include` / `exclude` (quoted bracket-string, e.g. `"[a, b]"`), `default_language` (quoted string), `languageOverride` (quoted string), and the 9 collation args: `collationLocale`/`collationCaseFirst`/`collationAlternate`/`collationMaxVariable` (quoted strings), `collationStrength` (number), `collationCaseLevel`/`collationNumericOrdering`/`collationBackwards`/`collationNormalization` (bools). - -## Step 1 — spec infrastructure in `mongo-attribute-specs.ts` -Add, composed dynamically per model from its field names (exactly like `buildDefaultSpec` composes from the registry): - -**(a) The field element** (the plan's `sortedFieldRef`/`wildcardPath` dissolve into `funcCall` composition — no new combinators): -```ts -const sortSig = { named: { sort: oneOf(identifier('Asc'), identifier('Desc')) } } satisfies FuncCallSig; -function indexFieldElement(fieldNames: readonly string[]) { - const fieldArms = fieldNames.map((name) => funcCall(name, sortSig)); // `field(sort: X)` → { fn: name, args: { sort } } - return oneOf( - fieldRef('self'), // bare field → "name" - funcCall('wildcard', { positional: [{ key: 'scope', type: optional(fieldRef('self')) }] }), // wildcard(scope) → { fn: 'wildcard', args: { scope? } } - ...fieldArms, - ); -} -``` -Element output is `string | { fn: string; span; args: { sort?: 'Asc'|'Desc'; scope?: string } }`. (If `fieldNames` is empty the model has no fields — guard so the `oneOf` tuple stays non-empty, e.g. skip the field arms; a bare `oneOf(fieldRef('self'), funcCall('wildcard', …))` is still valid.) - -**(b) A shared collation named-args object:** -```ts -const collationNamedArgs = { - collationLocale: optional(str()), - collationStrength: optional(int()), - collationCaseLevel: optional(bool()), - collationCaseFirst: optional(str()), - collationNumericOrdering: optional(bool()), - collationAlternate: optional(str()), - collationMaxVariable: optional(str()), - collationBackwards: optional(bool()), - collationNormalization: optional(bool()), -}; -``` - -**(c) An index-spec factory** (one shape for both `@@index` and `@@unique`; the attribute name differs, args are the same): -```ts -export function buildIndexModelSpec(name: 'index' | 'unique', fieldNames: readonly string[]) { - return modelAttribute(name, { - positional: [{ key: 'fields', type: list(indexFieldElement(fieldNames), { nonEmpty: true }) }], - named: { - type: optional(oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))), - sparse: optional(bool()), - expireAfterSeconds: optional(int()), - filter: optional(json()), - include: optional(str()), - exclude: optional(str()), - default_language: optional(str()), - languageOverride: optional(str()), - ...collationNamedArgs, - }, - }); -} -``` -Add the needed imports (`bool, fieldRef, funcCall, identifier, int, json, list, num, oneOf, optional, str`, and `type FuncCallSig`). - -## Step 2 — normalize + migrate `collectIndexes` -Introduce a helper that turns a spec-interpreted `@@index`/`@@unique` into the same normalized values the loop already uses, so the **rest of the loop is unchanged**. The loop today derives, per attribute: `parsedFields: ParsedIndexField[]` (`{ name, isWildcard, direction? }`), then `typeArg`, `sparse`, `expireAfterSeconds`, `partialFilterExpression` (filter), `include`/`exclude` → `wildcardProjection`, `collation`, `default_language`, `language_override`. Produce those from the spec output instead of the `getNamedArgument`/`parse*` calls: - -- **Field elements → `ParsedIndexField[]`:** map each interpreted element: - - `string` → `{ name, isWildcard: false }` - - `{ fn: 'wildcard', args: { scope? } }` → `{ name: scope ? \`${scope}.$**\` : '$**', isWildcard: true }` - - `{ fn, args: { sort } }` (fn is the field name) → `{ name: fn, isWildcard: false, direction: sort === 'Desc' ? -1 : 1 }` -- **`type`:** the interpreted value is already `1 | -1 | 'text' | '2dsphere' | '2d' | 'hashed' | undefined`; feed it where `parseIndexDirection(typeArg)` was used (default `1` when absent). This replaces `parseIndexDirection` for the index/unique path. -- **`filter`** → the `json()` object (was `parseJsonArg`). -- **`include`/`exclude`** → the raw `str()` value; feed to the existing `parseProjectionList(value, 1|0)` (parseProjectionList stays — it splits the bracket-string; the spec only supplies the decoded string). -- **collation** → build the `CollationOptions` from the interpreted collation args (locale/strength/etc.), replacing `parseCollation`. **Preserve the semantic rule:** if any collation arg is present but `collationLocale` is absent → the existing `PSL_INVALID_INDEX` "collationLocale is required" diagnostic. Keep that check (it is genuinely semantic, not arg-shape). -- **`default_language`/`languageOverride`** → the `str()` values (was `stripQuotesHelper`). - -Structure it cleanly: for `@@textIndex` keep the **existing** old-path parsing (branch `if (isTextIndex) { …old parseIndexFieldList/getNamedArgument path… } else { …spec path… }`); for `@@index`/`@@unique` use `interpretModelAttribute({ node: findModelAttributeNode(model, name), spec: buildIndexModelSpec(name, fieldNames), model, sourceFile, sourceId, diagnostics })`. Everything after normalization — the wildcard-count / unique+wildcard / hashed-single-field / wildcard+type / wildcard+expireAfterSeconds / include-xor-exclude / include-requires-wildcard checks, the `PSL_INDEX_FIELD_NOT_FOUND` existence check, the key mapping, and the `new MongoIndex({...})` construction — stays **exactly as is**, reading the normalized values. - -Note: `collectIndexes(pslModel, …)` currently takes `(pslModel, fieldMappings, modelNames, sourceId, diagnostics, indexSpans)` — it will also need `sourceFile` (thread it from the caller; the entry has it in scope). `fieldNames` for the spec = the model's field names (`Object.keys(pslModel.fields)`), the same set the existence check uses. - -## Diagnostic-code shifts (operator "Option A") -Arg-shape errors that were silent or bespoke become grammar `PSL_INVALID_ATTRIBUTE_SYNTAX`: an unknown `type` value (was silently defaulted to `1` by `parseIndexDirection`), a non-bool `sparse`, a non-numeric `expireAfterSeconds`, a malformed field element, a bad `sort` direction, invalid `filter`/`weights` JSON. The genuinely-semantic index-shape codes (`PSL_INVALID_INDEX` in all its forms, `PSL_INDEX_FIELD_NOT_FOUND`, the collation-locale-required rule) are **preserved**. Grep the Mongo index tests and update any shifted assertions per Option A; keep the `PSL_INVALID_INDEX`/`PSL_INDEX_FIELD_NOT_FOUND` cases as-is. - -## Step 3 — no parser deletions yet -`parseIndexDirection`, `parseCollation`, `parseNumericArg`, `parseBooleanArg`, `stripQuotesHelper`, `parseIndexFieldList`, `getNamedArgument`, `getPositionalArgument` are **still used by the `@@textIndex` old-path branch** — do NOT delete them here (D6 migrates textIndex, D7 deletes them). `parseProjectionList` and `parseJsonArg`-for-weights stay for now too. Confirm they're all still used after your edit. - -## Tests -`@@index`/`@@unique` lowering must be byte-identical for valid schemas — the Mongo contract-psl suite (`test/interpreter.test.ts` has extensive index coverage: ascending/descending/compound, `type`, `sparse`/`expireAfterSeconds`, `filter`, `include`/`exclude`, wildcard, collation, `@@unique` variants) + `fixtures:check` are the primary signal. Run them; update only the code-shifted bad-arg assertions. Tests-first for any added case. - -## Scope -**In:** the field-element helper + collation args + `buildIndexModelSpec`; the `@@index`/`@@unique` normalization + migration in `collectIndexes` (textIndex kept on the old branch); the `sourceFile` threading; code-shift test updates. **Out:** `@@textIndex` migration (D6); parser deletions (D7); `packages/1-framework` / `packages/2-sql`; all the index-shape semantics (unchanged). - -## Constraints -No `any`; no bare `as` (a narrow justified `blindCast` is acceptable only where mapping the heterogeneous element union forces it — narrow it as far as possible, prefer discriminating on `typeof x === 'string'` / `x.fn === 'wildcard'`); no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/mongo-contract-psl build && typecheck && test` -2. `pnpm fixtures:check` — clean, no Mongo contract drift -3. `pnpm lint:deps` (0) and `pnpm lint:framework-vocabulary` (threshold unchanged; reword rather than bump) - -## Report back -The field-element helper + `buildIndexModelSpec` shape; how you normalized the element union to `ParsedIndexField` (and any `blindCast` used); confirmation the index-shape validation + `MongoIndex` construction are unchanged and `@@textIndex` still works via the old branch; which bad-arg tests shifted to `PSL_INVALID_ATTRIBUTE_SYNTAX`; confirmation the legacy parsers are still used (not deleted); all gate results; the commit SHA. If the element-union normalization forces a wide cast, if a `PSL_INVALID_INDEX` span/message assertion breaks, or a gate goes red you can't resolve from the brief, STOP and report the exact blocker. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/06-mongo-textindex-cleanup.md b/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/06-mongo-textindex-cleanup.md deleted file mode 100644 index eb891e013a6b..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/dispatches/06-mongo-textindex-cleanup.md +++ /dev/null @@ -1,88 +0,0 @@ -# Brief: D6 — migrate Mongo `@@textIndex` + delete the legacy parsers (slice finish) - -> Fresh implementer. Slice `mongo-attributes`, branch `tml-2956-mongo-attributes`. Do NOT push or touch GitHub. ONE signed commit. Tests-first. Builds on D1–D5 (all on the branch). **This is the last implementation dispatch — after it the Mongo family is fully spec-driven.** - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool — it HANGS and deadlocks.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading named files/line-ranges is fine. If under-specified, STOP and report. - -## Why -D5 migrated `@@index`/`@@unique` to specs but left `@@textIndex` on the old parsing branch inside `collectIndexes` (`interpreter.ts`). Migrate that branch to a spec too — which orphans the remaining legacy parsers, so they must be deleted in this same commit (biome `noUnusedVariables: error` forbids leaving dead code). Read `collectIndexes` in full first; the D5 structure (a `if (isTextIndex) {…} else {…}` block feeding shared normalized locals, then unchanged validation/key-building/`MongoIndex`) is what you extend. - -## Step 1 — add `buildTextIndexModelSpec` to `mongo-attribute-specs.ts` -Reuse the D5 helpers (`indexFieldElement`, `collationNamedArgs`) already in that file. `@@textIndex` accepts: the field list + `filter` (json), `include`/`exclude` (str), `weights` (json), `language` (str — note: `language`, NOT `default_language`), `languageOverride` (str), and the collation args. It does **not** take `type`/`sparse`/`expireAfterSeconds`/`default_language`. -```ts -export function buildTextIndexModelSpec(fieldNames: readonly string[]) { - return modelAttribute('textIndex', { - positional: [{ key: 'fields', type: list(indexFieldElement(fieldNames), { nonEmpty: true }) }], - named: { - filter: optional(json()), - include: optional(str()), - exclude: optional(str()), - weights: optional(json()), - language: optional(str()), - languageOverride: optional(str()), - ...collationNamedArgs, - }, - }); -} -``` - -## Step 2 — migrate the `isTextIndex` branch in `collectIndexes` -Replace the old-path body of the `if (isTextIndex) {…}` block (the `getPositionalArgument`/`parseIndexFieldList`/`parseJsonArg`/`parseCollation`/`stripQuotesHelper`/`getNamedArgument` reads) with a spec interpretation that fills the same normalized locals. Prefer unifying both branches — since both now interpret a spec, select the spec by kind and normalize with `isTextIndex` conditionals: -```ts -const node = attributeNodes[attrIndex]; -if (node === undefined) continue; -const spec = isTextIndex - ? buildTextIndexModelSpec(specFieldNames) - : buildIndexModelSpec(isUnique ? 'unique' : 'index', specFieldNames); -const parsed = interpretModelAttribute({ node, spec, model: pslModel, sourceFile, sourceId, diagnostics }); -if (parsed === undefined) continue; -parsedFields = parsed.fields.map(normalizeIndexField); -if (parsedFields.length === 0) continue; -typeValue = isTextIndex ? undefined : parsed.type; -sparse = isTextIndex ? undefined : parsed.sparse; -expireAfterSeconds = isTextIndex ? undefined : parsed.expireAfterSeconds; -partialFilterExpression = parsed.filter; -includeArg = parsed.include; -excludeArg = parsed.exclude; -collation = buildCollationFromSpec(parsed); -default_language = isTextIndex ? parsed.language : parsed.default_language; -language_override = parsed.languageOverride; -weights = isTextIndex ? extractWeights(parsed.weights) : undefined; -``` -- `parsed.weights` from `json()` is `Record | undefined`; keep the existing number-filter (`extractWeights` = for each entry, keep only `typeof v === 'number'`) — extract it to a small local helper or inline it, cast-free (`unknown` narrowed by `typeof`). -- `buildCollationFromSpec(parsed)` (the D5 helper) already reads the collation args + preserves the `collationLocale`-required `PSL_INVALID_INDEX` rule — it works for the textIndex spec too since it carries `collationNamedArgs`. -- **Union the `parsed` types cleanly**: `buildTextIndexModelSpec` and `buildIndexModelSpec` infer different named-arg shapes. Read each field off `parsed` only in the branch where its spec declares it (guard the index-only reads with `isTextIndex ? undefined : parsed.` and the textIndex-only `parsed.language`/`parsed.weights` with `isTextIndex ? … : undefined`). If TypeScript can't reconcile the two `parsed` shapes in one binding, keep the two-branch `if (isTextIndex) {…} else {…}` structure (each branch interprets its own spec and fills the locals) rather than forcing a cast — no bare `as`. - -Everything after the normalized locals — the `textIndexCount`/one-per-collection guard, the wildcard/hashed/type/expireAfterSeconds/include-exclude `PSL_INVALID_INDEX` checks, `PSL_INDEX_FIELD_NOT_FOUND`, key-building, `parseProjectionList`-based `wildcardProjection`, and `new MongoIndex({...})` — stays **exactly as is**. - -## Step 3 — delete the now-dead legacy parsers -After Step 2, migrate-then-`rg` to confirm zero remaining uses, then delete (biome will fail otherwise): -- In `interpreter.ts`: `parseCollation`, `parseNumericArg`, `parseBooleanArg`, `parseJsonArg`, `stripQuotesHelper`, and the `parseIndexFieldList`/`getNamedArgument`/`getPositionalArgument` imports (drop from the `./psl-helpers` import). -- In `psl-helpers.ts`: `parseIndexFieldList`, `parseIndexFieldSegment`, `parseFieldList`, `splitTopLevel`, `getNamedArgument`, `getPositionalArgument`. -- **Keep** (still used): `parseProjectionList` (splits the spec's `include`/`exclude` bracket-string → `wildcardProjection`), `getAttribute` (field `@id`/`@unique` presence checks), `lowerFirst` (collection naming), `parseQuotedStringLiteral` if still referenced, and anything else `rg` shows still-used. -- **Verify each deletion with `rg` first** — if any candidate still has a use outside the index loop, keep it and report. - -## Step 4 — reword the stale comment -`interpreter.ts` ~L536 has a comment naming the removed `parseIndexDirection` ("Replaces `parseIndexDirection` for…"). Reword it to describe the function's purpose without naming removed code (e.g. "Normalizes the index `type` value to a Mongo key direction, defaulting to ascending (1) when absent."). - -## Diagnostic-code shifts (operator "Option A") -`@@textIndex` now rejects args it doesn't declare (`type`/`sparse`/`expireAfterSeconds`/`default_language`) as `PSL_INVALID_ATTRIBUTE_SYNTAX` (the old path silently ignored them). And bad-shape textIndex args (non-JSON `weights`/`filter`, malformed field element) become grammar errors. Non-existent-field references shift to `PSL_INVALID_ATTRIBUTE_SYNTAX` (via `fieldRef`), same as D5's split; relation-field-not-indexable stays `PSL_INDEX_FIELD_NOT_FOUND`. Grep the `@@textIndex` tests and update shifted assertions; keep the `PSL_INVALID_INDEX` (one-per-collection, textIndex+wildcard) cases. - -## Tests -`@@textIndex` lowering must be byte-identical for valid schemas — the Mongo contract-psl suite (`interpreter.test.ts` has textIndex coverage: basic, weights, language/languageOverride, one-per-collection, textIndex+wildcard) + `fixtures:check` are the primary signal. Update only shifted bad-arg assertions. Tests-first for anything added. - -## Scope -**In:** `buildTextIndexModelSpec`; the textIndex branch migration; deleting the orphaned parsers; the stale-comment reword; code-shift test updates. **Out:** `packages/1-framework` / `packages/2-sql`; the index-shape semantics (unchanged); `parseProjectionList`/`getAttribute`/`lowerFirst` (kept). - -## Constraints -No `any`; no bare `as` (narrow the `weights`/element unions with `typeof`; keep the two-branch structure if a union forces a cast); no file-ext imports; never suppress biome; `pnpm` not `npm`. Commit once: `git commit -s` (DCO), explicit staging, no `--amend`, NO push, no GitHub. Read-only on `projects/**`, `.agents/**`. - -## Gates (all green, in order) -1. `pnpm --filter @prisma-next/mongo-contract-psl build && typecheck && test` -2. `pnpm fixtures:check` — clean, no Mongo contract drift -3. `pnpm lint:deps` (0) and `pnpm lint:framework-vocabulary` (threshold unchanged; reword rather than bump) -4. **Grep gate:** `rg -n "parseCollation|parseIndexFieldList|parseFieldList|splitTopLevel|parseNumericArg|parseBooleanArg|parseJsonArg|stripQuotesHelper|getNamedArgument|getPositionalArgument|parseIndexDirection" packages/2-mongo-family/2-authoring/contract-psl/src` → **zero** (all Mongo attribute-argument parsing is now spec-driven). - -## Report back -`buildTextIndexModelSpec` shape; how you migrated the textIndex branch (unified vs two-branch) + how you handled the `parsed`-type union cast-free; the weights number-filter; which parsers you deleted (with the `rg`-confirmed zero) and which you kept + why; the stale-comment reword; which textIndex bad-arg tests shifted; the grep-gate result; all gate results; the commit SHA. If a `parsed`-type union forces a bare `as`, a deletion candidate is still used unexpectedly, or a `PSL_INVALID_INDEX`/textIndex assertion breaks in a way the brief doesn't cover, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/plan.md b/projects/typed-attribute-parsers/slices/mongo-attributes/plan.md deleted file mode 100644 index b385abf45037..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/plan.md +++ /dev/null @@ -1,31 +0,0 @@ -# Slice `mongo-attributes` — dispatch plan - -**Spec:** `./spec.md` · **Branch:** `tml-2956-mongo-attributes` (off `origin/main`) · **Linear:** umbrella [TML-2956](https://linear.app/prisma-company/issue/TML-2956). - -Substrate-then-consumers within the slice: D1 lands the Mongo-side wiring and proves the seam on the simplest attribute; D2–D4 migrate the simple attributes; D5–D6 tackle the heavy index grammar (and build the kit pieces they consume); D7 removes the legacy parsers behind a grep gate. Each dispatch leaves the Mongo contract-psl suite + `fixtures:check` green. - -| # | Dispatch | Outcome | Builds on | New kit | -| - | -------- | ------- | --------- | ------- | -| D1 ✅ | Mongo `InterpretCtx` wiring + `@map`/`@@map` | `mongo-attribute-specs.ts` exists (`buildFieldInterpretCtx`/`buildModelInterpretCtx`, `interpretFieldAttribute`/`interpretModelAttribute`, `findFieldAttributeNode`/`findModelAttributeNode`); `@map`/`@@map` lowered via a spec. Proves the seam end-to-end. **Done — commit `b6835dc09`.** | slice 1 kit | — | -| D2 | `@relation` (Mongo) | Mongo `@relation` (name positional/named alias, `fields`, `references`) spec-driven; `parseRelationAttribute` retired. | D1 wiring | — | -| D3 | `@@discriminator`, `@@base` | Polymorphism attribute **argument shapes** (field name; base/value) spec-driven; cross-model consistency (`resolvePolymorphism`) untouched. | D1 wiring | — | -| D4 | `@@index` / model `@@unique` core | Index field-element `oneOf(fieldRef, sortedFieldRef, wildcardPath)` + `type` `oneOf(num/str …)` spec-driven; `parseIndexFieldList`/`parseIndexDirection` retired; `PSL_INVALID_INDEX` + field-existence stay. | D1 wiring | `str(value)`, `sortedFieldRef`, `wildcardPath` | -| D5 | `@@textIndex` | Collation named args + `weights` (`map(fieldRef,int())`) + wildcardProjection spec-driven; `parseCollation`/`parseJsonArg`/`parseNumericArg`/`parseBooleanArg` retired; one-per-collection guard stays. | D4 | `map(key, value)` | -| D6 | Delete legacy Mongo parsers | Remove the now-dead `psl-helpers.ts` arg parsers + interpreter-local parsers; grep gate → zero; final `fixtures:check`. | D1–D5 | — | - -> **Correction (post-D1 grounding).** Field `@id` and field `@unique` are **presence-only** in Mongo (`getAttribute(field.attributes, 'id'|'unique') !== undefined` — no arguments to parse), so they need no spec migration; `getAttribute` stays for them. Model `@@unique` is handled inside the index loop alongside `@@index`/`@@textIndex`, so it folds into D4, not a separate dispatch. The original "D2: @id/@unique/@@unique" is dropped and the remaining dispatches renumbered. - -## Sequencing - -Stack: D1 (done) → (D2, D3 disjoint after D1) → D4 → D5 → D6. D2 (`@relation`) and D3 (polymorphism) touch disjoint attribute sites but share `interpreter.ts` + `mongo-attribute-specs.ts` as a write surface, so they run **sequentially on the branch** (not as parallel sub-agents) to avoid clobbering. D4 builds the index grammar; D5 builds on it for `@@textIndex`; D6 is the closing sweep once every consumer is migrated. - -## Sizing note (Open Question 1 in the spec) - -Target ≤ ~7 dispatches. If, at D5, the index/textIndex diff plus the earlier attributes can't be held in one code review, split D5–D6 into a sibling slice/PR (`mongo-index`) — mirroring how `@default` split out of `sql-attributes`. Decision deferred to D5; surfaced to the operator then. - -## Kit-additions provenance - -- `num(value)` — already shipped (SQL slice, D8). Reused by the index `type` set. -- `str(value)` — D5 (first consumer: index `type` digit-leading members). -- `sortedFieldRef` / `wildcardPath` — D5 (index element `oneOf`). -- `map(key, value)` — D6 (`@@textIndex` `weights`). `record(value)` = `map(str(), value)` already exists. diff --git a/projects/typed-attribute-parsers/slices/mongo-attributes/spec.md b/projects/typed-attribute-parsers/slices/mongo-attributes/spec.md deleted file mode 100644 index 077340d821fa..000000000000 --- a/projects/typed-attribute-parsers/slices/mongo-attributes/spec.md +++ /dev/null @@ -1,77 +0,0 @@ -# Slice: mongo-attributes - -_(In-project slice. Parent: `projects/typed-attribute-parsers/`. Parallel group B of the project plan — independent of the SQL slices, builds only on slice 1's already-merged kit. Outcome it contributes: the **Mongo** family becomes spec-driven, completing "every attribute in every family validates its arguments through the kit".)_ - -## At a glance - -Migrate the **Mongo** family's attribute argument-parsing off `ResolvedAttribute` + hand-written string helpers onto the declarative combinator kit (`interpretAttribute`), exactly as the SQL family already did. Mongo attributes in scope: `@id`, `@unique` / `@@unique`, `@@index`, `@@textIndex`, `@relation`, `@map` / `@@map`, `@@discriminator`, `@@base`. - -Current state (grounded): -- `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts` (~1363 lines) parses every attribute imperatively via `getPositionalArgument` / `getNamedArgument` and the `psl-helpers.ts` string parsers (`parseIndexFieldList`, `parseFieldList`, `parseRelationAttribute`, `stripQuotes`) plus interpreter-local `parseIndexDirection` / `parseNumericArg` / `parseBooleanArg` / `parseJsonArg` / `parseCollation`. -- **No kit usage yet** — Mongo does not import `interpretAttribute` / `fieldAttribute` / `modelAttribute` / `InterpretCtx`, and there is no `mongo-attribute-specs.ts` and no Mongo-side `InterpretCtx` wiring. - -## Chosen design - -Mirror the SQL family's shape, in the Mongo package: - -1. **Mongo `InterpretCtx` wiring + wrappers.** A new `mongo-attribute-specs.ts` provides `buildFieldInterpretCtx` / `buildModelInterpretCtx` and `interpretFieldAttribute` / `interpretModelAttribute` (drain parse failures into `diagnostics`, return the typed value or `undefined`) — parallel to SQL's. Plus `findFieldAttributeNode` / `findModelAttributeNode`. -2. **Per-attribute specs**, composed from kit combinators, replacing each imperative parse site in `interpreter.ts`. -3. **New shared-kit combinators** (built in `psl-parser`, first-consumed here — the plan's carry-in): - - `str(value)` — pinned string literal (parallel to `num(value)` from the SQL slice). First consumer: the index `type` set, whose digit-leading members (`"2dsphere"`, `"2d"`) can only be quoted-string literals. - - `map(key, value)` — reads a `{…}` object literal into `Record` (ADR § "Generic collections"). First consumer: `@@textIndex` `weights` (`map(fieldRef('self'), int())`). `record(value)` already exists as the `map(str(), value)` shorthand. - - `sortedFieldRef(scope)` and `wildcardPath()` — the two non-plain index-element shapes, so a Mongo index element is `oneOf(fieldRef('self'), sortedFieldRef('self'), wildcardPath())` (ADR § "Alternatives and function calls"). -4. **Mixed literal set as `oneOf`** (carry-in): the index `type` (`1`, `-1`, `"text"`, `"2dsphere"`, `"2d"`, `"hashed"`) becomes `oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))` — homogeneous-or-mixed with the quoted-vs-bare surface explicit per member (ADR § "Scalars"). - -**What moves into the grammar (specs):** argument syntax only — the relation args (`name` alias / `fields` / `references`), the index field-list element shapes, the index `type` set, the `@@textIndex` collation named-arg shapes and `weights` map, the `@map` name, `@@discriminator` / `@@base` argument shapes. - -**What stays semantic (in the interpreter):** the index-shape validation that is not single-argument syntax — `PSL_INVALID_INDEX` (at-most-one wildcard, unique+wildcard forbidden, hashed→single-field, wildcard+`hashed`/`2dsphere`/`2d` forbidden), `PSL_INDEX_FIELD_NOT_FOUND` (field-existence against the model), the **one-`@@textIndex`-per-collection** rule (stays in Mongo's model-level aggregation, per the project spec — not a per-attribute `refine`), and the polymorphism cross-model rules (`@@discriminator`/`@@base` consistency across models). - -**Diagnostic codes:** shape/arity errors that today produce bespoke codes move to `PSL_INVALID_ATTRIBUTE_SYNTAX` where that is honest (operator "Option A", consistent with the SQL slices); genuinely-semantic codes (`PSL_INVALID_INDEX`, `PSL_INDEX_FIELD_NOT_FOUND`, the polymorphism codes) are preserved. Per-case shifts are pre-investigated at dispatch-authoring time and the asserting tests updated. - -## Coherence rationale - -One outcome — "the Mongo family is spec-driven on every attribute; the Mongo string parsers (`psl-helpers.ts` arg helpers + `parseIndexFieldList` / `parseRelationAttribute` / the interpreter-local collation/number/bool/json parsers) are deleted; no legacy Mongo attribute-argument parser remains (grep gate)." It parallels the SQL family migration and shares no mutable surface with it beyond the already-merged kit. - -## Scope - -**In:** `mongo-attribute-specs.ts` (wiring + wrappers + per-attribute specs); the `interpretAttribute` call-site migration in `interpreter.ts` for all in-scope attributes; the four new kit combinators (`str(value)`, `map`, `sortedFieldRef`, `wildcardPath`) with unit tests; deletion of the now-dead Mongo string parsers. - -**Out:** -- **The interpreter's semantic checks** — `PSL_INVALID_INDEX` shape rules, `PSL_INDEX_FIELD_NOT_FOUND`, one-`@@textIndex`-per-collection, polymorphism cross-model consistency — all stay. -- **SQL family** — already migrated (slices `sql-attributes` + `sql-default`). -- **Language-server autocomplete** — project non-goal / deferred follow-up. -- **`@db.*` native types** — project-wide out of scope. - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| Index element `wildcard(scope)` / `field(sort: Desc)` / bare field | Grammar: `oneOf(fieldRef('self'), sortedFieldRef('self'), wildcardPath())` | Replaces `parseIndexFieldSegment`'s regexes. `sortedFieldRef` carries the `Asc`/`Desc` → `1`/`-1` direction; `wildcardPath` yields the `$**` / `scope.$**` path. | -| Index `type` mixed str/num set | Grammar: `oneOf(num(1), num(-1), str('text'), str('2dsphere'), str('2d'), str('hashed'))` | Replaces `parseIndexDirection`. | -| `@@textIndex` `weights: {field: n}` | Grammar: `map(fieldRef('self'), int())` (new `map` combinator) | Replaces `parseJsonArg` + the manual number-coercion loop. | -| `@@textIndex` collation (`collationLocale`, `collationStrength`, …) | Grammar: named optional args (str / int / bool) on the spec | Replaces `parseCollation` + `parseNumericArg` / `parseBooleanArg`. The both-or-neither / dependency rules (if any) go in the spec's `refine`. | -| Index-shape validity (wildcard count, unique+wildcard, hashed single-field, textIndex-per-collection) | Semantic; stays | `PSL_INVALID_INDEX` + the model-level textIndex-count guard remain in `interpreter.ts`. | -| Index field existence | Semantic; stays | `PSL_INDEX_FIELD_NOT_FOUND` stays (checked against the model's indexable fields). | -| `@relation` name positional-or-named alias | Grammar: positional `key:'name'` sharing the output key with named `name` | The alias mechanic the kit already models (ADR § "Positional and named arguments"). | -| `@@discriminator` / `@@base` args | Grammar for arg shapes; cross-model consistency stays semantic | Field name / (base, value) argument shapes move to specs; the `@@discriminator`⇄`@@base` cross-model rules stay in `resolvePolymorphism`. | - -## Slice-specific done conditions - -- [ ] Every in-scope Mongo attribute is validated + lowered via a spec through `interpretAttribute`. -- [ ] `psl-helpers.ts` arg-parsing (`getPositionalArgument`, `getNamedArgument`, `parseFieldList`, `parseIndexFieldList` + `parseIndexFieldSegment`, `parseRelationAttribute`, `stripQuotes`) and the interpreter-local `parseIndexDirection` / `parseNumericArg` / `parseBooleanArg` / `parseJsonArg` / `parseCollation` are deleted (`rg` each → zero) for every migrated attribute. Retained: `getMapName`/`getAttribute` only if still needed by surviving semantic code; `lowerFirst` (collection naming) stays. -- [ ] Semantic codes preserved: `PSL_INVALID_INDEX`, `PSL_INDEX_FIELD_NOT_FOUND`, the polymorphism codes; the one-`@@textIndex`-per-collection guard intact. -- [ ] `pnpm fixtures:check` clean (byte-identical Mongo contract output); the Mongo contract-psl test suite green; the four new combinators unit-tested; vocab green. - -## Open Questions - -_Open (surface to operator before the index/textIndex dispatches):_ - -1. **One slice or two?** This slice migrates the whole Mongo family including the heavy `@@index` / `@@textIndex` (collation + weights + wildcard element grammar). If the diff outgrows a single-sitting review, the index/textIndex portion is the natural split into its own slice/PR (as `@default` was split out of `sql-attributes`). Recommendation: build in the dispatch order below and re-evaluate at D5; split if the review can't hold it. -2. **`@@textIndex` collation surface.** Confirm whether the collation named args should stay a flat set of optional args on the `@@textIndex` spec (matches today's PSL surface) or move to a nested `map`/object — likely "flat optional args", but confirm against the ADR's native-literal surface policy at dispatch time. - -## References - -- Parent project: `projects/typed-attribute-parsers/spec.md`; project plan `projects/typed-attribute-parsers/plan.md` (slice `mongo-attributes`, parallel group B, with the D6 carry-in note). -- SQL precedent to mirror: `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` (wiring + wrappers + specs); the merged `sql-default` slice under `projects/typed-attribute-parsers/slices/sql-default/`. -- Mongo current state: `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts`, `.../src/psl-helpers.ts`. -- Kit: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/**`; ADR 231 (§ "The combinator kit", § "Alternatives and function calls"). diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/01-model-attribute-kit.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/01-model-attribute-kit.md deleted file mode 100644 index 74d3b8f01794..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/01-model-attribute-kit.md +++ /dev/null @@ -1,37 +0,0 @@ -# Brief: D1 — kit: `modelAttribute` constructor + model-level plumbing (+ `int`, `bool`) - -> Fresh implementer (session resume unavailable). Slice 2 (`sql-attributes`) of the `typed-attribute-parsers` project, on branch `tml-2956-sql-attributes` (off fresh `origin/main`; slice 1 merged in #891). Do NOT push or touch GitHub. - -## Context -- The kit is in `main`: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/` — engine `interpret.ts` (`interpretAttribute` already accepts `FieldAttributeAst | ModelAttributeAst`), `field-attribute.ts` (`fieldAttribute`), combinators (`str`, `identifier`, `oneOf`, `list`, `fieldRef`, `optional`), `types.ts` (`AttributeSpec`, `InterpretCtx`, `AttributeLevel` = `'field' | 'model' | 'block'`, `InferAttr`, `ArgType`). Exports in `src/exports/index.ts`. -- Slice-1 exemplar for the model-level plumbing you'll mirror: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` — `findRelationAttributeNode(field)`, `buildRelationInterpretCtx(...)`. You are building the **model** analogues. -- CST: `ModelAttributeAst` (`syntax/ast/attributes.ts`), `ModelDeclarationAst` (has `.attributes()`), `NumberLiteralExprAst` / `BooleanLiteralExprAst` (`syntax/ast/expressions.ts`). -- Slice spec (esp. Chosen design + Scope): `projects/typed-attribute-parsers/slices/sql-attributes/spec.md`; slice plan §D1: `.../plan.md`. - -## Task -Grow the kit so the model-level SQL attributes (D2+) can be migrated. No attribute is migrated in D1 — this is foundational kit + tests. - -1. **`modelAttribute(name, spec)` constructor** in `psl-parser` (new file `src/attribute-spec/model-attribute.ts` mirroring `field-attribute.ts`), fixing `level: 'model'`, same `{ positional, named, refine }` shape and `AttributeOut` inference as `fieldAttribute`. Export it from `src/exports/index.ts`. -2. **`int()` leaf** (`src/attribute-spec/combinators/int.ts`): parses a `NumberLiteralExprAst` whose value is an integer → `number`; non-number / non-integer → the standard leaf diagnostic (via `leafDiagnostic`). Export. -3. **`bool()` leaf** (`src/attribute-spec/combinators/bool.ts`): parses a `BooleanLiteralExprAst` → `boolean`; else leaf diagnostic. Export. -4. **Unit + type-level tests** for `modelAttribute` (constructs a `level:'model'` spec; `InferAttr` infers the same shape `fieldAttribute` would for equivalent params), `int`, `bool` (success + each diagnostic path). - -Note: **no separate model-ctx plumbing helper in `psl-parser`** — the model-level `findModelAttributeNode` + `buildModelInterpretCtx` belong in the SQL package where they're consumed (D2 builds them next to the specs, mirroring `@relation`'s helpers). D1 is purely the psl-parser kit growth. (If you find it cleaner to add a tiny generic helper in the kit, surface it — but default to keeping ctx-assembly in the consumer, as `@relation` did.) - -## Scope -**In:** `modelAttribute` constructor, `int` + `bool` leaves, their exports + tests, in `psl-parser`. -**Out:** any attribute migration (D2+); any SQL-package change; `record`/`entityRef`/`funcCall`/scalar-literal leaves (later dispatches); Mongo; `@db.*`. - -## Completed when -- [ ] `modelAttribute`, `int`, `bool` exported from `@internal/psl-parser` and usable (`modelAttribute('x', { positional: [{ key:'k', type: int() }] })` type-checks and infers `{ k: number }`). -- [ ] Unit + type-level tests cover the three (success + diagnostic paths; `modelAttribute` level + inference). -- [ ] Gates: `pnpm --filter @internal/psl-parser typecheck && test && lint`; `pnpm lint:framework-vocabulary` (kit growth may add framework lines — if the count moves, update `threshold` in `scripts/lint-framework-vocabulary.config.json`, keeping `allow: ["SymbolTable"]`, and report it). - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` with a reason, or types that avoid it — follow the existing combinators' style); no file-ext imports; no reexport outside `exports/`; tests-first. Explicit-staging commits with sign-off (`git commit -s`), no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Run the transient-ID scan on the `+` diff. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** thorough (foundational constructor + inference must mirror `fieldAttribute` exactly). -- **Halt conditions:** `modelAttribute` can't reuse `fieldAttribute`'s inference cleanly without duplicating the `AttributeOut` machinery (surface the shared-factory shape); the engine needs a change to handle model nodes (it shouldn't — it already accepts `ModelAttributeAst`). - -Return the structured report: the `modelAttribute` shape (and any shared-factory refactor with `fieldAttribute`), the `int`/`bool` leaves, ratchet result, gate results, commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/02-map.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/02-map.md deleted file mode 100644 index 948769753e99..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/02-map.md +++ /dev/null @@ -1,38 +0,0 @@ -# Brief: D2 — migrate `@map` (field) + `@@map` (model); delete `parseMapName` - -> Fresh implementer. Slice 2 (`sql-attributes`), branch `tml-2956-sql-attributes`. Do NOT push or touch GitHub. - -## Context -- Kit now has `modelAttribute` + `fieldAttribute` + `str`/`optional`/… (D1 landed on this branch, commit `7c982c739`). -- Exemplar for the plumbing: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` — `findRelationAttributeNode(field)` + `buildRelationInterpretCtx(...)`. You'll build the **model** analogues. -- `@map`/`@@map` are parsed today by `parseMapName` (`psl-attribute-parsing.ts:71-104`), called from `buildModelMappings` (`psl-field-resolution.ts` ~:571 model, ~:582 field) with a **default** when the attribute is absent: model → `lowerFirst(model.name)`, field → `field.name`. -- Slice spec + plan §D2: `projects/typed-attribute-parsers/slices/sql-attributes/{spec.md,plan.md}`. - -## Task -Migrate field `@map` and model `@@map` onto specs, and add the reusable model-level plumbing this and later `@@` dispatches need. - -1. **Model-level plumbing** (in the SQL package, next to where it's used — a new small module or beside the existing relation helpers): `findModelAttributeNode(model: ModelSymbol, name): ModelAttributeAst | undefined` and `buildModelInterpretCtx({ selfModel, symbols, sourceFile, sourceId }): InterpretCtx` — mirror `@relation`'s helpers but for model level (`level: 'model'`, no `field`, `resolveReferencedModel` may return `undefined`). Reuse for both this dispatch and D3–D6. -2. **Specs:** `const mapFieldSpec = fieldAttribute('map', { positional: [{ key: 'name', type: str() }] })` and `const mapModelSpec = modelAttribute('map', { positional: [{ key: 'name', type: str() }] })`. -3. **Route the call sites:** in `buildModelMappings`, replace the `parseMapName` calls. For each model / field: if its `map`/`@@map` attribute node exists → `interpretAttribute(node, spec, ctx)` and use `result.name`; if absent → apply the existing default (`lowerFirst(model.name)` for the model table name; `field.name` for the column). Thread the diagnostics through as `@relation` does. -4. **Delete `parseMapName`** (`psl-attribute-parsing.ts`). Confirm no other caller remains. - -## Scope -**In:** the two `map` specs; model-level plumbing (`findModelAttributeNode` + `buildModelInterpretCtx`); the `buildModelMappings` call-site migration; deleting `parseMapName`. -**Out:** every other attribute (D3+); other legacy helpers (`parseConstraintMapArgument` etc. stay — they serve other attributes); Mongo; `@db.*`. - -## Behaviour parity -`@map`/`@@map` set the same column/table names as before, incl. the absent-attribute defaults. `pnpm fixtures:check` must stay clean. Diagnostic: a malformed `@map` argument now emits the kit's `PSL_INVALID_ATTRIBUTE_SYNTAX` (was `PSL_INVALID_ATTRIBUTE_ARGUMENT`) — intentional per the slice spec; update any test asserting the old code. - -## Completed when -- [ ] `@map`/`@@map` lowered via `interpretAttribute`; absent-attribute defaults preserved. -- [ ] `parseMapName` deleted (`rg parseMapName packages/2-sql` → zero). -- [ ] Gates: `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; `pnpm lint:framework-vocabulary`; after `pnpm --filter @internal/psl-parser build` (only if you touched psl-parser — you shouldn't), workspace `pnpm typecheck`. - -## Constraints -No `any`; no bare `as`; no file-ext imports; tests-first where the emitted code/behaviour changes. Explicit-staging commit(s) with sign-off, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Transient-ID scan on the `+` diff. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** mid (mechanical migration once the plumbing exists; the plumbing is the one design bit). -- **Halt conditions:** the absent-attribute default can't be cleanly expressed at the call site (surface it); `parseMapName` has a caller outside `@map`/`@@map` (leave it, surface). - -Return: the model-plumbing shape (reused by D3+), the two specs, confirmation `parseMapName` is gone, gate results, commit SHA(s). diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/03-id-unique.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/03-id-unique.md deleted file mode 100644 index 659c8406f0fb..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/03-id-unique.md +++ /dev/null @@ -1,56 +0,0 @@ -# Brief: D3 — migrate `@id`/`@unique` (field) + `@@id`/`@@unique` (model) - -> Fresh implementer. Slice 2 (`sql-attributes`), branch `tml-2956-sql-attributes`. Do NOT push or touch GitHub. - -## ⛔ TOOLING PROHIBITION — READ FIRST -**NEVER call the `grep` / regex-search / codebase-search MCP tool. It HANGS this -environment and will deadlock your run.** For every search, shell out via the -terminal tool using `rg` (ripgrep) or `grep` as a command — e.g. -`rg -n "parseAttributeFieldList" packages/2-sql`. This is non-negotiable; two -prior dispatches died on this exact mistake. If you catch yourself reaching for a -search tool that isn't the terminal, STOP and use `rg` in the terminal instead. - -## Context -- The attribute-spec kit is at `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/`. It is already published on this branch — import kit funcs/types from `@internal/psl-parser`; AST types from `@internal/psl-parser/syntax`. Do NOT modify psl-parser in this dispatch. -- **D2 landed the SQL-side plumbing you reuse:** `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` — `findModelAttributeNode`, `findFieldAttributeNode`, `buildModelInterpretCtx`, `buildFieldInterpretCtx`. Add your new specs + helpers here. -- **Combinators you need (already exist):** - - `optional(str())` — optional value. - - `str()` — string literal → `string`. - - `fieldRef('self')` — bare identifier → field name (`string`); validates the field exists on `ctx.selfModel`. - - `list(fieldRef('self'), { nonEmpty: true, unique: true })` — `[a, b]` → `string[]`; enforces non-empty + no duplicates + per-element field existence. This **subsumes** `parseAttributeFieldList` + `findDuplicateFieldName` for the migrated attributes. -- **Today's handling:** - - Field `@id`/`@unique`: `extractFieldConstraintNames` in `psl-field-resolution.ts:249-279` — presence via `getAttribute`, constraint name via `parseConstraintMapArgument` (the `map:` named arg). Presence booleans feed `isIdField`/`isUnique` at `psl-field-resolution.ts:493-551`. - - Model `@@id`: `interpreter.ts:620-696`. Model `@@unique`/`@@index` share one branch at `interpreter.ts:697-` (`name === 'unique' || name === 'index'`), using `parseAttributeFieldList` + `findDuplicateFieldName`, then nullable check (`@@id` only), `mapFieldNamesToColumns`, `parseConstraintMapArgument`. -- Slice spec + plan §D3: `projects/typed-attribute-parsers/slices/sql-attributes/{spec.md,plan.md}`. - -## Task -Migrate the four constraint attributes so their **argument syntax** is parsed by specs, while the **semantic** checks (nullable-in-PK, column mapping, both-inline-and-block-PK, duplicate-declaration) stay in the interpreter. - -1. **Specs (in `sql-attribute-specs.ts`):** - - `fieldAttribute('id', { named: { map: optional(str()) } })` and the same for `'unique'`. - - `modelAttribute('id', { positional: [{ key: 'fields', type: list(fieldRef('self'), { nonEmpty: true, unique: true }) }], named: { map: optional(str()) } })` and the same for `'unique'`. - - Export small interpret helpers mirroring D2's `interpret*MapName` shape: return the parsed `{ fields, map }` (model) / `{ map }` (field) or push diagnostics + return a sentinel. Keep the call-site ergonomics close to D2. -2. **Field `@id`/`@unique` (`psl-field-resolution.ts`):** replace the two `parseConstraintMapArgument` calls in `extractFieldConstraintNames` with the field spec's interpretation to get `idName`/`uniqueName`. Presence detection (`idAttribute`/`uniqueAttribute` booleans) can stay via `getAttribute` OR via `findFieldAttributeNode`; keep whichever is cleaner — the downstream only needs a boolean + the map name. -3. **Model `@@id` / `@@unique` (`interpreter.ts`):** replace the `parseAttributeFieldList` + `findDuplicateFieldName` extraction with the model spec's `fields` result. **Split the shared `unique || index` branch:** route `@@unique` through the spec; **leave `@@index` on the legacy path unchanged** (it migrates in D4). Keep the nullable-field check, `mapFieldNamesToColumns`, the both-inline-and-block-PK guard, the duplicate-declaration guard, and the constraint `map` name (now from the spec, not `parseConstraintMapArgument`). -4. **Do NOT delete `parseAttributeFieldList` / `parseFieldList` / `findDuplicateFieldName`** — `@@index` still consumes them until D4. (The plan's D3 entry says to delete them; that is inaccurate because `@@index` shares the path. Leave them; D4 deletes them.) `parseConstraintMapArgument` + `mapFieldNamesToColumns` are also retained. - -## Scope -**In:** the four specs + their interpret helpers; the field `@id`/`@unique` map-name migration; the model `@@id`/`@@unique` field-list + map migration; splitting the `unique`/`index` branch. -**Out:** `@@index` (D4), `@@control` (D5), polymorphism (D6), `@default` (D7), Mongo, `@db.*`. Do not touch legacy helpers other than ceasing to call them from the migrated paths. - -## Behaviour parity -Same primary-key / unique-constraint output (columns, constraint names, inline-vs-block PK resolution) as before. `pnpm fixtures:check` must stay clean. Diagnostics for **argument-syntax** errors (non-list `@@id` arg, duplicate field in the list, unknown field in the list, non-string `map`) now surface the kit's `PSL_INVALID_ATTRIBUTE_SYNTAX` with the kit's messages (`Expected a list of field name`, `Duplicate list entry`, `Field "X" does not exist on model "Y"`, `Expected a string literal`) instead of the old `PSL_INVALID_ATTRIBUTE_ARGUMENT` — intentional per the slice spec. **Semantic** diagnostics that stay in the interpreter (nullable field in PK, both inline+block `@@id`, duplicate `@@id` declaration, column-mapping failures) keep their existing codes/messages. Update every test that asserts a changed code/message; find them with `rg` in the terminal. - -## Completed when -- [ ] Field `@id`/`@unique` map name + model `@@id`/`@@unique` fields+map lowered via specs; `@@index` untouched and still green. -- [ ] `parseAttributeFieldList`/`parseFieldList`/`findDuplicateFieldName` retained (still used by `@@index`); no longer called from the migrated `@@id`/`@@unique` paths. -- [ ] Gates: `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; `pnpm lint:framework-vocabulary`. - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts` if truly unavoidable); no file-ext imports; tests-first where emitted code/behaviour changes. Explicit-staging commit with `git commit -s` (DCO), no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** high — the branch split + preserving the exact semantic-check ordering is the design risk; the spec wiring is mechanical. -- **Halt conditions:** if migrating `@@unique` forces a change to `@@index` behaviour (it should NOT — split the branch cleanly), STOP and surface. If a semantic check can't be preserved because the spec consumes the arg the interpreter needed, surface it rather than dropping the check. - -Return: the four specs + helper shapes, the branch-split diff summary for `@@unique`/`@@index`, confirmation the three list helpers are retained, gate results, and the commit SHA. diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/04-index.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/04-index.md deleted file mode 100644 index d1441d8cc385..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/04-index.md +++ /dev/null @@ -1,53 +0,0 @@ -# Brief: D4 — kit `record` combinator + migrate `@@index`; delete the string-based helpers - -> Fresh implementer. Slice 2 (`sql-attributes`), branch `tml-2956-sql-attributes`. Do NOT push or touch GitHub. - -## ⛔ TOOLING PROHIBITION — READ FIRST -**NEVER call the `grep` / regex-search / codebase-search MCP tool. It HANGS this -environment and deadlocks your run.** For EVERY search, shell out via the terminal -tool with `rg` (ripgrep) or `grep`, e.g. `rg -n "parseObjectLiteralStringMap" packages`. -Non-negotiable — prior dispatches died on this. If you reach for a search tool that -isn't the terminal, STOP and use `rg` in the terminal instead. - -## Context -This dispatch **grows the kit** (adds a combinator to `@internal/psl-parser`) and then migrates `@@index`, which lets a batch of now-dead string-parsing helpers be deleted. - -- **Kit location:** `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/`. Existing leaves (`str.ts`, `list.ts`, `field-ref.ts`, `int.ts`, `bool.ts`, `one-of.ts`, `identifier.ts`) are your templates. Combinators are re-exported from `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts` (see the `export { list } from '../attribute-spec/combinators/list'` lines ~39–48). -- **AST for the new combinator:** `packages/1-framework/2-authoring/psl-parser/src/syntax/ast/expressions.ts` — `ObjectLiteralExprAst` (`.fields()` → `Iterable`); `ObjectFieldAst` has `.keyName(): string | undefined` (unquoted key) and `.value(): ExpressionAst | undefined`. String leaves are `StringLiteralExprAst` (`.value(): string | undefined`). -- **Current `@@index` handling:** `interpreter.ts` — the isolated `if (modelAttribute.name === 'index')` branch (~lines 700–800 after D3's split). It parses: `fields` (via `parseAttributeFieldList` + `findDuplicateFieldName`), `map` (via `parseConstraintMapArgument`), `type` (named, quoted string via `parseQuotedStringLiteral`), `options` (named, object literal via `parseObjectLiteralStringMap`), with the rule **`options` requires `type`**. -- **D2/D3 SQL plumbing you reuse:** `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` — `findModelAttributeNode`, `buildModelInterpretCtx`, and the `interpretModelConstraint` pattern. Add the `@@index` spec + interpret helper here, alongside the others. -- Slice spec + plan §D4: `projects/typed-attribute-parsers/slices/sql-attributes/{spec.md,plan.md}`. Note the spec's edge-case row: **field lists are positional-only** — do NOT declare `fields` as a named arg (operator decision; the legacy named `fields:` spelling is intentionally dropped). - -## Task -1. **Add the `record` combinator** (`combinators/record.ts`): `record(of: ArgType): ArgType>`. Parse an `ObjectLiteralExprAst`: for each `ObjectFieldAst`, take `keyName()` and parse `value()` through `of`. Reject: non-object-literal arg (`Expected an object literal`), a field with no key or no value, a duplicate key, and any element whose value fails `of.parse`. Accumulate leaf failures like `list` does. Use `leafDiagnostic(ctx, node, msg)` for all diagnostics (code = the kit's single `ATTRIBUTE_DIAGNOSTIC_CODE`). Export it from `exports/index.ts`. **Add a focused unit test** (`test/attribute-spec/record.test.ts` or beside the existing combinator tests — match the repo's layout) covering: single/multi key, empty object, duplicate key, non-object arg, non-matching leaf. Do NOT add a `map(key, value)` combinator — `@@index` needs only `record(str())` (YAGNI). -2. **Migrate `@@index`** to a spec in `sql-attribute-specs.ts`: - `modelAttribute('index', { positional: [{ key: 'fields', type: list(fieldRef('self'), { nonEmpty: true, unique: true }) }], named: { map: optional(str()), type: optional(str()), options: optional(record(str())) }, refine: (v, ctx) => v.options !== undefined && v.type === undefined ? [] : [] })`. - Add an interpret helper returning `{ fields, map, type, options }` (or the sentinel on failure), mirroring `interpretModelConstraint`. Wire it into the `@@index` branch, preserving `mapFieldNamesToColumns` and the shape pushed to `indexNodes` (`columns`, optional `name`/`type`/`options`). -3. **Delete the now-dead helpers** from `psl-attribute-parsing.ts` — but ONLY after confirming (via `rg`) each has zero remaining callers in `packages/`: - - `parseObjectLiteralStringMap`, `splitObjectLiteralEntries`, `findTopLevelColon` - - `parseAttributeFieldList`, `parseFieldList`, `findDuplicateFieldName` - - `parseConstraintMapArgument` - **Do NOT delete** shared primitives still used elsewhere: `parseQuotedStringLiteral`, `getNamedArgument`, `getPositionalArgument`, `getAttribute`, `lowerFirst`, `unquoteStringLiteral`, the `@db.*` helpers (`parseOptional*Argument`, `getPositionalArguments`) — `rg` each before touching it. -4. **Relocate the tests:** `test/psl-attribute-parsing.test.ts` currently unit-tests `parseObjectLiteralStringMap` (the `parseObjectLiteralStringMap` describe block). That coverage moves to the new `record` combinator test in psl-parser (step 1). Delete the now-orphaned `parseObjectLiteralStringMap` describe block; keep any tests for helpers that survive. - -## Scope -**In:** the `record` combinator + its unit test + export; the `@@index` spec + interpret helper + call-site migration; deletion of the seven dead helpers; the test relocation. -**Out:** `@@control` (D5), polymorphism (D6), `@default` (D7), Mongo, `@db.*`. Do not migrate any other attribute. - -## Behaviour parity -Same index output (`columns`, `name`, `type`, `options`) as before; `options`-requires-`type` still enforced. `pnpm fixtures:check` must stay clean. Argument-syntax errors now surface `PSL_INVALID_ATTRIBUTE_SYNTAX` with kit messages instead of `PSL_INVALID_ATTRIBUTE_ARGUMENT` — intentional; update every test asserting the old code/message (find via `rg`). The `options`-requires-`type` refine diagnostic may change wording; keep it clear. - -## Completed when -- [ ] `record` combinator added, exported, unit-tested; psl-parser typecheck + test green. -- [ ] `@@index` lowered via spec; index output unchanged; `options`-requires-`type` preserved. -- [ ] All seven helpers deleted; `rg` for each in `packages/` → zero. Shared primitives + `@db.*` helpers retained. -- [ ] Gates: `pnpm --filter @internal/psl-parser build` (kit changed — required before downstream typecheck), then `pnpm --filter @internal/psl-parser typecheck && test`; `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; `pnpm lint:framework-vocabulary` (the `record` combinator adds framework lines — if `count` exceeds `threshold`, bump the threshold in `scripts/lint-framework-vocabulary.config.json` to the new count and say so). - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts`, narrowed); no file-ext imports; tests-first where emitted code/behaviour changes. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** high — this is the slice's biggest dispatch (kit growth + migration + 7-helper sweep). Take the steps in order: combinator + test first (prove it in isolation), then the migration, then the deletions last (so `rg`-zero is meaningful). -- **Halt conditions:** if any of the seven helpers still has a caller you can't migrate within this dispatch's scope, leave it and surface. If `@@index` `options` parsing needs a non-string leaf (it shouldn't — V1 is string-only), surface rather than widening `record`. - -Return: the `record` combinator signature + where its test landed, the `@@index` spec + refine shape, the confirmed `rg`-zero list for all seven deleted helpers, whether you moved the vocabulary threshold (and to what), all gate results, and the commit SHA. diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/05-control.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/05-control.md deleted file mode 100644 index fa65f567e8a9..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/05-control.md +++ /dev/null @@ -1,43 +0,0 @@ -# Brief: D5 — migrate `@@control`; delete `parseControlPolicyAttribute` - -> Fresh implementer. Slice 2 (`sql-attributes`), branch `tml-2956-sql-attributes`. Do NOT push or touch GitHub. - -## ⛔ TOOLING PROHIBITION — READ FIRST -**NEVER call the `grep` / regex-search / codebase-search MCP tool. It HANGS this -environment and deadlocks your run.** For EVERY search, shell out via the terminal -tool with `rg` (ripgrep) or `grep`, e.g. `rg -n "parseControlPolicyAttribute" packages`. -Non-negotiable — prior dispatches died on this. If you reach for a search tool that -isn't the terminal, STOP and use `rg` in the terminal instead. - -## Context -A small, mechanical migration — no kit growth (all combinators exist). -- **Current handling:** `@@control` is parsed by `parseControlPolicyAttribute` (`packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts` ~lines 190–240), called from the interpreter's `if (modelAttribute.name === 'control')` branch (`interpreter.ts` ~line 596). The interpreter owns a `PSL_DUPLICATE_ATTRIBUTE` guard (`controlPolicyDeclared`) **which stays**. The parser validates: no named args, exactly one positional, and the token is one of `managed`/`tolerated`/`external`/`observed` (`ControlPolicy`). -- **Combinators:** `oneOf(...)` and `identifier(name)` already exist and are exported from `@internal/psl-parser`. `identifier('managed')` matches a bare identifier and returns the literal `'managed'`; `oneOf(identifier('managed'), identifier('tolerated'), identifier('external'), identifier('observed'))` yields `ControlPolicy`. -- **Plumbing to reuse:** `sql-attribute-specs.ts` — `findModelAttributeNode` + `buildModelInterpretCtx` + the `interpretModelConstraint`/`interpretModelIndex` pattern (D3/D4). Add the `@@control` spec + interpret helper here. -- Slice spec + plan §D5: `projects/typed-attribute-parsers/slices/sql-attributes/{spec.md,plan.md}`. Note the spec's edge-case row: `@@control` policy is **bare-identifier only** now (`@@control(external)`); the legacy quoted spelling `@@control("external")` is intentionally dropped (operator decision — no in-repo schema uses it). - -## Task -1. **Spec (in `sql-attribute-specs.ts`):** `modelAttribute('control', { positional: [{ key: 'policy', type: oneOf(identifier('managed'), identifier('tolerated'), identifier('external'), identifier('observed')) }] })`. Add an interpret helper returning the `ControlPolicy` (or the sentinel on failure), mirroring the existing ones. -2. **Wire it into the interpreter** `control` branch: keep the `controlPolicyDeclared` / `PSL_DUPLICATE_ATTRIBUTE` guard exactly as-is; replace only the `parseControlPolicyAttribute(...)` call with the spec interpretation; assign the result to `controlPolicy` as before. -3. **Delete** `parseControlPolicyAttribute`, `CONTROL_POLICY_LITERALS`, `CONTROL_POLICY_LITERAL_SET`, and `isControlPolicyLiteral` from `psl-attribute-parsing.ts`. If that removes the last use of the `ControlPolicy` import there, drop the import too. Before deleting, `rg` to confirm none of these four have another caller. **Do NOT** delete shared helpers `getPositionalArguments` / `unquoteStringLiteral` unless `rg` shows they now have zero callers across `packages/` (they likely still serve `@db.*` / other paths — leave them if so). - -## Scope -**In:** the `@@control` spec + interpret helper + call-site migration; deletion of the four control-policy helpers. -**Out:** polymorphism `@@discriminator`/`@@base` (D6), `@default` (D7), Mongo, `@db.*`, every other attribute. - -## Behaviour parity -Same `control` policy resolved and stored on the model node; the duplicate-`@@control` `PSL_DUPLICATE_ATTRIBUTE` diagnostic is unchanged (it stays in the interpreter). Argument-syntax errors (missing/too-many positional, named arg supplied, unknown policy word) now surface `PSL_INVALID_ATTRIBUTE_SYNTAX` with the kit's messages instead of `PSL_INVALID_ATTRIBUTE_ARGUMENT` — intentional. `pnpm fixtures:check` must stay clean. Update every test asserting the old code/message (the `@@control` cases live in `test/interpreter.control-policy.test.ts` and possibly `interpreter.diagnostics.test.ts` — find them with `rg`). - -## Completed when -- [ ] `@@control` lowered via the spec; duplicate-attribute guard retained. -- [ ] The four control-policy helpers deleted; `rg` for each in `packages/` → zero. -- [ ] Gates: `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; `pnpm lint:framework-vocabulary`. (No psl-parser change → no rebuild needed; if you somehow touched psl-parser, STOP and report.) - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts`, narrowed); no file-ext imports; tests-first where emitted code/behaviour changes. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** mid — mechanical; the one judgment call is not accidentally deleting a still-shared helper (`rg` before each delete). -- **Halt conditions:** if `getPositionalArguments`/`unquoteStringLiteral` turn out to have no remaining callers and you're unsure whether the `@db.*` path needs them, leave them and surface rather than deleting. - -Return: the `@@control` spec + helper shape, `rg`-zero confirmation for the four deleted control-policy helpers, which shared helpers you retained, gate results, and the commit SHA. diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/06-polymorphism.md b/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/06-polymorphism.md deleted file mode 100644 index 89239640ae99..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/dispatches/06-polymorphism.md +++ /dev/null @@ -1,52 +0,0 @@ -# Brief: D6 — kit `entityRef` combinator + migrate `@@discriminator` + `@@base` - -> Fresh implementer. Slice 2 (`sql-attributes`), branch `tml-2956-sql-attributes`. Do NOT push or touch GitHub. - -## ⛔ TOOLING PROHIBITION — READ FIRST -**NEVER call the `grep` / regex-search / codebase-search MCP tool. It HANGS this -environment and deadlocks your run.** For EVERY search, shell out via the terminal -tool with `rg` (ripgrep) or `grep`, e.g. `rg -n "collectPolymorphismDeclarations" packages`. -Non-negotiable — prior dispatches died on this. If you reach for a search tool that -isn't the terminal, STOP and use `rg` in the terminal instead. - -## Context -Grows the kit with one small combinator (`entityRef`), then migrates the two SQL polymorphism attributes. **SQL family only — do NOT touch `packages/2-mongo-family/**` (Mongo is slice 3).** - -- **Kit location:** `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/`. `field-ref.ts` is your closest template. Combinators re-export from `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts`. -- **`IdentifierAst`** lives in `packages/1-framework/2-authoring/psl-parser/src/syntax/ast/identifier.ts` (`.name(): string | undefined`). -- **Current handling:** `@@discriminator`/`@@base` are parsed in `collectPolymorphismDeclarations` (`interpreter.ts` ~lines 1251–1316), which iterates `model.attributes` (ResolvedAttribute). The main model-attribute loop just skips them (`interpreter.ts:596` `if (name === 'discriminator' || name === 'base') continue;` — leave that skip in place). Downstream, `resolvePolymorphism` owns the cross-model semantic checks (both-`@@discriminator`-and-`@@base`, base-model existence, orphaned-discriminator, etc.). -- Both use the singular `getPositionalArgument` helper (`psl-attribute-parsing.ts`), whose ONLY remaining callers are these three lines (verify with `rg -n "getPositionalArgument\\b" packages/2-sql`). After this migration it is dead → delete it. **Do NOT** delete the *plural* `getPositionalArguments` (serves `@db.*`) or `parseQuotedStringLiteral` (used widely) — `rg` to confirm before touching either. -- **Operator decision (Option A) — read carefully:** model `@@discriminator` via **`fieldRef('self')`** (it validates the field exists on the declaring model). This means an unknown discriminator field now fails at parse with the kit's `PSL_INVALID_ATTRIBUTE_SYNTAX` ("Field … does not exist on model …") — consistent with how `@@id`/`@@unique`/`@@index` already report unknown fields. The old dedicated `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` check in `resolvePolymorphism` (`interpreter.ts` ~line 1364–1372) becomes **unreachable** (the declaration is never set for a missing field) → **remove that block in the SQL interpreter** and update its test. Leave the Mongo copy of that code + test alone. -- Slice spec + plan §D6: `projects/typed-attribute-parsers/slices/sql-attributes/{spec.md,plan.md}`. - -## Task -1. **Add the `entityRef` combinator** (`combinators/entity-ref.ts`): `entityRef(): ArgType` — parses a bare `IdentifierAst` → its name; **does NOT** validate that any model with that name exists (base-model resolution stays in `resolvePolymorphism`). Reject a non-identifier / nameless arg with `leafDiagnostic(ctx, arg, 'Expected a model name')`. Export from `exports/index.ts`. **Unit-test it** beside the existing combinator tests (`packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts`): accepts a bare identifier; rejects a string literal / number / array. -2. **Specs (in `sql-attribute-specs.ts`):** - - `modelAttribute('discriminator', { positional: [{ key: 'field', type: fieldRef('self') }] })`. - - `modelAttribute('base', { positional: [{ key: 'base', type: entityRef() }, { key: 'value', type: str() }] })`. - Add interpret helpers returning `{ field }` / `{ base, value }` (or the sentinel on failure), mirroring the existing `interpretModel*` helpers. -3. **Rewire `collectPolymorphismDeclarations`:** thread a `sourceFile: SourceFile` param through (and pass it at the call site). For each model, use `findModelAttributeNode(model, 'discriminator')` / `findModelAttributeNode(model, 'base')` to get the node, interpret via the spec, and populate the same `discriminatorDeclarations` / `baseDeclarations` maps. **Keep the discriminator String-type semantic check** (`typeName !== 'String'` → `PSL_INVALID_ATTRIBUTE_ARGUMENT`, message contains "must be of type String") — after `fieldRef` the field is guaranteed to exist, so look it up and check its type. All the `resolvePolymorphism` cross-model checks stay unchanged **except** the now-dead `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` block, which you remove (SQL only). -4. **Delete the singular `getPositionalArgument`** from `psl-attribute-parsing.ts` once `rg` confirms zero callers in `packages/`. -5. **Update tests:** the SQL polymorphism test "diagnoses missing discriminator field on base model" (`interpreter.polymorphism.test.ts` ~line 735) now expects `PSL_INVALID_ATTRIBUTE_SYNTAX` with a "does not exist" message instead of `PSL_DISCRIMINATOR_FIELD_NOT_FOUND`. Keep the non-String test (~768) asserting `must be of type String` green. Find any other churned assertions with `rg`. - -## Scope -**In:** the `entityRef` combinator + test + export; the two polymorphism specs + interpret helpers; the `collectPolymorphismDeclarations` migration; removal of the dead `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` block (SQL); deletion of the singular `getPositionalArgument`. -**Out:** `@default` (D7); **all of `packages/2-mongo-family/**`** (slice 3 — its `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` + parsing stay); every other attribute; `@db.*`. - -## Behaviour parity -Same discriminator/base declarations resolved; the String-type check and all `resolvePolymorphism` cross-model diagnostics keep their codes/messages, EXCEPT unknown-discriminator-field which intentionally moves from `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` to `PSL_INVALID_ATTRIBUTE_SYNTAX` (Option A). `@@base` bad arg-count / non-string value now surface `PSL_INVALID_ATTRIBUTE_SYNTAX`. `@@base` model-name is a bare identifier (unchanged spelling). `pnpm fixtures:check` must stay clean. - -## Completed when -- [ ] `entityRef` added, exported, unit-tested; psl-parser typecheck + test green. -- [ ] `@@discriminator`/`@@base` lowered via specs; String-type check retained; dead `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` block removed (SQL only); Mongo untouched. -- [ ] Singular `getPositionalArgument` deleted; `rg -n "getPositionalArgument\\b" packages/2-sql` → only zero (plural `getPositionalArguments` retained). -- [ ] Gates: `pnpm --filter @internal/psl-parser build` (kit changed), then `pnpm --filter @internal/psl-parser typecheck && test`; `pnpm --filter @internal/sql-contract-psl typecheck && test`; `pnpm fixtures:check`; `pnpm lint:framework-vocabulary` (if the `entityRef` combinator moves the count above threshold, bump the threshold in `scripts/lint-framework-vocabulary.config.json` to the new count and say so). - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts`, narrowed); no file-ext imports; tests-first where emitted code/behaviour changes. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `spec.md`, plan files. Do NOT touch GitHub. - -## Operational metadata -- **Model tier:** high — the `collectPolymorphismDeclarations` rewire + safely removing the dead check without disturbing the other `resolvePolymorphism` diagnostics is the risk. Do the combinator + test first, then the specs, then the rewire, then the deletion. -- **Halt conditions:** if removing the `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` block turns out to be reachable by some path `fieldRef('self')` does NOT cover (e.g. a discriminator validated against a model other than `selfModel`), STOP and surface — do not remove a still-live check. If threading `sourceFile` into `collectPolymorphismDeclarations` is awkward at the call site, surface rather than hacking around it. - -Return: the `entityRef` signature + where its test landed; the two specs + helper shapes; confirmation the String-type check is retained and the dead block removed (SQL only, Mongo untouched); `rg`-zero for the singular `getPositionalArgument`; whether the vocabulary threshold moved; all gate results; and the commit SHA. diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/plan.md b/projects/typed-attribute-parsers/slices/sql-attributes/plan.md deleted file mode 100644 index b866d455f3d1..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/plan.md +++ /dev/null @@ -1,49 +0,0 @@ -# Slice: sql-attributes — Dispatch plan - -**Slice spec:** `projects/typed-attribute-parsers/slices/sql-attributes/spec.md` - -Sequential; each dispatch grows the kit just enough for the attributes it migrates, then deletes those attributes' now-dead syntax helpers. `@default` is the long pole and lands last. Target ≤ ~8 dispatches. - -### D1 — Kit: `modelAttribute` + model-level plumbing (+ `int`, `bool`) -- **Outcome:** `modelAttribute(name, {...})` constructor exists and is exported (mirrors `fieldAttribute`, fixes `level: 'model'`); a reusable model-level `findModelAttributeNode(model, name)` + `buildModelInterpretCtx(...)` (mirroring the `@relation` helpers, minus `resolveReferencedModel`/`field`); trivial `int` (`NumberLiteralExprAst`→number) and `bool` (`BooleanLiteralExprAst`→boolean) leaves added + tested. No attribute migrated yet (proven with a unit test using a stub model spec). -- **Builds on:** slice-1 engine + combinators (now in `main`). -- **Hands to:** the `modelAttribute` constructor + model-ctx plumbing every `@@` dispatch consumes. -- **Gate:** psl-parser typecheck + test + lint; `lint:framework-vocabulary`. - -### D2 — Migrate `@map` + `@@map` -- **Outcome:** field `@map` and model `@@map` lowered via specs (single positional `str()`); `parseMapName` deleted. Proves `modelAttribute` end-to-end against a real attribute. -- **Builds on:** D1. -- **Gate:** sql-contract-psl tests; fixtures:check; `rg parseMapName` → zero. - -### D3 — Migrate `@id`/`@unique` (field) + `@@id`/`@@unique` (model) -- **Outcome:** the four constraint attributes lowered via specs (`map: optional(str())` fields; model variants add `list(fieldRef('self'), { nonEmpty, unique })`). `parseAttributeFieldList`/`parseFieldList`/`findDuplicateFieldName` deleted (subsumed by `list`). `parseConstraintMapArgument` + `mapFieldNamesToColumns` retained (still used by `@@index` / semantic). -- **Builds on:** D1. -- **Gate:** sql suites; fixtures:check; relevant `rg` gates. - -### D4 — Kit `record` + migrate `@@index` -- **Outcome:** `record(value)` / `map(key, value)` combinator (`ObjectLiteralExprAst`→`Record`) added + tested; `@@index` lowered via a spec (`fields` list; `map`/`type`/`options` named; `options`-requires-`type` in `refine`). `parseObjectLiteralStringMap` (+ `splitObjectLiteralEntries`/`findTopLevelColon`) and the now-last-caller `parseConstraintMapArgument` deleted. -- **Builds on:** D1, D3. -- **Gate:** sql suites; fixtures:check; `rg` gates. - -### D5 — Migrate `@@control` -- **Outcome:** `@@control` lowered via `oneOf(identifier('managed'), …)`; `parseControlPolicyAttribute` + `CONTROL_POLICY_LITERALS`/`isControlPolicyLiteral` deleted. Interpreter's `PSL_DUPLICATE_ATTRIBUTE` guard retained. -- **Builds on:** D1. -- **Gate:** sql suites; fixtures:check; `rg` gate. - -### D6 — Kit `entityRef` + migrate `@@discriminator` + `@@base` -- **Outcome:** `entityRef()` (bare-identifier model-name reference) added + tested; `@@discriminator` via `fieldRef('self')`, `@@base` via `entityRef()` + `str()`. String-type + base-resolution checks stay in `resolvePolymorphism`. -- **Builds on:** D1. -- **Gate:** sql suites (polymorphism); fixtures:check. - -### D7 — Kit `funcCall`/`funcCallFrom` + scalar/array-literal leaf; migrate `@default` — SPLIT OUT - -> **Mid-flight demotion (operator decision):** `@default` was split into the follow-up slice `sql-default`. Grounding at pickup showed it introduces a novel registry-parameterised `funcCall` combinator plus six preserved semantic codes, pushing this slice's PR past a single coherent review. This slice closes at D6; the entry below is retained for provenance only and is delivered by `sql-default`. -- **Outcome:** `funcCall`/`funcCallFrom` (registry-parameterised — builds `ParsedDefaultFunctionCall` from `FunctionCallAst`, defers name/arg validation to `lowerDefaultFunctionWithRegistry`) + a matching-scalar-literal + array-literal leaf; `@default` lowered via `oneOf(matchingScalarLiteral(), funcCallFrom(registry), enum-member, list(...))`, covering literal / function / bare-enum-member / list defaults. Preserve `PSL_UNKNOWN_DEFAULT_FUNCTION` / `PSL_INVALID_DEFAULT_*` codes. -- **Builds on:** D1 (+ int/bool from D1). -- **Focus:** the long pole; do NOT bundle with anything else. If it balloons, surface for a slice split (spec Open Question 1). -- **Gate:** sql suites (defaults, incl. `interpreter.defaults.test.ts`); fixtures:check; `rg` gates for the retired default-literal helpers. - -### D8 (optional) — Cleanup sweep — NOT NEEDED -- No orphaned helpers remained after D2–D6 (each dispatch deleted its own dead helpers and `rg`-confirmed zero callers). Folded away; nothing to do. - -_(As-shipped: this slice delivered D1–D6 — every SQL attribute except `@default` is spec-driven; the syntax helpers for those attributes are gone; semantic checks, the `@db.*` helpers, and the three `@default`-only helpers are retained. `@default` (former D7) was split to the follow-up slice `sql-default`. D2/D3/D5 were mechanical once D1 landed; D1/D4/D6 carried the kit-growth risk.)_ diff --git a/projects/typed-attribute-parsers/slices/sql-attributes/spec.md b/projects/typed-attribute-parsers/slices/sql-attributes/spec.md deleted file mode 100644 index 2f7a6c2e6a29..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-attributes/spec.md +++ /dev/null @@ -1,77 +0,0 @@ -# Slice: sql-attributes - -_(In-project slice. Parent: `projects/typed-attribute-parsers/`. Outcome it contributes: the SQL family's attribute argument-parsing is spec-driven for every attribute except `@default` — finishing what slice 1 started with `@relation`. `@default` is split into the follow-up slice `sql-default`.)_ - -## At a glance - -Migrate the remaining SQL attributes off hand-written argument parsing onto declarative `AttributeSpec`s, growing the kit with the pieces they need. Slice 1 shipped the engine + `@relation`; this slice does `@id`/`@@id`, `@unique`/`@@unique`, `@@index`, `@map`/`@@map`, `@@control`, `@@discriminator`, `@@base` — and deletes the SQL family's now-dead syntax helpers. **`@default` was split out mid-flight into a follow-up slice `sql-default`** (see Open Questions — operator decision). - -## Chosen design - -Each attribute becomes a spec, mirroring `sqlRelation`. **Only argument *syntax* parsing moves to specs; the interpreter's *semantic* checks stay put** — existence resolution, type checks, cross-attribute conflicts, applicability, duplicate-attribute guards, and field-name→column-name mapping (`mapFieldNamesToColumns`) remain in `interpreter.ts` / `psl-field-resolution.ts`. - -Specs (from the grounded map): - -```ts -modelAttribute('map', { positional: [{ key: 'name', type: str() }] }) -fieldAttribute('map', { positional: [{ key: 'name', type: str() }] }) -fieldAttribute('id', { named: { map: optional(str()) } }) -fieldAttribute('unique',{ named: { map: optional(str()) } }) -modelAttribute('id', { positional: [{ key: 'fields', type: list(fieldRef('self'), { nonEmpty: true, unique: true }) }], named: { map: optional(str()) } }) -modelAttribute('unique',{ /* same shape as @@id */ }) -modelAttribute('index', { positional: [{ key: 'fields', type: list(fieldRef('self'), …) }], named: { map: optional(str()), type: optional(str()), options: optional(record(str())) }, refine: optionsRequiresType }) -modelAttribute('control', { positional: [{ key: 'policy', type: oneOf(identifier('managed'), identifier('tolerated'), identifier('external'), identifier('observed')) }] }) -modelAttribute('discriminator',{ positional: [{ key: 'field', type: fieldRef('self') }] }) -modelAttribute('base', { positional: [{ key: 'base', type: entityRef() }, { key: 'value', type: str() }] }) -``` - -`@default`'s spec (`oneOf(scalarLiteral(), list(...), funcCallFrom(registry), enum-member)`) lives in the `sql-default` slice. - -**Kit growth** (built as consumers need it, all shipped in this slice): `modelAttribute` constructor + model-level plumbing (`findModelAttributeNode` + `buildModelInterpretCtx`, mirroring `@relation`'s helpers); `int`, `bool`; `record` (object-literal → `Record`); `entityRef` (bare-identifier model reference — lighter than `fieldRef`, resolution stays in `resolvePolymorphism`). The `funcCall`/`funcCallFrom` + scalar-literal + array-literal leaves for `@default` are deferred to `sql-default`. The engine already accepts `ModelAttributeAst` and `AttributeLevel` already includes `'model'`; `fieldRef('self')` already resolves against `ctx.selfModel`, so it works unchanged at model level. - -## Coherence rationale - -One outcome — "the SQL family validates every attribute's arguments (except `@default`) through the kit; the hand-written syntax helpers for those attributes are gone." The kit-growth pieces exist only to serve these attributes and are reviewed alongside their first consumer. Large but singular; a reviewer holds "SQL attributes are now spec-driven" in one sitting. `@default`'s size (novel registry-parameterised `funcCall` + six preserved semantic codes) is what pushed it out to its own slice. - -## Scope - -**In:** the 7 attributes above (field + model levels); the kit growth listed (through `entityRef`); deletion of the SQL family's syntax helpers once their last caller migrates — `parseMapName`, `parseAttributeFieldList`/`parseFieldList`/`findDuplicateFieldName`, `parseObjectLiteralStringMap` (+ `splitObjectLiteralEntries`/`findTopLevelColon`), `parseControlPolicyAttribute` (+ `CONTROL_POLICY_LITERALS`/`isControlPolicyLiteral`), `parseConstraintMapArgument`, and the singular `getPositionalArgument`. - -**Out:** -- **`@default`** — split to the follow-up slice `sql-default`. Its three syntax helpers (`parseDefaultLiteralValue`, `parseDefaultFunctionCall`, `parseListDefaultExpression`) stay in place here and move to `sql-default`'s deletion set. -- **Mongo** — slice 3. -- **`@db.*` native types** — out of the whole project; **do NOT delete `parseOptionalSingleIntegerArgument` / `parseOptionalNumericArguments` / `getPositionalArguments`** (they serve the `@db.*` path). -- **The interpreter's semantic checks** — existence, type, conflict (`options`-requires-`type` is the one cross-*argument* rule that moves to `refine`; multi-attribute/model-level checks stay), applicability, duplicate-attribute (`PSL_DUPLICATE_ATTRIBUTE`), and `mapFieldNamesToColumns` — all stay in the interpreter. -- Pinned `str(value)`/`num(value)` literal matchers (Mongo index `type`, slice 3). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| `@db.*` helpers share `getPositionalArguments` + own `parseOptional*Argument` | Must NOT delete | They serve the out-of-scope native-type path; deleting breaks `@db.VarChar(n)` / `@db.Decimal(p,s)`. | -| `parseConstraintMapArgument` shared by 5 attrs (`@id`,`@unique`,`@@id`,`@@unique`,`@@index`) | Delete only after the last (`@@index`) migrates | Not per-attribute. | -| `fieldRef('self')` at model level | Works unchanged | Keys off `ctx.selfModel`; no separate "model field list" combinator needed. | -| `@default` function registry | Deferred to `sql-default`; `funcCall` will defer to it | Entries are pack-contributed via `ControlMutationDefaultRegistry`; `funcCall` must be registry-parameterised, not hardcode names. Preserve `PSL_UNKNOWN_DEFAULT_FUNCTION` etc. | -| Diagnostic codes | Syntax→`PSL_INVALID_ATTRIBUTE_SYNTAX`; semantic checks keep their codes | Expect fixture/test churn where an old `PSL_INVALID_ATTRIBUTE_ARGUMENT` *shape* error becomes `PSL_INVALID_ATTRIBUTE_SYNTAX` — intentional (consistent with slice 1). | -| Field-list spelling for `@@id`/`@@unique`/`@@index` | Positional-only (`@@index([a, b])`); named `fields:` spelling intentionally dropped | The legacy `parseAttributeFieldList` accepted `fields: [...]` as a named arg too; the specs model `fields` as a positional param only. Positional is Prisma's canonical form, and no in-repo schema/fixture/test/example uses the named spelling, so this narrowing is invisible in practice. Accepted deliberately (operator decision) to keep the specs clean rather than declaring `fields` in both positional and named. | -| `@@control` policy spelling | Bare identifier only (`@@control(external)`); quoted form dropped | The legacy parser unquoted the arg, so the quoted spelling also worked; the `oneOf(identifier(...))` spec accepts bare identifiers only. Bare is canonical and no in-repo schema uses the quoted form; same invisible narrowing as the field-list row, accepted deliberately. | -| `@@discriminator` unknown field | Now `PSL_INVALID_ATTRIBUTE_SYNTAX` via `fieldRef('self')` (was `PSL_DISCRIMINATOR_FIELD_NOT_FOUND`) | Operator decision (Option A): unify with how `@@id`/`@@unique`/`@@index` report unknown fields. The dead SQL-side `PSL_DISCRIMINATOR_FIELD_NOT_FOUND` block was removed; the Mongo copy is untouched. | - -## Slice-specific done conditions - -- [ ] Every listed SQL attribute (all except `@default`) is validated + lowered via a spec through `interpretAttribute`. -- [ ] The SQL syntax helpers listed in Scope-In are deleted (`rg` for each returns zero); the `@db.*` helpers and the three `@default` helpers are retained. -- [ ] `pnpm fixtures:check` clean; SQL interpreter suites green; `pnpm lint:framework-vocabulary` green (kit growth may add framework lines — update threshold if the count moves). -- [ ] Diagnostic **codes** preserved for semantic checks; syntax-error codes may become `PSL_INVALID_ATTRIBUTE_SYNTAX` (intentional, test assertions updated). - -## Open Questions - -1. **Is `@default` in this slice or its own?** _Resolved (operator decision, mid-flight): **its own.**_ `@default` (funcCall + registry + literal/enum/list) is a large, self-contained sub-problem. When reached as the planned last dispatch, grounding showed it introduces a novel registry-parameterised `funcCall` combinator plus six preserved semantic codes, pushing this slice's PR past a single coherent review. It was split into the follow-up slice `sql-default` via mid-flight demotion; this slice ships D1–D6 (every SQL attribute except `@default`). - -## References - -- Parent project: `projects/typed-attribute-parsers/spec.md`; project plan slice-2 entry. -- Slice-1 exemplar: `packages/2-sql/2-authoring/contract-psl/src/psl-relation-resolution.ts` (`sqlRelation`, `findRelationAttributeNode`, `buildRelationInterpretCtx`). -- Kit: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/**`. -- Legacy helpers: `packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts`; `@default` (deferred to `sql-default`): `default-function-registry.ts`, `psl-column-resolution.ts`, `framework-components/.../mutation-default-types.ts`. -- Follow-up slice: `projects/typed-attribute-parsers/slices/sql-default/`. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/01-kit-scalar-funccall.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/01-kit-scalar-funccall.md deleted file mode 100644 index d0c5dd27feec..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/01-kit-scalar-funccall.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brief: D1 — kit combinators `scalarLiteral()` + `funcCall()` - -> Fresh implementer. Slice `sql-default` (parent project `typed-attribute-parsers`), branch `tml-2956-sql-default` (off fresh `origin/main`). Do NOT push or touch GitHub. - -## ⛔ TOOLING PROHIBITION — READ FIRST -**NEVER call the `grep` / regex-search / codebase-search MCP tool. It HANGS this -environment and deadlocks your run.** For EVERY search, shell out via the terminal -tool with `rg` (ripgrep) or `grep`, e.g. `rg -n "ParsedDefaultFunctionCall" packages`. -Non-negotiable — prior dispatches died on this. If you reach for a search tool that -isn't the terminal, STOP and use `rg` in the terminal instead. - -## Context -This grows the attribute-spec kit with the two leaf combinators `@default` needs (the migration itself is D2). No SQL files change in this dispatch. -- **Kit location:** `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/`. Templates: `str.ts` (single-literal leaf), `field-ref.ts`, `entity-ref.ts`, `list.ts`. Re-export from `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts`. Combinator tests live in `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts`. -- **Literal AST nodes** (`packages/1-framework/2-authoring/psl-parser/src/syntax/ast/expressions.ts`): `StringLiteralExprAst.value(): string | undefined` (decoded — escapes/quotes resolved), `NumberLiteralExprAst.value(): number | undefined`, `BooleanLiteralExprAst.value(): boolean | undefined`. -- **`FunctionCallAst`** (same file): `.name(): QualifiedNameAst | undefined` and `.args(): Iterable`. Use the structural getters — for the function name, use `QualifiedNameAst.identifier()?.token()?.text` (do NOT stringify via `.path().join('.')`). -- **`ParsedDefaultFunctionCall`** is a framework type in `packages/1-framework/1-core/framework-components/src/shared/mutation-default-types.ts`, exported via `@internal/framework-components/control`. `psl-parser` already depends on `framework-components` (the kit imports `PslDiagnostic` from `@internal/framework-components/psl-ast`), so `funcCall()` can emit `ParsedDefaultFunctionCall` directly — layering-clean. Read that type + `DefaultFunctionArgument` before implementing; match their shape exactly (`{ name, raw, args: [{ raw, span }], span }` — confirm field names against the source). -- Slice spec + plan §D1: `projects/typed-attribute-parsers/slices/sql-default/{spec.md,plan.md}`. - -## Task -1. **`scalarLiteral()`** (`combinators/scalar-literal.ts`): `ArgType`. If the arg is a `StringLiteralExprAst`/`NumberLiteralExprAst`/`BooleanLiteralExprAst` and its `.value()` is defined, return it; else `notOk([leafDiagnostic(ctx, arg, 'Expected a string, number, or boolean literal')])`. Export it. -2. **`funcCall()`** (`combinators/func-call.ts`): `ArgType`. If the arg is a `FunctionCallAst`, build a `ParsedDefaultFunctionCall`: - - `name` from `.name()?.identifier()?.token()?.text` (reject with a leaf diagnostic if absent/qualified in a way that yields no simple name). - - `args` from `.args()` — for each, the `raw` source text (render via the AST — use the arg expression's decoded value where it is a literal, or `printSyntax(expr.syntax)` for the general case; producing text here is legitimate, the SQL registry re-parses these strings downstream) and its `span` via `nodePslSpan`. - - `raw` and the call `span` via `nodePslSpan`. - Reject a non-`FunctionCallAst` arg with `leafDiagnostic(ctx, arg, 'Expected a function call')`. **Registry-agnostic** — do NOT import any SQL type or validate the name against a registry (that stays in the interpreter, D2). Export it. -3. **Unit-test both** in `attribute-spec-combinators.test.ts` (match the file's existing `describe`/`GreenNodeBuilder`-or-`parse` convention): - - `scalarLiteral`: accepts string / number / boolean literals (returns the decoded value); rejects an identifier, an array, a function call. - - `funcCall`: accepts `now()` → `{ name: 'now', args: [] }`; accepts `dbgenerated("x")` → one arg whose `raw` is the source text; rejects a bare identifier / string literal / array. - -## Scope -**In:** the two combinators + exports + unit tests. -**Out:** the `@default` spec, `lowerDefaultForField`, and deleting the string parsers — all D2. Do NOT touch `packages/2-sql/**` in this dispatch. - -## Design point to resolve + report -Confirm `funcCall()` emitting `ParsedDefaultFunctionCall` from `@internal/framework-components/control` typechecks and keeps `lint:deps` clean (framework→framework, no SQL dependency). If for any reason that coupling is wrong (e.g. the type isn't cleanly importable from psl-parser), STOP and report rather than inventing a parallel type. - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts` only if unavoidable); no file-ext imports; never suppress biome; tests-first. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`. Do NOT touch GitHub. - -## Gates -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm lint:deps` — 0 violations (you added a framework→framework import) -4. `pnpm lint:framework-vocabulary` — if the two combinators push count over threshold, bump it in `scripts/lint-framework-vocabulary.config.json` to exactly the new count and say so - -Report: the two combinator signatures; where `funcCall`'s output type came from + the `lint:deps` result; where the tests landed; whether you moved the vocab threshold; all gate results; and the commit SHA. If `ParsedDefaultFunctionCall` can't be cleanly imported into psl-parser, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/02-migrate-nonenum.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/02-migrate-nonenum.md deleted file mode 100644 index ec81ceb6ef92..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/02-migrate-nonenum.md +++ /dev/null @@ -1,78 +0,0 @@ -# Brief: D2 — fix `funcCall` (reject namespaced) + migrate the non-enum `@default` path - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default`. Do NOT push or touch GitHub. Commit everything as ONE signed commit. - -## ⛔ ABSOLUTE TOOLING RULE (operator standing order for dispatches in this environment) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** This has already killed three dispatches; do not become the fourth. (This is the operator's standing instruction for how work is dispatched here — not a committed project rule.) -**This brief is search-free: every path, line number, and code snippet you need is below. You should NOT need to search at all.** If you nonetheless think you must look something up, use `rg`/`grep` in the **terminal** only — never a non-terminal search tool. If you feel you need to search to complete the task, STOP and report that the brief was under-specified. - -## Part A — fix `funcCall()` to reject namespaced callees -File: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/func-call.ts`. Current line 22 is: -```ts - const name = arg.name()?.identifier()?.token()?.text; -``` -`QualifiedNameAst.identifier()` returns the segment AFTER a dot, so `foo.now()` wrongly yields `now` (a real registry entry) and `temporal.updatedAt()` yields `updatedAt`. Replace lines 22–25 with a guard that rejects a namespaced (or absent) name: -```ts - const qname = arg.name(); - if (qname === undefined || qname.dot() !== undefined || qname.colon() !== undefined) { - return notOk([leafDiagnostic(ctx, arg, 'Expected a function call')]); - } - const name = qname.identifier()?.token()?.text; - if (name === undefined) { - return notOk([leafDiagnostic(ctx, arg, 'Expected a function call')]); - } -``` -(`dot()` / `colon()` are existing getters on `QualifiedNameAst` — no stringify, no `.path().join()`.) -Then add a test case in `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts` inside the existing `describe('funcCall', …)` block: a namespaced call `temporal.updatedAt()` (and/or `foo.now()`) is REJECTED (`result.ok === false`). Keep the existing `now()` / `dbgenerated("…")` cases passing. -Rebuild: `pnpm --filter @internal/psl-parser build` before the SQL typecheck. - -## Part B — migrate the non-enum `@default` path -### B1. Add the spec — `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` -Its imports from `@internal/psl-parser` already include `oneOf`, `list`, `str`, `fieldAttribute`. Add `scalarLiteral` and `funcCall` to that import block (both are exported from `@internal/psl-parser` — shipped in D1). Then add near the other field specs: -```ts -export const defaultSpec = fieldAttribute('default', { - positional: [{ key: 'value', type: oneOf(scalarLiteral(), list(scalarLiteral()), funcCall()) }], -}); -``` -`oneOf` output type: `string | number | boolean | (string | number | boolean)[] | ParsedDefaultFunctionCall`. - -### B2. Rewrite `lowerDefaultForField` — `packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts` (currently starts line 937) -- Change its input: add `readonly field: FieldSymbol`, `readonly model: ModelSymbol`, `readonly sourceFile: SourceFile`. You may drop the current `defaultAttribute: ResolvedAttribute` param (the node is located via the field). Keep `modelName`/`fieldName` (used in messages), `columnDescriptor`, `generatorDescriptorById`, `sourceId`, `defaultFunctionRegistry`, `diagnostics`, `isList`. -- Body: - ```ts - const node = findFieldAttributeNode(field, 'default'); - if (node === undefined) return {}; - const value = interpretFieldAttribute({ node, spec: defaultSpec, model, field, sourceFile, sourceId, diagnostics }); - if (value === undefined) return {}; - ``` - (`findFieldAttributeNode` + `interpretFieldAttribute` are exported from `./sql-attribute-specs` — import them.) -- Shape-switch on `value`: - - `Array.isArray(value)` → **array default**. If `isList`: `return { defaultValue: { kind: 'literal', value: [...value] } };`. If NOT `isList`: push `PSL_INVALID_DEFAULT_VALUE` with message `` `Unsupported default value "${...}"` `` (ruling: keep this exact code+message for array-on-scalar) and `return {}`. For the message's interpolated text, render the value however the old `PSL_INVALID_DEFAULT_VALUE` branch did — a readable form of the array is fine. - - else if `typeof value === 'object'` (a `ParsedDefaultFunctionCall`) → **function default**. Feed it to the existing registry call: `lowerDefaultFunctionWithRegistry({ call: value, registry: defaultFunctionRegistry, context: { sourceId, modelName, fieldName, columnCodecId: columnDescriptor.codecId } })`, then the existing `if (!lowered.ok) …`, storage-vs-generated branch, and the three generator-applicability/codec checks — **verbatim** from the current code (lines ~1009–1061). (The list-execution-default guard lives in the caller and is unchanged.) - - else (primitive `string | number | boolean`) → **scalar literal**. If `isList`: push `PSL_LIST_DEFAULT_NOT_ARRAY` (message unchanged) and `return {}`. Else `return { defaultValue: { kind: 'literal', value } };`. -- **Remove** the hand-rolled exactly-one-positional check at the top of the current function (the `namedEntries.length > 0 || positionalEntries.length !== 1` block emitting `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`). The spec's single positional param now enforces arity via the engine (`PSL_INVALID_ATTRIBUTE_SYNTAX` on missing/extra/named args). This case is untested; no test edit expected for it. -- **Preserve verbatim**: `PSL_UNKNOWN_DEFAULT_FUNCTION`, `PSL_INVALID_DEFAULT_APPLICABILITY`, `PSL_LIST_DEFAULT_NOT_ARRAY`, and the registry/codec/applicability logic. - -### B3. Update the call site — `packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts` line 479 -The `lowerDefaultForField({...})` call is inside `collectResolvedFields`, where `model`, `field`, and `input.sourceFile` are in scope. Pass `field`, `model`, `sourceFile: input.sourceFile`; drop `defaultAttribute` if you removed that param. Leave the sibling `lowerEnumDefaultForField(...)` call (line ~471) untouched. - -### B4. Delete the dead parsers (after confirming zero callers with `rg` in the TERMINAL) -- `psl-column-resolution.ts`: `parseDefaultLiteralValue` (line 884), `parseListDefaultExpression` (918), `decodeLiteralElement` (911), and the `ListDefaultParse` type (906). -- `default-function-registry.ts`: `parseDefaultFunctionCall` (line 123) — and `splitTopLevelArgs` (line 62) if it has no other caller. **RETAIN** `lowerDefaultFunctionWithRegistry`, the registry, and `ParsedDefaultFunctionCall`. - -## Test edits (exact — do NOT over-shift) -Only these change: -- **AC5g** — `packages/2-sql/2-authoring/contract-psl/test/interpreter.defaults.test.ts` line 946 (`rejects @default(temporal.updatedAt()) …`). With Part A, `funcCall` rejects the namespaced call → the kit's `PSL_INVALID_ATTRIBUTE_SYNTAX`. Change the asserted `code` (line 969) to `'PSL_INVALID_ATTRIBUTE_SYNTAX'`, drop the `message: stringContaining('temporal.updatedAt()')` line (the kit message won't contain the source text), and update the test title/comment to say it's rejected as invalid attribute syntax. -- **These MUST stay unchanged** (they are registry / semantic, NOT the arg-count guard): the `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT` assertions at lines ~277/282/287 (registry rejecting `uuid`/`nanoid`/`dbgenerated` args) and ~315 (optional-field execution default). Do NOT touch them — the migration does not change those code paths (a one-positional `@default(uuid(2))` still reaches the registry, which still emits `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`). - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts`, narrowed — narrow on `Array.isArray(value)` then `typeof value === 'object'`; the primitive branch is the remaining `string|number|boolean`); no file-ext imports; never suppress biome; tests-first for the funcCall guard. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. Do NOT touch the enum path `lowerEnumDefaultForField`. - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary`; `pnpm lint:deps` - -Report: the funcCall guard + its new test; the `defaultSpec` + shape-switch + `lowerDefaultForField`'s new signature; confirmation via terminal `rg` that the four/five deleted helpers have zero callers (and registry retained); the AC5g edit; explicit confirmation you did NOT touch the registry/optional-field `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT` tests; all gate results; and the commit SHA. If anything is not covered by this brief, STOP and report — do NOT search with a non-terminal tool. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/03-migrate-enum.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/03-migrate-enum.md deleted file mode 100644 index da9ea67560a0..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/03-migrate-enum.md +++ /dev/null @@ -1,93 +0,0 @@ -# Brief: D3 — kit `bareIdentifier()` + migrate the enum `@default` path - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default`. Do NOT push or touch GitHub. Commit as ONE signed commit. - -## ⛔ ABSOLUTE TOOLING RULE (operator standing order for dispatches here) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run** (it has already killed dispatches). This brief is SEARCH-FREE: every path, line, and snippet is inline. You should not need to search. If you must confirm something, use `rg`/`grep` in the **terminal** only. If you feel you cannot proceed without searching, STOP and report "brief under-specified" — do not touch any non-terminal search tool. - -## Part A — add the `bareIdentifier()` combinator -New file `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/bare-identifier.ts`. Model it on the sibling `entity-ref.ts` (same directory), which parses a bare `IdentifierAst` → its name. Difference: a neutral label and no "entity" framing: -```ts -import type { PslDiagnostic } from '@internal/framework-components/psl-ast'; -import { notOk, ok, type Result } from '@internal/utils/result'; -import { IdentifierAst } from '../../syntax/ast/identifier'; -import type { ArgType } from '../types'; -import { leafDiagnostic } from './diagnostic'; - -// A bare identifier (e.g. an enum member name) → its text. No validation; the -// caller decides what the identifier must resolve to. -export function bareIdentifier(): ArgType { - return { - kind: 'bareIdentifier', - label: 'an identifier', - parse: (arg, ctx): Result => { - if (!(arg instanceof IdentifierAst)) { - return notOk([leafDiagnostic(ctx, arg, 'Expected an identifier')]); - } - const name = arg.name(); - if (name === undefined) return notOk([leafDiagnostic(ctx, arg, 'Expected an identifier')]); - return ok(name); - }, - }; -} -``` -Export it from `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts` (add a line next to the other combinator exports, e.g. `export { bareIdentifier } from '../attribute-spec/combinators/bare-identifier';`). Add a unit test in `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts` (new `describe('bareIdentifier', …)`): accepts a bare identifier (returns its text); rejects a string literal, a number, and a function call. Rebuild: `pnpm --filter @internal/psl-parser build`. - -## Part B — migrate the enum `@default` path -### B1. Spec — `packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts` -Add `bareIdentifier` to the `@internal/psl-parser` import, and add: -```ts -export const enumDefaultSpec = fieldAttribute('default', { - positional: [{ key: 'member', type: bareIdentifier() }], -}); -``` - -### B2. Rewrite `lowerEnumDefaultForField` — `packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts` (currently lines 43–99) -Current body does: exactly-one-positional check (`PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`); `isQuotedString`/`isFunctionCall` regex rejection (`PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME`); member match against `input.enumHandle.enumMembers` (`PSL_ENUM_UNKNOWN_DEFAULT_MEMBER`); success → the member's value as a literal default. -Rewrite it to: -- Change its input: drop `defaultAttribute`; add `readonly field: FieldSymbol`, `readonly model: ModelSymbol`, `readonly sourceFile: SourceFile`. Keep `modelName`, `fieldName`, `enumHandle`, `sourceId`, `diagnostics`. -- Body: - ```ts - const node = findFieldAttributeNode(field, 'default'); - if (node === undefined) return {}; - const member = interpretFieldAttribute({ node, spec: enumDefaultSpec, model, field, sourceFile, sourceId, diagnostics }); - if (member === undefined) return {}; // arg-shape errors already pushed by the engine/bareIdentifier - const match = enumHandle.enumMembers.find((m) => m.name === member); - if (!match) { - const validNames = enumHandle.enumMembers.map((m) => m.name).join(', '); - diagnostics.push({ - code: 'PSL_ENUM_UNKNOWN_DEFAULT_MEMBER', - message: `Field "${modelName}.${fieldName}" @default(${member}) does not name a member of ${enumHandle.enumName}. Valid members: ${validNames}.`, - sourceId, span: nodePslSpan(node.syntax, sourceFile), - }); - return {}; - } - return { defaultValue: { kind: 'literal', value: blindCast(match.value) } }; - ``` - (`interpretFieldAttribute` + `findFieldAttributeNode` are already imported in this file; `nodePslSpan` is exported from `@internal/psl-parser` — import if not already present.) -- The `isQuotedString`/`isFunctionCall` regex block and the exactly-one-positional guard are **gone** — the `bareIdentifier()` spec + the engine's single-positional param now enforce those shapes (a quoted string / function call / array / missing-or-extra arg fails to `PSL_INVALID_ATTRIBUTE_SYNTAX`). -- **Keep** `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` (semantic — member not in the enum). - -### B3. Update the call site — `psl-field-resolution.ts` line 471 -The `lowerEnumDefaultForField({...})` call (inside the same branch as the D2-updated `lowerDefaultForField` call) has `model`, `field`, `input.sourceFile` in scope. Pass `field`, `model`, `sourceFile: input.sourceFile`; drop `defaultAttribute`. Leave the outer `defaultAttribute ?` presence check on line 469 as-is. - -## Test edits (exact) -In `packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts`: -- Line ~927 (`quoted raw value @default("low") … emits diagnostic`) and line ~949 (`function default @default(uuid()) … emits diagnostic`): these now fail at the spec (`bareIdentifier` rejects a string literal / function call) → change the asserted `code` from `'PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME'` to `'PSL_INVALID_ATTRIBUTE_SYNTAX'`. Update each test's title/comment to reflect that the shape is now rejected as invalid attribute syntax. -- Line ~903 (`non-member identifier … @default(Critical)`): **unchanged** — `bareIdentifier` accepts `Critical`, the interpreter matches against the enum and still emits `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` (message names `Critical` + `Priority`). Verify it stays green. -If `rg` (terminal) finds any other test asserting `PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME`, update it the same way. Do NOT touch `parseDefaultFunctionCall` or `default-function-registry.test.ts` — that cleanup is a separate dispatch (D4). - -## Scope -**In:** `bareIdentifier()` + test + export; `enumDefaultSpec`; the `lowerEnumDefaultForField` rewrite + call-site threading; the two enum test shifts. **Out:** `parseDefaultFunctionCall`/`splitTopLevelArgs` deletion (D4); the non-enum path (done in D2); Mongo. - -## Constraints -No `any`; keep the single existing `blindCast` for the enum member value (it is pre-existing and justified); no other bare `as`; no file-ext imports; never suppress biome; tests-first for `bareIdentifier`. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary`; `pnpm lint:deps` - -Report: the `bareIdentifier` signature + its test; the `enumDefaultSpec` + the rewritten `lowerEnumDefaultForField` signature/body; the two enum test shifts + confirmation `@default(Critical)` stays `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER`; confirmation you did NOT touch `parseDefaultFunctionCall`/its test; all gate results; and the commit SHA. If anything isn't covered here, STOP and report — do not use a non-terminal search tool. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/04-delete-legacy-funccall-parser.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/04-delete-legacy-funccall-parser.md deleted file mode 100644 index 6683cda57959..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/04-delete-legacy-funccall-parser.md +++ /dev/null @@ -1,50 +0,0 @@ -# Brief: D4 — delete the legacy `parseDefaultFunctionCall` string parser + refactor its test - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default`. Do NOT push or touch GitHub. Commit as ONE signed commit. This is the slice's final cleanup dispatch. - -## ⛔ ABSOLUTE TOOLING RULE (operator standing order for dispatches here) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** For any lookup use `rg`/`grep` in the **terminal** only. **Reading a named file with the file reader is fine and expected** (it is not "searching"). If you feel you cannot proceed without the search tool, STOP and report "brief under-specified." - -## Context -`funcCall()` (the kit combinator, shipped D1/D2) replaced the hand-written `parseDefaultFunctionCall` string parser for the production `@default` path. `parseDefaultFunctionCall` now has **no production caller** — only its own unit test uses it (as an input-builder for testing `lowerDefaultFunctionWithRegistry`, which STAYS). Remove the dead string parser and its exclusive support chain, and refactor the test to build inputs directly. - -## Part A — delete the dead parser from `packages/2-sql/2-authoring/contract-psl/src/default-function-registry.ts` -Delete these (all confirmed used ONLY by the string parser — verify with terminal `rg` before each if you like): -- `parseDefaultFunctionCall` (currently ~lines 123–168) -- `splitTopLevelArgs` (~line 62) -- `createSpanFromBase` (~line 48) and `resolveSpanPositionFromBase` (~line 14) -- the `DefaultFunctionArgument` interface (~line 9) -- the `import type { PslSpan } from '@internal/psl-parser';` (line 7) — it is used ONLY by the deleted helpers; drop it (confirm with `rg "PslSpan" ` on the file after deleting). - -**RETAIN** (unchanged): the imports of `ControlMutationDefaultRegistry` / `DefaultFunctionLoweringContext` / `LoweredDefaultResult` / `ParsedDefaultFunctionCall`, `formatSupportedFunctionList` (~line 170), and `lowerDefaultFunctionWithRegistry` (~line 182). After deletion, `default-function-registry.ts` should contain only those two functions + their imports. - -## Part B — refactor `packages/2-sql/2-authoring/contract-psl/test/default-function-registry.test.ts` -**Read the whole file first** (it is ~286 lines). It has two kinds of tests: -1. **Tests OF `parseDefaultFunctionCall`'s parsing** (e.g. `parseDefaultFunctionCall('uuid', span)` → `undefined`, `'uuid(4'` → `undefined`, `'4uuid()'` → `undefined`, trailing/empty-arg cases, and the "parses `X(a, b)` into a call" cases). **Delete these** — they test a parser that no longer exists. The equivalent parsing behaviour now lives in `funcCall()` and is covered by `attribute-spec-combinators.test.ts` in psl-parser; do not port them. -2. **Tests OF `lowerDefaultFunctionWithRegistry`** (they call `parseDefaultFunctionCall('cuid(2)', span)` etc. only to build a `ParsedDefaultFunctionCall` input, then assert on `lowerDefaultFunctionWithRegistry(...)`). **Keep these**, but replace the input construction: build the `ParsedDefaultFunctionCall` as an explicit object literal via a small local helper at the top of the file, e.g. - ```ts - function call(name: string, args: readonly string[]): ParsedDefaultFunctionCall { - const span = createSpan(); - return { name, raw: `${name}(${args.join(', ')})`, args: args.map((raw) => ({ raw, span })), span }; - } - ``` - (Import `ParsedDefaultFunctionCall` as a type from `@internal/framework-components/control`. Reuse the file's existing `createSpan()` helper for spans — the exact offsets don't matter to these registry-lowering assertions, only the `name`/`args` do.) Then `parseDefaultFunctionCall('cuid(2)', createSpan())` becomes `call('cuid', ['2'])`, `parseDefaultFunctionCall('mystery()', createSpan())` becomes `call('mystery', [])`, `parseDefaultFunctionCall('nanoid(16, 32)', createSpan())` becomes `call('nanoid', ['16', '32'])`, etc. -- Remove `parseDefaultFunctionCall` from the file's import (keep `lowerDefaultFunctionWithRegistry`). -- Every retained registry-lowering assertion must still pass unchanged (same codes/messages) — you are only changing how the input call object is built. - -## Scope -**In:** deleting the dead parser + its exclusive helpers from src; refactoring its test (delete parsing tests, rebuild lowering-test inputs via the local `call()` helper). **Out:** everything else — the `@default` specs (D2/D3), the enum path, Mongo. Touch only `default-function-registry.ts` and `default-function-registry.test.ts`. - -## Constraints -No `any`; no bare `as` (the `call()` helper needs none); no file-ext imports; never suppress biome. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. - -## Gates (all must pass) -1. `pnpm --filter @internal/sql-contract-psl typecheck` -2. `pnpm --filter @internal/sql-contract-psl test` — `default-function-registry.test.ts` green (fewer tests, since the parsing tests are gone); everything else unchanged -3. `pnpm fixtures:check` — clean -4. `pnpm lint:framework-vocabulary`; `pnpm lint:deps` -5. Terminal `rg -n "parseDefaultFunctionCall|splitTopLevelArgs|createSpanFromBase|resolveSpanPositionFromBase" packages/2-sql` → zero - -You should NOT need to touch `@internal/psl-parser` here. If you do, STOP and report. - -Report: confirmation of the src deletions + that `formatSupportedFunctionList`/`lowerDefaultFunctionWithRegistry` remain; the `call()` test helper + how many parsing tests you deleted; the `rg`-zero result; all gate results; and the commit SHA. If reading the test file reveals a `parseDefaultFunctionCall` use that ISN'T cleanly one of the two categories above, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/05-dynamic-nonenum.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/05-dynamic-nonenum.md deleted file mode 100644 index 9771227292c9..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/05-dynamic-nonenum.md +++ /dev/null @@ -1,56 +0,0 @@ -# Brief: D5 — `funcCall(name)` + `num()` + dynamic non-enum `@default` spec - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. - -## ⛔ TOOLING RULE (operator standing order) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** This brief is SEARCH-FREE: every path, line, and snippet is inline. Use `rg`/`grep` in the **terminal** only if you must confirm something. Reading a named file with the file reader is fine. If you feel you can't proceed without the search tool, STOP and report "brief under-specified." - -## Context -This evolves the *static* non-enum `@default` spec (shipped earlier in this PR) into a **dynamically composed** one, built per field from the registry + `isList`. Operator decisions baked in: literals stay flexible; `funcCallFrom` is not built (compose `oneOf(funcCall(name))`); unknown-function-name and array-on-scalar/scalar-on-list become **grammar** failures (`PSL_INVALID_ATTRIBUTE_SYNTAX`) — Option A. Enum path is D6; don't touch it. - -## Part A — kit changes (in `@internal/psl-parser`) -1. **`funcCall(name)`** — make the existing `funcCall` name-pinned. File `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/func-call.ts`. Today `export function funcCall(): ArgType` matches any (unqualified) call. Change it to `export function funcCall(name: string): ArgType` and, after the existing unqualified-name extraction (`const name = qname.identifier()?.token()?.text;` — rename the local to `calleeName` to avoid shadowing the param), add: `if (calleeName !== name) return notOk([leafDiagnostic(ctx, arg, \`Expected ${name}()\`)]);`. Keep the raw-arg capture unchanged. Update the `funcCall` unit tests in `attribute-spec-combinators.test.ts` to pass a name (e.g. `funcCall('now')` accepts `now()`, rejects `uuid()` and `foo.now()`). -2. **`num()`** — new atom `combinators/num.ts`: `ArgType` accepting ANY `NumberLiteralExprAst` (incl. floats — do NOT add an integer guard; that's what `int()` is for). Model on `int.ts` but drop the `Number.isInteger` check; label `'number'`; message `'Expected a number literal'`. Export from `src/exports/index.ts`. Unit-test it (accepts `5` and `1.5`; rejects a string / bool / identifier). - -## Part B — dynamic non-enum spec (`packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts`) -Replace the static `defaultSpec` (currently lines 143–145: `export const defaultSpec = fieldAttribute('default', { positional: [{ key: 'value', type: oneOf(scalarLiteral(), list(scalarLiteral()), funcCall()) }] });`) with a builder: -```ts -export function buildDefaultSpec(input: { - readonly isList: boolean; - readonly registry: ControlMutationDefaultRegistry; -}) { - const literal = () => oneOf(str(), num(), bool()); - const funcArms = [...input.registry.keys()].map((name) => funcCall(name)); - const valueArms = input.isList - ? [list(literal()), ...funcArms] - : [str(), num(), bool(), ...funcArms]; - return fieldAttribute('default', { positional: [{ key: 'value', type: oneOf(...valueArms) }] }); -} -``` -Add `num` to the `@internal/psl-parser` import; `oneOf`/`list`/`str`/`bool`/`funcCall`/`fieldAttribute` are already imported. Import `ControlMutationDefaultRegistry` as a type from `@internal/framework-components/control`. **Typing note:** the `oneOf(...valueArms)` spread of a heterogeneous array may not infer a clean tuple/union `OutOf`. If TS widens or errors, either construct `valueArms` with an explicit `ArgType[]` annotation, or wrap with a narrow `blindCast<…, 'reason'>` on the composed arg-type (mirror how `oneOf` itself uses `blindCast` internally). **No bare `as`.** Report the approach you took. Remove the old `defaultSpec` export and (if unused elsewhere) drop `scalarLiteral` from this file's imports — but do NOT delete the `scalar-literal.ts`/`bare-identifier.ts` combinators yet (that's D7; `enumDefaultSpec` still uses `bareIdentifier` until D6). - -## Part C — rewire `lowerDefaultForField` (`packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts`, ~line 880) -Currently it interprets the static `defaultSpec` and shape-switches (lines 896–988). Change: -- Build the spec dynamically: `const spec = buildDefaultSpec({ isList: input.isList ?? false, registry: input.defaultFunctionRegistry });` and pass `spec` (not the static `defaultSpec`) to `interpretFieldAttribute`. Update the import from `./sql-attribute-specs` (`buildDefaultSpec` instead of `defaultSpec`). -- The shape-switch simplifies because the grammar now guarantees shape ⇔ field kind: - - `Array.isArray(value)` → return `{ defaultValue: { kind: 'literal', value: [...value] } }` (list field guaranteed — the list arm only exists for list fields). **Delete** the old `!isList → PSL_INVALID_DEFAULT_VALUE` branch (lines ~914–920) — a non-list array is now a grammar failure. - - `typeof value === 'object'` → the registry path — keep lines ~923–977 **verbatim** (`lowerDefaultFunctionWithRegistry` + the three applicability/codec checks). - - else (primitive) → `return { defaultValue: { kind: 'literal', value } }`. **Delete** the old `isList → PSL_LIST_DEFAULT_NOT_ARRAY` branch (lines ~979–987) — a scalar on a list field is now a grammar failure. -- Leave `lowerDefaultFunctionWithRegistry` untouched. Its unknown-function branch (`PSL_UNKNOWN_DEFAULT_FUNCTION`) is now unreachable from this path (only known-name calls match a `funcCall(name)` arm) but is still exercised by its own direct unit test — that's fine, do NOT remove it or its test. - -## Test edits (`packages/2-sql/2-authoring/contract-psl/test/interpreter.defaults.test.ts`) -- The `PSL_UNKNOWN_DEFAULT_FUNCTION` assertion (~line 272) is now reached via the grammar (unknown callee → no `funcCall(name)` arm → `oneOf` fails): change its `code` to `'PSL_INVALID_ATTRIBUTE_SYNTAX'` and drop/relax the `message: stringContaining('…')` if it named the source text (the kit message is `Expected one of: …`). Update the test title/comment. -- Find (terminal `rg`) any test feeding an array default to a **non-list** field, or a scalar default to a **list** field, asserting `PSL_INVALID_DEFAULT_VALUE` / `PSL_LIST_DEFAULT_NOT_ARRAY`: those now assert `PSL_INVALID_ATTRIBUTE_SYNTAX`. Update them. -- **Do NOT touch** the registry arg-validation tests (`PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT` for `uuid(2)`/`nanoid(16,32)`) or the optional-field test — a known-name call still reaches the registry, which still validates args unchanged. Also do NOT touch `default-function-registry.test.ts`. - -## Constraints -No `any`; no bare `as` (use `blindCast`/`castAs` from `@internal/utils/casts`, narrowed — the `oneOf` spread typing is the only likely spot); no file-ext imports; never suppress biome; tests-first for the kit atoms. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. Do NOT touch the enum path (`lowerEnumDefaultForField`, `enumDefaultSpec`, `bareIdentifier`). - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary` (bump threshold to the new count if `num()` moves it); `pnpm lint:deps` - -Report: the `funcCall(name)` + `num()` signatures + tests; the `buildDefaultSpec` shape + how you resolved the `oneOf`-spread typing; the `lowerDefaultForField` shape-switch after removing the two branches; which tests shifted to `PSL_INVALID_ATTRIBUTE_SYNTAX` (and confirmation the registry arg-validation tests are untouched); all gate results; and the commit SHA. If the `oneOf` spread cannot type without a broad cast, STOP and report the options. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/06-dynamic-enum.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/06-dynamic-enum.md deleted file mode 100644 index cd2d253c0295..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/06-dynamic-enum.md +++ /dev/null @@ -1,45 +0,0 @@ -# Brief: D6 — dynamic enum `@default` spec (`oneOf(identifier(member)…)`) - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. - -## ⛔ TOOLING RULE (operator standing order) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** SEARCH-FREE brief: every path/line/snippet inline. `rg`/`grep` in the **terminal** only if needed; reading a named file is fine. Can't proceed without searching → STOP and report "brief under-specified." - -## Context -Evolve the enum `@default` path to build its spec dynamically from the enum's members, folding member-validity into the grammar (operator: Option A — the old semantic `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` becomes a `PSL_INVALID_ATTRIBUTE_SYNTAX` grammar failure). Non-enum path (D5) is done; don't touch it. - -## Part A — `buildEnumDefaultSpec` (`packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts`) -Replace the static `enumDefaultSpec` (currently lines 172–174: `export const enumDefaultSpec = fieldAttribute('default', { positional: [{ key: 'member', type: bareIdentifier() }] });`) with: -```ts -export function buildEnumDefaultSpec(memberNames: readonly string[]) { - const [first, ...rest] = memberNames.map((name) => identifier(name)); - // memberNames is non-empty for any real enum; guard defensively. - const arms: readonly [ArgType, ...ArgType[]] = first === undefined ? [identifier('')] : [first, ...rest]; - return fieldAttribute('default', { positional: [{ key: 'member', type: oneOf(...arms) }] }); -} -``` -`identifier` is already imported (used by `controlModelSpec`). `oneOf`'s non-empty-tuple constraint is satisfied by the `[first, ...rest]` tuple annotation (same pattern D5 used for `buildDefaultSpec`; no casts). Remove the static `enumDefaultSpec` and, if now unused in this file, drop `bareIdentifier` from the imports (do NOT delete the `bare-identifier.ts` combinator yet — that's D7). -- **Edge:** a real enum always has ≥1 member; the `first === undefined` guard keeps typing total for the degenerate empty case (it produces a spec that matches nothing meaningful — acceptable, empty enums can't have a valid default anyway). -- **Verify (terminal `rg`):** no enum **list** default (`SomeEnum[] @default([...])`) is exercised today — the current `lowerEnumDefaultForField` is member-only. If none exists, keep the spec member-only (no `list` arm). If one does, STOP and report. - -## Part B — rewire `lowerEnumDefaultForField` (`packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts`, lines 45–91) -- Build the spec from the enum's member names: `const spec = buildEnumDefaultSpec(enumHandle.enumMembers.map((m) => m.name));` and pass it to `interpretFieldAttribute` instead of the static `enumDefaultSpec` (update the `./sql-attribute-specs` import: `buildEnumDefaultSpec` in, `enumDefaultSpec` out). -- After a successful interpret, `interpreted.member` is guaranteed to be one of the enum's member names (the grammar enforced it), so `enumHandle.enumMembers.find((m) => m.name === interpreted.member)` always resolves. **Delete** the `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` diagnostic branch (lines ~71–80) — an unknown member is now a grammar failure. Keep a defensive `if (!match) return {};` (no diagnostic) if the type-narrowing needs it, then return the member value via the existing single `blindCast` (unchanged). -- `nodePslSpan` may become unused here — drop the import if so. - -## Test edits (`packages/2-sql/2-authoring/contract-psl/test/interpreter.enum.test.ts`) -- The `@default(Critical)` (non-member) test (~line 903, asserting `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` at ~line 918): `Critical` is no longer a spec arm → `oneOf` fails → `PSL_INVALID_ATTRIBUTE_SYNTAX`. Change the asserted `code`; relax/drop the `message` matches on `Critical`/`Priority` (the kit message is `Expected one of: Low | High`). Update the test title/comment. -- The two enum shape tests already shifted in D3 (`@default("low")` / `@default(uuid())` → `PSL_INVALID_ATTRIBUTE_SYNTAX`) stay as-is. -- If `rg` finds any other `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` assertion, update it the same way. - -## Constraints -No `any`; keep only the one pre-existing `blindCast` for the enum member value; no other bare `as`; no file-ext imports; never suppress biome. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. Do NOT touch the non-enum path or `bare-identifier.ts` (D7). - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary`; `pnpm lint:deps` - -Report: the `buildEnumDefaultSpec` shape + typing approach; the rewired `lowerEnumDefaultForField` (branch removed); the enum test shift; confirmation no enum-list default exists (or STOP); all gate results; and the commit SHA. If anything isn't covered here, STOP and report — no non-terminal search tool. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/07-remove-superseded-combinators.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/07-remove-superseded-combinators.md deleted file mode 100644 index 81ce3e23b849..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/07-remove-superseded-combinators.md +++ /dev/null @@ -1,50 +0,0 @@ -# Brief: D7 — remove `scalarLiteral`/`bareIdentifier`; fix the `num.ts` vocab line - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. Final dispatch of the slice. - -## ⛔ TOOLING RULE (operator standing order) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** Use `rg`/`grep` in the **terminal** only; reading a named file with the file reader is fine. Can't proceed without searching → STOP and report "brief under-specified." - -## Context -D5/D6 made the `@default` specs dynamic; `scalarLiteral()` and `bareIdentifier()` now have no callers (D5 replaced `scalarLiteral` with `oneOf(str(), num(), bool())`; D6 replaced `bareIdentifier` with `oneOf(identifier(member)…)`). Remove them. Also fix a pre-existing framework-vocabulary regression that D5 introduced: `num.ts`'s doc comment contains the flagged word "constraint". - -## Part A — fix the vocab line (`packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/num.ts`) -Its doc comment currently reads (line ~7–8): -``` -// … a general number literal (any number, incl. floats). For an -// integer-only constraint use `int()`. -``` -The word **"constraint"** is a flagged family/target-vocabulary term (the framework must stay family-blind), which pushed `lint:framework-vocabulary` to 906/905. Reword to drop it, e.g.: -``` -// A general number literal — any number, including floats. Use `int()` when only -// integer literals are allowed. -``` -(Keep the meaning; just avoid "constraint".) - -## Part B — delete the two superseded combinators -- Delete `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/scalar-literal.ts` and `.../combinators/bare-identifier.ts`. -- Remove their two `export { … }` lines from `packages/1-framework/2-authoring/psl-parser/src/exports/index.ts` (`scalarLiteral` line ~53, `bareIdentifier` line ~39). -- In `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts`, **read the file** and delete the `describe('scalarLiteral', …)` and `describe('bareIdentifier', …)` blocks (their coverage is obsolete — `str`/`num`/`bool`/`identifier` are tested individually and the composition is exercised by the SQL default suites). -- Confirm via terminal `rg` there are no remaining callers: `rg -n "scalarLiteral|bareIdentifier" packages` → only nothing (or, at most, stale *comments* in `interpreter.enum.test.ts` mentioning `bareIdentifier()` — reword those comments to describe the current `oneOf(identifier(member))` shape; they're not code). - -## Part C — vocab threshold hygiene -After A + B, run `pnpm lint:framework-vocabulary`. Removing the two combinator files may drop the count below 905. Set `threshold` in `scripts/lint-framework-vocabulary.config.json` to **exactly** the resulting count (keep the ratchet tight — lower it if the count dropped; it should be ≤ 905 now). Report the final count. - -## Part D — ADR note (no code) -ADR 231 is left **untouched** (operator instruction); the deviation (dropped `funcCallFrom` for `oneOf(funcCall(name))`; deferred `matchingScalarLiteral`) is already recorded in the slice spec (`projects/typed-attribute-parsers/slices/sql-default/spec.md`). Nothing to do here beyond confirming that record exists. - -## Scope -**In:** the `num.ts` comment reword; deletion of `scalar-literal.ts` + `bare-identifier.ts` (+ exports + their unit-test blocks); vocab threshold adjustment; rewording stale `bareIdentifier` comments in the enum test. **Out:** any behaviour change — this is pure dead-code removal + a comment/threshold fix. No spec or interpreter logic changes. - -## Constraints -No `any`; no bare `as`; no file-ext imports; never suppress biome. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**` (except reading), `.agents/**`. Do NOT touch GitHub. - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary` — **now green** (count ≤ threshold; you set threshold to the exact count); `pnpm lint:deps` -6. `rg -n "scalarLiteral|bareIdentifier" packages` → zero (comments reworded) - -Report: the `num.ts` reword; confirmation both combinators + their exports + test blocks are gone and `rg` is zero; the final vocab count + the threshold you set; all gate results; and the commit SHA. If either combinator turns out to still have a real caller, STOP and report. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/08-funccall-signatures-kit.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/08-funccall-signatures-kit.md deleted file mode 100644 index f060199cb77f..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/08-funccall-signatures-kit.md +++ /dev/null @@ -1,55 +0,0 @@ -# Brief: D8 — kit foundation for typed `funcCall(name, sig)` arguments - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. - -## ⛔ TOOLING RULE (operator standing order) -**NEVER call the regex/codebase-search MCP tool — it HANGS and deadlocks the run.** SEARCH-FREE brief. Use `rg`/`grep` in the **terminal** only; reading a named file is fine. Can't proceed → STOP and report "brief under-specified." - -## Why -ADR 231's `funcCall(sig)` specifies a function call's **arguments** via the recursive positional/named combinator model — each argument parsed by a combinator. Today `funcCall(name)` pins only the callee name and captures args as raw strings, deferring all arg parsing to the registry's imperative `lower`. This dispatch builds the **framework foundation** so a later dispatch can give each default function a real argument signature. Framework-only; no SQL/adapter changes here (those are D9/D10). - -## Part A — extract a shared argument-binding helper -File `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/interpret.ts`. `interpretAttribute` currently inlines the positional/named binding loop (walk `attrNode.argList()?.args()`, bind each to a positional slot or named param, dup/excess/missing diagnostics, `finalizeAbsentKey` for optional/required). Extract that binding into an exported helper: -```ts -export function interpretArgs( - args: Iterable, - spec: { readonly positional: readonly PositionalParam[]; readonly named: Readonly>> }, - ctx: InterpretCtx, - span: PslSpan, // for missing/excess diagnostics -): Result, readonly PslDiagnostic[]> -``` -Move the binding + `finalizeAbsentKey` logic verbatim into it (it returns the bound `output` record or the accumulated diagnostics). Then `interpretAttribute` becomes: build `attributeSpan`, call `interpretArgs(attrNode.argList()?.args() ?? [], spec, ctx, attributeSpan)`, and on success apply `spec.refine` (unchanged) before returning. Behaviour identical — all existing psl-parser + sql tests stay green with no edits. - -## Part B — `funcCall(name, sig?)` parses args via the signature -File `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/func-call.ts`. Give `funcCall` an optional signature: -```ts -export interface FuncCallSig { - readonly positional?: readonly PositionalParam[]; - readonly named?: Readonly>>; -} -export function funcCall(name: string, sig?: FuncCallSig): ArgType<{ readonly fn: string } & Record> -``` -- Keep the existing guards (reject non-`FunctionCallAst`, namespaced callee, name mismatch). -- **If `sig` is provided:** call `interpretArgs(arg.args(), { positional: sig.positional ?? [], named: sig.named ?? {} }, ctx, nodePslSpan(arg.syntax, ctx.sourceFile))`; on failure return its diagnostics; on success return `ok({ fn: name, ...boundArgs })` — the `fn` discriminant plus the typed argument record. -- **If `sig` is omitted:** keep today's behaviour exactly (capture raw `{ raw, span }` args, return the `ParsedDefaultFunctionCall` shape) so existing callers (`buildDefaultSpec`'s `funcCall(name)`) are unchanged until D9 migrates them. (Overload or a union return is fine; if the two return shapes make one signature awkward, use two overloads: `funcCall(name)` → `ArgType`, `funcCall(name, sig)` → `ArgType<{ fn: string } & …>`.) -- Update the `funcCall` unit tests: keep the no-sig cases; add a sig case (e.g. `funcCall('nanoid', { positional: [{ key: 'size', type: optional(int({ min: 2, max: 255 })) }] })` accepts `nanoid(16)` → `{ fn: 'nanoid', size: 16 }`, accepts `nanoid()` → `{ fn: 'nanoid' }`, rejects `nanoid(1)` and `nanoid(1, 2)`). - -## Part C — the literal atoms the signatures need -- **`num(value)`** — extend the existing `num()` (`combinators/num.ts`) to accept an optional pinned value: `num(): ArgType` and `num(value: number): ArgType` (matches only that number literal; label the number). Mirror how `str()` vs `str(value)` / `identifier(name)` pin. Unit-test the pinned form (`num(4)` accepts `4`, rejects `7`/`"4"`). -- **`int({ min, max })`** — extend `int()` (`combinators/int.ts`) to accept optional bounds: `int(opts?: { min?: number; max?: number })`, still integer-only, additionally rejecting out-of-range with a clear message (`Expected an integer between {min} and {max}`). Unit-test the bounded form. -(These realize ADR 231's `num(value)` and `int({ min, max })`. Keep the unbounded/unpinned forms working.) - -## Scope -**In:** `interpretArgs` extraction; `funcCall(name, sig?)`; `num(value)` + `int({min,max})` options; their unit tests. **Out:** the registry contract, the adapters, `buildDefaultSpec` wiring, `ParsedDefaultFunctionCall` arg-shape change (all D9/D10). Do NOT touch `packages/2-sql` or `packages/3-targets`. - -## Constraints -No `any`; no bare `as` (the `interpretArgs`/`funcCall` output records may need a narrow `blindCast` exactly as `interpretAttribute` already does for its dynamic output — reuse that justified pattern, narrowly); no file-ext imports; never suppress biome; tests-first. `git commit -s` (DCO), explicit staging, no amend, **no push**. Read-only on `projects/**`, `.agents/**`. Do NOT touch GitHub. - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/psl-parser build` -2. `pnpm --filter @internal/psl-parser typecheck` and `pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck` and `pnpm --filter @internal/sql-contract-psl test` (must stay green with NO edits — `funcCall(name)` no-sig behaviour is unchanged) -4. `pnpm fixtures:check` — clean -5. `pnpm lint:framework-vocabulary` (bump threshold to exact count if it moves); `pnpm lint:deps` - -Report: the `interpretArgs` signature + confirmation `interpretAttribute` is behaviour-identical; the `funcCall(name, sig?)` shape (overloads?) + its new sig test; the `num(value)` / `int({min,max})` additions + tests; how you handled the output-record typing (no bare `as`); all gate results; and the commit SHA. If the two `funcCall` return shapes can't coexist cleanly, STOP and report the options. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/09-typed-funccall-signatures.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/09-typed-funccall-signatures.md deleted file mode 100644 index 352221efe44e..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/09-typed-funccall-signatures.md +++ /dev/null @@ -1,340 +0,0 @@ -# Brief: D9 — typed `funcCall` end-to-end (SQL `@default` argument signatures) - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. Tests-first. - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool. It HANGS and deadlocks the run — it has killed multiple dispatches.** This brief is SEARCH-FREE: every path, symbol, and snippet you need is inline. For any lookup, use `rg` / `grep` **in the terminal** only. Reading a named file with the file reader is fine. If something is genuinely under-specified, STOP and report "brief under-specified: " — do not reach for the search tool. - -## Why -ADR 231's `funcCall(sig)` specifies a function call's **arguments** through the recursive positional/named combinator model — each argument parsed by a combinator, so arg shape/arity/range is a **grammar** concern and the parsed value is **typed**. Today the SQL `@default` registry uses `funcCall(name)` (raw): it captures args as verbatim strings and every registry `lower` re-parses them imperatively (`parseIntegerArgument`, `parseStringLiteral`, `expectNoArgs`, count checks). D8 already shipped the kit foundation (`interpretArgs`, `funcCall(name, sig)`, `num(value)`, `int({min,max})`). This dispatch wires it through: each default function declares an **argument signature**, `buildDefaultSpec` builds `funcCall(name, signature)`, and each `lower` consumes the **typed** args. Arg-shape errors become grammar failures (`PSL_INVALID_ATTRIBUTE_SYNTAX`, per operator Option A); genuinely-semantic errors keep their codes. - -This is **one atomic change** (retyping the registry `lower` contract forces the framework type, `buildDefaultSpec`, both adapters, and the test stub registry to move together). Land it as one signed commit with a green tree. - -## Layering constraint you must respect -- `FuncCallSig` / combinator types (`ArgType`, `PositionalParam`, `Param`) live in `@internal/psl-parser` (authoring layer, `1-framework/2-authoring`). -- `ControlMutationDefaultEntry` and friends live in `@internal/framework-components` (**core** layer, `1-framework/1-core`). **Core cannot import authoring** (`pnpm lint:deps` will fail). -- Therefore the entry's `signature` field is typed `unknown` in core (an opaque payload); the SQL family narrows it back with **one** justified `blindCast` in `buildDefaultSpec`. The **typed call** passed to `lower` (`{ fn, span, args }`) is plain-structural and core-safe. - ---- - -## Phase 1 — Framework types (`@internal/framework-components`) - -File `packages/1-framework/1-core/framework-components/src/shared/mutation-default-types.ts`. - -1. Add a new exported interface (place it just above `ControlMutationDefaultEntry`, reusing the existing `SourceSpan` already defined in this file): -```ts -// The typed form of a parsed default-function call: the `fn` discriminant, the call-site span -// (for lowering diagnostics), and the argument record already parsed + validated by the -// function's `funcCall(name, signature)` combinator. Replaces the raw `ParsedDefaultFunctionCall` -// on the registry lowering path — the registry no longer re-parses argument source text. -export interface TypedDefaultFunctionCall { - readonly fn: string; - readonly span: SourceSpan; - readonly args: Readonly>; -} -``` -2. Change `ControlMutationDefaultEntry` (currently lines ~96-102) to: -```ts -export interface ControlMutationDefaultEntry { - // The function's argument signature (a `FuncCallSig` from `@internal/psl-parser`), consumed by - // the SQL family's `buildDefaultSpec` to build a typed `funcCall(name, signature)` arm. Typed - // `unknown` here because `FuncCallSig` lives in the authoring layer, which the core framework - // cannot import; the registering family owns the entries and narrows it back. - readonly signature?: unknown; - readonly lower: (input: { - readonly call: TypedDefaultFunctionCall; - readonly context: DefaultFunctionLoweringContext; - }) => LoweredDefaultResult; - readonly usageSignatures?: readonly string[]; -} -``` -3. **Leave `ParsedDefaultFunctionCall`, `DefaultFunctionRegistryEntry`, `DefaultFunctionLoweringHandler`, `DefaultFunctionRegistry` defined** (still the return type of no-signature `funcCall(name)` and out of scope to remove). - -File `packages/1-framework/1-core/framework-components/src/exports/control.ts` — add `TypedDefaultFunctionCall` to the `export type { … } from '../shared/mutation-default-types'` list (keep it alphabetical: it sorts after `SourceSpan`). - -Gate: `pnpm --filter @internal/framework-components build && pnpm --filter @internal/framework-components typecheck && pnpm --filter @internal/framework-components test`. - ---- - -## Phase 2 — psl-parser `funcCall` typed output (`@internal/psl-parser`) - -File `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/func-call.ts`. - -Today `TypedFuncCall = { readonly fn: string } & Record` and `typedFuncCall` returns `ok({ ...bound.value, fn: name })` (flat). Change to a **nested** shape carrying the call span: - -1. Add `import type { PslSpan } from '@internal/framework-components/psl-ast';` -2. Replace the `TypedFuncCall` type with: -```ts -// The typed record a signed call binds to: the `fn` discriminant, the call-site span, and the -// parsed argument record produced by `interpretArgs`. -export interface TypedFuncCall { - readonly fn: string; - readonly span: PslSpan; - readonly args: Readonly>; -} -``` -3. Change `typedFuncCall` to compute the span once and nest the args: -```ts -function typedFuncCall(name: string, sig: FuncCallSig): ArgType { - return { - kind: 'funcCall', - label: 'function call', - parse: (arg, ctx): Result => { - const guard = matchCallee(arg, name, ctx); - if (!guard.ok) return guard; - const span = nodePslSpan(guard.value.syntax, ctx.sourceFile); - const bound = interpretArgs( - guard.value.args(), - { name, positional: sig.positional ?? [], named: sig.named ?? {} }, - ctx, - span, - ); - if (!bound.ok) return notOk(bound.failure); - return ok({ fn: name, span, args: bound.value }); - }, - }; -} -``` -Leave `rawFuncCall` (the no-signature overload) and the `funcCall` overload signatures unchanged. - -### Test update — `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts` -Only the `describe('funcCall with a signature', …)` block (currently ~L691-740) changes; the raw-`funcCall` block above it is unchanged. Update the two success assertions to the nested shape (the span is a real object — assert with `toMatchObject`, not `toEqual`): -- `nanoid(16)` case: `if (result.ok) expect(result.value).toMatchObject({ fn: 'nanoid', args: { size: 16 } });` -- `nanoid()` case: `if (result.ok) expect(result.value).toMatchObject({ fn: 'nanoid', args: {} });` -The three rejection cases (`nanoid(1)`, `nanoid(16, 2)`, `cuid(16)`) are unchanged (`ok === false`). - -Gate: `pnpm --filter @internal/psl-parser build && pnpm --filter @internal/psl-parser typecheck && pnpm --filter @internal/psl-parser test`. - ---- - -## Phase 3 — SQL contract wiring (`@internal/sql-contract-psl`) - -### 3a. `src/sql-attribute-specs.ts` — `buildDefaultSpec` -- Imports: add `castAs`? NO — use `blindCast` (already imported? check the top; if not, `import { blindCast } from '@internal/utils/casts';`). Add `import type { FuncCallSig, TypedFuncCall } from '@internal/psl-parser';`. Remove the `ParsedDefaultFunctionCall` import if it becomes unused. -- Change the `DefaultArgValue` type alias: replace `ParsedDefaultFunctionCall` with `TypedFuncCall`: -```ts -type DefaultArgValue = - | string - | number - | boolean - | (string | number | boolean)[] - | TypedFuncCall; -``` -- Change the func arm construction from `.keys()`/`funcCall(name)` to `.entries()`/typed: -```ts -const funcArms = [...input.registry.entries()].map(([name, entry]) => - funcCall( - name, - blindCast< - FuncCallSig, - 'The registry stores each signature opaquely as `unknown` because FuncCallSig lives in the authoring layer that core cannot name; the SQL family owns these entries and guarantees every one declares a FuncCallSig.' - >(entry.signature), - ), -); -``` -Everything else in `buildDefaultSpec` (the `literal`, `valueArms`, `oneOf`) stays; `funcArms` is now `ArgType[]`, still a member of the `DefaultArgValue` union. - -### 3b. `src/default-function-registry.ts` — `lowerDefaultFunctionWithRegistry` -- Imports: replace `ParsedDefaultFunctionCall` with `TypedDefaultFunctionCall` (from `@internal/framework-components/control`). -- Change the `call` param type to `TypedDefaultFunctionCall`, and `input.call.name` → `input.call.fn` (two sites: the `registry.get(...)` and the diagnostic message). `input.call.span` is unchanged (TypedDefaultFunctionCall has `span`). -- Keep the unknown-function branch + `formatSupportedFunctionList` as-is (it is defensive: in production the name is always a registry key because `buildDefaultSpec` only builds arms for registry keys, but the unit test exercises this branch directly). - -### 3c. `src/psl-column-resolution.ts` — `lowerDefaultForField` (~L879-974) -The `value` in the object branch is now a `TypedFuncCall`. Verify it compiles; the existing code already does `lowerDefaultFunctionWithRegistry({ call: value, … })` and reads `value.span` for the `PSL_INVALID_DEFAULT_APPLICABILITY` diagnostics — both still valid. Update the `ControlMutationDefaultRegistry`/import types only if the compiler complains. No behavioural change here. - -### 3d. Test stub registry — `test/fixtures.ts` -This file has a hand-written **parallel** registry (`createBuiltinLikeControlMutationDefaults`, ~L434-610) plus the helpers `invalidArgumentDiagnostic` (~L161), `executionGenerator` (~L177), `expectNoArgs` (~L191), `parseIntegerArgument` (~L206), `parseStringLiteral` (~L218). Migrate it to **mirror the adapters exactly** (see Phase 4 for the per-function lower bodies — use the postgres bodies verbatim, since this stub is postgres-flavoured): -- Imports (top of file, ~L20-28): drop `ParsedDefaultFunctionCall`; add `TypedDefaultFunctionCall` to the `@internal/framework-components/control` type import. Add `import { int, num, oneOf, optional, str } from '@internal/psl-parser';` and `import type { FuncCallSig } from '@internal/psl-parser';` (the file already imports other things from `@internal/psl-parser`). -- Change `invalidArgumentDiagnostic`'s `span: ParsedDefaultFunctionCall['span']` → `span: TypedDefaultFunctionCall['span']`. -- **Delete** `expectNoArgs`, `parseIntegerArgument`, `parseStringLiteral`. Keep `invalidArgumentDiagnostic`, `executionGenerator`. -- In `createBuiltinLikeControlMutationDefaults`, give each of the 7 entries a `signature` (see the FuncCallSig table in Phase 4) and rewrite each `lower` to read typed args (see the lower bodies in Phase 4 — postgres variants). The entries here are `[name, { signature, lower, usageSignatures }]`; keep the `usageSignatures` values already present. - -### 3e. `test/interpreter.defaults.test.ts` — code shifts -Only the block `it('returns diagnostics for unsupported default functions and invalid arguments', …)` (~L249-293) changes. The model has `cuid()`, `uuid(5)`, `nanoid(1)`, `dbgenerated("")`. After this change: -- `cuid()`, `uuid(5)`, `nanoid(1)` are **grammar** failures. Because they sit inside the outer `oneOf(str(), num(), bool(), …funcCall)`, a funcCall arm that matches the callee but fails on its args causes the **outer `oneOf`** to backtrack and emit its own generic `PSL_INVALID_ATTRIBUTE_SYNTAX` "Expected one of: …" diagnostic (this coarse-diagnostic behaviour is the ADR's explicit accepted trade-off, ADR 231 § "Alternatives and function calls"). So these three surface as `PSL_INVALID_ATTRIBUTE_SYNTAX` with a generic message. -- `dbgenerated("")` **parses** (empty string is a valid `str()`), then `lowerDbgenerated`'s empty check fires → **`PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`** (semantic, preserved). - -Rewrite the `expect(...).toEqual(expect.arrayContaining([...]))` to: -```ts -expect(result.failure.diagnostics).toEqual( - expect.arrayContaining([ - expect.objectContaining({ code: 'PSL_INVALID_ATTRIBUTE_SYNTAX', sourceId: 'schema.prisma' }), - expect.objectContaining({ - code: 'PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT', - sourceId: 'schema.prisma', - message: expect.stringContaining('dbgenerated'), - }), - ]), -); -``` -Leave every other block in this file unchanged (they use valid calls — `uuid()`, `uuid(7)`, `nanoid()`, `dbgenerated("…")`, `now()` — which still lower correctly; and the `token String? @default(nanoid())` optional-execution-default block at ~L295 whose diagnostic is an applicability check, not arg-parsing). **Run the whole file and fix only what actually fails.** - -### 3f. `test/default-function-registry.test.ts` — heavy rework -This file drives `lowerDefaultFunctionWithRegistry` **directly**, bypassing the grammar. Post-migration, arity/shape is grammar-enforced, so cases that fed malformed arg *counts/shapes* to `lower` test impossible states and must go. -- Change the `call(...)` helper to build the typed shape: -```ts -import type { TypedDefaultFunctionCall } from '@internal/framework-components/control'; -function call(fn: string, args: Record = {}): TypedDefaultFunctionCall { - return { fn, span: createSpan(), args }; -} -``` -- Custom registries typed `Map` → `Map` (import `ControlMutationDefaultEntry` instead of `DefaultFunctionRegistryEntry`); their `lower: () => ({...})` bodies are fine (they ignore the call). -- KEEP + migrate to typed calls: - - `cuid(2)` → `cuid2` (`call('cuid', { version: 2 })`). **Delete** the `cuid()` rejection half of that test (arity is now grammar; `lowerCuid` no longer rejects). - - `derives unknown-function supported list from registry keys` (`call('mystery')`) — keep. - - `uses contributed usage signatures when provided` — keep. - - `lists supported signatures for unknown generator-like function names` (`call('uuidv7')`) — keep. -- **Delete** (these tested deleted imperative parsing / now-grammar arity): - - `returns diagnostics for nanoid and dbgenerated invalid argument shapes` (nanoid too-many, dbgenerated no-arg, dbgenerated non-string). - - `preserves escaped dbgenerated string content` — the un-quoting/un-escaping now lives in `str()` at parse time; this direct-lower test no longer exercises it. Delete it (its concern is covered by psl-parser `str()` tests + the sqlite canonicalization tests). -- OPTIONAL keep: a dbgenerated empty-string semantic case — `call('dbgenerated', { expression: '' })` → `ok:false`, code `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`. - -### 3g. `test/composed-mutation-defaults.test.ts` -Not expected to reference the call shape, but **run it** and fix any fallout from the entry-type change (e.g. a registry literal now needing `signature`). Keep changes minimal. - -Gate: `pnpm --filter @internal/sql-contract-psl typecheck && pnpm --filter @internal/sql-contract-psl test`. - ---- - -## Phase 4 — Adapters - -### FuncCallSig table (identical for postgres + sqlite) -```ts -const nowSig: FuncCallSig = {}; -const autoincrementSig: FuncCallSig = {}; -const ulidSig: FuncCallSig = {}; -const uuidSig: FuncCallSig = { positional: [{ key: 'version', type: optional(oneOf(num(4), num(7))) }] }; -const cuidSig: FuncCallSig = { positional: [{ key: 'version', type: num(2) }] }; -const nanoidSig: FuncCallSig = { positional: [{ key: 'size', type: optional(int({ min: 2, max: 255 })) }] }; -const dbgeneratedSig: FuncCallSig = { positional: [{ key: 'expression', type: str() }] }; -``` - -### Lower bodies (read typed args off `input.call.args`; no imperative parsing; no casts — use `typeof` guards / literal comparisons on `unknown`) -```ts -// no-arg functions ignore the call entirely: -function lowerAutoincrement(): LoweredDefaultResult { - return { ok: true, value: { kind: 'storage', defaultValue: { kind: 'function', expression: 'autoincrement()' } } }; -} -function lowerNow(): LoweredDefaultResult { - return { ok: true, value: { kind: 'storage', defaultValue: { kind: 'function', expression: 'now()' } } }; -} -function lowerUlid(): LoweredDefaultResult { - return executionGenerator('ulid'); -} -// version is grammar-guaranteed 4 | 7 | undefined: -function lowerUuid(input: { call: TypedDefaultFunctionCall; context: DefaultFunctionLoweringContext }): LoweredDefaultResult { - return input.call.args.version === 7 ? executionGenerator('uuidv7') : executionGenerator('uuidv4'); -} -// version is grammar-guaranteed to be 2 (required num(2)): -function lowerCuid(): LoweredDefaultResult { - return executionGenerator('cuid2'); -} -// size is grammar-guaranteed number(2..255) | undefined: -function lowerNanoid(input: { call: TypedDefaultFunctionCall; context: DefaultFunctionLoweringContext }): LoweredDefaultResult { - const size = input.call.args.size; - return typeof size === 'number' ? executionGenerator('nanoid', { size }) : executionGenerator('nanoid'); -} -``` -Postgres `lowerDbgenerated` (expression grammar-guaranteed string; empty check is the only surviving semantic guard): -```ts -function lowerDbgenerated(input: { call: TypedDefaultFunctionCall; context: DefaultFunctionLoweringContext }): LoweredDefaultResult { - const expression = input.call.args.expression; - if (typeof expression !== 'string' || expression.trim().length === 0) { - return invalidArgumentDiagnostic({ - context: input.context, - span: input.call.span, - message: 'Default function "dbgenerated" argument cannot be empty.', - }); - } - return { ok: true, value: { kind: 'storage', defaultValue: { kind: 'function', expression } } }; -} -``` -Sqlite `lowerDbgenerated` (same, plus the existing `NOW_SYNONYMS` canonicalization on the trimmed value): -```ts -function lowerDbgenerated(input: { call: TypedDefaultFunctionCall; context: DefaultFunctionLoweringContext }): LoweredDefaultResult { - const raw = input.call.args.expression; - if (typeof raw !== 'string' || raw.trim().length === 0) { - return invalidArgumentDiagnostic({ - context: input.context, - span: input.call.span, - message: 'Default function "dbgenerated" argument cannot be empty.', - }); - } - const trimmed = raw.trim(); - const expression = NOW_SYNONYMS.has(trimmed.toLowerCase()) ? 'now()' : trimmed; - return { ok: true, value: { kind: 'storage', defaultValue: { kind: 'function', expression } } }; -} -``` - -### 4a. `packages/3-targets/6-adapters/postgres/src/core/control-mutation-defaults.ts` -- Import: replace `ParsedDefaultFunctionCall` with `TypedDefaultFunctionCall` in the `@internal/framework-components/control` import. Add `import { int, num, oneOf, optional, str } from '@internal/psl-parser';` and `import type { FuncCallSig } from '@internal/psl-parser';` (this package already depends on `@internal/psl-parser`). -- Change `invalidArgumentDiagnostic`'s `span: ParsedDefaultFunctionCall['span']` → `TypedDefaultFunctionCall['span']`. -- **Delete** `expectNoArgs`, `parseIntegerArgument`, `parseStringLiteral`. Keep `invalidArgumentDiagnostic`, `executionGenerator`. -- Replace the 7 `lowerX` bodies with the Phase-4 versions. -- Add the FuncCallSig consts and put `signature: ` on each entry in `postgresDefaultFunctionRegistryEntries` (keep the existing `usageSignatures`). The `satisfies ReadonlyArray` stays. - -### 4b. `packages/3-targets/6-adapters/sqlite/src/core/control-mutation-defaults.ts` -- **Add `@internal/psl-parser` to this package's `package.json` dependencies** (`"@internal/psl-parser": "workspace:0.14.0"`) — it is not currently a dependency and `pnpm lint:deps` will fail without it. After editing package.json run `pnpm install --lockfile-only` (or `pnpm install`) so the workspace link + lockfile update. -- Imports: it currently derives `type ParsedDefaultFunctionCall = Parameters[0]['call'];` (L26) — **delete that alias**. Add `TypedDefaultFunctionCall` to the `@internal/framework-components/control` import (the file already imports `ControlMutationDefaultEntry, MutationDefaultGeneratorDescriptor` from there). Add `import { int, num, oneOf, optional, str } from '@internal/psl-parser';` and `import type { FuncCallSig } from '@internal/psl-parser';`. Keep the `DefaultFunctionLoweringContext` / `LoweredDefaultResult` imports (adjust source if needed so they still resolve). -- Change `invalidArgumentDiagnostic`'s `span: ParsedDefaultFunctionCall['span']` → `TypedDefaultFunctionCall['span']`. -- **Delete** `expectNoArgs`, `parseIntegerArgument`, `parseStringLiteral`. Keep `NOW_SYNONYMS`, `invalidArgumentDiagnostic`, `executionGenerator`. -- Replace the 7 `lowerX` bodies (use the sqlite `lowerDbgenerated`), add FuncCallSig consts, put `signature` on each entry in `sqliteDefaultFunctionRegistryEntries`. - -### 4c. Adapter tests — `…/postgres/test/control-mutation-defaults.test.ts` and `…/sqlite/test/control-mutation-defaults.test.ts` -Both build calls by hand and invoke `handler.lower(...)`. The `lower` input is now `TypedDefaultFunctionCall`, and arity/shape rejections no longer come from `lower`. -- Replace the `makeCall` + `arg` helpers with: -```ts -function makeCall(fn: string, args: Record = {}) { - return { fn, span: stubSpan, args }; -} -``` - (delete `arg`, and in the postgres file delete `spanlessArg`.) -- **postgres** — keep & migrate these success cases to `makeCall(fn, argsRecord)`: - - `autoincrement()`→`makeCall('autoincrement')`; `now()`→`makeCall('now')`; `ulid()`→`makeCall('ulid')`; `nanoid()`→`makeCall('nanoid')` - - `uuid()`→`makeCall('uuid')` (uuidv4); `uuid(7)`→`makeCall('uuid', { version: 7 })`; `uuid(4)`→`makeCall('uuid', { version: 4 })` (uuidv4) - - `cuid(2)`→`makeCall('cuid', { version: 2 })` (cuid2) - - `nanoid(16)`→`makeCall('nanoid', { size: 16 })` (params.size 16) - - `dbgenerated("gen_random_uuid()")`→`makeCall('dbgenerated', { expression: 'gen_random_uuid()' })` - - keep `contains all builtin default function entries` and everything from `describe('createPostgresMutationDefaultGeneratorDescriptors'…)` onward unchanged. - - keep the empty-string semantic rejection: `makeCall('dbgenerated', { expression: '' })` → `{ ok: false }`. - - **Delete** every rejection test that exercised arity/shape via `lower`: cuid()-without-version, dbgenerated()-without-arg, uuid invalid-version, uuid too-many-args, nanoid out-of-range, autoincrement-with-args, cuid invalid-version, cuid too-many-args, nanoid too-many-args, dbgenerated non-string, now-with-args, ulid-with-args, uuid non-numeric, nanoid non-integer, and **all** the `spanlessArg` fallback tests. -- **sqlite** — migrate the dbgenerated canonicalization tests. `str()` already un-quotes, so pass the **un-quoted** value: - - `makeCall('dbgenerated', { expression: 'CURRENT_TIMESTAMP' })` → `now()` - - `makeCall('dbgenerated', { expression: 'current_timestamp' })` → `now()` - - `makeCall('dbgenerated', { expression: "datetime('now')" })` → `now()` - - `makeCall('dbgenerated', { expression: 'random()' })` → `random()` - - keep the descriptor/runtime blocks unchanged. - -### 4d. Coverage -Deleting the imperative helpers + their tests changes per-file branch coverage for `control-mutation-defaults.ts`. Run each adapter package's coverage and confirm no per-file threshold regression: -- `pnpm --filter @internal/adapter-postgres test` then `pnpm --filter @internal/adapter-postgres test:coverage` (or the package's coverage script — check `package.json` `scripts`). -- `pnpm --filter @internal/adapter-sqlite test` (+ coverage script). -If a surviving branch (e.g. `lowerNanoid`'s `typeof size` false path, `lowerUuid`'s non-7 path, `lowerDbgenerated`'s empty path) is uncovered, the success tests above should cover it — if coverage still dips, add one minimal `lower` test for the missing branch (do NOT re-introduce arity tests). - -Gate: `pnpm --filter @internal/adapter-postgres typecheck && test`; `pnpm --filter @internal/adapter-sqlite typecheck && test`. - ---- - -## Scope -**In:** everything in Phases 1-4. **Out:** removing `ParsedDefaultFunctionCall` / `DefaultFunctionRegistryEntry` / `DefaultFunctionLoweringHandler` (leave defined); Mongo `@default`; the language-server autocomplete; ADR 231 edits (the orchestrator updates the ADR + slice spec separately). - -## Constraints -No `any`; **no bare `as`** — the only cast is the single justified `blindCast` in `buildDefaultSpec`; the `lower` bodies must be cast-free (use `typeof`/literal comparisons on `unknown`). No file-extension imports. Never suppress biome. Tests-first. `git commit -s` (DCO), explicit staging, no `--amend`, **no push** (the orchestrator pushes). Read-only on `projects/**` and `.agents/**`. Do NOT touch GitHub. - -## Gates (all must pass, in order) -1. `pnpm --filter @internal/framework-components build && pnpm --filter @internal/framework-components typecheck && pnpm --filter @internal/framework-components test` -2. `pnpm --filter @internal/psl-parser build && pnpm --filter @internal/psl-parser typecheck && pnpm --filter @internal/psl-parser test` -3. `pnpm --filter @internal/sql-contract-psl typecheck && pnpm --filter @internal/sql-contract-psl test` -4. `pnpm --filter @internal/adapter-postgres typecheck && pnpm --filter @internal/adapter-postgres test` (+ its coverage script) -5. `pnpm --filter @internal/adapter-sqlite typecheck && pnpm --filter @internal/adapter-sqlite test` (+ its coverage script) -6. `pnpm fixtures:check` — clean -7. `pnpm lint:framework-vocabulary` (bump the threshold in the linter config to the exact new count ONLY if kit-comment wording moved it; prefer rewording), and `pnpm lint:deps` (must be 0 — this is where a missing `psl-parser` dep on adapter-sqlite would surface). - -## Report back -- The final `TypedDefaultFunctionCall` (core) + `TypedFuncCall` (psl-parser) shapes and confirmation `funcCall`'s no-signature path is unchanged. -- The `buildDefaultSpec` diff (the single `blindCast` and the `.entries()` map). -- Confirmation each adapter `lower` is cast-free; the deleted helpers (`expectNoArgs`/`parseIntegerArgument`/`parseStringLiteral`) per file. -- Which test cases you deleted vs migrated in `default-function-registry.test.ts` and the two adapter test files, and the final assertion shape for the `interpreter.defaults.test.ts` block. -- `pnpm lint:deps` result; the `adapter-sqlite` package.json dep addition; vocab threshold (moved or not). -- All gate results and the commit SHA. -- If anything forces a bare `as`, an `any`, a second cast, or a red gate you can't resolve from this brief — STOP and report the blocker with the exact error. Do NOT use the search tool to work around it. diff --git a/projects/typed-attribute-parsers/slices/sql-default/dispatches/10-remove-raw-funccall.md b/projects/typed-attribute-parsers/slices/sql-default/dispatches/10-remove-raw-funccall.md deleted file mode 100644 index d46a74777c36..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/dispatches/10-remove-raw-funccall.md +++ /dev/null @@ -1,75 +0,0 @@ -# Brief: D10 — remove the dead raw `funcCall` lineage - -> Fresh implementer. Slice `sql-default`, branch `tml-2956-sql-default` (PR #938). Do NOT push or touch GitHub. ONE signed commit. Tests-first. - -## ⛔ TOOLING RULE (operator standing order — non-negotiable) -**NEVER call the regex / codebase-search MCP tool. It HANGS and deadlocks the run.** SEARCH-FREE brief. Use `rg` / `grep` **in the terminal** only; reading a named file is fine. If under-specified, STOP and report — do not reach for the search tool. - -## Why -After D9 the SQL `@default` `funcCall` is fully typed: `buildDefaultSpec` builds `funcCall(name, signature)` for every registry entry, so the **no-signature `funcCall(name)` overload (`rawFuncCall`) has no production caller** — only its own unit tests exercise it. Its return type `ParsedDefaultFunctionCall`, and the legacy raw types kept alive only through it plus one indirection (`DefaultFunctionLoweringHandler`, `DefaultFunctionRegistryEntry`, `DefaultFunctionRegistry`, the `DefaultFunctionArgument` helper), are now vestigial. Remove the whole raw lineage so `funcCall` has a single typed shape. Type/test-level only — no runtime behaviour change. - -Before removing each type, confirm zero remaining importers with a terminal `rg` (e.g. `rg -rn "ParsedDefaultFunctionCall" packages`); if any live consumer outside the files below turns up, STOP and report rather than widening the change. - -## Changes - -### 1. `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/func-call.ts` -- Delete the two `funcCall` overload declarations and the union-return implementation, and delete `rawFuncCall` entirely. Collapse to a single required-signature function (body identical to today's `typedFuncCall`): -```ts -export function funcCall(name: string, sig: FuncCallSig): ArgType { - return { - kind: 'funcCall', - label: 'function call', - parse: (arg, ctx): Result => { - const guard = matchCallee(arg, name, ctx); - if (!guard.ok) return guard; - const span = nodePslSpan(guard.value.syntax, ctx.sourceFile); - const bound = interpretArgs( - guard.value.args(), - { name, positional: sig.positional ?? [], named: sig.named ?? {} }, - ctx, - span, - ); - if (!bound.ok) return notOk(bound.failure); - return ok({ fn: name, span, args: bound.value }); - }, - }; -} -``` -- Remove now-unused imports: `ParsedDefaultFunctionCall` (line 1) and `printSyntax` (line 7). Keep `nodePslSpan`, `interpretArgs`, `FunctionCallAst`/`ExpressionAst` (used by `matchCallee`), `ArgType`/`InterpretCtx`/`Param`/`PositionalParam`, `leafDiagnostic`, `notOk`/`ok`/`Result`. Keep `FuncCallSig`, `TypedFuncCall`, `matchCallee`. -- Replace the stale doc comment above `funcCall` (the "Without a signature the call is captured into the framework `ParsedDefaultFunctionCall` shape…" paragraph) with a short one describing the single typed form: it pins the callee `name`, parses the call's arguments through `sig`, and binds them into `{ fn, span, args }`. - -### 2. `packages/1-framework/2-authoring/psl-parser/test/attribute-spec-combinators.test.ts` -The `describe('funcCall', …)` block (~L599-689) drives the no-signature form. **Migrate the guard cases** to the empty signature `funcCall(name, {})` (the callee guards still apply), and **delete the two raw-arg-capture cases** which no longer have meaning: -- Keep + migrate: "accepts a nullary call whose callee matches the pinned name" (`funcCall('now', {})` on `now()` → assert `result.value` `toMatchObject({ fn: 'now', args: {} })` instead of `.name`/`.raw`/`.args` array), "rejects a call whose callee differs" (`funcCall('now', {})` on `uuid()`), "rejects a bare identifier" (`now`), "rejects a string literal" (`"now"`), "rejects an array literal" (`[1]`), "rejects a namespaced callee" (`foo.now()`) — all still `funcCall('now', {})`. -- **Delete**: "captures each argument as its verbatim source text" and "preserves a numeric argument as source text" (raw capture is gone). -- Leave the `describe('funcCall with a signature', …)` block unchanged. - -### 3. `packages/1-framework/1-core/framework-components/src/shared/mutation-default-types.ts` -Delete these now-unused declarations: `DefaultFunctionArgument` (interface), `ParsedDefaultFunctionCall` (interface), `DefaultFunctionLoweringHandler` (type), `DefaultFunctionRegistryEntry` (interface), `DefaultFunctionRegistry` (type). Keep everything else (`SourceSpan`, `SourceDiagnostic`, `DefaultFunctionLoweringContext`, `LoweredDefaultValue`, `LoweredDefaultResult`, `TypedDefaultFunctionCall`, `MutationDefaultGeneratorDescriptor`, `ControlMutationDefaultEntry`, `ControlMutationDefaultRegistry`, `ControlMutationDefaults`). - -### 4. `packages/1-framework/1-core/framework-components/src/exports/control.ts` -Remove `ParsedDefaultFunctionCall`, `DefaultFunctionLoweringHandler`, `DefaultFunctionRegistry`, `DefaultFunctionRegistryEntry` from the `export type { … } from '../shared/mutation-default-types'` list. Keep `DefaultFunctionLoweringContext`, `LoweredDefaultResult`, `LoweredDefaultValue`, `SourceDiagnostic`, `SourceSpan`, `TypedDefaultFunctionCall`, and the `ControlMutationDefault*` / `MutationDefaultGeneratorDescriptor` entries. - -### 5. `packages/2-sql/2-authoring/contract-psl/src/exports/index.ts` -Remove `DefaultFunctionLoweringHandler`, `DefaultFunctionRegistry`, `DefaultFunctionRegistryEntry` from the re-export block (lines ~4-6). **Keep `DefaultFunctionLoweringContext`** (still live). - -### 6. `packages/3-targets/6-adapters/sqlite/src/core/control-mutation-defaults.ts` -- Delete `type LoweredDefaultResult = ReturnType;` (line 28). -- Change the import block (lines 14-17) that pulls `DefaultFunctionLoweringContext, DefaultFunctionLoweringHandler` from `@internal/sql-contract-psl`: drop `DefaultFunctionLoweringHandler`, and import `DefaultFunctionLoweringContext` **and** `LoweredDefaultResult` from `@internal/framework-components/control` instead (add them to the existing `@internal/framework-components/control` type import that already brings in `ControlMutationDefaultEntry, MutationDefaultGeneratorDescriptor, TypedDefaultFunctionCall`). This removes the `@internal/sql-contract-psl` import entirely from this file if nothing else uses it — verify with the file contents. (postgres already imports `DefaultFunctionLoweringContext`/`LoweredDefaultResult` from framework-components/control — align sqlite to match.) - -## Scope -**In:** the six edits above. **Out:** any behavioural change; `DefaultFunctionLoweringContext` (keep); the adapter `lower` bodies / signatures (unchanged); `projects/**`, `.agents/**` (read-only). - -## Constraints -No `any`; no bare `as`; no file-extension imports; never suppress biome; tests-first. `git commit -s` (DCO), explicit staging, no `--amend`, **no push**. Do NOT touch GitHub. - -## Gates (all green, in order) -1. `pnpm --filter @internal/framework-components build && typecheck && test` -2. `pnpm --filter @internal/psl-parser build && typecheck && test` -3. `pnpm --filter @internal/sql-contract-psl typecheck && test` -4. `pnpm --filter @internal/adapter-postgres typecheck && test` -5. `pnpm --filter @internal/adapter-sqlite typecheck && test` -6. `pnpm lint:deps` (0 violations) and `pnpm lint:framework-vocabulary` (threshold unchanged; reword rather than bump if a comment moves it) - -## Report back -The final collapsed `funcCall` signature; the list of deleted types + confirmation (via `rg`) each had no remaining importer; the psl-parser test cases migrated vs deleted; the sqlite import fix; all gate results; the commit SHA. If any live consumer of a to-be-removed type surfaces, STOP and report it rather than widening scope. diff --git a/projects/typed-attribute-parsers/slices/sql-default/plan.md b/projects/typed-attribute-parsers/slices/sql-default/plan.md deleted file mode 100644 index ed5ce60296aa..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/plan.md +++ /dev/null @@ -1,55 +0,0 @@ -# Slice: sql-default — Dispatch plan - -**Slice spec:** `projects/typed-attribute-parsers/slices/sql-default/spec.md` - -D1–D4 migrated `@default` onto **static** specs (kit; non-enum; enum; legacy-parser deletion). D5–D7 then evolve those static specs into **dynamically composed** per-field specs — **still within this PR (#938)**, per operator direction + review. `@default` is one PR; both lowering paths migrate and both end up dynamically composed. - -### D1 — Kit: `scalarLiteral()` + `funcCall()` combinators -- **Outcome:** two new leaf combinators in `@internal/psl-parser`, each unit-tested in isolation: - - `scalarLiteral()` → `ArgType`: a `StringLiteralExprAst` / `NumberLiteralExprAst` / `BooleanLiteralExprAst` → its decoded `.value()`. Rejects anything else with a kit leaf diagnostic. (Array defaults reuse the existing `list(scalarLiteral())` — no new list combinator.) - - `funcCall()` → `ArgType`: a `FunctionCallAst` → a structured call `{ name, args: [{ text, span }], span }`. **Registry-agnostic** — no name validation, so the kit does not import any SQL type. Use `FunctionCallAst.name()` (the `QualifiedNameAst` — reuse `isSimpleName`/`identifier()` structurally, no stringify) and `.args()`; render each argument's source text via the AST's decoded value / `printSyntax` (text is a legitimate output here — the SQL registry re-parses arg strings downstream). -- **Design point to resolve in this dispatch:** where the parsed-call output type lives. The SQL registry consumes `ParsedDefaultFunctionCall` (`{ name, raw, args: [{ raw, span }], span }`). Either (a) `funcCall()` emits a generic framework-level parsed-call shape and D2 adapts it to `ParsedDefaultFunctionCall` at the call site, or (b) relocate/alias `ParsedDefaultFunctionCall` to a framework type `funcCall()` can emit directly. Pick whichever keeps layering clean (framework must not depend on SQL); surface the choice in the report. -- **Builds on:** the merged attribute-spec kit + slice-2 combinators. -- **Gate:** psl-parser build + typecheck + test; `lint:framework-vocabulary` (bump threshold to the new count if the two combinators move it); `lint:deps`. - -### D2 — Migrate `@default`; delete the three string parsers -- **Outcome:** `fieldAttribute('default', { positional: [{ key: 'value', type: oneOf(scalarLiteral(), list(scalarLiteral()), funcCall()) }] })` added to `sql-attribute-specs.ts`; `lowerDefaultForField` (`psl-column-resolution.ts`) rewritten to interpret via the generic `interpretFieldAttribute` wrapper and switch on the `oneOf` output by runtime shape — **primitive → literal default, array → list default, object (parsed call) → registry path**. Every semantic rule stays: `isList` + `PSL_LIST_DEFAULT_NOT_ARRAY`, `lowerDefaultFunctionWithRegistry` + `PSL_UNKNOWN_DEFAULT_FUNCTION`, generator applicability + codec matching + preset-only guard (`PSL_INVALID_DEFAULT_APPLICABILITY`), and exactly-one-positional (now enforced by the spec's single positional param — confirm the engine's "too many positional / missing" diagnostics read acceptably, else keep an interpreter guard). -- **Deletions:** `parseDefaultLiteralValue`, `parseDefaultFunctionCall`, `parseListDefaultExpression`, plus their private helpers (`decodeLiteralElement`, the `ListDefaultParse` type) once `rg` confirms zero callers. Retain `lowerDefaultFunctionWithRegistry`, the registry, and `ParsedDefaultFunctionCall`. -- **Behaviour parity:** contract output identical; `pnpm fixtures:check` clean. `@default(garbage)` (no valid arm) now emits `PSL_INVALID_ATTRIBUTE_SYNTAX` instead of `PSL_INVALID_DEFAULT_VALUE` (operator: Option A) — update the asserting test(s). Semantic default codes unchanged. `interpreter.defaults.test.ts` (24 cases) green with only the intentional code-shift edits. -- **Builds on:** D1. -- **Gate:** psl-parser build (D1 changed the kit); sql-contract-psl typecheck + test (`interpreter.defaults.test.ts`); `pnpm fixtures:check`; `rg` gates for the three deleted helpers; `lint:framework-vocabulary`; `lint:deps`. - -### D3 — Kit `bareIdentifier()` + migrate the enum `@default` path -- **Outcome:** a `bareIdentifier()` leaf combinator in `@internal/psl-parser` (bare `IdentifierAst` → its text; neutral label "an identifier"; no validation), unit-tested. `enumDefaultSpec = fieldAttribute('default', { positional: [{ key: 'member', type: bareIdentifier() }] })` added to `sql-attribute-specs.ts`; `lowerEnumDefaultForField` (`psl-field-resolution.ts`) rewritten to interpret via the generic `interpretFieldAttribute` wrapper and match the extracted member name against `enumHandle.enumMembers`. -- **Deletions:** the inline `isQuotedString` / `isFunctionCall` regex checks + the exactly-one-positional guard in `lowerEnumDefaultForField` (now spec-enforced). Keep the `enumHandle.enumMembers` matching + `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER`. -- **Behaviour parity:** enum-member defaults resolve to the same value; `pnpm fixtures:check` clean. `@default("x")` / `@default(fn())` on an enum field now emit `PSL_INVALID_ATTRIBUTE_SYNTAX` instead of `PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME` (operator: Option A) — find + update the asserting tests (in `interpreter.enum.test.ts`; `rg` for `PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME`). `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` unchanged. -- **Builds on:** D1 (kit pattern), D2 (the `defaultSpec` plumbing + interpret wiring it mirrors). -- **Gate:** psl-parser build + test (new combinator); sql-contract-psl typecheck + test (enum-default cases); `pnpm fixtures:check`; `rg` gates; `lint:framework-vocabulary`; `lint:deps`. - -### D4 — Delete the legacy `parseDefaultFunctionCall` string parser -- **Outcome:** `parseDefaultFunctionCall` + its exclusive support chain (`splitTopLevelArgs`, `createSpanFromBase`, `resolveSpanPositionFromBase`, `DefaultFunctionArgument`) deleted from `default-function-registry.ts` (dead once `funcCall` replaced it); the registry-lowering tests refactored to build `ParsedDefaultFunctionCall` inputs via a local `call()` helper. Retain `lowerDefaultFunctionWithRegistry` + `formatSupportedFunctionList`. -- **Builds on:** D2. -- **Gate:** sql-contract-psl typecheck + test; `fixtures:check`; `rg` zero for the deleted helpers; `lint:*`. - ---- - -_The dispatches below evolve the static specs above into dynamically-composed per-field specs (operator direction + #938 review). Resolve **Open Question 1** in the slice spec (do `PSL_UNKNOWN_DEFAULT_FUNCTION` / `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` shift to `PSL_INVALID_ATTRIBUTE_SYNTAX`) before D5._ - -### D5 — `funcCall(name)` + `num()` + dynamic non-enum `@default` spec -- **Kit:** `funcCall` becomes name-pinned (`funcCall(name)`, parallel to `identifier(name)`) — matches a call with that callee, still captures raw args. Add a general `num()` number-literal atom (any number incl. floats — `int()` is integer-only and would regress `Float @default(1.5)`). Both unit-tested. -- **Outcome:** `buildDefaultSpec({ isList, registry })` composes `oneOf(str(), num(), bool(), …(isList ? [list(oneOf(str(), num(), bool()))] : []), ...registry.keys().map(funcCall))`. `lowerDefaultForField` builds it per field; the `isList` shape-switch collapses (list arm present ⇔ list field). Unknown-function-name and array-on-scalar become grammar failures (`PSL_INVALID_ATTRIBUTE_SYNTAX`), retiring the interpreter's `PSL_UNKNOWN_DEFAULT_FUNCTION` emission + the array-on-scalar branch (per OQ1). Function **arg** validation stays in the registry (`PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT` unchanged). -- **Builds on:** D2. -- **Grounding:** the `ControlMutationDefaultRegistry` key/entry accessor; `oneOf` discriminated-output typing. -- **Gate:** psl-parser build + test; sql-contract-psl typecheck + test (update unknown-function / array-on-scalar assertions); `fixtures:check`; `lint:*`. - -### D6 — Dynamic enum `@default` spec -- **Outcome:** `buildEnumDefaultSpec(members)` = `oneOf(...members.map((m) => identifier(m.name)))` (add a `list(...)` wrapper only if enum-list defaults exist — verify). `lowerEnumDefaultForField` builds it from `enumHandle.enumMembers`; member-validity becomes a grammar failure, retiring the interpreter's `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` emission (per OQ1). `bareIdentifier()` loses its last caller. -- **Builds on:** D3, D5. -- **Gate:** sql-contract-psl typecheck + test (`interpreter.enum.test.ts` member-validity assertions shift); `fixtures:check`; `lint:*`. - -### D7 — Remove the superseded combinators -- **Outcome:** delete `scalar-literal.ts` + `bare-identifier.ts` (+ exports + unit tests) once `rg` confirms zero callers. Per OQ2, amend ADR 231 (§ "Alternatives and function calls": `funcCallFrom` dropped for `oneOf(funcCall(name))`; `matchingScalarLiteral` deferred) or record the deviation as agreed. -- **Builds on:** D5, D6. -- **Gate:** psl-parser build + test; sql-contract-psl typecheck + test; `fixtures:check`; `rg` zero for `scalarLiteral`/`bareIdentifier`; `lint:framework-vocabulary` (removing combinators may move the count — adjust threshold if so); `lint:deps`. - -_(As-shipped target: `@default` composed dynamically per field from atomic combinators; `funcCallFrom`/`bareIdentifier`/`scalarLiteral` gone; function-name + enum-member validity in the grammar; literals and function-args still flexible (codec/registry-validated). All within PR #938. The language-server autocomplete payoff rides on top in a later, LS-scoped slice.)_ diff --git a/projects/typed-attribute-parsers/slices/sql-default/spec.md b/projects/typed-attribute-parsers/slices/sql-default/spec.md deleted file mode 100644 index 906d54f71b89..000000000000 --- a/projects/typed-attribute-parsers/slices/sql-default/spec.md +++ /dev/null @@ -1,107 +0,0 @@ -# Slice: sql-default - -_(In-project slice. Parent: `projects/typed-attribute-parsers/`. Split out of the `sql-attributes` slice mid-flight — see that slice's Open Question 1. Outcome it contributes: `@default` becomes spec-driven on **both** its lowering paths, completing "every SQL attribute validates its arguments through the kit".)_ - -## At a glance - -Migrate the SQL `@default` attribute off its hand-written string parsers onto declarative `AttributeSpec`s. `@default` is the long pole of the SQL migration and has **two** lowering paths, selected by field type: - -- **Non-enum fields** (`lowerDefaultForField`, `psl-column-resolution.ts`) — the value is a scalar literal, an array literal, or a function call, parsed today by `parseDefaultLiteralValue` / `parseListDefaultExpression` / `parseDefaultFunctionCall`. -- **Enum-typed fields** (`lowerEnumDefaultForField`, `psl-field-resolution.ts`) — the value is a bare enum-member identifier (`@default(ADMIN)`), parsed today by inline regex checks. - -Both migrate in this slice. - -## Chosen design - -> **Design evolution (operator direction, in-PR).** D1–D4 first shipped two **static** specs with monolithic stand-ins (`scalarLiteral()`, a generic `funcCall()`, `bareIdentifier()`) plus interpreter post-validation. Per operator direction and the #938 review, the slice then evolves — **still within this PR** — to **dynamically composed** specs built per field (D5–D7). Then, per operator direction ("funcCall still does not follow ADR spec — where are the specifications for the arguments?"), it evolves once more (**D8–D9, still within this PR**) so each default function's **arguments** are declared with combinators per ADR 231, replacing the raw-string capture + imperative re-parse. See the `## Typed funcCall (D8–D9)` section below; the dynamic design in this section is the shape D5–D7 reached, and the typed-funcCall section is the current head. - -The `@default` spec is **built per field** by `buildDefaultSpec(ctx)` / `buildEnumDefaultSpec(members)`, composed entirely from atomic combinators via `oneOf`, using the field's resolved context (the composed default-function registry, `isList`, and the enum's members). This lifts function-name and enum-member validity **into the grammar** — reducing post-validation — and makes each per-field spec a precise description a future language server can read for autocomplete. - -**Non-enum** (built from `{ isList, registry }`): - -```ts -oneOf( - str(), num(), bool(), // flexible literals (codec still type-checks the value) - ...(isList ? [list(oneOf(str(), num(), bool()))] : []), // list arm ONLY on list fields - ...[...registry.keys()].map((name) => funcCall(name)), // one arm per registered default function -) -``` - -(`num()` is a **new** general number-literal atom — any number, incl. floats. The existing `int()` is integer-only, so it can't stand in for `scalarLiteral`'s number handling without regressing `Float @default(1.5)`.) - -**Enum** (built from `enumHandle.enumMembers`): - -```ts -oneOf(...enumMembers.map((m) => identifier(m.name))) // e.g. Expected one of: Low | High -``` - -**Key design points:** -- **`funcCall(name)` replaces the generic `funcCall()`; no `funcCallFrom`.** A name-pinned `funcCall(name)` (parallel to `identifier(name)`) matches a call with that callee and captures **raw args** (flexible — `lowerDefaultFunctionWithRegistry` still validates them). `oneOf(...registry.keys().map(funcCall))`, built dynamically, enumerates the open contributed set — the composition that makes the ADR's bespoke `funcCallFrom` unnecessary (ADR principle 4). The matched arm *is* the `fn` discriminant. -- **Enum defaults are `oneOf(identifier(member)…)`** from the members — dropping `bareIdentifier()` and folding member-validity into the grammar (resolves the #938 review comment). -- **Literals stay flexible**, composed as `oneOf(str(), num(), bool())` — dropping `scalarLiteral()` for composition of atoms (resolves the other #938 comment; adds a general `num()` atom since `int()` is integer-only). No codec-typed matching (`matchingScalarLiteral` is out — Non-goals); the codec's `encodeJson` remains the literal↔type authority. -- **The `list` arm is present only on list fields** (and is the only value arm there), so array-on-scalar and scalar-on-list are grammar misses — dissolving the `isList` shape-switch and its `PSL_LIST_DEFAULT_NOT_ARRAY` / array-on-scalar `PSL_INVALID_DEFAULT_VALUE` special cases. - -**What stays semantic (in the interpreter):** function **arg** validation (`PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`, via the registry), generator applicability + codec matching + preset-only guard (`PSL_INVALID_DEFAULT_APPLICABILITY`), and literal↔codec type (codec `encodeJson`). **Moved into the grammar** (Open Question 1): unknown-function-name (`PSL_UNKNOWN_DEFAULT_FUNCTION`) and unknown-enum-member (`PSL_ENUM_UNKNOWN_DEFAULT_MEMBER`). - -## Typed funcCall (D8–D9) - -ADR 231 specifies a function call's **arguments** via the recursive positional/named combinator model. D8 shipped the kit foundation (`interpretArgs` extracted from `interpretAttribute`; `funcCall(name, sig)` overload; `num(value)`; `int({min,max})`). D9 wired it through end-to-end: - -- Each default function declares a **`FuncCallSig`** (`now`/`autoincrement`/`ulid` → `{}`; `uuid` → `optional(oneOf(num(4), num(7)))`; `cuid` → required `num(2)`; `nanoid` → `optional(int({min:2,max:255}))`; `dbgenerated` → `str()`). `buildDefaultSpec` builds `funcCall(name, signature)` per registered function. -- `funcCall(name, sig)` parses to a typed `{ fn, span, args }`. Each registry `lower` reads **typed** args (`call.args.version`, `.size`, `.expression`) cast-free via `typeof`/literal comparisons; the imperative `parseIntegerArgument` / `parseStringLiteral` / `expectNoArgs` + count checks are **deleted** from both adapters and the sql-contract-psl test stub. -- **Diagnostic shifts (operator: Option A).** Arg shape / arity / range are now grammar failures → `PSL_INVALID_ATTRIBUTE_SYNTAX`: `uuid(5)`, `uuid(4,7)`, `cuid()` (missing required — was `PSL_UNKNOWN_DEFAULT_FUNCTION` with the "use `cuid(2)`" guidance, now a generic grammar message), `cuid(3)`, `nanoid(1)`/`nanoid(300)`, `dbgenerated()`/`dbgenerated(123)`, args-on-nullary. **Coarse-diagnostic trade-off (ADR 231 § "Alternatives and function calls"):** because the funcCall arms sit inside the outer `oneOf(str(), num(), bool(), …funcCall)`, a callee-matched-but-arg-failed arm makes the outer `oneOf` backtrack and emit its own generic `Expected one of: …` message — the function-specific arg hint is lost. Accepted per the ADR. The **only** surviving semantic arg check is `dbgenerated` empty → `PSL_INVALID_DEFAULT_FUNCTION_ARGUMENT`. -- **Layering.** `FuncCallSig` is authoring-layer (`@internal/psl-parser`); the core `ControlMutationDefaultEntry` cannot name it, so `signature` is typed `unknown` in core and narrowed with **one** justified `blindCast` in `buildDefaultSpec`. The typed `{fn,span,args}` call shape is plain-structural and core-safe. -- **⚠ Breaking change — extension-authoring contract.** `ControlMutationDefaultEntry.lower` now receives a typed `TypedDefaultFunctionCall` (was `ParsedDefaultFunctionCall`), and a contributed arg-bearing default function must declare a `signature` (a no-signature entry falls to the raw `funcCall(name)` path, whose value has no `.fn`, so it won't lower). Any extension contributing default functions must migrate. Surfaced by the `default-pack-slugid` parity fixture (migrated to `signature: {}` + typed `lower`). Records a downstream-upgrade need. -- **Bundling fix.** `@internal/psl-parser` was promoted from a **dev**- to a **runtime** dependency of `adapter-postgres` so tsdown externalises (rather than bundles) the combinators; a bundled copy gave `num()`/`str()` private AST classes, so `instanceof` failed against `sql-contract-psl`-parsed nodes and every valid argumented `@default` silently failed to parse — invisible to source-resolved unit tests, caught only by the dist-consuming real-pack parity suite. -- **ADR 231 left untouched** (operator instruction); the `funcCallFrom` → `oneOf(funcCall(name, sig))` composition and the `matchingScalarLiteral` deferral remain recorded here as deviations. - -## Coherence rationale - -One outcome — "`@default` is spec-driven on both paths; the default string-parsers (three non-enum + the enum inline regex checks) are deleted; the SQL family is now entirely spec-driven." Sized as its own PR because it introduces the kit's first structured-call combinator and preserves the most semantic diagnostic codes of any SQL attribute. - -## Scope - -**In:** the non-enum `defaultSpec` + enum `enumDefaultSpec`; the interpret wiring in `lowerDefaultForField` + `lowerEnumDefaultForField`; the `scalarLiteral` + `funcCall` (D1) and `bareIdentifier` (D3) combinators with unit tests; reuse of `list()`; deletion of `parseDefaultLiteralValue`, `parseDefaultFunctionCall`, `parseListDefaultExpression` (+ `decodeLiteralElement`, the `ListDefaultParse` type) and the inline regex checks in `lowerEnumDefaultForField`. - -**Out:** -- ~~**The string-based registry internals**~~ — _superseded by D8–D9 (see `## Typed funcCall`): the registry internals are now typed; each `lower` reads combinator-parsed args and the imperative string parsers are deleted._ -- **The interpreter's semantic checks** — list-vs-scalar, registry lowering, applicability, codec matching, exactly-one-positional, and enum-member matching — all stay. -- **Mongo `@default`** — slice 3 (family). - -## Pre-investigated edge cases - -| Edge case | Disposition | Notes | -| --------- | ----------- | ----- | -| Field type selects the path | Caller already branches on `enumHandle` | Non-enum → `defaultSpec`; enum → `enumDefaultSpec`. Two spec objects, both named `'default'`. | -| Non-enum list `@default([...])` vs scalar | Semantic; stays | `isList` + `PSL_LIST_DEFAULT_NOT_ARRAY` stay in `lowerDefaultForField`. | -| Function default (`now()`, `dbgenerated("…")`) | `funcCall` → `ParsedDefaultFunctionCall`; registry lowers | `funcCall` renders each arg's **verbatim source text** (quotes preserved — `dbgenerated`'s handler re-parses the quoted string). Preserve `PSL_UNKNOWN_DEFAULT_FUNCTION`. | -| Generator applicability / codec matching | Semantic; stays | `PSL_INVALID_DEFAULT_APPLICABILITY` + preset-only guard stay. | -| Enum member not in the enum | Semantic; stays | `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` stays (interpreter matches against `enumHandle`). | -| `@default(garbage)` on a non-enum field | Now `PSL_INVALID_ATTRIBUTE_SYNTAX` (was `PSL_INVALID_DEFAULT_VALUE`) | Operator: Option A. Update the asserting test(s). | -| `@default("x")` / `@default(fn())` on an enum field | Now `PSL_INVALID_ATTRIBUTE_SYNTAX` (was `PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME`) | Operator: Option A — the shape-check moves into the `bareIdentifier()` matcher. `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` (a real member miss) is unchanged. | -| `#906` native enums | No interaction with lowering | Native-enum *types* changed column resolution, not `@default` lowering; both lowering functions are unchanged on current `main`. | - -## Slice-specific done conditions - -- [ ] Both `@default` paths validated + lowered via specs through `interpretAttribute`. -- [ ] `parseDefaultLiteralValue`, `parseDefaultFunctionCall`, `parseListDefaultExpression` deleted (`rg` each → zero); `lowerEnumDefaultForField`'s inline `isQuotedString`/`isFunctionCall` regex checks gone. Registry + `lowerDefaultFunctionWithRegistry` + `enumHandle` matching retained. -- [ ] Semantic codes preserved: `PSL_UNKNOWN_DEFAULT_FUNCTION`, `PSL_INVALID_DEFAULT_APPLICABILITY`, `PSL_LIST_DEFAULT_NOT_ARRAY`, `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER`. Shape-error codes (`PSL_INVALID_DEFAULT_VALUE`, `PSL_ENUM_DEFAULT_MUST_BE_MEMBER_NAME`) shift to `PSL_INVALID_ATTRIBUTE_SYNTAX` (operator: Option A). -- [ ] `pnpm fixtures:check` clean; `interpreter.defaults.test.ts` + the enum-default tests green; vocab green (bump threshold if kit growth moves it). -- [x] **D9 typed funcCall (commit `d895e793b`):** framework-components 469 · psl-parser 623 · sql-contract-psl 333 · adapter-postgres 675 (coverage ≥ thresholds) · adapter-sqlite 220 · integration authoring parity 50 (incl. real-pack `instanceof` parity) · `fixtures:check` clean (no drift) · `lint:deps` 0 · vocab 836=836. Signatures declared for all 7 SQL default functions; imperative arg parsers removed; extension-authoring contract change recorded above. - -## Open Questions - -_Resolved:_ (1) both lowering paths in scope (operator); (2) shape-error codes → `PSL_INVALID_ATTRIBUTE_SYNTAX` (operator: Option A); (3) `funcCallFrom` dropped for `oneOf(funcCall(name))` composed dynamically from the registry (operator); (4) literals stay flexible — `matchingScalarLiteral` deferred, codec `encodeJson` remains the value authority (operator). - -_Open (needed before D5):_ - -1. **Do `PSL_UNKNOWN_DEFAULT_FUNCTION` and `PSL_ENUM_UNKNOWN_DEFAULT_MEMBER` shift to `PSL_INVALID_ATTRIBUTE_SYNTAX`?** The dynamic spec moves both membership checks into the grammar (goal: less post-validation); `oneOf`'s "Expected one of: …" message preserves the helpful supported-set list. Recommendation: **yes, shift them** and retire the interpreter's duplicate checks. Alternative: keep the semantic codes and use the dynamic spec only for structure + autocomplete (less reduction). Needs operator confirmation. -2. **ADR 231 update.** The dynamic design drops the ADR's `funcCallFrom` for `oneOf(funcCall(name))` and defers `matchingScalarLiteral`. Decide whether D7 amends ADR 231 (§ "Alternatives and function calls") or records the deviation elsewhere (ADR currently left untouched by operator instruction). - -## References - -- Parent project: `projects/typed-attribute-parsers/spec.md`; sibling slice `slices/sql-attributes/` (the generic `interpretFieldAttribute` wrapper + `sql-attribute-specs.ts` plumbing this slice reuses). -- Non-enum path: `packages/2-sql/2-authoring/contract-psl/src/psl-column-resolution.ts` (`lowerDefaultForField`, `parseDefaultLiteralValue`, `parseListDefaultExpression`); `default-function-registry.ts` (`parseDefaultFunctionCall`, `lowerDefaultFunctionWithRegistry`, `ParsedDefaultFunctionCall`). -- Enum path: `packages/2-sql/2-authoring/contract-psl/src/psl-field-resolution.ts` (`lowerEnumDefaultForField`). -- Kit: `packages/1-framework/2-authoring/psl-parser/src/attribute-spec/**`. -- Tests: `interpreter.defaults.test.ts` (non-enum, 24 cases) + the enum-default cases in `interpreter.enum.test.ts`. diff --git a/projects/typed-attribute-parsers/spec.md b/projects/typed-attribute-parsers/spec.md deleted file mode 100644 index 0b5309cb2dd4..000000000000 --- a/projects/typed-attribute-parsers/spec.md +++ /dev/null @@ -1,103 +0,0 @@ -# typed-attribute-parsers - -## Purpose - -Give every PSL attribute a single declarative description that the family interpreters read to validate and lower its arguments, so that "what an attribute accepts" lives as inspectable data in one place instead of as hand-written parsing code duplicated across the SQL and Mongo interpreters. The why is permanence-of-knowledge: the same description must later be readable by other consumers (the language server) without re-deriving it — this project earns that by making the interpreter the first consumer. - -## At a glance - -Today each interpreter pulls raw argument text out of the AST and re-checks its shape by hand. `@relation`'s `fields` argument is a string that gets `split(',')`; `onDelete` is a string compared against a literal list; an unknown named argument is rejected by ad-hoc code — the same patterns repeated in slightly different ways across `packages/2-sql/.../interpreter.ts` (2155 lines) and the Mongo interpreter, backed by string helpers like `parseFieldList` and `parseQuotedStringLiteral`. - -This project replaces that with one declarative `AttributeSpec` per attribute, composed from a fixed kit of argument combinators (ADR 231), and a single `interpretAttribute(node, spec, ctx)` that returns a strongly-typed object whose shape is **inferred from the spec** (`InferAttr`) — or structured diagnostics. - -``` -// before — hand-written, per attribute, per family -const raw = getNamedArgument(attr, 'fields'); // string -const fields = parseFieldList(raw); // split(',') + trim -const onDelete = normalizeReferentialAction(getNamedArgument(attr, 'onDelete')); -// … unknown-argument rejection, span anchoring, both-or-neither rule, all by hand - -// after — one description the interpreter reads -const sqlRelation = fieldAttribute('relation', { - positional: [{ key: 'name', type: optional(str()) }], - named: { - fields: optional(list(fieldRef('self'), { nonEmpty: true })), - references: optional(list(fieldRef('referenced'), { nonEmpty: true })), - onDelete: optional(enumOf('NoAction', 'Restrict', 'Cascade', 'SetNull', 'SetDefault')), - onUpdate: optional(enumOf('NoAction', 'Restrict', 'Cascade', 'SetNull', 'SetDefault')), - map: optional(str()), - }, - refine: relationInvariants, // fields + references are both-or-neither -}); -const parsed = interpretAttribute(relationNode, sqlRelation, ctx); // typed result | diagnostics -``` - -The emitted contract is unchanged; what changes is how the interpreter arrives at it. The output type is derived from the spec, so it cannot drift from the validation. - -## Non-goals - -- **Language-server integration.** Completion, go-to-definition, find-usages, and hovers over attribute arguments are the follow-up that this project's specs are designed to enable, but no language-server consumer is built here. Specs must carry the structure those features need (reference scopes, enum value sets), but wiring them into the editor is out of scope. -- **`@db.*` native types.** These are attributes on named-type declarations, not on fields or models, and are handled by a separate resolver path (ADR 231 § Out of scope). Untouched. -- **The TypeScript builder authoring surface.** Attributes are a PSL-only concept; the TS builders never use them. No combinator appears in the builder API. -- **New attribute syntax or new attributes.** This project re-expresses the *existing* attribute surface as specs; it does not add, remove, or change which attributes or argument shapes are accepted. -- **Generic-block `key = value` parameters and enum member values.** ADR 231 floats unifying these with the kit as an open question; this project does not pursue it. - -## Place in the larger world - -- **ADR 231 — Declarative attribute specifications** is the architectural driver; this project is its first (interpreter-only) implementation. The ADR is `Proposed`; this project's close-out should move it toward `Accepted` or record divergences. -- **The combinator kit lives in `psl-parser`** (`packages/1-framework/2-authoring/psl-parser`) — the PSL authoring-layer package that already owns the parser, the `ExpressionAst` CST, and the `SymbolTable`. It is not in the target-agnostic framework core, because attributes are PSL-specific. `psl-parser` exports the kit, `AttributeSpec`, `interpretAttribute`, and `InferAttr` alongside its existing AST exports. -- **The two consumers** are the family interpreters: `packages/2-sql/2-authoring/contract-psl` and `packages/2-mongo-family/2-authoring/contract-psl`. Each contributes the specs for the attributes it understands, registered by `(level, name)`; the kit dispatches generically and never learns an attribute's name (the ADR-225 contribution model). -- **Argument representation.** Combinators parse the parser's `ExpressionAst` directly — the CST union (`ArrayLiteralAst`, `ObjectLiteralExprAst`, `StringLiteralExprAst`, `FunctionCallAst`, …) that `psl-parser` already exports, which carries native `[…]` / `{…}` literals and real spans. No new intermediate argument representation is introduced. The migration routes each interpreter call site away from the string-flattened `ResolvedAttribute` (`readResolvedArgList`, which collapses arguments to `value: string`) and toward passing the CST attribute node (`FieldAttributeAst` / `ModelAttributeAst`) into `interpretAttribute`. The interpreter already receives the `SymbolTable` and `SourceFile` rather than a pre-flattened document, so the CST is in reach at every call site. -- **Resolution context.** Reference combinators draw on an `InterpretCtx` carrying the parser's `SymbolTable`, the declaring model, a referenced-model resolver, the declaring field (field level only), a codec lookup, and the default-function registry — all already present in the interpreters' existing wiring. - -## Cross-cutting requirements - -- **Behavioural parity, end to end.** For every attribute migrated, the interpreter produces the identical contract output and identical diagnostic **codes** it produced before. Diagnostic **spans** must be **no coarser** than before — narrower/more-precise spans (the natural result of the kit's per-argument anchoring, per ADR 231's native-literal-spans benefit) are acceptable; widening a span is not. Diagnostic **message text** may change to the combinator kit's phrasing, provided each message stays clear and actionable. Malformed inputs that legacy tolerated by coincidence (e.g. a quoted referential action `onDelete: "Cascade"`) may be rejected by the stricter typed leaves. `pnpm fixtures:check` and the interpreter test suites are the parity gate; no contract-output or diagnostic-code drift is acceptable without an explicit, reviewed rationale. _(Messages-may-change + spans-no-coarser + stricter-malformed-rejection relaxations authorised by operator, 2026-06-29.)_ -- **The spec is the only source of an attribute's argument shape.** Once an attribute is migrated, no hand-written argument-parsing path for it remains. Its output type is `InferAttr`, not a separately maintained interface. -- **Leaf parsing is pure.** A combinator returns diagnostics in a `Result`, never into a shared sink, so `oneOf` can try and discard branches cleanly. No combinator mutates a diagnostics array passed by reference. -- **Generic dispatch.** The kit and `interpretAttribute` never branch on a specific attribute name; families register specs and the engine dispatches structurally. - -## Transitional-shape constraints - -- **Every slice keeps CI green on `main`** — `pnpm typecheck`, `pnpm lint`, `pnpm test:packages`, and `pnpm fixtures:check` all pass at every merge. -- **Incremental, attribute-by-attribute migration.** Specs and hand-written parsing coexist while the migration is in flight; the interpreter may route some attributes through specs and others through legacy code simultaneously. A slice migrates a coherent group of attributes (e.g. all of `@relation`) and deletes the legacy path for exactly that group — never leaving two live validation paths for the same attribute. -- **`interpretAttribute` and the kit land with the first migrated attribute**, leaving the existing `ResolvedAttribute` string-flattening path (`readResolvedArgList` and the string helpers) in place for every not-yet-migrated attribute, so legacy parsing keeps working until its attribute is migrated. Those legacy paths are deleted only when their last caller is migrated. - -## Project Definition of Done - -- [ ] Team-DoD floor items (inherited from [`drive/calibration/dod.md`](../../drive/calibration/dod.md) — repo-wide gates, doc/migration, Linear close-out, manual-QA roll-up, ADR audit). -- [ ] The combinator kit, `AttributeSpec`, `interpretAttribute`, and `InferAttr` exist in `psl-parser` (exported alongside its existing AST exports) with unit tests covering each combinator's parse + diagnostic behaviour. -- [ ] Every field-, model-, and block-level attribute interpreted by the **SQL** family is described by a spec and lowered via `interpretAttribute`; the corresponding hand-written argument-parsing helpers are deleted. -- [ ] Every field-, model-, and block-level attribute interpreted by the **Mongo** family is described by a spec and lowered via `interpretAttribute`; the corresponding hand-written argument-parsing helpers are deleted. -- [ ] `pnpm fixtures:check` is clean and the SQL + Mongo interpreter test suites pass with no diagnostic-parity regressions. -- [ ] No remaining caller of the removed string helpers (`parseFieldList`, `parseAttributeFieldList`, per-attribute `getNamedArgument`/`getPositionalArgument` re-parsing) for any migrated attribute; a grep gate confirms this. -- [ ] ADR 231 updated to reflect what shipped (status advanced and/or divergences recorded). - -### Contract-impact - -The **emitted contract is unchanged** — this is a refactor of how interpreters validate arguments, gated by `fixtures:check`. There is no new argument representation: combinators consume the existing `ExpressionAst` CST. The internal change is that migrated interpreter call sites stop flattening attributes to `ResolvedAttribute` strings and instead pass CST attribute nodes into `interpretAttribute`. `ResolvedAttribute` / `readResolvedArgList` and the string helpers remain until their last caller is migrated, then are deleted. - -### Adapter-impact - -No `packages/3-targets/**` adapter is touched. The interpreters being migrated live in the family **authoring** layer (`packages/2-sql`, `packages/2-mongo-family`), upstream of the target adapters; adapter behaviour is reached only through the unchanged contract. - -## Resolved decisions - -- **Argument representation — `ExpressionAst`, no intermediate form.** Combinators consume the parser's `ExpressionAst` CST directly. No `PslArgAst` or other intermediate value is introduced. (Folded into _Place in the larger world_ and _Contract-impact_.) -- **Kit package — inside `psl-parser`.** The kit, `AttributeSpec`, `interpretAttribute`, and `InferAttr` ship from `psl-parser`, which already owns `ExpressionAst` and the `SymbolTable`. No new package. -- **Migration ordering — deferred to planning.** Which attribute groups become slices, and in what order, is a `drive-plan-project` concern, not a spec-level decision. -- **`refine` vs. model-level aggregation — single-attribute rules only.** A single-attribute cross-argument rule — `@relation`'s "`fields` and `references` are both-or-neither" — is implemented as the spec's `refine(parsed, ctx)` callback: a function that runs *after* every argument has parsed, receives the fully-typed result object, and returns diagnostics that no single combinator could produce (each combinator sees only its own argument). That is what "the rule lives in `refine`" means — it is a field on the `AttributeSpec`, not inline interpreter code. A rule that spans *several attributes on one model* — "at most one `@@textIndex` per collection" — is not attribute-level and stays in the existing model-level aggregation that runs above the individual `interpretAttribute` calls. Decision: move single-attribute cross-argument rules into `refine`; build no new aggregator and leave today's model-level checks untouched. - -## Open Questions - -None — design settled. Migration sequencing is handed to `drive-plan-project`. - -## References - -- ADR 231 — [Declarative attribute specifications](../../docs/architecture%20docs/adrs/ADR%20231%20-%20Declarative%20attribute%20specifications.md) (the architectural driver; advance its status at close-out). -- ADR 225 — Three-layer extensibility for pack-contributed entity kinds (the contribution/registration model the kit follows). -- ADR 224 — Control policy: framework-locked vocabulary, family-owned dispatch (the `@@control` value set this kit types as `enumOf(...)`). -- ADR 221 — Contract IR: uniform entity coordinate (the coordinate model reference combinators write into). -- Current interpreters: `packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`, `packages/2-mongo-family/2-authoring/contract-psl/src/interpreter.ts`. -- Current arg flattening: `packages/1-framework/2-authoring/psl-parser/src/resolve.ts` (`readResolvedArgList`); string helpers in `packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts`. -- Linear issue: [TML-2956](https://linear.app/prisma-company/issue/TML-2956) (under project _Language Tools Support Prisma Next PSL_, Terminal team).