Files
ScadaBridge/schemalib.md
T
Joseph Doherty a4e920902c docs: schema library wiring audit
Records that the SharedSchema resolver is fully consumed at deploy, runtime and in
the UI value-entry forms, but has no authoring path: SchemaBuilder is the only
schema editor on all four surfaces and offers no library-reference option, so the
page creates entries nothing can point at. SchemaBuilder also collapses a $ref it
is shown to {"type":"string"} — currently unreachable, since no ref exists and
there is no UI or CLI to make one, but a trap for the first one authored.

Includes live usage counts (zero entries, zero refs, both environments) and the
resulting recommendation: leave dormant with a trigger, rather than finish or
delete it now.
2026-08-11 09:16:36 -04:00

10 KiB

Schema Library (SharedSchema) — wiring audit

Date: 2026-08-11 Question: the Schema Library admin page doesn't seem wired to anything. Verdict: the read path is complete and careful; the write path doesn't exist. One of the three gaps is active silent data loss, not just a missing feature.


1. What IS wired

The {"$ref":"lib:Name"} resolver has four real consumers, plus guarded CRUD:

Consumer Location Role
Deploy-time flattening DeploymentManager/FlatteningPipeline.cs:150 Pre-loads the library once into a name→JSON map; an unresolved ref fails the deploy
Template validation TemplateEngine/Validation/ValidationService.cs:91,149 Dangling / cyclic / over-depth ref validation
Inbound API runtime InboundAPI/SchemaRefResolver.csParameterValidator.cs:35, ReturnValueValidator.cs:41 Resolves refs when validating live request/response bodies; gated on InboundApiSchema.MightContainRef so ref-free methods pay nothing
Central UI value entry CentralUI/Components/Shared/ParameterValueForm.razor:166 via ISchemaLibraryQueryService Inlines referenced schemas so value-entry forms render the real shape

Supporting cast, all present and correct:

  • Entity Commons/Entities/Schemas/SharedSchema.cs, repo ISharedSchemaRepository, EF mapping SharedSchemaConfiguration, registration in ConfigurationDatabase/ServiceCollectionExtensions.cs:63.
  • CRUD dispatched through ManagementActor (ManagementActor.cs:2818-2884) — the guard-running path, never a direct repo write from the UI.
  • Nav entry NavMenu.razor:35, page Pages/Design/SchemaLibrary.razor, tests SchemaLibraryPageTests.cs / SchemaLibraryHandlerTests.cs.

So the library is genuinely consumed at deploy time, at runtime, and in one UI surface.


2. Gap 1 — no UI can produce a lib: reference

SchemaBuilder is the only schema editor on every authoring surface:

  • Pages/Design/ApiMethodForm.razor:84,90 (parameters + return)
  • Pages/Design/SharedScriptForm.razor:71,76
  • Pages/Design/TemplateScriptDialog.razor:141,146
  • Pages/Design/SchemaLibrary.razor:46 — the library page itself

Its type picker is SchemaBuilderModel.PrimitiveTypes (SchemaBuilderModel.cs:37-38):

{ "string", "integer", "number", "boolean", "object", "array" }

No library-reference option, at root or property level. There is also no raw-JSON escape hatch for schemas — the Monaco editor on those forms edits the C# script body, and ParameterValueForm's raw mode edits values, not the schema.

Consequence: the Schema Library page creates entries that nothing else in the UI can point at. The only authoring routes for a ref are a direct DB edit or a bundle import.


3. Gap 2 — a ref authored outside the UI is silently destroyed on the next save

ParseSchema inspects only the type property (SchemaBuilderModel.cs:96-100). A $ref node carries no type, so the node defaults to "string", and Serialize writes that back.

Verified by running the round-trip through SchemaBuilderModel directly (throwaway probe, since removed):

{"$ref":"lib:Address"}
  → {"type":"string"}

{"type":"object","properties":{"addr":{"$ref":"lib:Address"},"name":{"type":"string"}},"required":["addr"]}
  → {"type":"object","properties":{"addr":{"type":"string"},"name":{"type":"string"}},"required":["addr"]}

Both root-level and nested (SchemaBuilderModel.cs:131-133) refs collapse.

Consequence: open any ApiMethod / SharedScript / TemplateScript whose definition carries a ref, change anything at all, save — and the contract is silently widened to a bare string. Deploy still passes (the ref is gone, so there is nothing left to dangle) and nothing warns.

Sharpest edge: the Schema Library page edits entries with SchemaBuilder Mode="value", so a library entry that references another library entry — which ManagementActor.cs:2921 explicitly permits — is flattened to {"type":"string"} the moment an operator edits it.

Reachability — correction. An earlier draft of this report called this "worse than merely unfinished: the feature is reachable, and touching it destroys data." That overstates it. The corruption requires a $ref to exist first, and per §6 no $ref exists anywhere in either environment — and since there is no UI path (§2) and no CLI path (§7) to author one, the only way to create one today is a hand-edit of the database. So this is a trap laid for the first ref, not an active defect. It becomes live the moment §5.2 ships, or the moment someone hand-authors a reference.


4. Gap 3 — library entries don't travel between environments

SharedSchema appears nowhere in src/ZB.MOM.WW.ScadaBridge.Transport/.

Contrast its sibling SharedScript, which is a first-class Transport artifact across Serialization/EntityDtos.cs, Serialization/EntitySerializer.cs, Export/BundleExporter.cs, Export/ResolvedExport.cs, Export/DependencyResolver.cs, Import/ArtifactDiff.cs and Import/BundleImporter.cs.

Consequence: a ref that resolves in dev dangles after import into prod. The import-time script trust gate won't catch it — that gate covers script bodies, and this is a schema.


5. Remediation, if the feature is kept

Three separable slices, in dependency order. Read §6 first — on current evidence none of these is urgent, and the prior question is whether the feature should exist at all.

5.1 Close the round-trip trap (small, independent)

Give SchemaNode a passthrough for subtrees the model doesn't understand, so an unrecognised node ($ref included) round-trips verbatim instead of collapsing to {"type":"string"}. Closes Gap 2 outright, needs no new UI, and is a hard prerequisite for 5.2 — shipping the authoring UI without it would make the corruption reachable for the first time.

Test to add alongside: a round-trip guard asserting a $ref node survives parse→serialize at both root and property level — run it against the current code first and observe it fail, or it proves nothing.

5.2 Make refs authorable (the actual "wire it up")

Add a library-reference entry to the SchemaBuilder type picker, populated from the existing ISchemaLibraryQueryService (already registered, CentralUI/ServiceCollectionExtensions.cs:142). Emits {"$ref":"lib:Name"}; with 5.1 in place it round-trips. This is what actually connects the Schema Library page to the rest of the system.

5.3 Carry SharedSchema in Transport bundles (separate, larger)

DTO + serializer + exporter + dependency resolution (walk definition bodies for lib: refs so referenced entries are pulled into the bundle) + differ + importer. Mirrors the SharedScript shape throughout. Worth its own pass, and worth deciding whether an unresolved ref on import is a blocker row or an advisory warning — the existing name-resolution severity split (template = advisory, ApiMethod = hard error) is the precedent.


6. Is the feature needed at all?

The gaps above describe an unfinished feature. The prior question — asked after the audit — is whether it should be finished, and the evidence says not on current usage.

6.1 Usage: zero, in both environments

Queried live against scadabridge-mssql on 2026-08-11:

ScadaBridgeConfig (docker rig) ScadaBridgeConfig2 (docker-env2)
SharedSchemas rows 0 0
Definitions containing $ref 0 0
ApiMethods 0 0
TemplateScripts 31 15
…of those, carrying any parameter definition 5 4

Not only has the library never been used — most scripts don't declare a typed parameter shape at all, which is the precondition for ever wanting a reusable one.

(Production wonder-app-vd03 was deliberately not queried; see §7.)

6.2 Origin: speculative, not requirement-driven

It was not built to satisfy a stated need. The schema-editor design doc lists it under deferred future work — reuse of $ref across templates "could be a future template-level schema library" (docs/plans/2026-05-12-script-schema-editor.md:117-118). It was built later as M9/T32. The Scope column is likewise documented as being for future namespacing — a speculative field on a speculative feature.

6.3 The benefit is authoring-time DRY only

Resolution is inlining, not indirection: FlatteningPipeline and SchemaRefResolver expand lib:Name into the schema body at deploy / validation / runtime. The library therefore adds no runtime capability whatsoever — an inline schema does the entire job. Its single benefit is not retyping a shared shape at authoring time.

That benefit also carries a cost that grows with adoption: there is no reverse index. Editing lib:Address silently changes every referencing contract at the next deploy, with no "what references this?" view and no versioning. Tolerable at 2 references, unpleasant at 20. Any decision to finish the feature should budget for an impact-analysis view alongside 5.2.

6.4 Options

  1. Delete it. The resolver plumbing (4 consumers), the page, entity, table, repository, ManagementActor handlers and tests are all carried for zero users. Defensible on the evidence.
  2. Leave it dormant, fix nothing. Zero cost today, because per §3 the defect cannot fire without a ref and no ref exists. Revisit when a genuinely repeated payload shape appears.
  3. Finish it (5.1 + 5.2 + 5.3, plus an impact-analysis view per 6.3). Justified only by known upcoming work with a repeated non-trivial shape — e.g. one MES order envelope shared across several inbound methods.

Recommendation: option 2, with a hard trigger. If a lib: ref is ever authored, §5.1 must land in the same change or the builder will destroy it. If nothing needs the library within roughly six months, take option 1 rather than leaving permanent unexercised surface in the codebase.


7. Notes

  • Nothing in this report was changed. No fix has been applied; the verification probe used in §3 was deleted after use.
  • The CLI has no schema-library commands either (src/ZB.MOM.WW.ScadaBridge.CLI/ has no SharedSchema reference), so there is no scriptable authoring path as a workaround. This is why §3's trap is currently unreachable: hand-editing the database is the only way to create a ref.
  • The §6.1 counts are from the two local docker environments only. Production (wonder-app-vd03) was not queried — that is an operator action, not one to take unasked. If that box carries inbound methods with $ref definitions, §3 is live there and the §6.4 recommendation changes.