# Schema-as-data

## Concept

Schema-as-data treats schemas — declarative shapes that participate in
contracts, in whatever form a language naturally expresses them — as
values produced by factory functions rather than as type or class
definitions evaluated at module / package / unit load time. The factory
is the schema's contract; calling it returns a fresh inspectable value
the rest of the program can examine, transform, serialize, or
version-multiplex. (Per-language schema vehicles enumerated in each
Render section; the worked example RFC #590 cites is Malli's data-driven
schema design as a multi-pattern composition reference.)

## Shape

Every schema-as-data implementation carries these invariants (see
`code-design.md` §Schema-as-data concept and Rule 5
`code_design.schema_as_functions`):

- **Factories produce schemas.** Every schema is the return value of a
  named factory function. The factory accepts parameter variants
  where the schema needs to vary (protocol, version, capability flags)
  and returns a freshly-constructed value on each call. No
  memoization at the factory layer; the caller decides whether to
  cache.
- **Schemas are values, not classes-as-namespaces.** The returned
  schema is a value the program can hand around, compare, transform,
  or serialize. Even when the language's natural schema vehicle is a
  class (Pydantic), the schema is the *instance* of the factory's
  output, not the class definition at module level.
- **Inheritance avoided as the variation mechanism.** Schema variation
  is achieved via factory parameters, not via subclassing. A
  `make_user(role="admin")` factory returns one shape; the same
  factory with `role="guest"` returns another. The two shapes are
  produced by parameter dispatch, not by class hierarchy navigation.
- **Inspectable structure.** The returned schema's structure is
  examinable at runtime — field names, field types, validators,
  constraints — via whichever introspection mechanism the language's
  natural schema vehicle provides. Tools that introspect schemas (web
  UI config inspector, gap detectors, render-phase prompts) read this
  structure directly. (Language-specific introspection APIs in each
  Render section.)
- **Factory tests are the schema's tests.** Per `code-design.md` Rule
  7 (`code_design.schema_tests_tagged`), the factory has at least one
  positive test (call factory, assert on returned shape) and at least
  one negative test (call factory with invalid input, assert correct
  error type). Tests are tagged `schema` and `negative` respectively.
- **No top-level evaluated schema state.** Module-level schema-class
  definitions, module-level decorated record declarations,
  module-level structural-type assignments, or any equivalent
  import-time schema evaluation are the anti-pattern. The factory
  wraps the construction; what lives at module / package / unit level
  is the factory function, not the schema instance. (Language-specific
  forms of this anti-pattern enumerated in each language's render
  section — see Rule 5's gate `code_design.schema_as_functions` for
  the mechanical detector.)
- **Version multiplexing trivializes.** When two schema versions must
  coexist (v1 backwards-compat plus v2 going forward), one factory
  with a `version=` parameter dispatches to the correct shape; the
  caller selects. No `models.py` plus `models_v2.py` plus parallel
  import graphs.

## Forecloses

When a schema is designed as schema-as-data, the following designs
are explicitly *not* the right choice for the same schema:

- **Top-level class with subclass variants.** A base class with
  per-variant subclasses (`UserBase` → `AdminUser`, `GuestUser`,
  `BannedUser`) is not schema-as-data — it is class-hierarchy
  variation. Choosing schema-as-data forecloses subclassing as the
  variation mechanism; parameter-driven factories are the
  replacement.
- **Top-level constants holding schema instances.** A module that
  declares `USER_SCHEMA = {...}` at top level participates only in
  the side-effect-of-import test surface. Choosing schema-as-data
  means choosing factory production; top-level constants are
  excluded.
- **Validation as method dispatch on a class hierarchy.** Pydantic
  validators or constraints expressed as methods on a class
  hierarchy (where overriding `validate_x` differs per subclass)
  participate in the class-hierarchy variation that schema-as-data
  forecloses. Validators belong in the factory's body or in
  factory-produced field configurations.
- **Frozen schema with no factory parameters.** A factory with no
  parameters that always returns the same shape provides only
  minimal benefit over a top-level constant. If the schema does
  not vary, evaluate whether schema-as-data is the right choice —
  the pattern's value comes from parameter-driven variation and
  factory-testability. A no-parameter factory is admissible as the
  forward-going style but the cost / benefit should be visible.
- **Inheritance-driven schema composition libraries.** Frameworks
  that require base-class inheritance for schema composition
  (registering serializers via class methods, plugging
  validators via metaclass tricks) collapse the schema-as-data
  semantics. The schema-as-data pattern prefers explicit factory
  composition over framework-mediated inheritance.

## Judgment probes

These checks verify that a schema actually implements the
schema-as-data pattern:

- **Module-level vs factory-produced.** Inspect the module / package /
  compilation unit declaring the schema. Is the schema produced by a
  factory function, or does it appear as a top-level schema-class
  definition, decorated record declaration, or structural-type
  assignment evaluated at import / load time? Top-level evaluated
  schemas fail the pattern (and the gate
  `code_design.schema_as_functions` flags them mechanically per
  language).
- **Factory parameter usefulness.** Does the factory accept
  parameters that meaningfully vary the schema shape? A
  zero-parameter factory is admissible (forward-going style) but
  carries less of the pattern's benefit. A factory with
  parameters that are not actually used in the body is a
  red flag — perhaps the operator anticipated variation that has
  not yet arrived (which is OK if the third caller is imminent;
  not OK if the parameter has been unused for many releases).
- **Fresh value per call.** Call the factory twice with the same
  arguments. Are the two returned values distinct objects (identity
  comparison reports them as different instances), or are they
  cached? Caching at the factory layer breaks the "fresh per call"
  property; the caller, not the factory, decides caching policy.
- **Inspectability check.** Walk the returned schema with the
  language's natural introspection mechanism (concrete API named in
  each Render section). Does it expose the field set, types,
  validators, constraints? If introspection requires going through
  the class / type definition rather than the produced instance, the
  data nature is incomplete.
- **No inheritance for variation.** Are schema variants produced
  by factory parameters — or by class hierarchies declared at
  module level? If subclassing is the variation mechanism, the
  pattern's variation-via-data property is missing.
- **Schema tests present and tagged.** Per Rule 7, every factory
  has a positive test (`test_make_x_basic_shape`) and (per Rule 8)
  every factory has a negative test
  (`test_make_x_rejects_invalid_role`). Are the test targets tagged
  `schema` and `negative`? Untagged tests do not satisfy the
  detection mechanism.
- **Cross-reference Rule 5.** This pattern operationalizes
  `code-design.md` Rule 5
  (`code_design.schema_as_functions`). A code-design audit run
  alongside the design-phase audit should produce zero new findings
  against this rule for any function tagged as schema-as-data.

## Render: Python

Python's idiomatic schema-as-data rendering is a `@cache`-decorated
factory function returning the schema class (Pydantic `BaseModel`,
`@dataclass`, `TypedDict`, or JSON-schema dict). The factory accepts
parameters where the schema varies (protocol, version, capability);
calling it returns a fresh class instance the caller binds at module
level via `_schema_class()`. This is the project's existing pattern from
the 3.4.1 #309 migration — `models.py`, `interceptors.py`, and
`gate_registry.py` all use it.

```python
from functools import cache
from pydantic import BaseModel, Field, field_validator

@cache
def _repo_config_class(*, protocol: str = "github") -> type:
    """Factory returns a parameter-varied schema class.

    Same protocol → same class (cached). Different protocol → distinct
    class with protocol-specific validation. Callers cache the returned
    class at module level via `RepoConfig = _repo_config_class()` for the
    common case.
    """
    separator = "/" if protocol == "github" else ":"

    class RepoConfig(BaseModel):
        model_config = {"frozen": True, "extra": "forbid"}
        owner: str = Field(..., description="account / org / user")
        name: str = Field(..., description="repo name")

        @field_validator("name")
        @classmethod
        def _no_separator_in_name(cls, v: str) -> str:
            if separator in v:
                raise ValueError(f"name must not contain {separator!r}")
            return v

        @property
        def fullname(self) -> str:
            return f"{self.owner}{separator}{self.name}"

    return RepoConfig

# Module-level binding for the common case (zero-parameter call).
RepoConfig = _repo_config_class()

# Schema introspection: the factory's output is a value the program can
# walk — field names, types, validators — via pydantic.model_fields.
def schema_field_names(schema_class: type) -> list[str]:
    return list(schema_class.model_fields.keys())

# Positive test (tagged `schema` per Rule 7):
def test_repo_config_basic_shape():
    Cls = _repo_config_class(protocol="github")
    instance = Cls(owner="acme", name="widgets")
    assert instance.fullname == "acme/widgets"

# Negative test (tagged `negative` per Rule 8):
def test_repo_config_rejects_separator_in_name():
    Cls = _repo_config_class(protocol="github")
    import pytest
    with pytest.raises(ValueError, match="must not contain"):
        Cls(owner="acme", name="bad/name")
```

The same shape applies to `@dataclass`-based schemas (frozen + slots,
factory-wrapped) and `TypedDict`-based schemas (constructed inside the
factory body via `TypedDict("Name", {...})` dynamic form). The
load-bearing property is "factory produces the schema," not the choice
of schema vehicle.

### Probes

- **factory-produced.** For each schema class (Pydantic `BaseModel`
  subclass, `@dataclass`-decorated class, `TypedDict`), the
  `ClassDef` must appear inside a `FunctionDef` body that returns the
  class — never at module top level. AST: walk module-top-level
  statements; flag any `ClassDef` whose base list includes `BaseModel`
  or whose decorator list includes `dataclass` or whose body assigns a
  `TypedDict(...)` call. This is the existing
  `code_design.schema_as_functions` gate's mechanical detector.
- **factory accepts parameters or is documented as no-parameter.** AST:
  for each schema factory, check `FunctionDef.args.args`. Zero
  parameters are admissible but flagged as a candidate review (Judgment
  probe §Factory parameter usefulness). One-or-more parameters are
  required when the schema varies across consumers.
- **`@cache` on the factory.** The factory should be `@cache`-decorated
  to satisfy "same arguments → same class" identity (required for
  isinstance checks to work consistently across call sites). AST: walk
  the factory's `decorator_list` for `Name("cache")` /
  `Attribute(Name("functools"), "cache")`.
- **module-level binding pattern.** For each factory, expect a
  module-level `Assign(targets=[Name("XClass")], value=Call(func=Name
  ("_x_class")))` pairing — the project's `_x_class()` → `X` convention.
  Absence is admissible but inconsistent with the project's style.
- **introspection-API exposed shape.** For each factory return type,
  verify the schema vehicle's introspection API is reachable (Pydantic's
  `model_fields` / `model_dump`, dataclass's `fields()`,
  TypedDict's `__annotations__`). AST: detect at least one call site
  using the introspection API on the factory output to confirm the
  schema's data nature is exercised.
- **no inheritance for variation.** AST: walk all schema factories'
  outputs; for each, if there are multiple factories producing
  related-but-different shapes, verify they do not form a `class B(A)`
  hierarchy. Variation should be parameter-driven within a single
  factory (`_x_class(role="admin")` vs `_x_class(role="guest")`), not
  subclass-driven (`AdminX(BaseX)`, `GuestX(BaseX)` at module level).
- **schema tests tagged.** Per Rule 7, each factory has a test target
  tagged `schema`. AST: search the test module for the factory's call
  sites and verify the surrounding test target's tag list includes
  `schema`. Per Rule 8, at least one test target is tagged `negative`.

### Example

`mcp-server/src/forge_manager_mcp/models.py` — every Pydantic model
(`RepoConfig`, `RelatedRepo`, `ProjectBoard`, `DiscussionCategory`,
`GithubConfig`, etc.) is produced by a corresponding `_<name>_class()`
`@cache`-wrapped factory. The 3.4.1 #309 migration converted all
module-level `class X(BaseModel)` declarations to this shape. The same
pattern appears in `interceptors.py` (`_context_class`,
`_interceptor_class`) and `gate_registry.py`.

### Anti-patterns

- **Top-level `class RepoConfig(BaseModel): ...`.** The schema class is
  evaluated at module import time; the only test surface is "import the
  module and assert on the class." Violates Forecloses §Top-level class
  with subclass variants (variation requires subclassing) and
  No-top-level-evaluated-schema-state. Flagged by
  `code_design.schema_as_functions`.
- **`USER_SCHEMA = {"fields": [...]}` constant at module top level.**
  A JSON-Schema-shaped dict bound at import time. No factory means no
  parameter-driven variation; the only versioning mechanism is editing
  the constant. Violates Forecloses §Top-level constants holding schema
  instances.
- **`AdminUser(BaseUser)` / `GuestUser(BaseUser)` class hierarchy.**
  Schema variation by subclassing — `validate_x` overridden per
  subclass, fields added per subclass. Violates Forecloses
  §Validation as method dispatch on a class hierarchy and Judgment
  probe §No inheritance for variation. The factory replacement is a
  single `_user_class(role="admin" | "guest")` with role-conditional
  validators inside the factory body.

## Render: Go

Go's natural schema vehicle is the struct definition — typically with
tag-driven serialization annotations (`json:"..."`,
`protobuf:"..."`, `validate:"..."`). The schema-as-data pattern in Go
prefers factory functions that return *populated descriptor values*
(field-set struct + per-field metadata) over module-level struct type
declarations whose only schema-carrying information is the type's tags.

Two renderings are admissible:

1. **Reflective rendering** — the schema is a populated
   `Schema` value carrying `Field` descriptors; factories construct
   it. The runtime uses `reflect.Value` or the descriptor itself for
   marshal/validate/inspect. This satisfies the pattern fully.
2. **Generated rendering** — code generation (a `go generate`
   directive) produces a factory function returning a populated
   schema value from a higher-level description (often a YAML or DSL
   input). The generated code IS the factory; the higher-level
   description IS the schema-as-data source.

The reflective form below is the canonical rendering. Module-level
`type User struct { Name string \`json:"name\"\` }` declarations,
while ubiquitous in Go, are the *anti-pattern* this section forecloses
— the schema lives at type-declaration time and cannot be varied by
factory parameter without subclassing tricks Go does not natively
support.

```go
package schemas

import "errors"

// Field is the per-field schema descriptor. Values, not types.
type Field struct {
	Name      string
	Kind      string // "string", "int", "bool", or a registered kind
	Required  bool
	Default   any
	Validator func(any) error
	Meta      map[string]string
}

// Schema is the schema value the factory returns. Walkable,
// comparable (modulo Validator function identity), serializable
// for the data portions.
type Schema struct {
	Name   string
	Fields []Field
}

// Validate runs the per-field validators against an input map.
// The schema value carries everything Validate needs.
func (s Schema) Validate(input map[string]any) error {
	for _, f := range s.Fields {
		v, ok := input[f.Name]
		if !ok {
			if f.Required {
				return errors.New("missing required field: " + f.Name)
			}
			v = f.Default
		}
		if f.Validator != nil {
			if err := f.Validator(v); err != nil {
				return err
			}
		}
	}
	return nil
}

// Factory: produces a fresh User schema per call. Parameters drive
// variation. No memoization; the caller decides caching.
func MakeUserSchema(role string, version int) Schema {
	fields := []Field{
		{Name: "id", Kind: "string", Required: true},
		{Name: "email", Kind: "string", Required: true,
			Validator: nonEmptyString},
	}
	if role == "admin" {
		fields = append(fields, Field{
			Name: "permissions", Kind: "string", Required: true,
		})
	}
	if version >= 2 {
		fields = append(fields, Field{
			Name: "created_at", Kind: "string", Required: false,
		})
	}
	return Schema{Name: "user", Fields: fields}
}

func nonEmptyString(v any) error {
	s, ok := v.(string)
	if !ok || s == "" {
		return errors.New("empty string")
	}
	return nil
}

// Inspection — the field set is reachable by reading the returned
// value. No type-system introspection needed:
//
//   sch := MakeUserSchema("admin", 2)
//   for _, f := range sch.Fields {
//       fmt.Println(f.Name, f.Kind, f.Required)
//   }
```

### Probes

- **No module-level schema struct declarations for tagged schemas
  (AST).** For schemas tagged as `schema-as-data`, the pattern
  forbids the module-level form `type X struct { ... }` where the
  struct's tags carry the contract (`json:"..."`,
  `validate:"..."`). AST probe: walk
  `*ast.GenDecl` of kind `token.TYPE`, find `*ast.StructType` whose
  fields have `Tag != nil` carrying schema-significant keys, and flag
  the type as a candidate anti-pattern (gate
  `code_design.schema_as_functions`).
- **Factory returns Schema value (AST).** Each tagged schema must
  be produced by a function returning `Schema` (or the project's
  named equivalent). AST probe: function's `*ast.FuncType.Results`
  must have exactly one result whose type resolves to `Schema`. A
  function returning `*X` for a module-level struct `X` is the
  wrong rendering.
- **Factory accepts variation parameters.** Walk the factory's
  parameter list. A no-parameter factory is admissible but flagged
  as a soft warning — the pattern's value comes from parameter-driven
  variation. AST probe: count `Params.List`; zero is a soft warning,
  one or more passes.
- **Parameters used in the body.** For each parameter, AST walk the
  body for references. Unused parameters indicate the factory
  anticipates variation that has not arrived (acceptable if a third
  caller is imminent; suspicious otherwise).
- **Fresh value per call (runtime probe).** Write a test:
  `s1 := MakeUserSchema("admin", 2); s2 := MakeUserSchema("admin",
  2); assert &s1.Fields[0] != &s2.Fields[0]`. The schema's underlying
  slice must be a fresh allocation per call. Caching at the factory
  layer breaks the fresh-per-call invariant.
- **No top-level constant Schema.** Search the package for
  package-level `var X = Schema{...}` declarations or `var X =
  MakeXSchema(...)` package-level invocations. Both are caching at
  module-load time; the pattern requires the caller to construct.
- **Inheritance-via-embedding for variation (AST).** Walk struct
  declarations adjacent to the factory; flag any pattern of struct
  embedding (`type AdminUser struct { User; Permissions []string
  }`) used to express variation. The pattern foreclosed
  class-hierarchy variation; Go's embedding is the equivalent.
- **Factory test tagged `schema` and `negative`.** Per Rule 7,
  `TestMakeUserSchema_BasicShape` (positive) and
  `TestMakeUserSchema_RejectsInvalidRole` (negative) must exist and
  carry build-tag or comment markers matching the project's
  `// schema` and `// negative` conventions. Detector reads the
  test file's marker comments.

### Example

`encoding/json`'s tag-driven schema (`json:"name,omitempty"`) is the
ubiquitous Go schema-carrier but evaluates at struct-declaration time
— it is the *anti-pattern* this section forecloses. For
factory-produced schemas: `github.com/santhosh-tekuri/jsonschema`
returns compiled schema values from a `Compile` factory call;
`google.golang.org/protobuf/reflect/protoreflect` exposes
descriptors as runtime values constructed by the protobuf
codegen (a Generated rendering per option 2 above).
`github.com/go-playground/validator` straddles both — the validator
function looks up tag rules at runtime but the struct definition is
still module-level.

### Anti-patterns

- **Module-level struct with `json:` tags as the only schema
  carrier.** `type User struct { Name string \`json:"name"\` Role
  string \`json:"role"\` }` — schema is fixed at type-declaration
  time, varies only via embedded-struct subclassing. The
  schema-as-data pattern forecloses exactly this idiom; choose
  `MakeUserSchema(role string) Schema` instead.
- **`var UserSchema = Schema{...}` at package level.** Package-level
  schema constants execute the factory once at package-init, caching
  the value. The pattern requires factory production at call sites
  so the caller controls timing and parameter variation.
- **Schema variation via embedded struct.** `type AdminUser struct
  { User; Permissions []string }` expresses the admin schema as
  inheritance over a base User struct. Go's embedding is the
  language's equivalent of class-hierarchy variation; the pattern
  prescribes parameter-driven factories instead
  (`MakeUserSchema(role)`).
- **Factory returning `*Schema` shared pointer.** If `MakeUserSchema`
  returns the same `*Schema` pointer on each call (caching) instead
  of a fresh value, the fresh-per-call invariant breaks. Mutating
  one returned schema then mutates all callers' schemas.
- **Schema methods doing the variation.** A `User` struct with
  `func (u User) RoleSpecificFields() []Field` that switches on a
  Role field is hand-rolling factory dispatch inside a method. The
  factory function call should be the public surface, not a
  type's method.

## Render: Ada

In Ada, schemas are rendered as discriminated record types produced
by factory functions in generic packages. The factory takes the
variation parameters (protocol, version, capability flags) and
returns a fully-constructed record value; the discriminant
constrains the variant part so different schema shapes are reachable
only by calling the factory with different parameters. The
"introspection" mechanism is the language itself: a client can
`case Schema.Kind is` over the discriminant and read the variant
components — no runtime reflection library needed. Factory
contracts (`Post`) assert the produced shape matches the parameters
— the schema's contract is the factory's postcondition.

Ada's package generics are the project-wide analogue of Python's
`typing.Generic` factories — `Ada.Containers.Hashed_Maps` is the
canonical example of "instantiate a generic package with type
parameters, get back a fully-formed schema-of-operations" — and the
RFC's pointer to Malli's data-driven schema model carries over: the
generic instantiation is the factory call, and the resulting package
is the freshly-constructed schema.

```ada
package Schemas is

   type Auth_Kind is (None, API_Key, OAuth, Mutual_TLS);

   --  Discriminant + variant: shape varies with Auth_Kind. Compiler
   --  refuses to read OAuth-specific components when Kind = None.
   --  No top-level constant of this type — only the factory exists
   --  at module scope.
   type User_Schema (Kind : Auth_Kind := None) is record
      Name_Max_Len : Positive;
      case Kind is
         when None       => null;
         when API_Key    => Key_Header : String (1 .. 32);
         when OAuth      => Issuer_URI : String (1 .. 128);
         when Mutual_TLS => Cert_Path  : String (1 .. 256);
      end case;
   end record;

   --  Factory. Parameters drive variation; postcondition asserts
   --  the resulting discriminant matches.
   function Make_User_Schema
     (Auth         : Auth_Kind;
      Name_Max_Len : Positive := 64) return User_Schema
     with Post => Make_User_Schema'Result.Kind = Auth
              and Make_User_Schema'Result.Name_Max_Len = Name_Max_Len;

   --  Negative-test target. Factory raises Constraint_Error when
   --  Name_Max_Len is out of range; the contract is the schema's
   --  validation surface.
   Invalid_Name_Length : exception;

end Schemas;

package body Schemas is

   function Make_User_Schema
     (Auth         : Auth_Kind;
      Name_Max_Len : Positive := 64) return User_Schema
   is
   begin
      if Name_Max_Len > 256 then
         raise Invalid_Name_Length
           with "Name_Max_Len must be <= 256";
      end if;

      case Auth is
         when None =>
            return (Kind => None, Name_Max_Len => Name_Max_Len);
         when API_Key =>
            return (Kind => API_Key, Name_Max_Len => Name_Max_Len,
                    Key_Header => (others => ' '));
         when OAuth =>
            return (Kind => OAuth, Name_Max_Len => Name_Max_Len,
                    Issuer_URI => (others => ' '));
         when Mutual_TLS =>
            return (Kind => Mutual_TLS, Name_Max_Len => Name_Max_Len,
                    Cert_Path => (others => ' '));
      end case;
   end Make_User_Schema;

end Schemas;
```

### Probes

- **No top-level object of the schema record type.** Walk the
  package public part for an `ObjectDecl` whose type is
  `User_Schema` (or equivalent schema record). Any such top-level
  object is the schema-as-constant anti-pattern and fails the
  pattern's "factories produce schemas" invariant. (libadalang:
  walk `PackageDecl.f_public_part.f_decls`, filter `ObjectDecl`,
  resolve type — flag any whose resolved type is the schema
  record.)
- **At least one factory exists per schema type.** For every
  discriminated-record schema type declared in the package, locate
  a function whose return type is the same record type and whose
  name begins with `Make_` (or per project convention). Without a
  factory the schema is "constructed via aggregate literal at every
  call site" — equivalent to top-level constants per use.
- **Factory has at least one parameter that drives variation.**
  Walk the factory's `ParamSpec` list; assert at least one
  parameter's type is the discriminant's subtype (`Auth_Kind` in the
  example). A factory whose parameters are only "fill the
  fixed-size payload" without varying the discriminant is the
  frozen-schema variant of the pattern — admissible but probe-warned
  per Rule 5.
- **Factory has a `Post` aspect asserting the shape.** Walk the
  factory's `SubpSpec.f_aspects`; assert presence of a `Post`
  aspect whose expression references `'Result.Kind` (the
  discriminant). Without the postcondition the factory's contract
  is only documented, not enforced.
- **Negative test exists per factory.** Per `code-design.md` Rule
  7 / Rule 8, the factory's test target must include at least one
  test that calls the factory with arguments expected to raise the
  declared exception. Walk the package's test suite (project
  convention: `tests/<package>.adb` or `tests/test_<package>.adb`);
  assert a `procedure Test_*_Rejects_*` exists.
- **No inheritance for schema variation.** Walk the package for
  any tagged-type declaration deriving from the schema record type
  with `record` extensions. Schema variation must be driven by
  factory parameters, not by `type Admin_User_Schema is new
  User_Schema with record ...`.
- **Factory body does not memoize.** Walk the factory body for any
  package-level `Cache : ... ; ` lookups or any `if Cache.Contains
  (Key) then return Cache (Key);` shape. Each call must produce a
  fresh value; caching is the caller's choice.

### Example

The Ada standard library's `Ada.Containers.Hashed_Maps` is the
canonical "generic package as schema factory" — instantiating
`package Int_Map is new Ada.Containers.Hashed_Maps (Key_Type =>
Integer, ...)` is calling the schema factory; the resulting package
is the freshly-constructed schema. AdaCore's GNATCOLL.JSON also
exposes factory-style construction (`Create_Object`, `Create_Array`)
returning inspectable values, mapping naturally onto this pattern.

### Anti-patterns

- **Top-level constant aggregate.** A package declaring `User_Schema
  : constant := (Kind => OAuth, ..., Issuer_URI => "...");` at
  module scope — the schema is evaluated at unit elaboration and
  cannot vary by call. Mechanically: any `ObjectDecl` whose
  initialisation expression is a record aggregate of the schema
  type fails the pattern.
- **Tagged-type hierarchy for variation.** A base `abstract tagged`
  type `User_Schema_Base` with child types `Admin_User_Schema`,
  `Guest_User_Schema`, `Banned_User_Schema` — schema variation is
  driven by class-hierarchy navigation, not by factory parameters.
  This is the Ada equivalent of the "subclass per variant"
  anti-pattern called out in Forecloses; choosing tagged-type
  variation forecloses the factory-driven shape.
- **Schema validation as primitive operations on the schema type.**
  Declaring `procedure Validate (S : User_Schema; Input : ...)
  with Pre => ...;` as a primitive operation, with overrides per
  child type — the validators are tied to the type hierarchy
  rather than living in the factory body. Validation belongs in
  factory parameters and factory-produced contract assertions, not
  in dispatched primitives.

## Render: TypeScript

TypeScript's idiomatic schema-as-data rendering is the zod discipline:
schema values built by factory functions returning `z.object(...)`
values — not class/decorator declarations fixed at module load. The
factory is the schema's contract; calling it returns a fresh
inspectable value that can be examined, transformed, or
version-multiplexed.

```typescript
import { z } from "zod";

// Factory-produced: variation is parameter-driven, not load-time.
const issueSchema = (opts: { withMilestone: boolean }) => {
  const base = z.object({
    number: z.number().int(),
    title: z.string().min(1),
  });
  return opts.withMilestone
    ? base.extend({ milestone: z.string() })
    : base;
};

const v1 = issueSchema({ withMilestone: false });
const v2 = issueSchema({ withMilestone: true });
type IssueV2 = z.infer<typeof v2>;
```

### Probes

Mechanical (wired in `_TYPESCRIPT_PROBES`):

- **factory_produced_schema.** No module-top-level (column-0)
  `const X = z.object(...)` binding — the schema value must be built
  inside a function that returns it, so variation is parameter-driven
  and nothing is evaluated at import time. (Class + decorator
  declarations are the same violation in class-vehicle form.)

Judgment (prose-only):

- **factory parameters are used.** A zero-parameter factory returning
  a constant literal is a load-time schema wearing factory syntax.
- **introspection is exercised.** Somewhere, the returned value is
  examined / transformed (`.shape`, `.extend`, `.pick`) — the reason
  it is data.

### Example

The SolidJS UI's payload validators for `/swimlanes` responses;
`mcp-server/src/forge_manager_mcp/code_design/schema_as_functions.py`
is the Python-side mechanical gate this mirrors.

### Anti-patterns

- **`export const UserSchema = z.object({...})` at module top
  level.** Import-time evaluation; variation now requires a second
  top-level constant per variant.
- **Decorator-declared classes (`class X { @IsString() name }`).**
  The schema is a type-system artifact, not a value the program can
  hold, transform, or multiplex.
