RFD 1022 details: the shape, the port-as-test, the first port, method, risks, status
The shape
src/core/
domain/ pure rules. No I/O, no React, no three.js.
ports/ contracts, and the test that defines each one.
adapters/ one per source of data.
The rule for each directory is short:
domain/may import fromdomain/only.ports/declare shape and behavior. They hold no logic.adapters/may import ports and domain. Nothing imports an adapter except the composition step.
The port is a test, not an interface
JavaScript has no interfaces. A comment that says “an adapter must return an array” does not hold an adapter to anything.
So each port ships a contract test. The test takes an adapter and runs the whole port against it. Every adapter runs the same test.
describeCatalogSourceContract('static', makeStaticCatalogSource)
describeCatalogSourceContract('http', makeHttpCatalogSource)
An adapter that passes is usable. An adapter that fails is not. This is what makes the port real, and it is the reason to build the contract test before either adapter.
The first port
Start with the model catalog. It is the smallest part that can move, for four reasons:
- The Elixir core already serves it, at
GET /api/v1/models. - A parity test already guards the two lists.
- It needs no three.js and no React.
- 13 modules read it, so the port earns its keep at once.
Gall’s law: a working simple system first.
CatalogSource answers three questions:
| Function | Returns |
|---|---|
listModels() |
Every model, as { value, label, feature }. |
listFeatures() |
Every feature name. |
listModelsForFeature(feature) |
The models that serve one feature. |
The names follow WeftspunStudio.Ports.CatalogSource, so a reader of one side can read the other.
Two adapters
staticCatalogSource reads the list in src/library/aiModelsCatalog.js. It works with no server, so the client keeps working offline and today’s behavior does not change.
httpCatalogSource reads GET /api/v1/models from the studio core. It carries the fresh list, and it is the direction RFD 1019 goes.
The composition step picks one. VITE_STUDIO_API names the server. Without that variable the client takes the static adapter, so no existing setup breaks.
Method
Red, green, refactor. The RFD is the first red: it states the contract before any code exists.
- Red. This document, then the contract test. The test fails, because no adapter exists.
- Green. Write the two adapters until the contract passes.
- Refactor. Move the pure selection rules out of
aiModelsCatalog.jsintodomain/, behind the port.
What this does not do
It does not touch the other 38 network call sites. It does not touch React, three.js, or the job lifecycle. Those need their own ports, and each needs its own contract test first.
It does not fix the 33 test failures the client already carries. Those predate the rebrand: the same 33 fail at commit f0fa556f, before any rebrand commit. They are a separate task.
Risks
Two adapters mean two behaviors. The contract test is the answer: a difference that the contract does not cover is a hole in the contract, not a license to differ.
The HTTP adapter adds a network dependency to a list that is fixed today. The static adapter stays as the fallback for that reason.
src/core/ is a second home for logic that also lives in src/library/. Until the refactor step finishes, a reader may find both. The port marks which one is current.
Status
Done:
src/core/holdsdomain/,ports/, andadapters/.- The
CatalogSourcecontract test runs against both adapters. staticCatalogSourceandhttpCatalogSourceboth pass it.src/core/composition.jspicks one fromVITE_STUDIO_API.- The selection rules moved into
domain/catalog.js.aiModelsCatalog.jsnow calls them, so one implementation serves every consumer. - 47 tests cover the port, the rules, and the composition step.
- The client still builds, and the suite holds at the 33 failures it carried before this work.
Open:
- Ports for the job lifecycle, assets, and the scene.
- The other 38 direct network call sites.
- The 33 inherited test failures.