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.
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
|||||||
|
# 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.cs` → `ParameterValidator.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`):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
{ "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.
|
||||||
Reference in New Issue
Block a user