Phase 7b: page render plans — per-page boundary optimizer

Add page-render-plan to deps.sx: given page source + env + IO names,
computes a dict mapping each needed component to "server" or "client",
with server/client lists and IO dep collection. 5 new spec tests.

Integration:
- PageDef.render_plan field caches the plan at registration
- compute_page_render_plans() called from auto_mount_pages()
- Client page registry includes :render-plan per page
- Affinity demo page shows per-page render plans

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 00:02:53 +00:00
parent a70ff2b153
commit 2da80c69ed
14 changed files with 214 additions and 5 deletions

View File

@@ -49,7 +49,7 @@
;; --- Main page component ---
(defcomp ~affinity-demo-content (&key components)
(defcomp ~affinity-demo-content (&key components page-plans)
(div :class "space-y-8"
(div :class "border-b border-stone-200 pb-6"
(h1 :class "text-2xl font-bold text-stone-900" "Affinity Annotations")
@@ -154,6 +154,35 @@
(td :class "px-3 py-2 font-bold text-orange-700" "server")
(td :class "px-3 py-2 text-stone-600" "Both affinity and IO say server"))))))
;; Per-page render plans
(~doc-section :title "Page Render Plans" :id "plans"
(p "Phase 7b: render plans are pre-computed at registration time for each page. The plan maps every component needed by the page to its render target.")
(when (> (len page-plans) 0)
(div :class "space-y-4 mt-4"
(map (fn (plan)
(div :class "rounded border border-stone-200 p-4"
(div :class "flex items-center justify-between mb-3"
(div
(span :class "font-mono font-medium text-stone-800" (get plan "name"))
(span :class "text-stone-400 ml-2 text-sm" (get plan "path")))
(div :class "flex gap-2"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-orange-100 text-orange-700"
(str (get plan "server-count") " server"))
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-100 text-green-700"
(str (get plan "client-count") " client"))))
(when (> (get plan "server-count") 0)
(div :class "mb-2"
(span :class "text-xs font-medium text-stone-500 uppercase" "Server-expanded: ")
(span :class "text-sm font-mono text-orange-700"
(join " " (get plan "server")))))
(when (> (get plan "client-count") 0)
(div
(span :class "text-xs font-medium text-stone-500 uppercase" "Client-rendered: ")
(span :class "text-sm font-mono text-green-700"
(join " " (get plan "client")))))))
page-plans))))
;; How it integrates
(~doc-section :title "How It Works" :id "how"
(ol :class "list-decimal list-inside text-stone-700 space-y-2"

View File

@@ -2014,8 +2014,30 @@
(li "Backward compatible: existing defcomp without :affinity defaults to \"auto\""))))
(~doc-subsection :title "7b. Runtime Boundary Optimizer"
(div :class "rounded border border-green-300 bg-green-50 p-3 mb-4"
(div :class "flex items-center gap-2 mb-1"
(span :class "inline-block px-2 py-0.5 rounded text-xs font-bold bg-green-600 text-white uppercase" "Complete"))
(p :class "text-green-800 text-sm" "Per-page render plans computed at registration time. Each page knows exactly which components render server-side vs client-side, cached on PageDef."))
(p "Given component tree + IO dependency graph + affinity annotations, decide per-component: server-expand, client-render, or stream. Planning step cached at registration, recomputed on component change.")
(p :class "text-stone-500 text-sm italic" "Next: integrate render-target into the bundle analyzer, page registry, and orchestration.sx."))
(p (code "page-render-plan") " in deps.sx computes per-page boundary decisions:")
(~doc-code :code (highlight "(page-render-plan page-source env io-names)\n;; Returns:\n;; {:components {~name \"server\"|\"client\" ...}\n;; :server (list of server-expanded names)\n;; :client (list of client-rendered names)\n;; :io-deps (IO primitives needed by server components)}" "lisp"))
(~doc-subsection :title "Integration Points"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li (code "shared/sx/ref/deps.sx") " — " (code "page-render-plan") " spec function")
(li (code "shared/sx/deps.py") " — Python wrapper, dispatches to bootstrapped code")
(li (code "shared/sx/pages.py") " — " (code "compute_page_render_plans()") " called at mount time, caches on PageDef")
(li (code "shared/sx/helpers.py") " — " (code "_build_pages_sx()") " includes " (code ":render-plan") " in client page registry")
(li (code "shared/sx/types.py") " — " (code "PageDef.render_plan") " field")))
(~doc-subsection :title "Verification"
(ul :class "list-disc pl-5 text-stone-700 space-y-1"
(li "5 new spec tests (page-render-plan suite)")
(li "Render plans visible on " (a :href "/isomorphism/affinity" "affinity demo page"))
(li "Client page registry includes :render-plan for each page"))))
(~doc-subsection :title "7c. Optimistic Data Updates"
(p "Extend existing apply-optimistic/revert-optimistic in engine.sx from DOM-level to data-level. Client updates cached data optimistically, sends mutation to server, reverts on rejection."))

View File

@@ -881,9 +881,10 @@ async def _streaming_demo_data():
def _affinity_demo_data() -> dict:
"""Return affinity analysis for the demo components."""
"""Return affinity analysis for the demo components + page render plans."""
from shared.sx.jinja_bridge import get_component_env
from shared.sx.types import Component
from shared.sx.pages import get_all_pages
env = get_component_env()
demo_names = [
@@ -904,4 +905,20 @@ def _affinity_demo_data() -> dict:
"io-refs": sorted(val.io_refs),
"is-pure": val.is_pure,
})
return {"components": components}
# Collect render plans from all sx service pages
page_plans = []
for page_def in get_all_pages("sx").values():
plan = page_def.render_plan
if plan:
page_plans.append({
"name": page_def.name,
"path": page_def.path,
"server-count": len(plan.get("server", [])),
"client-count": len(plan.get("client", [])),
"server": plan.get("server", []),
"client": plan.get("client", []),
"io-deps": plan.get("io-deps", []),
})
return {"components": components, "page-plans": page_plans}