[Issue #798] Transforms PoC: TypeScript#825
Draft
SnowboardTechie wants to merge 1 commit into
Draft
Conversation
Port the Python transforms PoC (PR #810, branch 799-transform-poc-fetch) to @common-grants/sdk so the ADR-0022 / ADR-0017 contract is validated in both SDKs before either is locked in for full implementation. Public additions under @common-grants/sdk/extensions: - buildTransforms() — compile a pair of ADR-0017 mapping objects into typed (toCommon, fromCommon) callables with call-time structural validation. Optional commonModel Zod schema turns parse failures into PluginError[] rather than thrown exceptions. - TransformResult<T> — unconditional { result, errors } return shape (ADR-0022 Decision #7). - PluginError — structured error class with path / handler / sourceValue / cause (ADR-0022 Decision #9). Docstring documents the PII surface on both sourceValue (carries the full input record) and message (data-bearing on the Zod-validation path because Zod's default error map embeds received values). - transformFromMapping(), getFromPath(), DEFAULT_HANDLERS — mapping runtime; six built-in handlers (const, field, match, switch alias, numberToString, stringToNumber). - definePlugin() accepts optional meta and transformSchemas. Existing callers passing only `extensions` are unaffected. Security hardening (mapping JSON may be reconstituted from untrusted sources via mergeExtensions(), so the runtime must fail loud on hostile shapes): - buildTransforms() rejects custom handler names that collide with the default registry or shadow Object.prototype keys (constructor, toString, __proto__, etc.) at call time. - validateMapping() rejects `__proto__` as an output field name at build time; transformFromMapping() rejects it again at walk time so the JSON attack vector (own-enumerable __proto__ key from JSON.parse) fails fast in both places. - stringToNumber's error message does not embed the source value (would flow into PluginError.message and bypass the sourceValue PII guard). Out of scope (matches Python PoC; deferred to full SDK): - Auto-generation of transforms from declarative extensions.schemas[obj].mappings inside definePlugin() (Decision #6 TODO). - Always-on commonModel validation inside definePlugin() — opt-in at buildTransforms() for now (Decision #7 TODO). Includes: - examples/transforms.ts round-trip (pnpm example:transforms) - README "Plugin transformations" section + API reference table - 5 new define-plugin specs, 30 transformation-handler specs, 12 buildTransforms specs (427 tests total, all passing) - Minor changeset bump for @common-grants/sdk Targets HOLD-transforms per the SDK Plugin Enhancements branching strategy.
8394a3c to
bda51e1
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
TypeScript proof-of-concept for the plugin transformation interface under
@common-grants/sdk/extensions, mirroring the Python PoC (#810) so the ADR-0022 / ADR-0017 contract is validated in both SDKs. ShipsbuildTransforms(),TransformResult<T>,PluginError, the mapping runtime, adefinePlugin()extension formeta+transformSchemas, and a runnable round-trip example.Changes proposed
New public surface under
@common-grants/sdk/extensions:buildTransforms()— compiles a pair of ADR-0017 mapping objects into typed(toCommon, fromCommon)callables with call-time structural validation. OptionalcommonModelZod schema turns parse failures intoPluginError[]instead of thrown exceptions.TransformResult<T>— unconditional{ result, errors }return shape (ADR-0022 Decision ADR - Specification framework #7).PluginError— structured error carryingpath,handler,sourceValue,cause(ADR-0022 Decision ADR - Website hosting #9). Docstring documents the PII surface on bothsourceValue(full input record) andmessage(data-bearing on the Zod-validation path).transformFromMapping(),getFromPath(),DEFAULT_HANDLERS— mapping runtime, six built-in handlers (const,field,match,switchalias,numberToString,stringToNumber).definePlugin()accepts optionalmeta: PluginMetaandtransformSchemas: Partial<Record<ExtensibleSchemaName, ObjectSchemasInput>>. Existing callers passing onlyextensionsare unaffected.Handler,ObjectSchemasInput,ObjectSchemas,PluginMeta,PluginCapability,ObjectMappings,PluginExtensionsObjectConfig,PluginExtensions,TransformSchemasInput.Security guards (mapping JSON may be reconstituted from untrusted sources via
mergeExtensions(), per ADR-0022 Decision #8):__proto__rejected as an output field name atbuildTransforms()call time (validateMapping()) and at walk time (transformFromMapping()).DEFAULT_HANDLERSor shadowObject.prototypekeys (constructor,toString, etc.) rejected at call time.stringToNumber's thrown message does not embed the source value (it would otherwise flow intoPluginError.message).Out of scope (matches Python PoC, deferred to full SDK):
extensions.schemas[obj].mappingsinsidedefinePlugin()(Decision Publish static site to GitHub pages #6 TODO).commonModelvalidation insidedefinePlugin(), opt-in atbuildTransforms()for now (Decision ADR - Specification framework #7 TODO).Files:
src/extensions/transforms.ts,src/extensions/transformation.ts,examples/transforms.ts,__tests__/extensions/transforms.spec.ts,__tests__/extensions/transformation.spec.ts,.changeset/transforms-poc-typescript.mdsrc/extensions/types.ts(10 new transform types),src/extensions/define-plugin.ts(meta+transformSchemas),src/extensions/index.ts(exports trimmed to match Python'sextensions/__init__.pysurface),src/extensions/README.md(new "Plugin transformations" section + API reference table),__tests__/extensions/define-plugin.spec.ts(five new tests),package.json(example:transformsscript).Context for reviewers
How verified:
pnpm run checks— eslint, prettier,tsc --noEmitclean.pnpm run test— 427 tests across 24 suites. PoC adds 30 handler-runtime tests, 12buildTransformstests (both directions covered for handler errors), 5 newdefinePlugintests formeta+transformSchemas.pnpm run example:transforms— synthetic grants.gov record round-trips throughtoCommon→ Zod-validated CommonGrantsOpportunity→fromCommon, with customjoin/splithandlers and verifiedopportunity_numberrecovery.Conform-before-extend. ADR-0022's TypeScript code blocks and the Python PoC at
799-transform-poc-fetchwere treated as the spec, transliterated snake_case → camelCase, swapped Pydantic → Zod, and reused existing TS SDK type patterns (const generics, mapped types). Two intentional divergences from the ADR's TS shape are documented in code comments:DefinePluginOptions.transformSchemas(notschemas) mirrors Python'stransform_schemasworkaround for the collision with the existingPlugin.schemasfield. Full SDK target: resolve in [TS SDK] Extend definePlugin() to accept schemas with toCommon/fromCommon #756.BuildTransformsOptions.commonModel: z.ZodType<TCommon, z.ZodTypeDef, any>usesanyin the contravariant input position to accept schemas with input/output asymmetry (e.g..transform()producingDatefromstring). The ADR'sZodType<TCommon>would reject the SDK's ownOpportunityBaseSchema.Why
HOLD-transforms. Issue #798 sits in the transforms feature bucket (#656, #745, #756, #757, #768, #798, #799, #813) per the branching strategy doc. The bucket batches tomainat the C2 (July 21) checkpoint. PR #810 already targetsHOLD-transformsfor the Python PoC.Cross-SDK design findings for the full SDK work in #756 / #757 (10 items, including the
schemasnaming collision,commonModel-must-be-extended-schema gotcha, plain-object normalization, lack of a TS code generator for an extendedcommonmodel, Zod-strip interaction with derived fields, and theanywidening oncommonModel) live in the issue-work state dir, not the repo.Additional information
Example output (from
pnpm example:transforms):