Porting an app onto CVN
The port CVN came from moved a 7,400-line application runtime into this shape without a rewrite. The order below is the order the obstacles fell; each step was blocked by the previous one.
0. Publish the measure
Add a declaration to the runtime: owned, delegated, hostCapabilities. Make delegated a list the code produces, and make the loop that drives the port read that list — not a plan file. Twice the port "finished" because every box was ticked while the work behind one was half done.
1. Move decisions, one verb family at a time
For each action family, extract the decision — expression in, patch or Cmd out — into a pure kernel module. Leave effect execution where it is. Measure inline state rebinds inside the action body; they fall from dozens to one.
2. Stop reading and assigning the host's state
A function that assigns to a variable it does not own cannot move to another module. Replace every state = … with cell.commit / cell.replace, every read with cell.value. Count the sites from the function body, not the file — the plan's "≈430 reads" was a whole-file count; the body had 78.
3. Move the body; inject what it imports
The action body becomes a kernel module taking an ActionPorts object. Anything it imported from the host tree that node cannot load (a directory import, a DOM-only module) becomes an injected port. Declare the interface; the contract gate keeps it honest.
4. Let the kernel build the dispatch
createRuntime builds the runner inside the dispatcher, with _perform bound to it. No fallback to a host action: a fallback is how a method is counted as owned while something else still does the work.
5. Shrink the port surface by disposition, not by count
Each remaining port has a written reason: platform, host data, session state, blocked on another move. Trade behaviour ports for data ports. Refuse repackaging.
The traps, so they are not re-derived
let state = getState()in an async function is a stale snapshot after the firstawait.- A kernel module that imports a host-tree module with a directory import is one node cannot load.
- A port the kernel declares and never travels stays wrong forever.
- A
rt: anyports bag hides a missing port until a branch throws inside acatch. - The runner must use the dispatcher's
perform, or the dispatcher records nothing. - Suite "contention" is an app loop until a profile of the page says otherwise.