Skip to content

docs: ProfileMethod descriptor taxonomy + field type draft - #163

Closed
ryukzak wants to merge 6 commits into
mainfrom
ts/profile-method-taxonomy
Closed

docs: ProfileMethod descriptor taxonomy + field type draft#163
ryukzak wants to merge 6 commits into
mainfrom
ts/profile-method-taxonomy

Conversation

@ryukzak

@ryukzak ryukzak commented May 20, 2026

Copy link
Copy Markdown
Collaborator

Stacks on #158.

Design groundwork for the follow-up PR series that moves profile-method derivation out of the TS writer onto `SnapshotProfileTypeSchema`.

What this PR contains

Just docs — `docs/design/profile-methods.md`. Two sections:

1. Taxonomy

Catalogs every emit the TS writer currently produces inside a profile module, grouped into 8 categories:

  • Class constants — `canonicalUrl`, slice-match records
  • Static factory — `from`, `is`, `apply`, `create`, `createResource`, `resolveInput`
  • Lifecycle — `constructor`, `toResource`
  • Field accessors — param / auto / choice
  • Slice accessors — single / unbounded / constrained-choice / type-discriminator
  • Extension accessors — complex / single-value / generic
  • Validator — `validate()`
  • Module type aliases — `Raw`, `Flat`, `*SliceFlat`, `SliceFlatAll`, `_extFlat`

Each entry has its source helper and emit condition.

2. `methods` field type — first draft

Proposes the language-neutral `ProfileMethod` tagged union (22 variants) plus shared records:

```ts
export type ProfileMethod =
| { kind: "static-canonical-url" }
| { kind: "static-slice-match"; def: SliceMatchConstant }
| { kind: "static-from"; baseType: TypeIdentifier; hasMeta: boolean }
| { kind: "static-is"; baseType: TypeIdentifier; mode: "resource" | "extension" }
| { kind: "static-apply"; ... }
| { kind: "static-create-resource"; ... }
| { kind: "static-create"; viaResolveInput: boolean }
| { kind: "static-resolve-input"; subSlices: SubExtensionSlice[] }
| { kind: "constructor"; baseType: TypeIdentifier }
| { kind: "to-resource"; baseType: TypeIdentifier }
| { kind: "field-get"; field: FieldAccessorSpec }
| { kind: "field-set"; field: FieldAccessorSpec }
| { kind: "slice-get"; def: SliceSpec }
| { kind: "slice-set"; def: SliceSpec }
| { kind: "extension-get"; spec: ExtensionAccessorSpec }
| { kind: "extension-set"; spec: ExtensionAccessorSpec }
| { kind: "validate"; checks: ValidationCheck[] }
| { kind: "type-alias-raw"; ... }
| { kind: "type-alias-flat"; subSlices: SubExtensionSlice[] }
| { kind: "type-alias-slice-flat"; ... }
| { kind: "type-alias-slice-flat-all"; ... }
| { kind: "type-alias-extension-flat"; ... };
```

Everything is language-neutral — identifiers stay as `TypeIdentifier`, values stay as `unknown`. Renderers consume the array and dispatch on `kind`.

Open questions

Listed at the bottom of the doc:

  • Single ordered array vs. bucketed-by-kind.
  • Slice/extension `flavor` as a sub-field vs. a sub-variant.
  • Whether validation belongs in `methods` or a separate array.
  • Whether to encode helper imports or let renderers re-derive.
  • Whether type aliases should split out of `methods`.

What this enables

  • Multi-language reuse — Python/C# renderers consume the same descriptors with their own emitters.
  • Snapshot inspectability — `bun run src/cli/index.ts typeschema generate` could dump descriptors without running a language writer.
  • Testability — descriptors are pure data; today's tests assert generated text via snapshots, which is coarse.
  • Stability — adding a new method kind becomes "add a variant + a renderer" instead of "edit the writer's monolithic generator".

Not in this PR

  • Concrete implementation of `SnapshotProfileTypeSchema.methods`.
  • Renderer migration (writer becoming a dispatcher).
  • Multi-language renderers (Python/C#).

Those land in follow-up PRs once the descriptor shape is approved.

ryukzak added 6 commits May 20, 2026 16:08
…e index

Adds a writer-ready profile representation alongside the raw
ProfileTypeSchema. Snapshots are flattened (fields merged across the
hierarchy, extensions consolidated) and discriminated by
`identifier.kind === "profile-snapshot"`.

Type additions:
- SnapshotProfileIdentifier in the Identifier union.
- SnapshotProfileTypeSchema in the TypeSchema union.
- Guards: isSnapshotProfileIdentifier, isSnapshotProfileTypeSchema.
- Helper: snapshotIdentifier(id) — ProfileIdentifier → SnapshotProfileIdentifier.
- Export PrimitiveTypeSchema (was implicitly public via TypeSchema; the
  resolve overload signature now needs the name directly).

Index integration in mkTypeSchemaIndex:
- New `snapshotIndex` (parallel to nestedIndex) holds pre-built snapshots.
- Snapshots are eagerly built for every well-formed profile during index
  construction. Profiles whose hierarchy lacks a non-profile ancestor are
  skipped structurally via tryHierarchy + guard checks, so direct
  flatProfile callers still see the original exception path.
- New `collectSnapshotProfiles()` returns all built snapshots.
- `resolve` and `resolveType` route SnapshotProfileIdentifier (kind
  "profile-snapshot") to the snapshot store.
- Both functions are now overloaded — passing a specific identifier
  variant (Resource, Profile, Snapshot, Binding, ValueSet, Logical,
  Primitive, ComplexType; plus Nested for resolveType) narrows the
  return type without a cast. Implementation uses a named callable type
  (ResolveFn/ResolveTypeFn) to avoid circular type inference through
  TypeSchemaIndex.
- findLastSpecialization uses guard-based filter (skips both profile
  and profile-snapshot variants) so it composes when called via
  findLastSpecializationByIdentifier on a snapshot identifier.
- entityTree includes the "profile-snapshot" bucket so EntityTree stays
  exhaustive over Identifier["kind"].

Tree-shake short-circuit:
- treeShakeTypeSchema returns snapshots untouched (they're derived data;
  they never reach tree shaking in practice, but the typed switch needs
  to acknowledge them).
Pipes snapshots from the index through the writer end-to-end.

- writer.generate() now collects via tsIndex.collectSnapshotProfiles().
- generateResourceModule dispatches on isSnapshotProfileTypeSchema; the
  inline `tsIndex.resolve(...)` call inside the profile branch is gone —
  the iterated schema is already the snapshot.
- generateFhirPackageIndexFile and the per-package filter use
  isSnapshotProfileTypeSchema.
- generateProfileIndexFile signature takes SnapshotProfileTypeSchema[].
- resolveExtensionProfile resolves the snapshot via the overloaded
  resolve() — no cast — and returns it as `snapshot` on
  ExtensionProfileInfo (replacing the previous `flatProfile` field).
- All profile-generation helpers (factory info, slice defs, extension
  methods, validation, slice setters/getters) take
  `snapshot: SnapshotProfileTypeSchema` instead of `flatProfile: ProfileTypeSchema`.
- Name helpers (tsProfileModuleName/FileName/ClassName) narrow to
  SnapshotProfileTypeSchema. tsProfileModuleName switches to
  findLastSpecializationByIdentifier so it no longer depends on the
  TypeSchema union.

Drive-by: biome --write trimmed a redundant `!!canonicalUrl` (already
inside an `&&` chain).
Captures the 8 emit categories the TS writer currently produces inside a
profile module (constants, factory, lifecycle, field/slice/extension
accessors, validator, type aliases), the descriptor data each needs, and
the migration target: a `methods` array on SnapshotProfileTypeSchema
where each entry is a tagged descriptor and the writer becomes a
renderer that dispatches on `entry.kind`.

This is the foundation for the follow-up PR series that moves method
derivation out of the writer.
…ema.methods

First pass at the language-neutral descriptor model that future code
generation will dispatch on. Adds a "methods field type — first draft"
section to the design doc:

- Shared records (AutoField, ParamField, SliceAutoField,
  SliceMatchConstant, FieldAccessorSpec, SliceSpec, ExtensionAccessorSpec,
  ValidationCheck, SubExtensionSlice).
- Tagged ProfileMethod union with 22 variants covering all eight
  taxonomy categories.
- Open questions: ordering, hasMeta inlining vs derivation, flavor as
  sub-field vs sub-variant, where validation lives, helper-import
  gating, type aliases as a separate array.
Base automatically changed from ts/snapshot-profile-type-schema to main May 20, 2026 18:05
@ryukzak ryukzak closed this Jun 3, 2026
@ryukzak
ryukzak deleted the ts/profile-method-taxonomy branch July 24, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant