SY-4068: Strongly Typed Schematic Node and Edge Configs#2474
Open
emilbon99 wants to merge 108 commits into
Open
SY-4068: Strongly Typed Schematic Node and Edge Configs#2474emilbon99 wants to merge 108 commits into
emilbon99 wants to merge 108 commits into
Conversation
…ntary code points)
Internally-tagged discriminated unions in Oracle (`Scale union on type { ... }`)
generated across TypeScript (z.discriminatedUnion), Go (sealed-interface variants +
wrapper + two-pass codec), Python (Annotated discriminated unions), C++ (std::variant),
and protobuf (oneof). Includes a representative NI example schema.
Squashed from the original 15-commit branch; stacked on array defaults (#2432).
…for-struct-unions # Conflicts: # oracle/analyzer/analyzer.go # oracle/analyzer/analyzer_test.go # oracle/formatter/formatter.go # oracle/formatter/formatter_test.go # oracle/parser/OracleParser.g4 # oracle/parser/OracleParser.interp # oracle/parser/oracle_parser.go # oracle/parser/oracleparser_base_listener.go # oracle/parser/oracleparser_listener.go # oracle/plugin/cpp/types/types.go # oracle/plugin/cpp/types/types_test.go # oracle/plugin/py/types/types.go # oracle/plugin/py/types/types_test.go # oracle/plugin/ts/types/types.go # oracle/plugin/ts/types/validation_test.go # oracle/resolution/domain.go # oracle/resolution/type.go # oracle/resolution/types.go
String-valued Oracle enums generate as a Python Literal alias with module-level constants, not an Enum class, so emitting an enum default as Units.volts is member access on a typing special form and fails mypy. Reference the variant's string value directly for string enums (mirroring the TypeScript plugin), while integer enums keep Enum.member access since they generate as IntEnum classes.
…ributes) Union variants now render documentation consistently with regular structs: the TypeScript variant schema's JSDoc comment sits directly above the export (no stray blank line), and the Python variant model emits a full Google-style docstring with an Attributes section for its flattened fields instead of only a one-line summary.
The union-level doc was misplaced or dropped: TypeScript emitted it above the first variant schema (reading as that variant's doc) with a stray blank line, C++ emitted it above the std::variant alias with a stray blank line, and Python dropped it entirely. It now attaches directly above each language's union type -- the z.discriminatedUnion export, the std::variant using-alias, and (as a line comment, since aliases cannot carry a docstring) the Annotated alias.
Union variant type names doubled the union's category acronym (AIChannel's ai_voltage variant became AIChannelAiVoltage). Added casing.VariantTypeName, shared across the TS, Python, Go, and C++ plugins, which factors out the union's leading acronym when the variant repeats it -- AIChannelAiVoltage becomes AIVoltageChannel, CIChannel + ci_two_edge_sep becomes CITwoEdgeSepChannel. Unions without an acronym prefix keep the union-name prefix (Scale + linear stays ScaleLinear) so variant types never collide with a field struct that shares the variant's bare name. Known initialisms (AI, CI, AO, RTD, IEPE, RMS, ...) stay fully upper-cased via whole-segment matching.
Union variants previously flattened the union's shared base and the variant
payload into a standalone struct, duplicating every shared field across all
variants and discarding the type relationships the schema expresses. Each
variant now composes those structs through its language's existing
struct-extends mechanism, declaring only the discriminator:
- Python: class AIVoltageChannel(BaseAIChannel, AIVoltageFields)
- TypeScript: baseAIChannelZ.extend(aiVoltageFieldsZ.shape).extend({ type })
- C++: struct AIVoltageChannel : BaseAIChannel, AIVoltageFields, with the
codec routed through the existing HasExtends parse/to_json branch
- Go already embedded the base and payload
The serialized JSON is unchanged (all three flatten inherited fields on the
wire), so this is wire-compatible; verified against real pydantic and zod that
a composed variant round-trips through the discriminated union. Field docs now
live once on the base/payload classes instead of being copied onto every
variant.
Use set.Set for the acronym membership set instead of map[string]struct{},
and maps.Copy for the union domain copy loop.
- validateUnion now rejects a variant value equal to the discriminator field name (would clash with the generated discriminator type) and a union base struct that declares a field named "variant" (reserved for the proto oneof). - The C++ JSON codec leaves the parse parameter unnamed for field-less structs so they no longer trip an unused-parameter warning. - The TypeScript enum values const no longer doubles a trailing S (UNITS stays UNITS instead of UNITSS).
The union feature is fully exercised by the oracle plugin and analyzer tests. The NI schema and its generated output belong to SY-4299, which will introduce ni.oracle as the feature's first real consumer. The generated files were also stale, predating the later codegen changes on this branch.
Defines Segment, SegmentedEdgeConfig, and the EdgeConfig discriminated union (7 variants tagged on variant) in schemas/schematic.oracle and regenerates TS, Go, and protobuf output. Pluto's hand-written edge schemas now re-export the generated ones: segmentZ and the per-variant configZ schemas come from @synnaxlabs/client, the edge registry's variant enum comes from the generated EdgeConfigType, and a satisfies guard keeps the registry keys in lockstep with the schema. The wire configs field stays an opaque record until node configs are typed in the next step.
TS zod schemas are always camelCase; the JSON codec handles wire-case
conversion. The union plugin passed the discriminator field name through
verbatim, so a union on value_type produced z.discriminatedUnion
("value_type", ...) with snake_case literal keys that never match
runtime data.
…strongly-type-schematic-node-and-edge-props
The go/pb plugin skipped union types entirely: no <Union>ToPB/FromPB functions were generated, and union-typed struct fields fell through to direct assignment between the Go sealed-interface form and the protobuf oneof wrapper pointer, which does not compile. Unions now get dedicated translators (nil variant maps to nil message, each variant converts its payload through the payload struct's translator and wraps it in the protoc oneof wrapper), and struct fields, optional fields, and array elements route through them. Unions with extends are rejected with an explicit error until base-field translation is implemented.
…strongly-type-schematic-node-and-edge-props
… translators Oneof wrapper names now gain protoc's trailing underscore when a variant value camelizes to a generated message method (a "string" variant produces Sink_String_, not the nonexistent Sink_String). Record array fields convert through shared recordsToPB/recordsFromPB helpers instead of falling through to a direct assignment between []msgpack.EncodedJSON and []*structpb.Struct.
…strongly-type-schematic-node-and-edge-props
Models all 100 schematic node symbol configurations in schemas/schematic.oracle as a NodeConfig discriminated union: 44 static labeled symbols, 31 telemetry-actuated toggles, and 7 local dummy toggles share three payload structs, while the remaining 18 symbols get bespoke structs. Telemetry source/sink references are modeled as single-variant unions on value_type with role-pinned variants and opaque, factory-validated props, preserving pluto's per-value-type compile-time narrowing. A new border module owns the per-corner radius canonical form: generated Go/pb types with a crude-accepting UnmarshalJSON, and a hand-written crude zod union in x/ts, mirroring how color handles crude inputs. Pluto's hand-written node schemas are replaced by re-exports of the generated ones: the symbol factories resolve their config schemas from NODE_CONFIG_SCHEMAS, every singleton config file re-exports its generated variant schema, and the node registry's variant enum and union come from the generated NodeConfigType with a satisfies guard against schema/registry drift. Custom symbols layer a TS-only refinement that narrows stateOverrides to the symbol service's typed states on top of the wire-opaque record. The per-category discriminated unions are removed; the generated union is the single aggregation point.
…9-oracle-support-for-struct-unions
…strongly-type-schematic-node-and-edge-props # Conflicts: # client/ts/src/schematic/types.gen.ts
A union may now extend other unions, mirroring enum extension: the extending union's variant set is the union of its parents' variants plus its own declared variants. The analyzer expands the membership before validation, so code generators see a plain, fully populated union and need no changes. Parents must share the extending union's discriminator, inherited variants with conflicting payload types are rejected, struct and union bases cannot be mixed in one declaration, and composition over a union that extends base structs is rejected until base-field flattening is needed.
…strongly-type-schematic-node-and-edge-props
…keys preserve_case halts wire case conversion for an entire subtree, which is wrong once a map's values are typed payloads: the keys are semantic identifiers that must stay verbatim, but the values' field names must still convert between the camelCase TS schemas and the snake_case wire. caseconv.preserveKeys marks a record schema for exactly that split, and the @ts preserve_keys field domain emits it.
…strongly-type-schematic-node-and-edge-props
The storage codec generator rejected unions wherever they appeared as a stored value. Unions now encode through their generated internally tagged MarshalJSON/UnmarshalJSON pair as length-prefixed JSON, keeping the stored bytes self-describing across variant additions, and the codec round-trip test generator builds a first-variant sample value for union-typed fields.
…strongly-type-schematic-node-and-edge-props
…strongly-type-schematic-node-and-edge-props
3 tasks
…8-strongly-type-schematic-node-and-edge-props
…8-strongly-type-schematic-node-and-edge-props
…8-strongly-type-schematic-node-and-edge-props
Delete the 26 per-symbol and per-edge config.ts re-export shims in the pluto schematic package. Symbol/edge modules now reference the Oracle-generated schematic.* types directly: Config is inlined as schematic.NodeConfig<X>/EdgeConfig<X>, and VARIANT/NAME are inlined as string literals. The edge-level Config/configZ aggregate moves into edge/registry.ts to mirror node/registry.ts, keeping the public Edge.Config type intact.
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.
Issue Pull Request
Linear Issue
SY-4068
Description
Replaces the schematic's opaque per-element config storage with a fully typed model, end to end. The schema now declares every symbol and edge configuration: a
NodeConfigunion over all 100 node variants (44 static labeled symbols, 31 telemetry-actuated toggles, and 7 local dummy toggles share three payload structs; the remaining 18 symbols get bespoke structs), anEdgeConfigunion over the 7 edge styles, and anElementConfigunion composed from both via union extends. Theconfigsmap flips frommap<string, record>tomap<string, ElementConfig>, so configs are validated on every write path instead of trusted: the Go reducer decodes merged action payloads through the union (normalizing the camelCase keys the Console previously stored verbatim, with telem props and custom-symbol state overrides exempt since their keys are semantic), and the TS reducer parses throughelementConfigZfor parity.Pluto's hand-written schemas become re-exports of the generated ones: symbol and edge factories resolve their config schemas from the generated schema maps, every singleton config file re-exports its generated variant schema, and the registries' variant enums come from the generated types with
satisfiesguards against schema/registry drift. Telemetry source/sink references are modeled as single-variant unions onvalue_typewith role-pinned variants and opaque, factory-validated props, preserving the existing per-value-type compile-time narrowing. Border radius moves into a newbordermodule following the color pattern: a canonical generated struct with a crude-accepting GoUnmarshalJSONand a hand-written crude zod union in x.Optional config fields whose absence is meaningful (enums, colors, dimensions, telem specs, nested configs) are hard-optional so absence round-trips faithfully instead of collapsing to zero values that downstream optional schemas reject; only fields where zero and absent coincide stay soft-optional. Map keys stay verbatim on the wire via the new
preserve_keysdomain while value fields convert like any typed payload.Storage migrates losslessly: the migration chain re-anchors on the v56 snapshot, the v55 blob lift moves into the v56 mirror package, and a new
v56_typed_element_configsstep normalizes stored camelCase config keys and decodes them into the union, dropping entries that match no variant. Without the migration, pre-flip datastores would silently shed multi-word config fields on read.Stacked on #2416, which gained the generator capabilities this branch exercises: union composition via extends, union pb translators and orc codec support, multi-word discriminator camelization, nil-variant null marshaling, the
preserve_keyscaseconv mode, and skipping auto-copy for fields whose union-ness changed.Basic Readiness
Greptile Summary
This PR replaces the schematic's opaque
map<string, record>config storage with a fully typedmap<string, ElementConfig>union covering all 100+ node variants and 7 edge styles, validated end-to-end on every read and write path. A two-step migration chain (v55→v56 blob lift, then v56→current key normalization + telem arg extraction + union decode) converts existing stored camelCase configs losslessly, with the same logic mirrored in the TypeScript console migration.v56/migrate.godoes the structural reshape (node/edge lift, stump stripping);migrate.godoes the new v56→current step that normalizes camelCase keys, extracts legacy telem pipeline specs into semantic args (channel,rolling_average, etc.), and decodes through the typed union—dropping only entries that fail to decode.SetNodePayload,SetConfigPayload) now run every write throughnormalizeConfigKeys+decodeElementConfig, rejecting configs that match no known variant; telem arg extraction is intentionally omitted from the live write path since new Console clients use the semantic-arg schema directly.UnmarshalJSONthat accepts bare numbers,{x,y}pairs, per-corner numbers, and the canonical per-corner pair form for backward-compatible deserialization of legacy data.Confidence Score: 5/5
Safe to merge; the migration chain is correctly ordered and losslessly converts stored camelCase configs to the typed union, and the live write path rejects invalid configs rather than silently storing them.
All migration steps are well-tested with snapshot fixtures, unit tests for telem extraction, and an end-to-end schema validation test on the Console side. The three concerns flagged in earlier review threads are the only known issues, all of which affect theoretical paths or documentation rather than data correctness. No new correctness issues were found during this review.
core/pkg/service/schematic/actions.go — the edge source-color inheritance guard has a logic inversion noted in a previous thread. core/pkg/service/schematic/migrate.go — the doc comment on the drop condition is broader than stated, also from a previous thread.
Important Files Changed
extractTelemArgsmirroring the Go logic and appliescaseconv.camelToSnaketo variant keys during props-to-configs migration. Edge and node paths are symmetric with the server side.UnmarshalJSONaccepting four legacy radius shapes; well-tested with round-trip test. Corner key aliasing (camelCase/snake_case) enables lossless migration of pre-typed configs.v56_typed_element_configsmigration step betweenv55_lift_typed_schematicand the current schema; dependency chain is correctly ordered.radiusZwith four accepted formats as a zod union, matching the Go custom unmarshaler.Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[v55 Schematic\nopaque blob] -->|v55_lift_typed_schematic| B[v56 Schematic\nConfigs: map string EncodedJSON] B -->|v56_typed_element_configs| C[Current Schematic\nConfigs: map string ElementConfig] subgraph v56_step["v56→current: migrateConfigEntry"] D[normalizeConfigKeys\ncamelCase→snake_case keys\nvariant value normalized] --> E[extractTelemArgs\nlift telem pipeline specs\ninto semantic args] E --> F[decodeElementConfig\nvalidate through\nElementConfig union] F -->|decode fails| G[drop entry silently] F -->|decode succeeds| H[typed ElementConfig] end B --> D subgraph live_write["Live Write Path: SetConfigPayload.Handle"] I[p.Config EncodedJSON] --> J[normalizeConfigKeys] J -->|existing entry| K[elementConfigFields\nmarshal→unmarshal typed→map] K --> L[maps.Copy merge] L --> M[decodeElementConfig] J -->|new entry + edge| N[source color inheritance\nfrom srcCfg] N --> M M -->|error| O[return error to caller] M -->|ok| P[store typed ElementConfig] endReviews (2): Last reviewed commit: "Merge branch 'sy-3999-oracle-support-for..." | Re-trigger Greptile